Compare commits

...

3 Commits

Author SHA1 Message Date
mkaoj697q 394fc16642
2 months ago
李嫚嫚 31c104bb15
2 months ago
李嫚嫚 ab3fcaf7ac
2 months ago

@ -1,8 +0,0 @@
root = true
[*]
charset = utf-8
indent_size = 4
indent_style = space
insert_final_newline = false
trim_trailing_whitespace = true

108
.gitignore vendored

@ -1,108 +0,0 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
#ide
.idea
.vscode
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# TypeScript v1 declaration files
typings/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env
.env.test
# parcel-bundler cache (https://parceljs.org/)
.cache
# Next.js build output
.next
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and *not* Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port

@ -0,0 +1,33 @@
<template>
<div id="app">
<transition name="fade">
<router-view />
</transition>
</div>
</template>
<style>
img.image-sm {
max-width: 80px;
max-height: 80px;
}
.el-col .el-select,
.el-col .el-date-editor {
width: 100%;
}
.demo-table-expand {
font-size: 0;
}
.demo-table-expand label {
width: 90px;
color: #99a9bf;
}
.demo-table-expand .el-form-item {
margin-right: 0;
margin-bottom: 0;
width: 50%;
}
.text-warning {
color: #e6a23c;
}
</style>

@ -1,201 +0,0 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

@ -1,45 +0,0 @@
# wx-manage
wx-manage是一个支持公众号管理系统支持多公众号接入。
wx-manage提供公众号菜单、自动回复、公众号素材、简易CMS、等管理功能请注意本项目仅为管理后台界面需配合后端程序[wx-api](https://github.com/niefy/wx-api)一起使用
### [📖项目文档](https://www.yuque.com/nifury/wx) | [Github仓库](https://github.com/niefy/wx-manage) | [码云仓库](https://gitee.com/niefy/wx-manage)
## 项目说明
- wx-api是一个轻量级的公众号开发种子项目可快速接入微信公众号管理功能
- 管理后台前端项目wx-managehttps://github.com/niefy/wx-manage
- 移动端示例wx-client: https://github.com/niefy/wx-client
## [docker方式启动文档](https://www.yuque.com/nifury/wx/nf1rvm)
## [开发环境启动文档](https://www.yuque.com/nifury/wx/guobb7)
## [生产环境部署步骤](https://www.yuque.com/nifury/wx/ofehhv)
## 技术选型:
- 核心框架Spring Boot
- 安全框架Apache Shiro
- 持久层框架MyBatis-Plus
- 公众号开发框架:[WxJava](https://github.com/Wechat-Group/WxJava)
- 后端脚手架:[renren-fast](https://gitee.com/renrenio/renren-fast)
- 页面交互:[Vue2.x](https://cn.vuejs.org/v2/guide/)
- UI框架[ElementUI](https://element.eleme.cn/#/zh-CN/component/quickstart)
- 管理后台界面模板:[renren-fast-vue](https://gitee.com/renrenio/renren-fast-vue)
- 富文本编辑器:[tinymce5](https://www.tiny.cloud/docs/quick-start/)
## 截图
![公众号账号](https://s1.ax1x.com/2020/06/23/NUTQAg.png)
![公众号菜单](https://s1.ax1x.com/2020/06/23/NUTlNQ.png)
![自动回复](https://s1.ax1x.com/2020/04/10/GTqyQA.png)
![模板消息配置](https://s1.ax1x.com/2020/04/18/JnKZhF.jpg)
![模板消息发送](https://s1.ax1x.com/2020/04/18/JnKEkT.jpg)
![粉丝管理](https://s1.ax1x.com/2020/04/18/JnKVtU.jpg)
![带参二维码](https://s1.ax1x.com/2020/04/18/JnKF00.jpg)
![素材管理](https://s1.ax1x.com/2020/05/20/Y7djHI.jpg)
![公众号消息](https://s1.ax1x.com/2020/05/20/Y7dXDA.jpg)
![文章编辑](https://s1.ax1x.com/2020/04/10/GTqrzd.png)
![系统菜单管理](https://s1.ax1x.com/2020/04/18/JnKk7V.jpg)
![管理员列表](https://s1.ax1x.com/2020/04/18/JnKimq.jpg)
## [项目开发进度](https://www.yuque.com/nifury/wx/kens6d)
## [代码贡献指南](https://www.yuque.com/nifury/wx/ykqswi)
## 开发交流
QQ群1023785886已满、993128490 技术交流群严禁广告,发广告立即踢出+拉黑+举报加群密码wx

@ -1,9 +0,0 @@
module.exports = {
presets: [
'@vue/cli-plugin-babel/preset'
],
"plugins": [
"@babel/plugin-syntax-dynamic-import"
],
sourceType: 'unambiguous'
}

@ -0,0 +1,32 @@
import Vue from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'
import VueCookie from 'vue-cookie'
import ElementUI from 'element-ui';
import moment from 'moment'
import 'element-ui/lib/theme-chalk/index.css';
import './assets/css/common.css'
import './assets/scss/index.scss'
import httpRequest from '@/utils/httpRequest' // api: https://github.com/axios/axios
import { isAuth } from '@/utils'
import VueClipboard from 'vue-clipboard2'
Vue.use(ElementUI);
Vue.use(VueClipboard)
Vue.use(VueCookie)
Vue.config.productionTip = false
// 挂载全局
Vue.prototype.$http = httpRequest // ajax请求方法
Vue.prototype.isAuth = isAuth // 权限方法
moment.locale('zh-cn');
Vue.prototype.$moment = moment; //时间处理
new Vue({
router,
store,
render: h => h(App)
}).$mount('#app')

18266
package-lock.json generated

File diff suppressed because it is too large Load Diff

@ -1,37 +0,0 @@
{
"name": "wx-manage",
"version": "0.8.2",
"private": true,
"scripts": {
"serve": "vue-cli-service serve",
"build": "vue-cli-service build"
},
"dependencies": {
"@tinymce/tinymce-vue": "^3.2.6",
"axios": "^1.4.0",
"element-ui": "^2.15.8",
"moment": "^2.29.3",
"vue": "^2.6.12",
"vue-clipboard2": "^0.3.1",
"vue-cookie": "^1.1.4",
"vue-router": "^3.4.9",
"vuex": "^3.6.0"
},
"devDependencies": {
"@babel/plugin-syntax-dynamic-import": "^7.2.0",
"@vue/cli-plugin-babel": "^5.0.8",
"@vue/cli-service": "^5.0.8",
"sass": "^1.51.0",
"sass-loader": "10.2.0",
"vue-template-compiler": "^2.6.12"
},
"postcss": {
"plugins": {
"autoprefixer": {}
}
},
"browserslist": [
"> 1%",
"last 2 versions"
]
}

@ -1,19 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="referrer" content="never">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<title>微信后台管理系统</title>
<!-- tinymce编辑器 -->
<script src="https://cdn.bootcdn.net/ajax/libs/tinymce/5.10.4/tinymce.min.js"></script>
</head>
<body>
<noscript>
<strong>We're sorry but weixin-manage doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
</noscript>
<div id="app"></div>
<!-- built files will be auto injected -->
</body>
</html>

@ -1,96 +0,0 @@
/* html相关样式 */
a {
color: #4285f4;
}
h1,h2,h3,h4,h5,h6{
margin: 0.3rem 0;
color: #0064A8;
line-height: 2rem;
}
h1{
font-size: 1.4rem;
}
h2{
font-size: 1.2rem;
}
h3{
font-size: 1.1rem;
}
h4,
h5,
h6 {
font-size: 1rem;
}
hr {
height: 0.2em;
border: 0;
color: #CCCCCC;
background-color: #CCCCCC;
}
p,
blockquote,
ul,
ol,
dl,
li,
table,
pre {
margin: 8px 0;
}
p {
margin: 1em 0;
line-height: 1.5rem;
}
pre {
background-color: #F8F8F8;
border: 1px solid #CCCCCC;
border-radius: 3px;
overflow: auto;
padding: 5px;
}
blockquote {
color: #666666;
margin: 0;
border-left: 0.2em #EEE solid;
}
ul,
ol {
margin: 1em 0;
padding: 0 0 0 2em;
}
li p:last-child {
margin: 0
}
dd {
margin: 0 0 0 2em;
}
img {
border: 0;
max-width: 300px;
display: block;
object-fit: contain;
width: auto !important;
height: auto !important;
}
table {
border-collapse: collapse;
border-spacing: 0;
width: 100%;
border: 1px solid #eee;
}
td {
vertical-align: top;
padding: 0.2em 0;
border-top: 1px solid #EEEEEE;
}

@ -1,389 +0,0 @@
tinymce.addI18n('zh_CN',{
"Redo": "\u91cd\u505a",
"Undo": "\u64a4\u9500",
"Cut": "\u526a\u5207",
"Copy": "\u590d\u5236",
"Paste": "\u7c98\u8d34",
"Select all": "\u5168\u9009",
"New document": "\u65b0\u6587\u4ef6",
"Ok": "\u786e\u5b9a",
"Cancel": "\u53d6\u6d88",
"Visual aids": "\u7f51\u683c\u7ebf",
"Bold": "\u7c97\u4f53",
"Italic": "\u659c\u4f53",
"Underline": "\u4e0b\u5212\u7ebf",
"Strikethrough": "\u5220\u9664\u7ebf",
"Superscript": "\u4e0a\u6807",
"Subscript": "\u4e0b\u6807",
"Clear formatting": "\u6e05\u9664\u683c\u5f0f",
"Align left": "\u5de6\u8fb9\u5bf9\u9f50",
"Align center": "\u4e2d\u95f4\u5bf9\u9f50",
"Align right": "\u53f3\u8fb9\u5bf9\u9f50",
"Justify": "\u4e24\u7aef\u5bf9\u9f50",
"Bullet list": "\u9879\u76ee\u7b26\u53f7",
"Numbered list": "\u7f16\u53f7\u5217\u8868",
"Decrease indent": "\u51cf\u5c11\u7f29\u8fdb",
"Increase indent": "\u589e\u52a0\u7f29\u8fdb",
"Close": "\u5173\u95ed",
"Formats": "\u683c\u5f0f",
"Your browser doesn't support direct access to the clipboard. Please use the Ctrl+X\/C\/V keyboard shortcuts instead.": "\u4f60\u7684\u6d4f\u89c8\u5668\u4e0d\u652f\u6301\u6253\u5f00\u526a\u8d34\u677f\uff0c\u8bf7\u4f7f\u7528Ctrl+X\/C\/V\u7b49\u5feb\u6377\u952e\u3002",
"Headers": "\u6807\u9898",
"Header 1": "\u6807\u98981",
"Header 2": "\u6807\u98982",
"Header 3": "\u6807\u98983",
"Header 4": "\u6807\u98984",
"Header 5": "\u6807\u98985",
"Header 6": "\u6807\u98986",
"Headings": "\u6807\u9898",
"Heading 1": "\u6807\u98981",
"Heading 2": "\u6807\u98982",
"Heading 3": "\u6807\u98983",
"Heading 4": "\u6807\u98984",
"Heading 5": "\u6807\u98985",
"Heading 6": "\u6807\u98986",
"Preformatted": "\u9884\u5148\u683c\u5f0f\u5316\u7684",
"Div": "Div",
"Pre": "Pre",
"Code": "\u4ee3\u7801",
"Paragraph": "\u6bb5\u843d",
"Blockquote": "\u5f15\u6587\u533a\u5757",
"Inline": "\u6587\u672c",
"Blocks": "\u57fa\u5757",
"Paste is now in plain text mode. Contents will now be pasted as plain text until you toggle this option off.": "\u5f53\u524d\u4e3a\u7eaf\u6587\u672c\u7c98\u8d34\u6a21\u5f0f\uff0c\u518d\u6b21\u70b9\u51fb\u53ef\u4ee5\u56de\u5230\u666e\u901a\u7c98\u8d34\u6a21\u5f0f\u3002",
"Fonts": "\u5b57\u4f53",
"Font Sizes": "\u5b57\u53f7",
"Class": "\u7c7b\u578b",
"Browse for an image": "\u6d4f\u89c8\u56fe\u50cf",
"OR": "\u6216",
"Drop an image here": "\u62d6\u653e\u4e00\u5f20\u56fe\u50cf\u81f3\u6b64",
"Upload": "\u4e0a\u4f20",
"Block": "\u5757",
"Align": "\u5bf9\u9f50",
"Default": "\u9ed8\u8ba4",
"Circle": "\u7a7a\u5fc3\u5706",
"Disc": "\u5b9e\u5fc3\u5706",
"Square": "\u65b9\u5757",
"Lower Alpha": "\u5c0f\u5199\u82f1\u6587\u5b57\u6bcd",
"Lower Greek": "\u5c0f\u5199\u5e0c\u814a\u5b57\u6bcd",
"Lower Roman": "\u5c0f\u5199\u7f57\u9a6c\u5b57\u6bcd",
"Upper Alpha": "\u5927\u5199\u82f1\u6587\u5b57\u6bcd",
"Upper Roman": "\u5927\u5199\u7f57\u9a6c\u5b57\u6bcd",
"Anchor...": "\u951a\u70b9...",
"Name": "\u540d\u79f0",
"Id": "\u6807\u8bc6\u7b26",
"Id should start with a letter, followed only by letters, numbers, dashes, dots, colons or underscores.": "\u6807\u8bc6\u7b26\u5e94\u8be5\u4ee5\u5b57\u6bcd\u5f00\u5934\uff0c\u540e\u8ddf\u5b57\u6bcd\u3001\u6570\u5b57\u3001\u7834\u6298\u53f7\u3001\u70b9\u3001\u5192\u53f7\u6216\u4e0b\u5212\u7ebf\u3002",
"You have unsaved changes are you sure you want to navigate away?": "\u4f60\u8fd8\u6709\u6587\u6863\u5c1a\u672a\u4fdd\u5b58\uff0c\u786e\u5b9a\u8981\u79bb\u5f00\uff1f",
"Restore last draft": "\u6062\u590d\u4e0a\u6b21\u7684\u8349\u7a3f",
"Special characters...": "\u7279\u6b8a\u5b57\u7b26...",
"Source code": "\u6e90\u4ee3\u7801",
"Insert\/Edit code sample": "\u63d2\u5165\/\u7f16\u8f91\u4ee3\u7801\u793a\u4f8b",
"Language": "\u8bed\u8a00",
"Code sample...": "\u793a\u4f8b\u4ee3\u7801...",
"Color Picker": "\u9009\u8272\u5668",
"R": "R",
"G": "G",
"B": "B",
"Left to right": "\u4ece\u5de6\u5230\u53f3",
"Right to left": "\u4ece\u53f3\u5230\u5de6",
"Emoticons...": "\u8868\u60c5\u7b26\u53f7...",
"Metadata and Document Properties": "\u5143\u6570\u636e\u548c\u6587\u6863\u5c5e\u6027",
"Title": "\u6807\u9898",
"Keywords": "\u5173\u952e\u8bcd",
"Description": "\u63cf\u8ff0",
"Robots": "\u673a\u5668\u4eba",
"Author": "\u4f5c\u8005",
"Encoding": "\u7f16\u7801",
"Fullscreen": "\u5168\u5c4f",
"Action": "\u64cd\u4f5c",
"Shortcut": "\u5feb\u6377\u952e",
"Help": "\u5e2e\u52a9",
"Address": "\u5730\u5740",
"Focus to menubar": "\u79fb\u52a8\u7126\u70b9\u5230\u83dc\u5355\u680f",
"Focus to toolbar": "\u79fb\u52a8\u7126\u70b9\u5230\u5de5\u5177\u680f",
"Focus to element path": "\u79fb\u52a8\u7126\u70b9\u5230\u5143\u7d20\u8def\u5f84",
"Focus to contextual toolbar": "\u79fb\u52a8\u7126\u70b9\u5230\u4e0a\u4e0b\u6587\u83dc\u5355",
"Insert link (if link plugin activated)": "\u63d2\u5165\u94fe\u63a5 (\u5982\u679c\u94fe\u63a5\u63d2\u4ef6\u5df2\u6fc0\u6d3b)",
"Save (if save plugin activated)": "\u4fdd\u5b58(\u5982\u679c\u4fdd\u5b58\u63d2\u4ef6\u5df2\u6fc0\u6d3b)",
"Find (if searchreplace plugin activated)": "\u67e5\u627e(\u5982\u679c\u67e5\u627e\u66ff\u6362\u63d2\u4ef6\u5df2\u6fc0\u6d3b)",
"Plugins installed ({0}):": "\u5df2\u5b89\u88c5\u63d2\u4ef6 ({0}):",
"Premium plugins:": "\u4f18\u79c0\u63d2\u4ef6\uff1a",
"Learn more...": "\u4e86\u89e3\u66f4\u591a...",
"You are using {0}": "\u4f60\u6b63\u5728\u4f7f\u7528 {0}",
"Plugins": "\u63d2\u4ef6",
"Handy Shortcuts": "\u5feb\u6377\u952e",
"Horizontal line": "\u6c34\u5e73\u5206\u5272\u7ebf",
"Insert\/edit image": "\u63d2\u5165\/\u7f16\u8f91\u56fe\u7247",
"Image description": "\u56fe\u7247\u63cf\u8ff0",
"Source": "\u5730\u5740",
"Dimensions": "\u5927\u5c0f",
"Constrain proportions": "\u4fdd\u6301\u7eb5\u6a2a\u6bd4",
"General": "\u666e\u901a",
"Advanced": "\u9ad8\u7ea7",
"Style": "\u6837\u5f0f",
"Vertical space": "\u5782\u76f4\u8fb9\u8ddd",
"Horizontal space": "\u6c34\u5e73\u8fb9\u8ddd",
"Border": "\u8fb9\u6846",
"Insert image": "\u63d2\u5165\u56fe\u7247",
"Image...": "\u56fe\u7247...",
"Image list": "\u56fe\u7247\u5217\u8868",
"Rotate counterclockwise": "\u9006\u65f6\u9488\u65cb\u8f6c",
"Rotate clockwise": "\u987a\u65f6\u9488\u65cb\u8f6c",
"Flip vertically": "\u5782\u76f4\u7ffb\u8f6c",
"Flip horizontally": "\u6c34\u5e73\u7ffb\u8f6c",
"Edit image": "\u7f16\u8f91\u56fe\u7247",
"Image options": "\u56fe\u7247\u9009\u9879",
"Zoom in": "\u653e\u5927",
"Zoom out": "\u7f29\u5c0f",
"Crop": "\u88c1\u526a",
"Resize": "\u8c03\u6574\u5927\u5c0f",
"Orientation": "\u65b9\u5411",
"Brightness": "\u4eae\u5ea6",
"Sharpen": "\u9510\u5316",
"Contrast": "\u5bf9\u6bd4\u5ea6",
"Color levels": "\u989c\u8272\u5c42\u6b21",
"Gamma": "\u4f3d\u9a6c\u503c",
"Invert": "\u53cd\u8f6c",
"Apply": "\u5e94\u7528",
"Back": "\u540e\u9000",
"Insert date\/time": "\u63d2\u5165\u65e5\u671f\/\u65f6\u95f4",
"Date\/time": "\u65e5\u671f\/\u65f6\u95f4",
"Insert\/Edit Link": "\u63d2\u5165\/\u7f16\u8f91\u94fe\u63a5",
"Insert\/edit link": "\u63d2\u5165\/\u7f16\u8f91\u94fe\u63a5",
"Text to display": "\u663e\u793a\u6587\u5b57",
"Url": "\u5730\u5740",
"Open link in...": "\u94fe\u63a5\u6253\u5f00\u4f4d\u7f6e...",
"Current window": "\u5f53\u524d\u7a97\u53e3",
"None": "\u65e0",
"New window": "\u5728\u65b0\u7a97\u53e3\u6253\u5f00",
"Remove link": "\u5220\u9664\u94fe\u63a5",
"Anchors": "\u951a\u70b9",
"Link...": "\u94fe\u63a5...",
"Paste or type a link": "\u7c98\u8d34\u6216\u8f93\u5165\u94fe\u63a5",
"The URL you entered seems to be an email address. Do you want to add the required mailto: prefix?": "\u4f60\u6240\u586b\u5199\u7684URL\u5730\u5740\u4e3a\u90ae\u4ef6\u5730\u5740\uff0c\u9700\u8981\u52a0\u4e0amailto:\u524d\u7f00\u5417\uff1f",
"The URL you entered seems to be an external link. Do you want to add the required http:\/\/ prefix?": "\u4f60\u6240\u586b\u5199\u7684URL\u5730\u5740\u5c5e\u4e8e\u5916\u90e8\u94fe\u63a5\uff0c\u9700\u8981\u52a0\u4e0ahttp:\/\/:\u524d\u7f00\u5417\uff1f",
"Link list": "\u94fe\u63a5\u5217\u8868",
"Insert video": "\u63d2\u5165\u89c6\u9891",
"Insert\/edit video": "\u63d2\u5165\/\u7f16\u8f91\u89c6\u9891",
"Insert\/edit media": "\u63d2\u5165\/\u7f16\u8f91\u5a92\u4f53",
"Alternative source": "\u955c\u50cf",
"Alternative source URL": "\u66ff\u4ee3\u6765\u6e90\u7f51\u5740",
"Media poster (Image URL)": "\u5c01\u9762(\u56fe\u7247\u5730\u5740)",
"Paste your embed code below:": "\u5c06\u5185\u5d4c\u4ee3\u7801\u7c98\u8d34\u5728\u4e0b\u9762:",
"Embed": "\u5185\u5d4c",
"Media...": "\u591a\u5a92\u4f53...",
"Nonbreaking space": "\u4e0d\u95f4\u65ad\u7a7a\u683c",
"Page break": "\u5206\u9875\u7b26",
"Paste as text": "\u7c98\u8d34\u4e3a\u6587\u672c",
"Preview": "\u9884\u89c8",
"Print...": "\u6253\u5370...",
"Save": "\u4fdd\u5b58",
"Find": "\u67e5\u627e",
"Replace with": "\u66ff\u6362\u4e3a",
"Replace": "\u66ff\u6362",
"Replace all": "\u5168\u90e8\u66ff\u6362",
"Previous": "\u4e0a\u4e00\u4e2a",
"Next": "\u4e0b\u4e00\u4e2a",
"Find and replace...": "\u67e5\u627e\u5e76\u66ff\u6362...",
"Could not find the specified string.": "\u672a\u627e\u5230\u641c\u7d22\u5185\u5bb9.",
"Match case": "\u533a\u5206\u5927\u5c0f\u5199",
"Find whole words only": "\u5168\u5b57\u5339\u914d",
"Spell check": "\u62fc\u5199\u68c0\u67e5",
"Ignore": "\u5ffd\u7565",
"Ignore all": "\u5168\u90e8\u5ffd\u7565",
"Finish": "\u5b8c\u6210",
"Add to Dictionary": "\u6dfb\u52a0\u5230\u5b57\u5178",
"Insert table": "\u63d2\u5165\u8868\u683c",
"Table properties": "\u8868\u683c\u5c5e\u6027",
"Delete table": "\u5220\u9664\u8868\u683c",
"Cell": "\u5355\u5143\u683c",
"Row": "\u884c",
"Column": "\u5217",
"Cell properties": "\u5355\u5143\u683c\u5c5e\u6027",
"Merge cells": "\u5408\u5e76\u5355\u5143\u683c",
"Split cell": "\u62c6\u5206\u5355\u5143\u683c",
"Insert row before": "\u5728\u4e0a\u65b9\u63d2\u5165",
"Insert row after": "\u5728\u4e0b\u65b9\u63d2\u5165",
"Delete row": "\u5220\u9664\u884c",
"Row properties": "\u884c\u5c5e\u6027",
"Cut row": "\u526a\u5207\u884c",
"Copy row": "\u590d\u5236\u884c",
"Paste row before": "\u7c98\u8d34\u5230\u4e0a\u65b9",
"Paste row after": "\u7c98\u8d34\u5230\u4e0b\u65b9",
"Insert column before": "\u5728\u5de6\u4fa7\u63d2\u5165",
"Insert column after": "\u5728\u53f3\u4fa7\u63d2\u5165",
"Delete column": "\u5220\u9664\u5217",
"Cols": "\u5217",
"Rows": "\u884c",
"Width": "\u5bbd",
"Height": "\u9ad8",
"Cell spacing": "\u5355\u5143\u683c\u5916\u95f4\u8ddd",
"Cell padding": "\u5355\u5143\u683c\u5185\u8fb9\u8ddd",
"Show caption": "\u663e\u793a\u6807\u9898",
"Left": "\u5de6\u5bf9\u9f50",
"Center": "\u5c45\u4e2d",
"Right": "\u53f3\u5bf9\u9f50",
"Cell type": "\u5355\u5143\u683c\u7c7b\u578b",
"Scope": "\u8303\u56f4",
"Alignment": "\u5bf9\u9f50\u65b9\u5f0f",
"H Align": "\u6c34\u5e73\u5bf9\u9f50",
"V Align": "\u5782\u76f4\u5bf9\u9f50",
"Top": "\u9876\u90e8\u5bf9\u9f50",
"Middle": "\u5782\u76f4\u5c45\u4e2d",
"Bottom": "\u5e95\u90e8\u5bf9\u9f50",
"Header cell": "\u8868\u5934\u5355\u5143\u683c",
"Row group": "\u884c\u7ec4",
"Column group": "\u5217\u7ec4",
"Row type": "\u884c\u7c7b\u578b",
"Header": "\u8868\u5934",
"Body": "\u8868\u4f53",
"Footer": "\u8868\u5c3e",
"Border color": "\u8fb9\u6846\u989c\u8272",
"Insert template...": "\u63d2\u5165\u6a21\u677f...",
"Templates": "\u6a21\u677f",
"Template": "\u6a21\u677f",
"Text color": "\u6587\u5b57\u989c\u8272",
"Background color": "\u80cc\u666f\u8272",
"Custom...": "\u81ea\u5b9a\u4e49...",
"Custom color": "\u81ea\u5b9a\u4e49\u989c\u8272",
"No color": "\u65e0",
"Remove color": "\u79fb\u9664\u989c\u8272",
"Table of Contents": "\u5185\u5bb9\u5217\u8868",
"Show blocks": "\u663e\u793a\u533a\u5757\u8fb9\u6846",
"Show invisible characters": "\u663e\u793a\u4e0d\u53ef\u89c1\u5b57\u7b26",
"Word count": "\u5b57\u6570",
"Words: {0}": "\u5b57\u6570\uff1a{0}",
"{0} words": "{0} \u5b57",
"File": "\u6587\u4ef6",
"Edit": "\u7f16\u8f91",
"Insert": "\u63d2\u5165",
"View": "\u89c6\u56fe",
"Format": "\u683c\u5f0f",
"Table": "\u8868\u683c",
"Tools": "\u5de5\u5177",
"Powered by {0}": "\u7531{0}\u9a71\u52a8",
"Rich Text Area. Press ALT-F9 for menu. Press ALT-F10 for toolbar. Press ALT-0 for help": "\u5728\u7f16\u8f91\u533a\u6309ALT-F9\u6253\u5f00\u83dc\u5355\uff0c\u6309ALT-F10\u6253\u5f00\u5de5\u5177\u680f\uff0c\u6309ALT-0\u67e5\u770b\u5e2e\u52a9",
"Image title": "\u56fe\u7247\u6807\u9898",
"Border width": "\u8fb9\u6846\u5bbd\u5ea6",
"Border style": "\u8fb9\u6846\u6837\u5f0f",
"Error": "\u9519\u8bef",
"Warn": "\u8b66\u544a",
"Valid": "\u6709\u6548",
"To open the popup, press Shift+Enter": "\u6309Shitf+Enter\u952e\u6253\u5f00\u5bf9\u8bdd\u6846",
"Rich Text Area. Press ALT-0 for help.": "\u7f16\u8f91\u533a\u3002\u6309Alt+0\u952e\u6253\u5f00\u5e2e\u52a9\u3002",
"System Font": "\u7cfb\u7edf\u5b57\u4f53",
"Failed to upload image: {0}": "\u56fe\u7247\u4e0a\u4f20\u5931\u8d25: {0}",
"Failed to load plugin: {0} from url {1}": "\u63d2\u4ef6\u52a0\u8f7d\u5931\u8d25: {0} \u6765\u81ea\u94fe\u63a5 {1}",
"Failed to load plugin url: {0}": "\u63d2\u4ef6\u52a0\u8f7d\u5931\u8d25 \u94fe\u63a5: {0}",
"Failed to initialize plugin: {0}": "\u63d2\u4ef6\u521d\u59cb\u5316\u5931\u8d25: {0}",
"example": "\u793a\u4f8b",
"Search": "\u641c\u7d22",
"All": "\u5168\u90e8",
"Currency": "\u8d27\u5e01",
"Text": "\u6587\u5b57",
"Quotations": "\u5f15\u7528",
"Mathematical": "\u6570\u5b66",
"Extended Latin": "\u62c9\u4e01\u8bed\u6269\u5145",
"Symbols": "\u7b26\u53f7",
"Arrows": "\u7bad\u5934",
"User Defined": "\u81ea\u5b9a\u4e49",
"dollar sign": "\u7f8e\u5143\u7b26\u53f7",
"currency sign": "\u8d27\u5e01\u7b26\u53f7",
"euro-currency sign": "\u6b27\u5143\u7b26\u53f7",
"colon sign": "\u5192\u53f7",
"cruzeiro sign": "\u514b\u9c81\u8d5b\u7f57\u5e01\u7b26\u53f7",
"french franc sign": "\u6cd5\u90ce\u7b26\u53f7",
"lira sign": "\u91cc\u62c9\u7b26\u53f7",
"mill sign": "\u5bc6\u5c14\u7b26\u53f7",
"naira sign": "\u5948\u62c9\u7b26\u53f7",
"peseta sign": "\u6bd4\u585e\u5854\u7b26\u53f7",
"rupee sign": "\u5362\u6bd4\u7b26\u53f7",
"won sign": "\u97e9\u5143\u7b26\u53f7",
"new sheqel sign": "\u65b0\u8c22\u514b\u5c14\u7b26\u53f7",
"dong sign": "\u8d8a\u5357\u76fe\u7b26\u53f7",
"kip sign": "\u8001\u631d\u57fa\u666e\u7b26\u53f7",
"tugrik sign": "\u56fe\u683c\u91cc\u514b\u7b26\u53f7",
"drachma sign": "\u5fb7\u62c9\u514b\u9a6c\u7b26\u53f7",
"german penny symbol": "\u5fb7\u56fd\u4fbf\u58eb\u7b26\u53f7",
"peso sign": "\u6bd4\u7d22\u7b26\u53f7",
"guarani sign": "\u74dc\u62c9\u5c3c\u7b26\u53f7",
"austral sign": "\u6fb3\u5143\u7b26\u53f7",
"hryvnia sign": "\u683c\u91cc\u592b\u5c3c\u4e9a\u7b26\u53f7",
"cedi sign": "\u585e\u5730\u7b26\u53f7",
"livre tournois sign": "\u91cc\u5f17\u5f17\u5c14\u7b26\u53f7",
"spesmilo sign": "spesmilo\u7b26\u53f7",
"tenge sign": "\u575a\u6208\u7b26\u53f7",
"indian rupee sign": "\u5370\u5ea6\u5362\u6bd4",
"turkish lira sign": "\u571f\u8033\u5176\u91cc\u62c9",
"nordic mark sign": "\u5317\u6b27\u9a6c\u514b",
"manat sign": "\u9a6c\u7eb3\u7279\u7b26\u53f7",
"ruble sign": "\u5362\u5e03\u7b26\u53f7",
"yen character": "\u65e5\u5143\u5b57\u6837",
"yuan character": "\u4eba\u6c11\u5e01\u5143\u5b57\u6837",
"yuan character, in hong kong and taiwan": "\u5143\u5b57\u6837\uff08\u6e2f\u53f0\u5730\u533a\uff09",
"yen\/yuan character variant one": "\u5143\u5b57\u6837\uff08\u5927\u5199\uff09",
"Loading emoticons...": "\u52a0\u8f7d\u8868\u60c5\u7b26\u53f7...",
"Could not load emoticons": "\u4e0d\u80fd\u52a0\u8f7d\u8868\u60c5\u7b26\u53f7",
"People": "\u4eba\u7c7b",
"Animals and Nature": "\u52a8\u7269\u548c\u81ea\u7136",
"Food and Drink": "\u98df\u7269\u548c\u996e\u54c1",
"Activity": "\u6d3b\u52a8",
"Travel and Places": "\u65c5\u6e38\u548c\u5730\u70b9",
"Objects": "\u7269\u4ef6",
"Flags": "\u65d7\u5e1c",
"Characters": "\u5b57\u7b26",
"Characters (no spaces)": "\u5b57\u7b26(\u65e0\u7a7a\u683c)",
"Error: Form submit field collision.": "\u9519\u8bef: \u8868\u5355\u63d0\u4ea4\u5b57\u6bb5\u51b2\u7a81\u3002",
"Error: No form element found.": "\u9519\u8bef: \u6ca1\u6709\u8868\u5355\u63a7\u4ef6\u3002",
"Update": "\u66f4\u65b0",
"Color swatch": "\u989c\u8272\u6837\u672c",
"Turquoise": "\u9752\u7eff\u8272",
"Green": "\u7eff\u8272",
"Blue": "\u84dd\u8272",
"Purple": "\u7d2b\u8272",
"Navy Blue": "\u6d77\u519b\u84dd",
"Dark Turquoise": "\u6df1\u84dd\u7eff\u8272",
"Dark Green": "\u6df1\u7eff\u8272",
"Medium Blue": "\u4e2d\u84dd\u8272",
"Medium Purple": "\u4e2d\u7d2b\u8272",
"Midnight Blue": "\u6df1\u84dd\u8272",
"Yellow": "\u9ec4\u8272",
"Orange": "\u6a59\u8272",
"Red": "\u7ea2\u8272",
"Light Gray": "\u6d45\u7070\u8272",
"Gray": "\u7070\u8272",
"Dark Yellow": "\u6697\u9ec4\u8272",
"Dark Orange": "\u6df1\u6a59\u8272",
"Dark Red": "\u6df1\u7ea2\u8272",
"Medium Gray": "\u4e2d\u7070\u8272",
"Dark Gray": "\u6df1\u7070\u8272",
"Black": "\u9ed1\u8272",
"White": "\u767d\u8272",
"Switch to or from fullscreen mode": "\u5207\u6362\u5168\u5c4f\u6a21\u5f0f",
"Open help dialog": "\u6253\u5f00\u5e2e\u52a9\u5bf9\u8bdd\u6846",
"history": "\u5386\u53f2",
"styles": "\u6837\u5f0f",
"formatting": "\u683c\u5f0f\u5316",
"alignment": "\u5bf9\u9f50",
"indentation": "\u7f29\u8fdb",
"permanent pen": "\u8bb0\u53f7\u7b14",
"comments": "\u5907\u6ce8",
"Anchor": "\u951a\u70b9",
"Special character": "\u7279\u6b8a\u7b26\u53f7",
"Code sample": "\u4ee3\u7801\u793a\u4f8b",
"Color": "\u989c\u8272",
"Emoticons": "\u8868\u60c5",
"Document properties": "\u6587\u6863\u5c5e\u6027",
"Image": "\u56fe\u7247",
"Insert link": "\u63d2\u5165\u94fe\u63a5",
"Target": "\u6253\u5f00\u65b9\u5f0f",
"Link": "\u94fe\u63a5",
"Poster": "\u5c01\u9762",
"Media": "\u5a92\u4f53",
"Print": "\u6253\u5370",
"Prev": "\u4e0a\u4e00\u4e2a",
"Find and replace": "\u67e5\u627e\u548c\u66ff\u6362",
"Whole words": "\u5168\u5b57\u5339\u914d",
"Spellcheck": "\u62fc\u5199\u68c0\u67e5",
"Caption": "\u6807\u9898",
"Insert template": "\u63d2\u5165\u6a21\u677f"
});

@ -1,52 +0,0 @@
<template>
<!-- 根元素应用的主容器 -->
<!-- 根元素应用的主容器 -->
<!-- 根元素应用的主容器 -->
<!-- 根元素应用的主容器 -->
<div id="app">
<!-- 使用fade过渡效果来切换路由视图 -->
<transition name="fade">
<router-view /> <!-- 路由出口用于渲染匹配的组件 -->
</transition>
</div>
</template>
<style>
/* 样式定义 */
/* 定义小图片的样式 */
img.image-sm {
max-width: 80px; /* 最大宽度 */
max-height: 80px; /* 最大高度 */
}
/* 为el-col内的el-select和el-date-editor设置宽度 */
.el-col .el-select,
.el-col .el-date-editor {
width: 100%; /* 宽度设置为父容器的100% */
}
/* 设置表格展开行的样式 */
.demo-table-expand {
font-size: 0; /* 将字体大小设为0常用于清除子元素的默认间隙 */
}
/* 设置表格展开行内label的样式 */
.demo-table-expand label {
width: 90px; /* 宽度 */
color: #99a9bf; /* 文字颜色 */
}
/* 设置el-form-item在表格展开行中的样式 */
.demo-table-expand .el-form-item {
margin-right: 0; /* 右侧外边距 */
margin-bottom: 0; /* 底部外边距 */
width: 50%; /* 宽度设置为父容器的50% */
}
/* 定义警告文本的样式 */
.text-warning {
color: #e6a23c; /* 文字颜色 */
}
</style>

@ -1,70 +0,0 @@
// 导入Vue框架
import Vue from 'vue';
// 导入根组件
import App from './App.vue';
// 导入Vue Router实例用于页面路由管理
import router from './router';
// 导入Vuex状态管理实例
import store from './store';
// 导入vue-cookie插件用于处理cookie
import VueCookie from 'vue-cookie';
// 导入Element UI组件库
import ElementUI from 'element-ui';
// 导入moment.js库用于日期和时间处理
import moment from 'moment';
// 导入Element UI的CSS样式
import 'element-ui/lib/theme-chalk/index.css';
// 导入全局CSS样式
import './assets/css/common.css';
// 导入全局SCSS样式
import './assets/scss/index.scss';
// 导入自定义的httpRequest工具基于axios的封装注意这里注释的api链接可能不准确因为httpRequest的具体实现未在代码中给出
import httpRequest from '@/utils/httpRequest';
// 导入自定义的权限验证工具
import { isAuth } from '@/utils';
// 导入vue-clipboard2插件用于复制文本到剪贴板
import VueClipboard from 'vue-clipboard2';
// 使用Element UI组件库
Vue.use(ElementUI);
// 使用vue-clipboard2插件
Vue.use(VueClipboard);
// 使用vue-cookie插件
Vue.use(VueCookie);
// 禁止Vue在启动时生成生产提示
Vue.config.productionTip = false;
// 挂载全局属性和方法
// 全局ajax请求方法
Vue.prototype.$http = httpRequest;
// 全局权限验证方法
Vue.prototype.isAuth = isAuth;
// 设置moment.js的语言环境为中文
moment.locale('zh-cn');
// 将moment挂载到Vue原型上作为全局的时间处理方法
Vue.prototype.$moment = moment;
// 创建Vue实例并挂载到#app元素上
new Vue({
router, // 注入路由使得我们可以通过this.$router访问路由实例
store, // 注入store使得我们可以通过this.$store访问状态管理实例
render: h => h(App) // 渲染App组件
}).$mount('#app'); // 挂载到DOM上的#app元素

@ -1,39 +0,0 @@
<script>
// SubMenu
import SubMenu from './main-sidebar-sub-menu'
export default {
name: 'sub-menu', //
props: {
menu: {
type: Object, //
required: true //
},
dynamicMenuRoutes: {
type: Array, //
required: true //
}
},
components: {
SubMenu // SubMenu
},
computed: {
sidebarLayoutSkin() {
// Vuex storesidebarLayoutSkin
return this.$store.state.common.sidebarLayoutSkin
}
},
methods: {
// menuId
gotoRouteHandle(menu) {
// dynamicMenuRoutesmenuIdmenumenuId
var route = this.dynamicMenuRoutes.filter(item => item.meta.menuId === menu.menuId)
// 使$router.push
if (route.length >= 1) {
this.$router.push({ name: route[0].name })
}
}
}
}
</script>

@ -1,168 +0,0 @@
<template>
<div class="mod-role">
<!-- 搜索和管理角色的表单 -->
<el-form :inline="true" :model="dataForm" @keyup.enter.native="getDataList()">
<el-form-item>
<!-- 角色名称输入框 -->
<el-input v-model="dataForm.roleName" placeholder="角色名称" clearable></el-input>
</el-form-item>
<el-form-item>
<!-- 查询按钮 -->
<el-button @click="getDataList()"></el-button>
<!-- 新增按钮只有在有权限时才显示 -->
<el-button v-if="isAuth('sys:role:save')" type="primary" @click="addOrUpdateHandle()"></el-button>
<!-- 批量删除按钮只有在有权限时才显示且未选择则禁用 -->
<el-button v-if="isAuth('sys:role:delete')" type="danger" @click="deleteHandle()" :disabled="dataListSelections.length <= 0"></el-button>
</el-form-item>
</el-form>
<!-- 显示角色的表格 -->
<el-table :data="dataList" border v-loading="dataListLoading" @selection-change="selectionChangeHandle" style="width: 100%;">
<!-- 选择列用于多选 -->
<el-table-column type="selection" header-align="center" align="center" width="50"></el-table-column>
<!-- ID列 -->
<el-table-column prop="roleId" header-align="center" align="center" width="80" label="ID"></el-table-column>
<!-- 角色名称列 -->
<el-table-column prop="roleName" header-align="center" align="center" label="角色名称"></el-table-column>
<!-- 备注列 -->
<el-table-column prop="remark" header-align="center" align="center" label="备注"></el-table-column>
<!-- 操作列 -->
<el-table-column fixed="right" header-align="center" align="center" width="150" label="操作">
<template slot-scope="scope">
<!-- 修改按钮只有在有权限时才显示 -->
<el-button v-if="isAuth('sys:role:update')" type="text" size="small" @click="addOrUpdateHandle(scope.row.roleId)"></el-button>
<!-- 删除按钮只有在有权限时才显示 -->
<el-button v-if="isAuth('sys:role:delete')" type="text" size="small" @click="deleteHandle(scope.row.roleId)"></el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页组件 -->
<el-pagination
@size-change="sizeChangeHandle"
@current-change="currentChangeHandle"
:current-page="pageIndex"
:page-sizes="[10, 20, 50, 100]"
:page-size="pageSize"
:total="totalCount"
layout="total, sizes, prev, pager, next, jumper">
</el-pagination>
<!-- 弹窗, 新增 / 修改 -->
<add-or-update v-if="addOrUpdateVisible" ref="addOrUpdate" @refreshDataList="getDataList"></add-or-update>
</div>
</template>
<script>
import AddOrUpdate from './role-add-or-update' // /
export default {
data() {
return {
//
dataForm: {
roleName: ''
},
//
dataList: [],
//
pageIndex: 1,
//
pageSize: 10,
//
totalCount: 0,
//
dataListLoading: false,
//
dataListSelections: [],
//
addOrUpdateVisible: false
}
},
components: {
AddOrUpdate // /
},
activated() {
//
this.getDataList()
},
methods: {
//
getDataList() {
this.dataListLoading = true //
this.$http({
url: this.$http.adornUrl('/sys/role/list'), // API
method: 'get', // HTTP
params: this.$http.adornParams({
'page': this.pageIndex, //
'limit': this.pageSize, //
'roleName': this.dataForm.roleName //
})
}).then(({ data }) => {
if (data && data.code === 200) {
//
this.dataList = data.page.list //
this.totalCount = data.page.totalCount //
} else {
//
this.dataList = []
this.totalCount = 0
}
this.dataListLoading = false //
})
},
//
sizeChangeHandle(val) {
this.pageSize = val //
this.pageIndex = 1 //
this.getDataList() //
},
//
currentChangeHandle(val) {
this.pageIndex = val //
this.getDataList() //
},
//
selectionChangeHandle(val) {
this.dataListSelections = val //
},
// /
addOrUpdateHandle(id) {
this.addOrUpdateVisible = true //
this.$nextTick(() => {
this.$refs.addOrUpdate.init(id) // ID
})
},
//
deleteHandle(id) {
// ID
var ids = id ? [id] : this.dataListSelections.map(item => item.roleId)
this.$confirm(`确定对[id=${ids.join(',')}]进行[${id ? '删除' : '批量删除'}]操作?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
//
this.$http({
url: this.$http.adornUrl('/sys/role/delete'), // API
method: 'post', // HTTP
data: this.$http.adornData(ids, false) // ID
}).then(({ data }) => {
if (data && data.code === 200) {
//
this.$message({
message: '操作成功',
type: 'success',
duration: 1500,
onClose: () => this.getDataList() //
})
} else {
//
this.$message.error(data.msg) //
}
})
}).catch(() => { }) //
}
}
}
</script>

@ -1,202 +0,0 @@
<template>
<el-dialog :title="!dataForm.id ? '新增' : '修改'" :close-on-click-modal="false" :visible.sync="visible">
<!-- 表单使用dataForm作为模型进行用户信息的新增或修改 -->
<el-form :model="dataForm" :rules="dataRule" ref="dataForm" @keyup.enter.native="dataFormSubmit()" label-width="80px">
<!-- 用户名输入框 -->
<el-form-item label="用户名" prop="userName">
<el-input v-model="dataForm.userName" placeholder="登录帐号"></el-input>
</el-form-item>
<!-- 密码输入框若为新增则必填 -->
<el-form-item label="密码" prop="password" :class="{ 'is-required': !dataForm.id }">
<el-input v-model="dataForm.password" type="password" placeholder="密码"></el-input>
</el-form-item>
<!-- 确认密码输入框若为新增则必填 -->
<el-form-item label="确认密码" prop="comfirmPassword" :class="{ 'is-required': !dataForm.id }">
<el-input v-model="dataForm.comfirmPassword" type="password" placeholder="确认密码"></el-input>
</el-form-item>
<!-- 邮箱输入框 -->
<el-form-item label="邮箱" prop="email">
<el-input v-model="dataForm.email" placeholder="邮箱"></el-input>
</el-form-item>
<!-- 手机号输入框 -->
<el-form-item label="手机号" prop="mobile">
<el-input v-model="dataForm.mobile" placeholder="手机号"></el-input>
</el-form-item>
<!-- 角色选择 -->
<el-form-item label="角色" size="mini" prop="roleIdList">
<el-checkbox-group v-model="dataForm.roleIdList">
<el-checkbox v-for="role in roleList" :key="role.roleId" :label="role.roleId">{{ role.roleName }}</el-checkbox>
</el-checkbox-group>
</el-form-item>
<!-- 状态选择 -->
<el-form-item label="状态" size="mini" prop="status">
<el-radio-group v-model="dataForm.status">
<el-radio :label="0">禁用</el-radio>
<el-radio :label="1">正常</el-radio>
</el-radio-group>
</el-form-item>
</el-form>
<!-- 对话框底部按钮 -->
<span slot="footer" class="dialog-footer">
<el-button @click="visible = false">取消</el-button>
<el-button type="primary" @click="dataFormSubmit()"></el-button>
</span>
</el-dialog>
</template>
<script>
import { isEmail, isMobile } from '@/utils/validate' //
export default {
data() {
//
var validatePassword = (rule, value, callback) => {
if (!this.dataForm.id && !/\S/.test(value)) { //
callback(new Error('密码不能为空'))
} else {
callback() //
}
}
var validateComfirmPassword = (rule, value, callback) => {
if (!this.dataForm.id && !/\S/.test(value)) { //
callback(new Error('确认密码不能为空'))
} else if (this.dataForm.password !== value) { //
callback(new Error('确认密码与密码输入不一致'))
} else {
callback() //
}
}
//
var validateEmail = (rule, value, callback) => {
if (!isEmail(value)) {
callback(new Error('邮箱格式错误'))
} else {
callback() //
}
}
//
var validateMobile = (rule, value, callback) => {
if (!isMobile(value)) {
callback(new Error('手机号格式错误'))
} else {
callback() //
}
}
return {
visible: false, //
roleList: [], //
//
dataForm: {
id: 0, // ID
userName: '', //
password: '', //
comfirmPassword: '', //
salt: '', //
email: '', //
mobile: '', //
roleIdList: [], // ID
status: 1 // 1: , 0:
},
//
dataRule: {
userName: [
{ required: true, message: '用户名不能为空', trigger: 'blur' }
],
password: [
{ validator: validatePassword, trigger: 'blur' }
],
comfirmPassword: [
{ validator: validateComfirmPassword, trigger: 'blur' }
],
email: [
{ required: true, message: '邮箱不能为空', trigger: 'blur' },
{ validator: validateEmail, trigger: 'blur' }
],
mobile: [
{ required: true, message: '手机号不能为空', trigger: 'blur' },
{ validator: validateMobile, trigger: 'blur' }
]
}
}
},
methods: {
//
init(id) {
this.dataForm.id = id || 0 // ID
//
this.$http({
url: this.$http.adornUrl('/sys/role/select'),
method: 'get',
params: this.$http.adornParams() //
}).then(({ data }) => {
this.roleList = data && data.code === 200 ? data.list : [] //
}).then(() => {
this.visible = true //
this.$nextTick(() => {
this.$refs['dataForm'].resetFields() //
})
}).then(() => {
//
if (this.dataForm.id) {
this.$http({
url: this.$http.adornUrl(`/sys/user/info/${this.dataForm.id}`),
method: 'get',
params: this.$http.adornParams()
}).then(({ data }) => {
if (data && data.code === 200) {
//
this.dataForm.userName = data.user.username //
this.dataForm.salt = data.user.salt // 使
this.dataForm.email = data.user.email //
this.dataForm.mobile = data.user.mobile //
this.dataForm.roleIdList = data.user.roleIdList // ID
this.dataForm.status = data.user.status //
}
})
}
})
},
//
dataFormSubmit() {
//
this.$refs['dataForm'].validate((valid) => {
if (valid) { //
this.$http({
// ID
url: this.$http.adornUrl(`/sys/user/${!this.dataForm.id ? 'save' : 'update'}`),
method: 'post',
data: this.$http.adornData({
//
'userId': this.dataForm.id || undefined,
'username': this.dataForm.userName,
'password': this.dataForm.password,
'salt': this.dataForm.salt,
'email': this.dataForm.email,
'mobile': this.dataForm.mobile,
'status': this.dataForm.status,
'roleIdList': this.dataForm.roleIdList
})
}).then(({ data }) => {
//
if (data && data.code === 200) {
//
this.$message({
message: '操作成功',
type: 'success',
duration: 1500,
onClose: () => {
this.visible = false //
this.$emit('refreshDataList') //
}
})
} else {
//
this.$message.error(data.msg) //
}
})
}
})
}
}
}
</script>

@ -1,168 +0,0 @@
<template>
<div class="mod-user">
<!-- 搜索表单 -->
<el-form :inline="true" :model="dataForm" @keyup.enter.native="getDataList()">
<el-form-item>
<!-- 用户名输入框 -->
<el-input v-model="dataForm.userName" placeholder="用户名" clearable></el-input>
</el-form-item>
<el-form-item>
<!-- 查询按钮 -->
<el-button @click="getDataList()"></el-button>
<!-- 新增按钮只有在有权限时显示 -->
<el-button v-if="isAuth('sys:user:save')" type="primary" @click="addOrUpdateHandle()"></el-button>
<!-- 批量删除按钮只有在有权限时显示并且在没有选择项时禁用 -->
<el-button v-if="isAuth('sys:user:delete')" type="danger" @click="deleteHandle()" :disabled="dataListSelections.length <= 0"></el-button>
</el-form-item>
</el-form>
<!-- 用户数据表格 -->
<el-table :data="dataList" border v-loading="dataListLoading" @selection-change="selectionChangeHandle" style="width: 100%;">
<!-- 选择框列 -->
<el-table-column type="selection" header-align="center" align="center" width="50">
</el-table-column>
<!-- ID列 -->
<el-table-column prop="userId" header-align="center" align="center" width="80" label="ID">
</el-table-column>
<!-- 用户名列 -->
<el-table-column prop="username" header-align="center" align="center" label="用户名">
</el-table-column>
<!-- 邮箱列 -->
<el-table-column prop="email" header-align="center" align="center" label="邮箱">
</el-table-column>
<!-- 手机号列 -->
<el-table-column prop="mobile" header-align="center" align="center" label="手机号">
</el-table-column>
<!-- 状态列 -->
<el-table-column prop="status" header-align="center" align="center" label="状态">
<template slot-scope="scope">
<el-tag v-if="scope.row.status === 0" size="small" type="danger"></el-tag>
<el-tag v-else size="small">正常</el-tag>
</template>
</el-table-column>
<!-- 操作列 -->
<el-table-column fixed="right" header-align="center" align="center" width="150" label="操作">
<template slot-scope="scope">
<!-- 修改按钮只有在有权限时显示 -->
<el-button v-if="isAuth('sys:user:update')" type="text" size="small" @click="addOrUpdateHandle(scope.row.userId)"></el-button>
<!-- 删除按钮只有在有权限时显示 -->
<el-button v-if="isAuth('sys:user:delete')" type="text" size="small" @click="deleteHandle(scope.row.userId)"></el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页组件 -->
<el-pagination @size-change="sizeChangeHandle" @current-change="currentChangeHandle" :current-page="pageIndex" :page-sizes="[10, 20, 50, 100]" :page-size="pageSize" :total="totalCount" layout="total, sizes, prev, pager, next, jumper">
</el-pagination>
<!-- 弹窗用于新增或修改用户 -->
<add-or-update v-if="addOrUpdateVisible" ref="addOrUpdate" @refreshDataList="getDataList"></add-or-update>
</div>
</template>
<script>
import AddOrUpdate from './user-add-or-update' // /
export default {
data() {
return {
dataForm: {
userName: '' //
},
dataList: [], //
pageIndex: 1, //
pageSize: 10, //
totalCount: 0, //
dataListLoading: false, //
dataListSelections: [], //
addOrUpdateVisible: false // /
}
},
components: {
AddOrUpdate // /
},
activated() {
this.getDataList() //
},
methods: {
//
getDataList() {
this.dataListLoading = true // true
this.$http({
url: this.$http.adornUrl('/sys/user/list'), // API
method: 'get',
params: this.$http.adornParams({
'page': this.pageIndex, //
'limit': this.pageSize, //
'username': this.dataForm.userName //
})
}).then(({ data }) => {
if (data && data.code === 200) {
//
this.dataList = data.page.list
this.totalCount = data.page.totalCount
} else {
//
this.dataList = []
this.totalCount = 0
}
this.dataListLoading = false // false
})
},
//
sizeChangeHandle(val) {
this.pageSize = val //
this.pageIndex = 1 //
this.getDataList() //
},
//
currentChangeHandle(val) {
this.pageIndex = val //
this.getDataList() //
},
//
selectionChangeHandle(val) {
this.dataListSelections = val //
},
//
addOrUpdateHandle(id) {
this.addOrUpdateVisible = true // /
this.$nextTick(() => {
this.$refs.addOrUpdate.init(id) // ID
})
},
//
deleteHandle(id) {
// ID
var userIds = id ? [id] : this.dataListSelections.map(item => item.userId)
this.$confirm(`确定对[id=${userIds.join(',')}]进行[${id ? '删除' : '批量删除'}]操作?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
//
this.$http({
url: this.$http.adornUrl('/sys/user/delete'), // API
method: 'post',
data: this.$http.adornData(userIds, false) // ID
}).then(({ data }) => {
if (data && data.code === 200) {
//
this.$message({
message: '操作成功',
type: 'success',
duration: 1500,
onClose: () => this.getDataList() //
})
} else {
//
this.$message.error(data.msg)
}
})
}).catch(() => {
//
})
}
}
}
</script>

@ -1,63 +0,0 @@
<template>
<!-- 开发接入信息对话框 -->
<el-dialog title="开发接入信息" :close-on-click-modal="false" :visible.sync="visible">
<div>
<!-- 公众号信息 -->
<div class="list-item"><span class="label">公众号:</span>{{account.name}}</div>
<!-- token 信息 -->
<div class="list-item"><span class="label">token:</span>{{account.token}}</div>
<!-- aesKey 信息 -->
<div class="list-item"><span class="label">aesKey:</span>{{account.aesKey}}</div>
<!-- 接入链接 -->
<div class="list-item">
<span class="label">接入链接:</span>
<span v-html="accessUrl"></span> <!-- 使 v-html -->
</div>
</div>
</el-dialog>
</template>
<script>
export default {
data() {
return {
visible: false, //
account: {}, //
}
},
computed: {
//
accessUrl() {
let host = location.host; //
// IP localhost
if(/^(\d(.\d){3})|localhost/.test(host)){
host='<span class="text-red">正式域名</span>' // IP localhost
}
//
return location.protocol + '//' + host + '/wx/wx/msg/' + this.account.appid
}
},
methods: {
//
init(item) {
this.visible = true //
if (item && item.appid) {
this.account = item // appid account
}
},
}
}
</script>
<style scoped>
.list-item{
line-height: 30px; /* 每一项的行高 */
}
.label{
width: 100px; /* 标签的宽度 */
display: inline-block; /* 使标签在每行中内联显示 */
margin-right: 10px; /* 标签与内容之间的右边距 */
font-weight: bold; /* 标签加粗 */
text-align: right; /* 标签文本右对齐 */
}
</style>

@ -1,145 +0,0 @@
<template>
<!-- 新增/修改对话框 -->
<el-dialog
title="新增/修改"
:close-on-click-modal="false"
:visible.sync="visible">
<!-- 表单区域 -->
<el-form :model="dataForm" :rules="dataRule" ref="dataForm" @keyup.enter.native="dataFormSubmit()" label-width="100px">
<!-- 公众号名称输入 -->
<el-form-item label="公众号名称" prop="name">
<el-input v-model="dataForm.name" placeholder="公众号名称"></el-input>
</el-form-item>
<!-- 提示信息 -->
<div class="padding text-gray">测试号可选择服务号不同类型账号是否认证可使用功能权限不同<a href="https://developers.weixin.qq.com/doc/offiaccount/Getting_Started/Explanation_of_interface_privileges.html">参考文档</a></div>
<el-row>
<el-col :span="12">
<!-- 公众号类型选择 -->
<el-form-item label="公众号类型" prop="type">
<el-select v-model="dataForm.type" placeholder="公众号类型">
<el-option v-for="(name,key) in ACCOUNT_TYPES" :key="name" :label="name" :value="key"></el-option>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<!-- 是否认证开关 -->
<el-form-item label="是否认证" prop="verified">
<el-switch v-model="dataForm.verified" placeholder="是否认证"></el-switch>
</el-form-item>
</el-col>
</el-row>
<!-- appid 输入 -->
<el-form-item label="appid" prop="appid">
<el-input v-model="dataForm.appid" placeholder="appid"></el-input>
</el-form-item>
<!-- appsecret 输入 -->
<el-form-item label="appsecret" prop="secret">
<el-input v-model="dataForm.secret" placeholder="appsecret"></el-input>
</el-form-item>
<!-- token 输入 -->
<el-form-item label="token" prop="token">
<el-input v-model="dataForm.token" placeholder="token"></el-input>
</el-form-item>
<!-- aesKey 输入 -->
<el-form-item label="aesKey" prop="aesKey">
<el-input v-model="dataForm.aesKey" placeholder="aesKey可为空"></el-input>
</el-form-item>
</el-form>
<!-- 对话框底部按钮 -->
<span slot="footer" class="dialog-footer">
<el-button @click="visible = false">取消</el-button>
<el-button type="primary" @click="dataFormSubmit()"></el-button>
</span>
</el-dialog>
</template>
<script>
import { mapState } from 'vuex' // Vuex mapState
export default {
data () {
return {
visible: false, //
dataForm: { //
appid: '',
name: '',
type:'2', //
verified: true, // true
secret: '',
token: 'my_weixin_token_', // token
aesKey: '' // aesKey
},
dataRule: { //
name: [
{ required: true, message: '公众号名称不能为空', trigger: 'blur' } //
],
appid: [
{ required: true, message: 'appid不能为空', trigger: 'blur' } // appid
],
secret: [
{ required: true, message: 'appsecret不能为空', trigger: 'blur' } // appsecret
]
}
}
},
computed: mapState({
ACCOUNT_TYPES: state => state.wxAccount.ACCOUNT_TYPES // store
}),
methods: {
//
init (item) {
this.visible = true //
if(item && item.appid) {
this.dataForm = item //
this.dataForm.type = item.type + '' //
} else {
//
this.$nextTick(() => {
this.$refs['dataForm'].resetFields()
})
}
},
//
dataFormSubmit () {
//
this.$refs['dataForm'].validate((valid) => {
if (valid) {
// POST
this.$http({
url: this.$http.adornUrl(`/manage/wxAccount/save`), // URL
method: 'post',
data: this.$http.adornData(this.dataForm) // 使 adornData
}).then(({data}) => {
if (data && data.code === 200) {
//
this.$message({
message: '操作成功',
type: 'success',
duration: 1500,
onClose: () => {
this.visible = false //
this.$emit('refreshDataList') //
}
})
} else {
//
this.$message.error(data.msg)
}
})
}
})
}
}
}
</script>

@ -1,44 +0,0 @@
<template>
<!-- 选择素材的对话框 -->
<el-dialog title="选择素材" :visible.sync="dataVisible" :modal="true" append-to-body @close="onClose">
<!-- 根据选择的素材类型加载不同的子组件 -->
<material-news v-if="selectType === 'news'" @selected="onSelect" selectMode></material-news>
<material-file v-else :fileType="selectType" @selected="onSelect" selectMode></material-file>
</el-dialog>
</template>
<script>
export default {
name: "assets-selector", //
data: function () {
return {
dataVisible: this.visible // dataVisible visible
}
},
components: {
//
MaterialFile: () => import('./material-file'), // MaterialFile
MaterialNews: () => import('./material-news') // MaterialNews
},
props: {
selectType: { // imagevoicevideonews
type: String,
default: 'image' // 'image'
},
visible: { //
type: Boolean,
default: false //
}
},
methods: {
//
onSelect(itemInfo) {
this.$emit('selected', itemInfo) //
},
//
onClose() {
this.$emit('onClose') //
}
}
}
</script>

@ -1,118 +0,0 @@
<template>
<!-- 对话框标题根据是否有 id 决定是新增还是修改 -->
<el-dialog :title="!dataForm.id ? '新增' : '修改'" :close-on-click-modal="false" :visible.sync="visible">
<!-- 表单区域 -->
<el-form :model="dataForm" :rules="dataRule" ref="dataForm" @keyup.enter.native="dataFormSubmit()" label-width="80px">
<!-- 媒体文件选择 -->
<el-form-item label="媒体文件">
<el-button type="primary">
选择文件
<!-- 隐藏的文件输入框选择文件后触发 onFileChange 方法 -->
<input type="file" style="opacity: 0; height: 100%; position: absolute; left: 0; top: 0;" @change="onFileChange" />
</el-button>
<!-- 显示已选择的文件名称 -->
<div>{{dataForm.file.name}}</div>
</el-form-item>
<!-- 媒体类型选择 -->
<el-form-item label="媒体类型" prop="mediaType">
<el-select v-model="dataForm.mediaType" placeholder="媒体类型" style="width:100%">
<el-option label="图片2M以内支持PNG\JPEG\JPG\GIF" value="image"></el-option>
<el-option label="视频10M以内只支持MP4" value="video"></el-option>
<el-option label="语音2M、60s以内支持AMR\MP3" value="voice"></el-option>
<el-option label="缩略图64K以内JPG" value="thumb"></el-option>
</el-select>
</el-form-item>
<!-- 素材名称输入 -->
<el-form-item label="素材名称" prop="fileName">
<el-input v-model="dataForm.fileName" placeholder="为便于管理建议按用途分类+素材内容命名"></el-input>
</el-form-item>
</el-form>
<!-- 对话框底部按钮 -->
<span slot="footer" class="dialog-footer">
<el-button @click="visible = false">取消</el-button>
<el-button type="primary" @click="dataFormSubmit()" :disabled="uploading">{{uploading ? '提交中...' : '提交'}}</el-button>
</span>
</el-dialog>
</template>
<script>
export default {
data() {
return {
visible: false, //
uploading: false, //
dataForm: { //
mediaId: '', // ID
file: '', //
fileName: '', //
mediaType: 'image' // 'image'
},
dataRule: { //
fileName: [
{ required: true, message: '素材名称不能为空', trigger: 'blur' } //
],
mediaType: [
{ required: true, message: '素材类型不能为空', trigger: 'blur' } //
]
}
}
},
methods: {
//
init(fileType) {
if (fileType) this.dataForm.mediaType = fileType //
this.visible = true //
},
//
dataFormSubmit() {
if (this.uploading) return //
//
this.$refs['dataForm'].validate((valid) => {
if (valid) {
this.uploading = true //
console.log(this.dataForm) //
let form = new FormData(); // FormData
// FormData
form.append('mediaId', this.dataForm.mediaId || '')
form.append('file', this.dataForm.file)
form.append('fileName', this.dataForm.fileName)
form.append('mediaType', this.dataForm.mediaType)
// POST
this.$http({
url: this.$http.adornUrl(`/manage/wxAssets/materialFileUpload`), // URL
method: 'post',
data: form,
headers: { 'Content-Type': 'multipart/form-data' } // multipart/form-data
}).then(({ data }) => {
if (data && data.code === 200) {
//
this.$message({
message: '操作成功',
type: 'success',
duration: 1500,
onClose: () => {
this.visible = false //
this.$emit('refreshDataList') //
}
})
} else {
//
this.$message.error(data.msg)
}
this.uploading = false //
})
}
})
},
//
onFileChange(e) {
let file = e.currentTarget.files[0] //
this.dataForm.file = file; //
this.dataForm.fileName = file.name.substring(0, file.name.lastIndexOf('.')) //
let mediaType = file.type.substring(0, file.type.lastIndexOf('/')) //
if (mediaType === 'audio') mediaType = 'voice' // 'voice'
this.dataForm.mediaType = mediaType //
}
}
}
</script>

@ -1,207 +0,0 @@
<template>
<div class="mod-menu">
<!-- 表单用于新增按钮 -->
<el-form :inline="true" :model="dataForm">
<el-form-item v-show="!selectMode">
<!-- 新增按钮只有在有权限时才显示 -->
<el-button size="mini" v-if="isAuth('wx:wxassets:save')" type="primary" @click="addOrUpdateHandle()"></el-button>
</el-form-item>
</el-form>
<!-- 数据列表加载状态 -->
<div v-loading="dataListLoading">
<!-- 遍历数据列表生成每个素材的卡片 -->
<div class="card" v-for="item in dataList" :key="item.mediaId" @click="onSelect(item)">
<!-- 如果是图片类型显示图片 -->
<el-image v-if="fileType === 'image'" class="card-image" :src="item.url" fit="contain" lazy></el-image>
<div v-else class="card-preview">
<!-- 根据不同的媒体类型显示不同的图标和提示 -->
<div v-if="fileType === 'voice'" class="card-preview-icon el-icon-microphone"></div>
<div v-if="fileType === 'video'" class="card-preview-icon el-icon-video-camera-solid"></div>
<div class="card-preview-text">管理后台不支持预览<br/>微信中可正常播放</div>
</div>
<div class="card-footer">
<!-- 显示素材名称和更新时间 -->
<div class="text-cut-name">{{item.name}}</div>
<div>{{$moment(item.updateTime).calendar()}}</div>
<div class="flex justify-between align-center" v-show="!selectMode">
<!-- 复制 media_id 的按钮 -->
<el-button size="mini" type="text" icon="el-icon-copy-document" v-clipboard:copy="item.mediaId" v-clipboard:success="onCopySuccess" v-clipboard:error="onCopyError">复制media_id</el-button>
<!-- 删除素材的按钮 -->
<el-button size="mini" type="text" icon="el-icon-delete" @click="deleteHandle(item.mediaId)"></el-button>
</div>
</div>
</div>
</div>
<!-- 分页组件 -->
<el-pagination @current-change="currentChangeHandle" :current-page="pageIndex" :page-sizes="[20]" :page-size="20" :total="totalCount" layout="total, prev, pager, next, jumper">
</el-pagination>
<!-- 弹窗用于新增/修改素材 -->
<add-or-update v-if="addOrUpdateVisible" ref="addOrUpdate" @refreshDataList="onChange"></add-or-update>
</div>
</template>
<script>
import AddOrUpdate from './material-file-add-or-update' // /
export default {
name: 'material-file', //
props: {
fileType: { // imagevoicevideo
type: String,
default: 'image'
},
selectMode: { //
type: Boolean,
default: false
}
},
components: {
AddOrUpdate // /
},
data() {
return {
dataForm: {}, //
addOrUpdateVisible: false, // /
dataList: [], //
pageIndex: 1, //
pageSize: 20, //
totalCount: 0, //
dataListLoading: false, //
}
},
mounted() {
this.init() //
},
methods: {
init() {
//
if (!this.dataList.length) {
this.getDataList()
}
},
getDataList() {
//
if (this.dataListLoading) return //
this.dataListLoading = true //
this.$http({
url: this.$http.adornUrl('/manage/wxAssets/materialFileBatchGet'), // URL
params: this.$http.adornParams({
'page': this.pageIndex, //
'type': this.fileType //
})
}).then(({ data }) => {
if (data && data.code == 200) {
//
this.dataList = data.data.items
this.totalCount = data.data.totalCount
this.pageIndex++; //
} else {
//
this.$message.error(data.msg);
}
this.dataListLoading = false //
})
},
// /
addOrUpdateHandle() {
this.addOrUpdateVisible = true // /
this.$nextTick(() => {
this.$refs.addOrUpdate.init(this.fileType) // /
})
},
onSelect(itemInfo) {
//
if (!this.selectMode) return //
this.$emit('selected', itemInfo) //
},
//
deleteHandle(id) {
this.$confirm(`确定对[mediaId=${id}]进行删除操作?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
this.$http({
url: this.$http.adornUrl('/manage/wxAssets/materialDelete'), // URL
method: 'post',
data: { mediaId: id } // ID
}).then(({ data }) => {
if (data && data.code === 200) {
//
this.$message({
message: '操作成功',
type: 'success',
duration: 1500,
onClose: () => this.onChange() //
})
} else {
//
this.$message.error(data.msg)
}
})
})
},
//
currentChangeHandle(val) {
this.pageIndex = val //
this.getDataList() //
},
onCopySuccess() {
//
this.$message.success('已复制')
},
onCopyError(err) {
//
this.$message.error('复制失败, 可能是此浏览器不支持复制')
},
onChange() {
//
this.pageIndex = 1 // 1
this.getDataList() //
this.$emit('change') //
}
}
}
</script>
<style scoped>
.card {
width: 170px; /* 卡片宽度 */
display: inline-block; /* 以行内块元素方式显示,允许多个卡片横向排列 */
background: #FFFFFF; /* 背景颜色 */
border: 1px solid #EBEEF5; /* 边框颜色 */
box-shadow: 1px 1px 20px 0 rgba(0, 0, 0, 0.1); /* 盒子阴影 */
margin: 0 10px 10px 0; /* 外边距 */
vertical-align: top; /* 垂直对齐 */
border-radius: 5px; /* 圆角 */
box-sizing: border-box; /* 包含内边距和边框在内的盒模型 */
}
.card:hover {
border: 2px solid #66b1ff; /* 鼠标悬停时加粗边框 */
margin-bottom: 6px; /* 下方外边距调整 */
}
.card-image {
line-height: 170px; /* 设定行高以居中内容 */
max-height: 170px; /* 最大高度 */
width: 100%; /* 宽度100% */
}
.card-preview {
padding: 20px 0; /* 上下内边距 */
color: #d9d9d9; /* 字体颜色 */
display: flex; /* 使用弹性布局 */
justify-content: center; /* 内容居中对齐 */
align-items: center; /* 垂直居中对齐 */
}
.card-preview-icon {
font-size: 30px; /* 图标字体大小 */
margin-right: 5px; /* 右侧外边距 */
}
.card-preview-text {
font-size: 12px; /* 文本大小 */
}
.card-footer {
color: #ccc; /* 字体颜色 */
font-size: 12px; /* 字体大小 */
padding: 15px 10px; /* 内边距 */
}
</style>

@ -1,232 +0,0 @@
<template>
<div v-show="visible"> <!-- -->
<div class="flex">
<div class="card-list">
<div class="text-center margin-bottom">图文列表</div>
<!-- 遍历所有文章生成列表项 -->
<div class="card-item" :class="{'selected':selectedIndex==index}" v-for="(item,index) in articles" :key="index" @click="selectedIndex=index">
<div class="text-cut-name">{{item.title}}</div>
</div>
<!-- 如果文章少于8篇且没有媒体ID显示新增文章的按钮 -->
<div v-show="articles.length<8 && !mediaId" class="card-add el-icon-plus" @click="addArticle()"></div>
</div>
<!-- 文章表单绑定到选中的文章 -->
<el-form size="mini" v-if="articles.length" :model="articles[selectedIndex]" :rules="dataRule" ref="dataForm" label-width="100px">
<el-form-item label="标题" prop="title">
<el-input v-model="articles[selectedIndex].title" placeholder="标题"></el-input>
</el-form-item>
<el-form-item label="封面图" prop="thumbMediaId">
<el-input v-model="articles[selectedIndex].thumbMediaId" placeholder="封面图media_id">
<div slot="append" @click="assetsSelectorVisible=true"></div> <!-- -->
</el-input>
</el-form-item>
<el-form-item label="摘要" prop="digest">
<el-input v-model="articles[selectedIndex].digest" placeholder="摘要"></el-input>
</el-form-item>
<el-form-item label="原文地址" prop="contentSourceUrl">
<el-input v-model="articles[selectedIndex].contentSourceUrl" placeholder="阅读原文链接"></el-input>
</el-form-item>
<el-row>
<el-col :span="9">
<el-form-item label="作者" prop="author">
<el-input v-model="articles[selectedIndex].author" placeholder="作者"></el-input>
</el-form-item>
</el-col>
<el-col :span="5">
<el-form-item label="显示封面" prop="showCoverPic">
<el-switch v-model="articles[selectedIndex].showCoverPic"></el-switch>
</el-form-item>
</el-col>
<el-col :span="5">
<el-form-item label="允许评论" prop="needOpenComment">
<el-switch v-model="articles[selectedIndex].needOpenComment"></el-switch>
</el-form-item>
</el-col>
<el-col :span="5">
<el-form-item label="仅粉丝可评论" prop="onlyFansCanComment">
<el-switch v-model="articles[selectedIndex].onlyFansCanComment"></el-switch>
</el-form-item>
</el-col>
</el-row>
<el-form-item label="内容" prop="content">
<tinymce-editor ref="editor" v-model="articles[selectedIndex].content"> </tinymce-editor> <!-- 富文本编辑器 -->
</el-form-item>
</el-form>
</div>
<div class="dialog-footer">
<el-button @click="$emit('hide')"></el-button> <!-- 取消按钮 -->
<el-button type="primary" @click="dataFormSubmit()" :disabled="uploading">{{this.mediaId?'修改此篇':'全部提交(共'+articles.length+'篇)'}}</el-button>
<!-- 提交按钮根据是否有mediaId来决定按钮显示内容 -->
</div>
<assets-selector v-if="assetsSelectorVisible" :visible="assetsSelectorVisible" selectType="image" @selected="onAssetsSelect"></assets-selector> <!-- 选择素材框 -->
</div>
</template>
<script>
const articleTemplate = { //
templateId: 0,
title: '',
content: '',
author: '',
showCoverPic: true,
contentSourceUrl: '',
digest: '',
thumbMediaId: '',
needOpenComment: false,
onlyFansCanComment: false
}
export default {
components: {
TinymceEditor: () => import('@/components/tinymce-editor'), //
AssetsSelector: () => import('./assets-selector') //
},
props: {
visible: { //
type: Boolean,
default: false
}
},
data() {
return {
assetsSelectorVisible: false, //
mediaId: '', // ID
selectedIndex: 0, //
articles: [], //
uploading: false, //
dataRule: { //
title: [
{ required: true, message: '标题不能为空', trigger: 'blur' }
],
content: [
{ required: true, message: '内容不能为空', trigger: 'blur' }
],
thumbMediaId: [
{ required: true, message: '封面图media_id不能为空', trigger: 'blur' }
],
contentSourceUrl: [
{ required: true, message: '原文地址不得为空', trigger: 'blur' }
]
}
}
},
methods: {
init(news) { //
if (news && news.mediaId) {
this.mediaId = news.mediaId; // mediaIdmediaId
this.articles = news.content.articles; //
} else {
this.mediaId = ''; //
this.articles = [{ ...articleTemplate }]; // 使
}
},
//
dataFormSubmit() {
if (this.uploading) return; //
this.$refs['dataForm'].validate((valid) => { //
if (valid) {
if (this.mediaId) { // mediaId
this.materialArticleUpdate();
} else { //
this.materialNewsUpload();
}
}
});
},
//
materialNewsUpload() {
this.uploading = true; // true
this.$http({
url: this.$http.adornUrl(`/manage/wxAssets/materialNewsUpload`), // URL
method: 'post', //
data: this.$http.adornData(this.articles, false) //
}).then(({ data }) => {
//
if (data && data.code === 200) {
this.$message({
message: "操作成功", //
type: "success",
duration: 1500,
onClose: () => {
this.$emit("refreshDataList"); //
this.$emit('hide'); //
}
});
} else {
this.$message.error(data.msg); //
}
this.uploading = false; //
});
},
//
materialArticleUpdate() {
this.uploading = true; // true
this.$http({
url: this.$http.adornUrl(`/manage/wxAssets/materialArticleUpdate`), // URL
method: 'post', //
data: this.$http.adornData({
'mediaId': this.mediaId, // mediaId
'index': this.selectedIndex, //
'articles': this.articles[this.selectedIndex] //
})
}).then(({ data }) => {
//
if (data && data.code === 200) {
this.$message.success('操作成功'); //
} else {
this.$message.error(data.msg); //
}
this.uploading = false; //
});
},
//
addArticle() {
this.articles.push({ ...articleTemplate }); //
this.selectedIndex = this.articles.length - 1; //
},
//
onAssetsSelect(assetsInfo) {
Vue.set(this.articles[this.selectedIndex], 'thumbMediaId', assetsInfo.mediaId); // mediaId
this.assetsSelectorVisible = false; //
}
}
}
</script>
<style scoped>
.card-list {
width: 300px; /* 卡片列表的宽度 */
padding-right: 10px; /* 右侧内边距 */
border-right: 1px solid #eeeeee; /* 右侧边框 */
}
.card-item {
margin-top: 2px; /* 上外边距 */
padding: 20px 5px; /* 内边距 */
border: 1px solid #ddd; /* 边框 */
font-size: 12px; /* 字体大小 */
line-height: 15px; /* 行高 */
}
.card-item.selected {
border: 2px solid #409EFF; /* 选中时的边框颜色 */
}
.text-cut-name {
display: -webkit-box; /* 使用盒子模型 */
word-wrap: break-word; /* 自动换行 */
word-break: break-all; /* 强制断行 */
-webkit-box-orient: vertical; /* 垂直方向的盒子模型 */
-webkit-line-clamp: 2; /* 限制显示的行数 */
overflow: hidden; /* 溢出隐藏 */
}
.card-add {
margin-top: 2px; /* 上外边距 */
display: block; /* 块级元素 */
border: 1px dotted #ddd; /* 虚线边框 */
color: #ddd; /* 文字颜色 */
text-align: center; /* 居中对齐 */
font-size: 30px; /* 文字大小 */
line-height: 50px; /* 行高 */
}
.dialog-footer {
margin-top: 20px; /* 上外边距 */
text-align: right; /* 右对齐 */
}
</style>

@ -1,195 +0,0 @@
<template>
<!-- el-dialog组件用于创建一个对话框设置了标题点击模态框不关闭双向绑定显示隐藏状态等属性 -->
<el-dialog title="模板配置" :close-on-click-modal="false" :visible.sync="visible">
<!-- el-form组件用于创建表单绑定了表单数据模型验证规则设置了标签宽度尺寸等属性 -->
<el-form :model="dataForm" :rules="dataRule" ref="dataForm" label-width="100px" size="mini">
<!-- 表单项目用于输入标题绑定了dataForm中的title字段设置了提示文字 -->
<el-form-item label="标题" prop="title">
<el-input v-model="dataForm.title" placeholder="标题"></el-input>
</el-form-item>
<!-- 表单项目用于输入链接绑定了dataForm中的url字段设置了提示文字 -->
<el-form-item label="链接" prop="url">
<el-input v-model="dataForm.url" placeholder="跳转链接"></el-input>
</el-form-item>
<div>
<!-- 表单项目用于输入小程序appid绑定了dataForm.miniprogram.appid字段设置了提示文字 -->
<el-form-item label="小程序appid" prop="miniprogram.appid">
<el-input v-model="dataForm.miniprogram.appid" placeholder="小程序appid"></el-input>
</el-form-item>
<!-- 表单项目用于输入小程序路径绑定了dataForm.miniprogram.pagePath字段设置了提示文字 -->
<el-form-item label="小程序路径" prop="miniprogram.pagePath">
<el-input v-model="dataForm.miniprogram.pagePath" placeholder="小程序pagePath"></el-input>
</el-form-item>
</div>
<el-row>
<el-col :span="16">
<!-- 表单项目用于输入模版名称绑定了dataForm中的name字段设置了提示文字 -->
<el-form-item label="模版名称" prop="name">
<el-input v-model="dataForm.name" placeholder="模版名称"></el-input>
</el-form-item>
</el-col>
<el-col :span="8">
<!-- 表单项目使用el-switch组件用于切换是否有效状态绑定了dataForm中的status字段 -->
<el-form-item label="有效" prop="status">
<el-switch v-model="dataForm.status" placeholder="是否有效" :active-value="true" :inactive-value="false"></el-switch>
</el-form-item>
</el-col>
</el-row>
<div class="form-group-area">
<!-- 表单项目用于显示提示信息告知消息填充数据的相关要求 -->
<el-form-item class="form-group-title">消息填充数据请对照模板内容填写</el-form-item>
<!-- 表单项目使用textarea类型的el-input展示模板内容设置为禁用状态 -->
<el-form-item>
<el-input type="textarea" disabled autosize v-model="dataForm.content" placeholder="模版"></el-input>
</el-form-item>
<!-- 使用v-for循环遍历dataForm.data数组动态生成表单项目用于输入消息填充数据及设置颜色 -->
<el-row v-for="(item, index) in dataForm.data" :key="item.name">
<el-col :span="16">
<!-- 表单项目根据循环的索引和字段名绑定对应的数据项的value字段设置了必填验证规则 -->
<el-form-item :label="item.name" :prop="'data.' + index + '.value'" :rules="[{required: true, message: '填充内容不得为空', trigger: 'blur' }]">
<el-input type="textarea" autosize rows="1" v-model="item.value" placeholder="填充内容"></el-input>
</el-form-item>
</el-col>
<el-col :span="8">
<!-- 表单项目用于输入颜色值绑定对应数据项的color字段 -->
<el-form-item label="颜色">
<el-input type="color" v-model="item.color" placeholder="颜色"></el-input>
</el-form-item>
</el-col>
</el-row>
</div>
</el-form>
<!-- 在对话框的底部插槽中添加取消和确定按钮分别绑定对应的点击事件 -->
<span slot="footer" class="dialog-footer">
<el-button @click="visible = false">取消</el-button>
<el-button type="primary" @click="dataFormSubmit()"></el-button>
</span>
</el-dialog>
</template>
<script>
export default {
data() {
return {
visible: false, //
// idtemplateId
dataForm: {
id: 0,
templateId: '',
title: '',
data: [], //
url: '',
miniprogram: { appid: '', pagePath: '' }, //
content: '', //
status: true, //
name: '' //
},
//
dataRule: {
title: [
{ required: true, message: '标题不能为空', trigger: 'blur' }
],
data: [
{ required: true, message: '内容不能为空', trigger: 'blur' }
],
name: [
{ required: true, message: '模版名称不能为空', trigger: 'blur' }
]
}
}
},
methods: {
// id
init(id) {
console.log('init', id);
// idid0
this.dataForm.id = id || 0;
//
this.visible = true;
this.$nextTick(() => {
//
this.$refs['dataForm'].resetFields();
if (this.dataForm.id) {
// id使axios$httpaxiosGET
this.$http({
url: this.$http.adornUrl(`/manage/msgTemplate/info/${this.dataForm.id}`),
method: 'get',
params: this.$http.adornParams()
}).then(({ data }) => {
if (data && data.code === 200) {
//
this.transformTemplate(data.msgTemplate);
} else {
//
this.$message.error(data.msg);
}
});
}
});
},
/**
* 根据content信息展开data配置项(content为微信公众平台后台配置的模板)
* 如content='{{first.DATA}} ↵商品名称:{{keyword1.DATA}} ↵购买时间:{{keyword2.DATA}} ↵{{remark.DATA}}'
* 则生成data=[{name:'first',value:'',color:''},{name:'first',value:'',color:''},{name:'first',value:'',color:''}]
* 展示表单让管理员给对应的字段填充内容
*/
transformTemplate(template) {
if (!template.miniprogram) template.miniprogram = { appid: '', pagePath: '' };
if (template.data instanceof Array) { //
this.dataForm = template;
return;
}
template.data = [];
// content{{xxx.DATA}}xxxkeysArray
let keysArray = template.content.match(/\{\{(\w*)\.DATA\}\}/g) || []; // ["{{first.DATA}}", "{{keyword1.DATA}}", "{{keyword2.DATA}}", "{{remark.DATA}}"]
keysArray.map(item => {
const name = item.replace('{{', '').replace('.DATA}}', '');
// datavalue
template.data.push({ "name": name, "value": "", color: "#000000" });
});
this.dataForm = template; // dataForm
},
//
dataFormSubmit() {
//
this.$refs['dataForm'].validate((valid) => {
if (valid) {
// 使axios$httpaxiosPOSTid
this.$http({
url: this.$http.adornUrl(`/manage/msgTemplate/${!this.dataForm.id ? 'save' : 'update'}`),
method: 'post',
data: this.$http.adornData(this.dataForm) //
}).then(({ data }) => {
if (data && data.code === 200) {
// refreshDataList
this.$message({
message: '操作成功',
type: 'success',
duration: 1500,
onClose: () => {
this.visible = false; //
this.$emit('refreshDataList'); //
}
});
} else {
//
this.$message.error(data.msg);
}
});
}
});
}
}
}
</script>
<style scoped>
.form-group-area {
border: 1px dotted gray; /* 添加边框样式,使其更明显 */
}
.form-group-title {
color: gray; /* 设置标题颜色 */
font-size: 12px; /* 设置标题字体大小 */
}
</style>

@ -1,274 +0,0 @@
<template>
<!-- 整个模块的容器类用于包裹后续的表单表格分页以及相关弹窗组件等 -->
<div class="mod-config">
<!-- 内联表单绑定了dataForm数据模型监听回车键native修饰符表示监听原生的键盘事件按下时调用getDataList方法 -->
<el-form :inline="true" :model="dataForm" @keyup.enter.native="getDataList()">
<!-- 标题输入框所在的表单项使用el-input组件实现输入框双向绑定dataForm.title有占位提示文字且可清空输入内容 -->
<el-form-item>
<el-input v-model="dataForm.title" placeholder="标题" clearable></el-input>
</el-form-item>
<!-- 操作按钮所在的表单项 -->
<el-form-item>
<!-- 查询按钮点击时调用getDataList方法获取符合条件的数据列表 -->
<el-button @click="getDataList()"></el-button>
<!-- 批量复制按钮根据权限调用isAuth方法判断决定是否显示类型为成功按钮点击时调用copyHandle方法当没有选中的数据项时禁用按钮 -->
<el-button v-if="isAuth('wx:msgtemplate:save')" type="success" @click="copyHandle()" :disabled="dataListSelections.length <= 0"></el-button>
<!-- 推送消息按钮根据权限决定是否显示类型为成功按钮点击时调用templateMsgTaskHandle方法当选中的数据项数量不为1时禁用按钮 -->
<el-button v-if="isAuth('wx:msgtemplate:save')" type="success" @click="templateMsgTaskHandle()" :disabled="dataListSelections.length!= 1"></el-button>
<!-- 批量删除按钮根据权限决定是否显示类型为危险按钮点击时调用deleteHandle方法当没有选中的数据项时禁用按钮 -->
<el-button v-if="isAuth('wx:msgtemplate:delete')" type="danger" @click="deleteHandle()" :disabled="dataListSelections.length <= 0"></el-button>
</el-form-item>
<!-- 靠右对齐的表单项包含同步公众号模板按钮和模板管理指引链接按钮 -->
<el-form-item class="fr">
<!-- 同步公众号模板按钮根据权限决定是否显示点击时调用syncWxTemplate方法按钮文字根据同步状态动态变化正在同步时显示同步中...否则显示同步公众号模板同步过程中按钮禁用 -->
<el-button v-if="isAuth('wx:msgtemplate:save')" icon="el-icon-sort" type="success" @click="syncWxTemplate()" :disabled="synchonizingWxTemplate">{{synchonizingWxTemplate? '...' : ''}}</el-button>
<!-- 模板管理指引链接按钮点击后在新标签页打开指定的链接 -->
<el-button><el-link type="primary" icon="el-icon-link" target="_blank" href="https://kf.qq.com/faq/170209E3InyI170209nIF7RJ.html">模板管理指引</el-link></el-button>
</el-form-item>
</el-form>
<!-- el-table组件用于展示数据列表绑定了dataList数据显示边框加载数据时显示加载提示监听选择项变化事件 -->
<el-table :data="dataList" border v-loading="dataListLoading" @selection-change="selectionChangeHandle" style="width: 100%;">
<!-- 选择列用于多选操作设置了表头和内容的对齐方式以及宽度 -->
<el-table-column type="selection" header-align="center" align="center" width="50">
</el-table-column>
<!-- 模板ID列对应dataList中数据项的templateId属性显示溢出提示设置了表头和内容的对齐方式以及列标题 -->
<el-table-column prop="templateId" show-overflow-tooltip header-align="center" align="center" label="模板ID">
</el-table-column>
<!-- 标题列对应dataList中数据项的title属性通过插槽使用a标签包裹内容使其可点击跳转链接地址取自数据项的url属性设置了表头和内容的对齐方式以及列标题 -->
<el-table-column prop="title" header-align="center" align="center" label="标题">
<a :href="scope.row.url" slot-scope="scope">{{scope.row.title}}</a>
</el-table-column>
<!-- 模版名称列对应dataList中数据项的name属性设置了表头和内容的对齐方式以及列标题 -->
<el-table-column prop="name" header-align="center" align="center" label="模版名称">
</el-table-column>
<!-- 模版字段列对应dataList中数据项的content属性显示溢出提示设置了表头和内容的对齐方式以及列宽度 -->
<el-table-column prop="content" show-overflow-tooltip header-align="center" align="center" label="模版字段" width="200">
</el-table-column>
<!-- 是否有效列对应dataList中数据项的status属性通过插槽根据数据项的status值判断显示设置了表头和内容的对齐方式以及列标题 -->
<el-table-column prop="status" header-align="center" align="center" label="是否有效">
<span slot-scope="scope">{{scope.row.status? "是" : "否"}}</span>
</el-table-column>
<!-- 操作列固定在右侧设置了表头和内容的对齐方式宽度以及列标题通过插槽定义了配置和删除按钮 -->
<el-table-column fixed="right" header-align="center" align="center" width="150" label="操作">
<template slot-scope="scope">
<el-button type="text" size="small" @click="addOrUpdateHandle(scope.row.id)"></el-button>
<el-button type="text" size="small" @click="deleteHandle(scope.row.id)"></el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页组件绑定了相关的分页事件和属性用于切换每页显示数量当前页码等操作 -->
<el-pagination @size-change="sizeChangeHandle" @current-change="currentChangeHandle" :current-page="pageIndex" :page-sizes="[10, 20, 50, 100]" :page-size="pageSize" :total="totalCount" layout="total, sizes, prev, pager, next, jumper">
</el-pagination>
<!-- 新增/修改组件通过属性和事件与父组件进行交互根据addOrUpdateVisible的值决定是否显示 -->
<add-or-update v-if="addOrUpdateVisible" ref="addOrUpdate" @refreshDataList="getDataList"></add-or-update>
<!-- 模板消息任务组件根据templateMsgTaskVisible的值决定是否显示 -->
<template-msg-task v-if="templateMsgTaskVisible" ref="templateMsgTask"></template-msg-task>
</div>
</template>
<script>
// /
import AddOrUpdate from './msg-template-add-or-update'
//
import TemplateMsgTask from '@/components/template-msg-task'
export default {
data() {
return {
dataForm: {
title: ''
},
dataList: [],
pageIndex: 1,
pageSize: 10,
totalCount: 0,
dataListLoading: false,
dataListSelections: [],
addOrUpdateVisible: false,
templateMsgTaskVisible: false,
synchonizingWxTemplate: false
}
},
components: {
AddOrUpdate, TemplateMsgTask
},
activated() {
// getDataList
this.getDataList()
},
methods: {
//
getDataList() {
// true
this.dataListLoading = true;
// HTTPURL
this.$http({
url: this.$http.adornUrl('/manage/msgTemplate/list'),
method: 'get',
params: this.$http.adornParams({
'page': this.pageIndex,
'limit': this.pageSize,
'title': this.dataForm.title,
'sidx': 'id',
'order': 'desc'
})
}).then(({ data }) => {
//
if (data && data.code === 200) {
//
this.dataList = data.page.list;
this.totalCount = data.page.totalCount;
} else {
//
this.dataList = [];
this.totalCount = 0;
}
// false
this.dataListLoading = false;
});
},
// 1
sizeChangeHandle(val) {
this.pageSize = val;
this.pageIndex = 1;
this.getDataList();
},
//
currentChangeHandle(val) {
this.pageIndex = val;
this.getDataList();
},
//
selectionChangeHandle(val) {
this.dataListSelections = val;
},
// //trueDOM/id
addOrUpdateHandle(id) {
this.addOrUpdateVisible = true;
this.$nextTick(() => {
this.$refs.addOrUpdate.init(id);
});
},
// idHTTP
deleteHandle(id) {
// id使id
var ids = id? [id] : this.dataListSelections.map(item => item.id);
this.$confirm(`确定对[id=${ids.join(',')}]进行[${id? '删除' : '批量删除'}]操作?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
// HTTP
this.$http({
url: this.$http.adornUrl('/manage/msgTemplate/delete'),
method: 'post',
data: this.$http.adornData(ids, false)
}).then(({ data }) => {
//
if (data && data.code === 200) {
//
this.$message({
message: '操作成功',
type: 'success',
duration: 1500,
onClose: () => {
this.getDataList();
}
});
} else {
//
this.$message.error(data.msg);
}
});
});
},
syncWxTemplate() {
//
if (this.synchonizingWxTemplate) return;
// true
this.synchonizingWxTemplate = true;
// HTTP
this.$http({
url: this.$http.adornUrl('/manage/msgTemplate/syncWxTemplate'),
method: 'post'
}).then(({ data }) => {
// false
this.synchonizingWxTemplate = false;
//
if (data && data.code === 200) {
//
this.$message({
message: '同步完成',
type: 'success',
duration: 1500,
onClose: () => {
this.getDataList();
}
});
} else {
//
this.$message.error(data.msg);
}
}).catch(() => this.synchonizingWxTemplate = false);
},
templateMsgTaskHandle() {
// true
this.templateMsgTaskVisible = true;
this.$nextTick(() => {
// DOM
this.$refs.templateMsgTask.init(this.dataListSelections[0]);
});
},
async copyHandle() {
let loading;
//
for (let i = 0; i < this.dataListSelections.length; i++) {
let item = this.dataListSelections[i];
//
loading = this.$loading({
lock: true,
text: "复制模板:" + item.title,
spinner: 'el-icon-loading',
background: 'rgba(0, 0, 0, 0.7)'
});
// id
item.id = '';
//
item.updateTime = new Date();
// _COPY
item.name += '_COPY';
// addMsgTemplate使await
await this.addMsgTemplate(item).catch(() => loading.close());
//
loading.close();
}
//
loading.close();
//
this.getDataList();
},
addMsgTemplate(msgTemplate) {
return new Promise((resolve, reject) => {
// HTTP
this.$http({
url: this.$http.adornUrl('/manage/msgTemplate/save'),
method: 'post',
data: this.$http.adornData(msgTemplate)
}).then(({ data }) => {
//
if (data && data.code === 200) {
// resolve
resolve();
} else {
// reject
this.$message.error(data.msg);
reject(data.msg);
}
}).catch(err => reject(err));
});
}
}
}
</script>

@ -1,169 +0,0 @@
<template>
<!-- 整个模块的容器类用于包裹后续的表单表格以及分页组件等 -->
<div class="mod-config">
<!-- 内联表单绑定了dataForm数据模型监听回车键native修饰符表示监听原生的键盘事件按下时调用getDataList方法 -->
<el-form :inline="true" :model="dataForm" @keyup.enter.native="getDataList()">
<!-- openid输入框所在的表单项使用el-input组件实现输入框双向绑定dataForm.touser有占位提示文字且可清空输入内容 -->
<el-form-item>
<el-input v-model="dataForm.touser" placeholder="openid" clearable></el-input>
</el-form-item>
<!-- 操作按钮所在的表单项 -->
<el-form-item>
<!-- 查询按钮点击时调用getDataList方法获取符合条件的数据列表 -->
<el-button @click="getDataList()"></el-button>
<!-- 批量删除按钮根据权限调用isAuth方法判断决定是否显示类型为危险按钮点击时调用deleteHandle方法当没有选中的数据项时禁用按钮 -->
<el-button v-if="isAuth('wx:templatemsglog:delete')" type="danger" @click="deleteHandle()" :disabled="dataListSelections.length <= 0"></el-button>
</el-form-item>
</el-form>
<!-- el-table组件用于展示数据列表绑定了dataList数据显示边框加载数据时显示加载提示监听选择项变化事件 -->
<el-table :data="dataList" border v-loading="dataListLoading" @selection-change="selectionChangeHandle" style="width: 100%;">
<!-- 选择列用于多选操作设置了表头和内容的对齐方式以及宽度 -->
<el-table-column type="selection" header-align="center" align="center" width="50">
</el-table-column>
<!-- openid列对应dataList中数据项的touser属性设置了表头和内容的对齐方式以及列宽度 -->
<el-table-column prop="touser" header-align="center" align="center" label="openid" width="100">
</el-table-column>
<!-- 内容列对应dataList中数据项的data属性使用formatter函数tableJsonFormat方法格式化显示内容设置了表头和内容的对齐方式以及列宽度 -->
<el-table-column prop="data" header-align="center" align="center" :formatter="tableJsonFormat" label="内容" width="300">
</el-table-column>
<!-- 发送结果列对应dataList中数据项的sendResult属性显示溢出提示设置了表头和内容的对齐方式以及列标题和宽度 -->
<el-table-column prop="sendResult" header-align="center" align="center" show-overflow-tooltip label="发送结果" width="150">
</el-table-column>
<!-- 发送时间列对应dataList中数据项的sendTime属性设置了表头和内容的对齐方式以及列标题和宽度 -->
<el-table-column prop="sendTime" header-align="center" align="center" width="100" label="发送时间">
</el-table-column>
<!-- 链接列对应dataList中数据项的url属性显示溢出提示设置了表头和内容的对齐方式以及列标题 -->
<el-table-column prop="url" header-align="center" align="center" show-overflow-tooltip label="链接">
</el-table-column>
<!-- 小程序列对应dataList中数据项的miniprogram属性使用formatter函数tableJsonFormat方法格式化显示内容显示溢出提示设置了表头和内容的对齐方式以及列标题 -->
<el-table-column prop="miniprogram" header-align="center" align="center" :formatter="tableJsonFormat" show-overflow-tooltip label="小程序">
</el-table-column>
<!-- 模板ID列对应dataList中数据项的templateId属性设置了表头和内容的对齐方式以及列标题和宽度 -->
<el-table-column prop="templateId" header-align="center" align="center" label="模板ID" width="150">
</el-table-column>
<!-- 操作列固定在右侧设置了表头和内容的对齐方式宽度以及列标题通过插槽定义了删除按钮 -->
<el-table-column fixed="right" header-align="center" align="center" width="150" label="操作">
<template slot-scope="scope">
<el-button type="text" size="small" @click="deleteHandle(scope.row.logId)"></el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页组件绑定了相关的分页事件和属性用于切换每页显示数量当前页码等操作 -->
<el-pagination @size-change="sizeChangeHandle" @current-change="current-changeHandle" :current-page="pageIndex" :page-sizes="[10, 20, 50, 100]" :page-size="pageSize" :total="totalCount" layout="total, sizes, prev, pager, next, jumper">
</el-pagination>
</div>
</template>
<script>
export default {
data() {
return {
dataForm: {
// openid
touser: ''
},
dataList: [],
pageIndex: 1,
pageSize: 10,
totalCount: 0,
dataListLoading: false,
dataListSelections: [],
addOrUpdateVisible: false
}
},
activated() {
// getDataList
this.getDataList()
},
methods: {
//
getDataList() {
// true
this.dataListLoading = true;
// HTTPURL
this.$http({
url: this.$http.adornUrl('/manage/templateMsgLog/list'),
method: 'get',
params: this.$http.adornParams({
'page': this.pageIndex,
'limit': this.pageSize,
'touser': this.dataForm.touser,
'sidx': 'send_time',
'order': 'desc'
})
}).then(({ data }) => {
//
if (data && data.code === 200) {
//
this.dataList = data.page.list;
this.totalCount = data.page.totalCount;
} else {
//
this.dataList = [];
this.totalCount = 0;
}
// false
this.dataListLoading = false;
});
},
// 1
sizeChangeHandle(val) {
this.pageSize = val;
this.pageIndex = 1;
this.getDataList();
},
//
currentChangeHandle(val) {
this.pageIndex = val;
this.getDataList();
},
//
selectionChangeHandle(val) {
this.dataListSelections = val;
},
// idHTTP
deleteHandle(id) {
// id使logId
var ids = id? [id] : this.dataListSelections.map(item => item.logId);
this.$confirm(`确定对[id=${ids.join(',')}]进行[${id? '删除' : '批量删除'}]操作?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
// HTTP
this.$http({
url: this.$http.adornUrl('/manage/templateMsgLog/delete'),
method: 'post',
data: this.$http.adornData(ids, false)
}).then(({ data }) => {
//
if (data && data.code === 200) {
//
this.$message({
message: '操作成功',
type: 'success',
duration: 1500,
onClose: () => {
this.getDataList();
}
});
} else {
//
this.$message.error(data.msg);
}
});
});
},
tableJsonFormat(row, column, cellValue) {
//
if (!cellValue) {
return '';
}
// JSONJSON便
return JSON.stringify(cellValue);
}
}
}
</script>

@ -1,174 +0,0 @@
<template>
<!-- 整个模块的容器类用于包裹后续的表单表格以及相关弹窗组件等 -->
<div class="mod-config">
<!-- 内联表单绑定了dataForm数据模型监听回车键native修饰符表示监听原生的键盘事件按下时调用getDataList方法 -->
<el-form :inline="true" :model="dataForm" @keyup.enter.native="getDataList()">
<!-- 参数名输入框所在的表单项使用el-input组件实现输入框双向绑定dataForm.key有占位提示文字且可清空输入内容 -->
<el-form-item>
<el-input v-model="dataForm.key" placeholder="参数名" clearable></el-input>
</el-form-item>
<!-- 操作按钮所在的表单项 -->
<el-form-item>
<!-- 查询按钮点击时调用getDataList方法获取符合条件的数据列表 -->
<el-button @click="getDataList()"></el-button>
<!-- 新增按钮根据权限调用isAuth方法判断决定是否显示类型为主要按钮点击时调用addOrUpdateHandle方法 -->
<el-button v-if="isAuth('wx:wxaccount:save')" type="primary" @click="addOrUpdateHandle()"></el-button>
<!-- 批量删除按钮根据权限决定是否显示类型为危险按钮点击时调用deleteHandle方法当没有选中的数据项时禁用按钮 -->
<el-button v-if="isAuth('wx:wxaccount:delete')" type="danger" @click="deleteHandle()" :disabled="dataListSelections.length <= 0"></el-button>
</el-form-item>
</el-form>
<!-- el-table组件用于展示数据列表绑定了dataList数据显示边框加载数据时显示加载提示监听选择项变化事件 -->
<el-table :data="dataList" border v-loading="dataListLoading" @selection-change="selectionChangeHandle" style="width: 100%;">
<!-- 选择列用于多选操作设置了表头和内容的对齐方式以及宽度 -->
<el-table-column type="selection" header-align="center" align="center" width="50">
</el-table-column>
<!-- appid列对应dataList中数据项的appid属性设置了表头和内容的对齐方式以及列标题 -->
<el-table-column prop="appid" header-align="center" align="center" label="appid">
</el-table-column>
<!-- 公众号名称列对应dataList中数据项的name属性设置了表头和内容的对齐方式以及列标题 -->
<el-table-column prop="name" header-align="center" align="center" label="公众号名称">
</el-table-column>
<!-- 类型列对应dataList中数据项的type属性使用formatter函数accountTypeFormat方法格式化显示内容设置了表头和内容的对齐方式以及列标题 -->
<el-table-column prop="type" header-align="center" align="center" label="类型" :formatter="accountTypeFormat">
</el-table-column>
<!-- 是否认证列对应dataList中数据项的verified属性通过插槽根据数据项的verified值判断显示设置了表头和内容的对齐方式以及列标题 -->
<el-table-column prop="verified" header-align="center" align="center" label="是否认证">
<span slot-scope="scope">{{scope.row.verified? "是" : "否"}}</span>
</el-table-column>
<!-- 操作列固定在右侧设置了表头和内容的对齐方式宽度以及列标题通过插槽定义了接入修改和删除按钮 -->
<el-table-column fixed="right" header-align="center" align="center" width="150" label="操作">
<template slot-scope="scope">
<el-button type="text" size="small" @click="accessInfo(scope.row)"></el-button>
<el-button type="text" size="small" @click="addOrUpdateHandle(scope.row)"></el-button>
<el-button type="text" size="small" @click="deleteHandle(scope.row.appid)"></el-button>
</template>
</el-table-column>
</el-table>
<!-- 新增/修改组件通过属性和事件与父组件进行交互根据addOrUpdateVisible的值决定是否显示 -->
<add-or-update v-if="addOrUpdateVisible" ref="addOrUpdate" @refreshDataList="getDataList"></add-or-update>
<!-- 账号接入信息组件根据accountAccessVisible的值决定是否显示 -->
<account-access v-if="accountAccessVisible" ref="accountAccessDialog"></account-access>
</div>
</template>
<script>
// /
import AddOrUpdate from './account/wx-account-add-or-update'
//
import AccountAccess from './account/wx-account-access-info'
// VuexmapStateVuex
import { mapState } from 'vuex'
export default {
data() {
return {
dataForm: {
//
key: ''
},
dataList: [],
dataListLoading: false,
dataListSelections: [],
addOrUpdateVisible: false,
accountAccessVisible: false
}
},
components: {
AddOrUpdate, AccountAccess
},
computed: mapState({
// VuexstatewxAccountACCOUNT_TYPES
ACCOUNT_TYPES: state => state.wxAccount.ACCOUNT_TYPES
}),
activated() {
// getDataList
this.getDataList()
},
methods: {
//
getDataList() {
// true
this.dataListLoading = true;
// HTTPURL
this.$http({
url: this.$http.adornUrl('/manage/wxAccount/list'),
method: 'get',
params: this.$http.adornParams({
'key': this.dataForm.key
})
}).then(({ data }) => {
//
if (data && data.code === 200) {
// Vuexmutation
this.dataList = data.list;
this.$store.commit('wxAccount/updateAccountList', data.list);
} else {
//
this.dataList = [];
}
// false
this.dataListLoading = false;
});
},
//
selectionChangeHandle(val) {
this.dataListSelections = val;
},
// //trueDOM/
addOrUpdateHandle(item) {
this.addOrUpdateVisible = true;
this.$nextTick(() => {
this.$refs.addOrUpdate.init(item);
});
},
accessInfo(item) {
// true
this.accountAccessVisible = true;
this.$nextTick(() => {
// DOM
this.$refs.accountAccessDialog.init(item);
});
},
// appidHTTP
deleteHandle(appid) {
// appid使appid
var ids = appid? [appid] : this.dataListSelections.map(item => {
return item.appid
});
this.$confirm(`确定对[appid=${ids.join(',')}]进行[${appid? '删除' : '批量删除'}]操作?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
// HTTP
this.$http({
url: this.$http.adornUrl('/manage/wxAccount/delete'),
method: 'post',
data: this.$http.adornData(ids, false)
}).then(({ data }) => {
//
if (data && data.code === 200) {
//
this.$message({
message: '操作成功',
type: 'success',
duration: 1500,
onClose: () => {
this.getDataList();
}
});
} else {
//
this.$message.error(data.msg);
}
});
});
},
accountTypeFormat(row, column, cellValue) {
// ACCOUNT_TYPES
return this.ACCOUNT_TYPES[cellValue];
}
}
}
</script>

@ -1,81 +0,0 @@
<template>
<!-- 使用el-tabs组件创建选项卡通过v-model双向绑定activeTab来控制当前选中的选项卡监听tab-click事件触发handleTabClick方法 -->
<el-tabs v-model="activeTab" @tab-click="handleTabClick">
<!-- 图片素材选项卡面板标签显示为图片素材数量数量通过assetsCount.imageCount动态获取设置name属性为image采用懒加载模式 -->
<el-tab-pane :label="'图片素材('+assetsCount.imageCount+')'" name="image" lazy>
<!-- 引入自定义组件material-file设置其fileType属性为image用于展示和管理图片素材相关功能同时监听其@change事件触发materialCount方法 -->
<material-file fileType="image" ref="imagePanel" @change="materialCount"></material-file>
</el-tab-pane>
<!-- 语音素材选项卡面板标签显示为语音素材数量数量通过assetsCount.voiceCount动态获取设置name属性为voice采用懒加载模式 -->
<el-tab-pane :label="'语音素材('+assetsCount.voiceCount+')'" name="voice" lazy>
<!-- 引入自定义组件material-file设置其fileType属性为voice用于展示和管理语音素材相关功能同时监听其@change事件触发materialCount方法 -->
<material-file fileType="voice" ref="voicePanel" @change="materialCount"></material-file>
</el-tab-pane>
<!-- 视频素材选项卡面板标签显示为视频素材数量数量通过assetsCount.videoCount动态获取设置name属性为video采用懒加载模式 -->
<el-tab-pane :label="'视频素材('+assetsCount.videoCount+')'" name="video" lazy>
<!-- 引入自定义组件material-file设置其fileType属性为video用于展示和管理视频素材相关功能同时监听其@change事件触发materialCount方法 -->
<material-file fileType="video" ref="videoPanel" @change="materialCount"></material-file>
</el-tab-pane>
<!-- 图文素材选项卡面板标签显示为图文素材数量数量通过assetsCount.newsCount动态获取设置name属性为news采用懒加载模式 -->
<el-tab-pane :label="'图文素材('+assetsCount.newsCount+')'" name="news" lazy>
<!-- 引入自定义组件material-news用于展示和管理图文素材相关功能同时监听其@change事件触发materialCount方法 -->
<material-news ref="newsPanel" @change="materialCount"></material-news>
</el-tab-pane>
</el-tabs>
</template>
<script>
export default {
data() {
return {
// el-tabsimage
activeTab: 'image',
assetsCount: {
// ..
imageCount: '..',
// ..
videoCount: '..',
// ..
voiceCount: '..',
// ..
newsCount: '..'
}
};
},
components: {
// material-file
MaterialFile: () => import('./assets/material-file'),
// material-news
MaterialNews: () => import('./assets/material-news')
},
mounted() {
// materialCount
this.materialCount();
},
methods: {
handleTabClick(tab, event) {
// DOM
this.$nextTick(() => {
// refinitinit
this.$refs[tab.name + 'Panel'].init();
});
},
materialCount() {
// HTTPURL
this.$http({
url: this.$http.adornUrl('/manage/wxAssets/materialCount')
}).then(({ data }) => {
//
if (data && data.code == 200) {
// assetsCount
this.assetsCount = data.data;
} else {
//
this.$message.error(data.msg);
}
});
}
}
};
</script>

@ -1,184 +0,0 @@
<template>
<!-- 最外层的 div 作为整体容器 -->
<div>
<!-- 菜单输入组容器设置底部边框样式用于展示菜单名称以及删除菜单按钮 -->
<div class="menu-input-group" style="border-bottom: 2px #e8e8e8 solid;">
<!-- 展示菜单名称通过插值表达式绑定 button.name 获取名称值 -->
<div class="menu-name">{{button.name}}</div>
<!-- 删除菜单按钮点击时通过 $emit 触发父组件的 'delMenu' 事件由父组件来处理菜单删除相关逻辑 -->
<div class="menu-del" @click="$emit('delMenu')"></div>
</div>
<!-- 菜单输入组容器用于输入菜单名称 -->
<div class="menu-input-group">
<!-- 菜单名称标签 -->
<div class="menu-label">菜单名称</div>
<!-- 菜单名称输入框所在的容器 -->
<div class="menu-input">
<!-- 文本输入框用于输入菜单名称绑定 v-model button.name 实现双向数据绑定监听 input 事件触发 checkMenuName 方法来检查名称长度是否合规 -->
<input type="text" name="name" placeholder="请输入菜单名称" class="menu-input-text" v-model="button.name" @input="checkMenuName(button.name)">
<!-- 提示信息 menuNameBounds true 时显示提示字数超过上限通过 v-show 根据条件控制显示与否 -->
<p class="menu-tips" style="color:#e15f63" v-show="menuNameBounds"></p>
<!-- 提示信息显示菜单名称的字数限制规则根据 selectedMenuLevel 的值动态展示不同的字数上限说明 -->
<p class="menu-tips">字数不超过{{selectedMenuLevel==1?'5':'8'}}个汉字</p>
</div>
</div>
<!-- 根据按钮是否有子按钮或者子按钮数组长度是否为 0 来决定是否显示下面的内容若没有子按钮则展示以下配置项 -->
<div v-show="!button.subButtons || button.subButtons.length==0">
<!-- 菜单输入组容器用于选择菜单内容类型 -->
<div class="menu-input-group">
<!-- 菜单内容类型标签 -->
<div class="menu-label">菜单内容</div>
<!-- 下拉选择框用于选择菜单的类型通过 v-model 双向绑定 button.type绑定不同的选项值每个选项对应不同的菜单功能 -->
<div class="menu-input">
<select v-model="button.type" name="type" class="menu-input-text">
<option value="view">跳转网页(view)</option>
<option value="media_id">发送消息(media_id)</option>
<!--<option value="view_limited">跳转公众号图文消息链接(view_limited)</option>-->
<option value="miniprogram">打开指定小程序(miniprogram)</option>
<option value="click">自定义点击事件(click)</option>
<option value="scancode_push">扫码上传消息(scancode_push)</option>
<option value="scancode_waitmsg">扫码提示下发(scancode_waitmsg)</option>
<option value="pic_sysphoto">系统相机拍照(pic_sysphoto)</option>
<option value="pic_photo_or_album">弹出拍照或者相册(pic_photo_or_album)</option>
<option value="pic_weixin">弹出微信相册(pic_weixin)</option>
<option value="location_select">弹出地理位置选择器(location_select)</option>
</select>
</div>
</div>
<!-- 当菜单类型为 'view' 时显示的内容用于配置跳转网页相关信息 -->
<div class="menu-content" v-if="button.type=='view'">
<!-- 菜单输入组容器用于输入页面地址 -->
<div class="menu-input-group">
<!-- 提示信息说明点击该子菜单后的跳转行为 -->
<p class="menu-tips">订阅者点击该子菜单会跳到以下链接</p>
<!-- 页面地址标签 -->
<div class="menu-label">页面地址</div>
<!-- 页面地址输入框所在的容器 -->
<div class="menu-input">
<!-- 文本输入框用于输入页面地址通过 v-model 双向绑定 button.url -->
<input type="text" placeholder="" class="menu-input-text" v-model="button.url">
</div>
</div>
</div>
<!-- 当菜单类型为 'media_id' 时显示的内容用于配置发送图文消息相关信息 -->
<div class="menu-content" v-else-if="button.type=='media_id'">
<!-- 菜单输入组容器用于输入图文消息的 media_id -->
<div class="menu-input-group">
<!-- 提示信息说明点击该菜单后的行为 -->
<p class="menu-tips">订阅者点击该菜单会收到以下图文消息</p>
<!-- media_id 标签 -->
<div class="menu-label">media_id</div>
<!-- media_id 输入框所在的容器 -->
<div class="menu-input">
<!-- 文本输入框用于输入图文消息的 media_id通过 v-model 双向绑定 button.mediaId -->
<input type="text" placeholder="图文消息media_id" class="menu-input-text" v-model="button.mediaId">
</div>
</div>
</div>
<!-- 当菜单类型为 'miniprogram' 时显示的内容用于配置小程序相关信息包含 appId页面路径以及备用网页等 -->
<div class="menu-content" v-else-if="button.type=='miniprogram'">
<!-- 菜单输入组容器用于输入小程序的 appId -->
<div class="menu-input-group">
<!-- 提示信息说明点击该子菜单后的跳转行为 -->
<p class="menu-tips">订阅者点击该子菜单会跳到以下小程序</p>
<!-- 小程序 appId 标签 -->
<div class="menu-label">小程序appId</div>
<!-- 小程序 appId 输入框所在的容器 -->
<div class="menu-input">
<!-- 文本输入框用于输入小程序的 appId通过 v-model 双向绑定 button.appId并提示仅认证公众号可配置 -->
<input type="text" placeholder="小程序的appId仅认证公众号可配置" class="menu-input-text" v-model="button.appId">
</div>
</div>
<!-- 菜单输入组容器用于输入小程序的页面路径 -->
<div class="menu-input-group">
<!-- 小程序路径标签 -->
<div class="menu-label">小程序路径</div>
<!-- 小程序路径输入框所在的容器 -->
<div class="menu-input">
<!-- 文本输入框用于输入小程序的页面路径通过 v-model 双向绑定 button.pagePath并给出示例路径 -->
<input type="text" placeholder="小程序的页面路径 pages/index/index" class="menu-input-text" v-model="button.pagePath">
</div>
</div>
<!-- 菜单输入组容器用于输入备用网页地址 -->
<div class="menu-input-group">
<!-- 备用网页标签 -->
<div class="menu-label">备用网页</div>
<!-- 备用网页地址输入框所在的容器 -->
<div class="menu-input">
<!-- 文本输入框用于输入备用网页地址通过 v-model 双向绑定 button.url并给出相关说明 -->
<input type="text" placeholder="" class="menu-input-text" v-model="button.url">
<p class="menu-tips">旧版微信客户端无法支持小程序用户点击菜单时将会打开备用网页</p>
</div>
</div>
</div>
<!-- 当菜单类型为其他值非上述几种情况时显示的内容用于配置菜单 KEY -->
<div class="menu-content" v-else>
<!-- 菜单输入组容器用于输入菜单 KEY -->
<div class="menu-input-group">
<!-- 提示信息说明 KEY 值的长度限制及用途 -->
<p class="menu-tips">用于消息接口推送不超过128字节</p>
<!-- 菜单 KEY 值标签 -->
<div class="menu-label">菜单KEY值</div>
<!-- 菜单 KEY 值输入框所在的容器 -->
<div class="menu-input">
<!-- 文本输入框用于输入菜单 KEY 通过 v-model 双向绑定 button.key -->
<input type="text" placeholder="" class="menu-input-text" v-model="button.key">
</div>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
//
props: {
// 1
selectedMenuLevel: {
type: Number,
default: 1
},
//
button: {
type: Object,
required: true
}
},
data() {
return {
// false
menuNameBounds: false,
}
},
methods: {
//
checkMenuName: function (val) {
// 1 getMenuNameLen 10
if (this.selectedMenuLevel == 1 && this.getMenuNameLen(val) <= 10) {
// menuNameBounds false
this.menuNameBounds = false;
}
// 2 16
else if (this.selectedMenuLevel == 2 && this.getMenuNameLen(val) <= 16) {
this.menuNameBounds = false;
}
// menuNameBounds true
else {
this.menuNameBounds = true;
}
},
//
getMenuNameLen: function (val) {
var len = 0;
//
for (var i = 0; i < val.length; i++) {
var a = val.charAt(i);
// ASCII 2 1
a.match(/[^\x00-\xff]/ig)!= null? len += 2 : len += 1;
}
return len;
}
}
}
</script>

@ -1,108 +0,0 @@
<template>
<!-- 消息回复对话框 -->
<el-dialog title="消息回复" :close-on-click-modal="false" :visible.sync="visible">
<!-- 表单区域 -->
<el-form :model="dataForm" :rules="dataRule" ref="dataForm">
<!-- 表单项回复内容 -->
<el-form-item prop="replyContent">
<!-- 文本区域输入框用于输入回复内容 -->
<el-input v-model="dataForm.replyContent" type="textarea" :rows="5" placeholder="回复内容" maxlength="600" show-word-limit :autosize="{ minRows: 5, maxRows: 30 }" autocomplete></el-input>
<!-- 当回复类型为文本时显示点击插入链接 -->
<el-button type="text" v-show="'text'==dataForm.replyType" @click="addLink"></el-button>
</el-form-item>
</el-form>
<!-- 对话框底部操作区域 -->
<span slot="footer" class="dialog-footer">
<!-- 取消按钮点击关闭对话框 -->
<el-button @click="visible = false">取消</el-button>
<!-- 发送按钮根据uploading状态显示不同文本并控制是否禁用 -->
<el-button type="success" @click="dataFormSubmit()" :disabled="uploading">{{uploading ? '发送中...' : '发送'}}</el-button>
</span>
</el-dialog>
</template>
<script>
export default {
data() {
return {
//
visible: false,
//
uploading: false,
//
dataForm: {
//
openid: '',
//
replyType: 'text',
//
replyContent: ''
},
//
dataRule: {
//
replyContent: [
{ required: true, message: "回复内容不能为空", trigger: "blur" }
]
}
}
},
// 使
components: {
WxMsgPreview: () => import('@/components/wx-msg-preview')
},
methods: {
// openid
init(openid) {
if (!openid) throw '参数异常'; // openid
this.dataForm.openid = openid; // openid
this.visible = true; //
},
//
dataFormSubmit() {
if (this.uploading) return; //
this.uploading = true; // true
this.$refs['dataForm'].validate((valid) => { //
if (valid) { //
// $http
this.$http({
url: this.$http.adornUrl(`/manage/wxMsg/reply`), //
method: 'post', //
data: this.$http.adornData(this.dataForm) //
}).then(({ data }) => { //
if (data && data.code === 200) { // 200
//
this.$message({
message: '回复成功',
type: 'success',
duration: 1500,
onClose: () => {
this.visible = false;
}
});
this.$emit("success", { ...this.dataForm }); // success
this.dataForm.replyContent = ''; //
} else { //
//
this.$message.error(data.msg);
}
this.uploading = false; //
});
}
});
},
//
addLink() {
this.dataForm.replyContent += '<a href="链接地址">链接文字</a>';
}
}
}
</script>
<style scoped>
/* 样式部分,但.msg-container类在模板中未使用 */
.msg-container {
background: #eee;
}
</style>

@ -1,122 +0,0 @@
<template>
<el-dialog :title="modeDesc[mode]+'用户标签'" :close-on-click-modal="false" :visible.sync="dialogVisible">
<!-- 对话框内容开始 -->
<div>
<!-- 标签选择器用户可以选择一个标签 -->
<el-select v-model="selectedTagid" filterable placeholder="请选择标签" style="width:100%">
<!-- 遍历所有可选标签生成下拉选项 -->
<el-option v-for="tagid in tagidsInOption" :key="tagid" :label="getTagName(tagid)" :value="tagid"></el-option>
</el-select>
<!-- 显示已选择用户的数量 -->
<div style="margin-top:20px;">已选择用户数{{wxUsers.length}}</div>
</div>
<!-- 对话框底部按钮 -->
<span slot="footer" class="dialog-footer">
<!-- 关闭按钮 -->
<el-button @click="dialogVisible=false"></el-button>
<!-- 提交按钮根据提交状态显示不同文字 -->
<el-button type="primary" @click="dataFormSubmit()" :disabled="submitting">{{submitting?'保存中...':'确定'}}</el-button>
</span>
</el-dialog>
</template>
<script>
import { mapState } from 'vuex'
export default {
name: 'wx-user-tagging',
//
props: {
wxUsers: Array,
},
data() {
return {
//
mode: 'tagging',
//
modeDesc: {
'tagging': '绑定',
'untagging': '解绑'
},
// ID
selectedTagid: '',
//
dialogVisible: false,
//
submitting: false
}
},
// Vuexstate
computed: mapState({
//
wxUserTags: state => state.wxUserTags.tags,
//
tagidsInOption() {
// ID
let userTags = this.wxUsers.map(u => u.tagidList || []);
//
if (this.mode === 'tagging') {
let all = this.wxUserTags.map(item => item.id);
return all.filter(tagid => !userTags.every(tagsIdArray => tagsIdArray.indexOf(tagid) > -1));
}
//
else if (this.mode === 'untagging') {
let unionSet = new Set();
userTags.forEach(tagsIdArray => {
tagsIdArray.forEach(tagid => unionSet.add(tagid));
});
return Array.from(unionSet);
}
return [];
}
}),
methods: {
//
init(mode) {
if (mode === 'tagging' || mode === 'untagging') {
this.mode = mode;
this.dialogVisible = true;
} else {
throw ('mode参数有误');
}
},
// ID
getTagName(tagid) {
let tag = this.wxUserTags.find(item => item.id == tagid);
return tag ? tag.name : "?";
},
//
dataFormSubmit() {
if (this.submitting) return;
if (!this.selectedTagid) {
this.$message.error('未选择标签');
return;
}
this.submitting = true;
let openidList = this.wxUsers.map(u => u.openid);
//
this.$http({
url: this.$http.adornUrl(`/manage/wxUserTags/${this.mode === 'tagging' ? 'batchTagging' : 'batchUnTagging'}`),
method: 'post',
data: this.$http.adornData({
tagid: this.selectedTagid,
openidList: openidList
})
}).then(({ data }) => {
this.submitting = false;
if (data && data.code === 200) {
//
this.$message({
message: '操作成功,列表数据需稍后刷新查看',
type: 'success',
onClose: () => this.dialogVisible = false
});
} else {
//
this.$message.error(data.msg);
}
});
}
}
}
</script>

@ -1,9 +1,7 @@
<template>
<main class="site-content" :class="{ 'site-content--tabs': $route.meta.isTab }">
<!-- 如果当前路由元数据中包含isTab则显示标签页组件 -->
<el-tabs v-if="$route.meta.isTab" v-model="mainTabsActiveName" :closable="true"
@tab-click="selectedTabHandle" @tab-remove="removeTabHandle">
<!-- 标签页操作菜单包括关闭当前关闭其他关闭全部和刷新 -->
<!-- 主入口标签页 s -->
<el-tabs v-if="$route.meta.isTab" v-model="mainTabsActiveName" :closable="true" @tab-click="selectedTabHandle" @tab-remove="removeTabHandle">
<el-dropdown class="site-tabs__tools" :show-timeout="0">
<i class="el-icon-arrow-down el-icon--right"></i>
<el-dropdown-menu slot="dropdown">
@ -13,20 +11,17 @@
<el-dropdown-item @click.native="refresh()">刷新当前标签页</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
<!-- 循环渲染标签页 -->
<el-tab-pane v-for="item in mainTabs" :key="item.name" :label="item.title" :name="item.name">
<el-card :body-style="siteContentViewHeight">
<!-- 如果标签页类型为iframe则显示iframe -->
<iframe v-if="item.type === 'iframe'" :src="item.iframeUrl" width="100%" height="100%" frameborder="0" scrolling="yes"></iframe>
<!-- 否则使用keep-alive缓存组件并显示当前激活的标签页内容 -->
<iframe v-if="item.type === 'iframe'" :src="item.iframeUrl" width="100%" height="100%" frameborder="0" scrolling="yes">
</iframe>
<keep-alive v-else>
<router-view v-if="item.name === mainTabsActiveName" />
</keep-alive>
</el-card>
</el-tab-pane>
</el-tabs>
<!-- 如果当前路由不包含isTab则显示单个页面 -->
<!-- 主入口标签页 e -->
<el-card v-else :body-style="siteContentViewHeight">
<keep-alive>
<router-view />
@ -36,80 +31,73 @@
</template>
<script>
import { isURL } from '@/utils/validate' // URL
import { isURL } from '@/utils/validate'
export default {
inject: ['refresh'], //
inject: ['refresh'],
data() {
return {
//
}
},
computed: {
//
documentClientHeight: {
get() { return this.$store.state.common.documentClientHeight }
},
//
menuActiveName: {
get() { return this.$store.state.common.menuActiveName },
set(val) { this.$store.commit('common/updateMenuActiveName', val) }
},
//
mainTabs: {
get() { return this.$store.state.common.mainTabs },
set(val) { this.$store.commit('common/updateMainTabs', val) }
},
//
mainTabsActiveName: {
get() { return this.$store.state.common.mainTabsActiveName },
set(val) { this.$store.commit('common/updateMainTabsActiveName', val) }
},
//
siteContentViewHeight() {
var height = this.documentClientHeight - 50 - 30 - 2
if (this.$route.meta.isTab) {
height -= 40
// metaiframeUrlURL
return isURL(this.$route.meta.iframeUrl) ? { height: height + 'px' } : { minHeight: height + 'px' }
}
return { minHeight: height + 'px' }
}
},
methods: {
//
// tabs, tab
selectedTabHandle(tab) {
tab = this.mainTabs.filter(item => item.name === tab.name)
if (tab.length >= 1) {
this.$router.push({ name: tab[0].name, query: tab[0].query, params: tab[0].params })
}
},
//
// tabs, tab
removeTabHandle(tabName) {
this.$store.commit('common/removeTab', tabName)
},
//
// tabs,
tabsCloseCurrentHandle() {
this.removeTabHandle(this.mainTabsActiveName)
},
//
// tabs,
tabsCloseOtherHandle() {
this.mainTabs = this.mainTabs.filter(item => item.name === this.mainTabsActiveName)
},
//
// tabs,
tabsCloseAllHandle() {
this.mainTabs = []
this.menuActiveName = ''
this.$router.push({ name: 'home' })
},
// methodstemplate
// tabsRefreshCurrentHandle refresh() methodstabsRefreshCurrentHandle
// tabsRefreshCurrentHandle() {
// var tab = this.$route
// this.removeTabHandle(tab.name)
// this.$nextTick(() => {
// this.$router.push({ name: tab.name, query: tab.query, params: tab.params })
// })
// }
// tabs,
tabsRefreshCurrentHandle() {
var tab = this.$route
this.removeTabHandle(tab.name)
this.$nextTick(() => {
this.$router.push({ name: tab.name, query: tab.query, params: tab.params })
})
}
}
}
</script>
</script>

@ -1,27 +1,19 @@
<template>
<!-- 使用Element UI的对话框组件 -->
<el-dialog title="修改密码" :visible.sync="visible" :append-to-body="true">
<!-- 表单用于输入原密码和新密码 -->
<el-form :model="dataForm" :rules="dataRule" ref="dataForm" @keyup.enter.native="dataFormSubmit()" label-width="80px">
<!-- 显示用户名不可编辑 -->
<el-form-item label="账号">
<span>{{ userName }}</span>
</el-form-item>
<!-- 输入原密码 -->
<el-form-item label="原密码" prop="password">
<el-input type="password" v-model="dataForm.password"></el-input>
</el-form-item>
<!-- 输入新密码 -->
<el-form-item label="新密码" prop="newPassword">
<el-input type="password" v-model="dataForm.newPassword"></el-input>
</el-form-item>
<!-- 确认新密码 -->
<el-form-item label="确认密码" prop="confirmPassword">
<el-input type="password" v-model="dataForm.confirmPassword"></el-input>
</el-form-item>
</el-form>
<!-- 对话框底部按钮 -->
<span slot="footer" class="dialog-footer">
<el-button @click="visible = false">取消</el-button>
<el-button type="primary" @click="dataFormSubmit()"></el-button>
@ -30,10 +22,9 @@
</template>
<script>
import { clearLoginInfo } from '@/utils' //
import { clearLoginInfo } from '@/utils'
export default {
data() {
//
var validateConfirmPassword = (rule, value, callback) => {
if (this.dataForm.newPassword !== value) {
callback(new Error('确认密码与新密码不一致'))
@ -42,15 +33,12 @@ export default {
}
}
return {
//
visible: false,
//
dataForm: {
password: '', //
newPassword: '', //
confirmPassword: '' //
password: '',
newPassword: '',
confirmPassword: ''
},
//
dataRule: {
password: [
{ required: true, message: '原密码不能为空', trigger: 'blur' }
@ -60,65 +48,55 @@ export default {
],
confirmPassword: [
{ required: true, message: '确认密码不能为空', trigger: 'blur' },
{ validator: validateConfirmPassword, trigger: 'blur' } //
{ validator: validateConfirmPassword, trigger: 'blur' }
]
}
}
},
computed: {
//
userName: {
get() { return this.$store.state.user.name }
},
//
mainTabs: {
get() { return this.$store.state.common.mainTabs },
set(val) { this.$store.commit('common/updateMainTabs', val) }
}
},
methods: {
//
//
init() {
this.visible = true
this.$nextTick(() => {
this.$refs['dataForm'].resetFields()
})
},
//
//
dataFormSubmit() {
//
this.$refs['dataForm'].validate((valid) => {
if (valid) {
//
this.$http({
url: this.$http.adornUrl('/sys/user/password'), //
method: 'post', //
data: this.$http.adornData({ //
url: this.$http.adornUrl('/sys/user/password'),
method: 'post',
data: this.$http.adornData({
'password': this.dataForm.password,
'newPassword': this.dataForm.newPassword
})
}).then(({ data }) => {
if (data && data.code === 200) {
//
this.$message({
message: '操作成功',
type: 'success',
duration: 1500,
onClose: () => {
//
this.visible = false
this.$nextTick(() => {
//
this.mainTabs = []
//
clearLoginInfo()
//
this.$router.replace({ name: 'login' })
})
}
})
} else {
//
this.$message.error(data.msg)
}
})
@ -127,4 +105,5 @@ export default {
}
}
}
</script>
</script>

@ -1,77 +1,60 @@
<template>
<!-- 导航栏组件类名根据navbarLayoutType动态设置 -->
<nav class="site-navbar" :class="'site-navbar--' + navbarLayoutType">
<!-- 导航栏头部包含品牌标识 -->
<div class="site-navbar__header">
<h1 class="site-navbar__brand" @click="$router.push({ name: 'home' })">
<!-- 大尺寸品牌标识点击返回首页 -->
<a class="site-navbar__brand-lg" href="javascript:;">微信管理系统</a>
<!-- 小尺寸品牌标识点击无效果 -->
<a class="site-navbar__brand-mini" href="javascript:;">W</a>
</h1>
</div>
<!-- 导航栏主体包含菜单 -->
<div class="site-navbar__body clearfix">
<!-- 左侧菜单包含侧边栏折叠/展开按钮 -->
<el-menu class="site-navbar__menu" mode="horizontal">
<el-menu-item class="site-navbar__switch" index="0" @click="sidebarFold = !sidebarFold">
<!-- 侧边栏折叠/展开图标根据sidebarFold动态切换 -->
<i :class="sidebarFold?'el-icon-s-unfold':'el-icon-s-fold'"></i>
</el-menu-item>
</el-menu>
<!-- 右侧菜单包含设置微信账号选择用户信息等 -->
<el-menu class="site-navbar__menu site-navbar__menu--right" mode="horizontal">
<!-- 设置菜单项 -->
<el-menu-item index="1" @click="$router.push({ name: 'theme' })">
<template slot="title">
<i class="el-icon-setting"></i>
</template>
</el-menu-item>
<!-- 微信账号选择器需要权限验证 -->
<el-menu-item index="2" v-if="isAuth('wx:wxaccount:list')">
<template slot="title">
<wx-account-selector></wx-account-selector>
</template>
</el-menu-item>
<!-- 用户信息菜单项包含下拉菜单 -->
<el-menu-item class="site-navbar__avatar" index="3">
<el-dropdown :show-timeout="0" placement="bottom">
<span class="el-dropdown-link">
{{ userName }}
</span>
<!-- 下拉菜单内容 -->
<el-dropdown-menu slot="dropdown">
<!-- 修改密码菜单项 -->
<el-dropdown-item @click.native="updatePasswordHandle()">修改密码</el-dropdown-item>
<!-- 退出菜单项 -->
<el-dropdown-item @click.native="logoutHandle()">退出</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
</el-menu-item>
</el-menu>
</div>
<!-- 修改密码弹窗组件根据updatePassowrdVisible显示/隐藏 -->
<!-- 弹窗, 修改密码 -->
<update-password v-if="updatePassowrdVisible" ref="updatePassowrd"></update-password>
</nav>
</template>
<script>
import UpdatePassword from './main-navbar-update-password' //
import WxAccountSelector from '@/components/wx-account-selector' //
import { clearLoginInfo } from '@/utils' //
import UpdatePassword from './main-navbar-update-password'
import WxAccountSelector from '@/components/wx-account-selector'
import { clearLoginInfo } from '@/utils'
export default {
data() {
return {
updatePassowrdVisible: false // /
updatePassowrdVisible: false
}
},
components: {
UpdatePassword, //
WxAccountSelector //
UpdatePassword,WxAccountSelector
},
computed: {
// navbarLayoutTypesidebarFoldmainTabsuserName
navbarLayoutType: {
get() { return this.$store.state.common.navbarLayoutType }
},
@ -88,28 +71,26 @@ export default {
}
},
methods: {
//
//
updatePasswordHandle() {
this.updatePassowrdVisible = true
this.$nextTick(() => {
this.$refs.updatePassowrd.init()
})
},
// 退
// 退
logoutHandle() {
this.$confirm(`确定进行[退出]操作?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
// 退
this.$http({
url: this.$http.adornUrl('/sys/logout'),
method: 'post',
data: this.$http.adornData()
}).then(({ data }) => {
if (data && data.code === 200) {
//
clearLoginInfo()
this.$router.push({ name: 'login' })
}
@ -118,4 +99,4 @@ export default {
}
}
}
</script>
</script>

@ -0,0 +1,50 @@
<template>
<el-submenu v-if="menu.list && menu.list.length >= 1" :index="menu.menuId + ''" :popper-class="'site-sidebar--' + sidebarLayoutSkin + '-popper'">
<template slot="title">
<i class="site-sidebar__menu-icon" :class="menu.icon"></i>
<!-- <icon-svg :name="menu.icon || ''" class="site-sidebar__menu-icon"></icon-svg> -->
<span>{{ menu.name }}</span>
</template>
<sub-menu v-for="item in menu.list" :key="item.menuId" :menu="item" :dynamicMenuRoutes="dynamicMenuRoutes">
</sub-menu>
</el-submenu>
<el-menu-item v-else :index="menu.menuId + ''" @click="gotoRouteHandle(menu)">
<!-- <icon-svg :name="menu.icon || ''" class="site-sidebar__menu-icon"></icon-svg> -->
<i class="site-sidebar__menu-icon fa" :class="menu.icon"></i>
<span>{{ menu.name }}</span>
</el-menu-item>
</template>
<script>
import SubMenu from './main-sidebar-sub-menu'
export default {
name: 'sub-menu',
props: {
menu: {
type: Object,
required: true
},
dynamicMenuRoutes: {
type: Array,
required: true
}
},
components: {
SubMenu
},
computed: {
sidebarLayoutSkin: {
get() { return this.$store.state.common.sidebarLayoutSkin }
}
},
methods: {
// menuId()
gotoRouteHandle(menu) {
var route = this.dynamicMenuRoutes.filter(item => item.meta.menuId === menu.menuId)
if (route.length >= 1) {
this.$router.push({ name: route[0].name })
}
}
}
}
</script>

@ -1,79 +1,69 @@
<template>
<!-- 侧边栏容器根据sidebarLayoutSkin动态设置类名 -->
<aside class="site-sidebar" :class="'site-sidebar--' + sidebarLayoutSkin">
<div class="site-sidebar__inner">
<!-- 侧边栏菜单默认激活项为menuActiveName或'home'可折叠无折叠动画 -->
<el-menu :default-active="menuActiveName || 'home'" :collapse="sidebarFold" :collapseTransition="false" class="site-sidebar__menu">
<!-- 首页菜单项点击后跳转到'home'路由 -->
<el-menu-item index="home" @click="$router.push({ name: 'home' })">
<i class="site-sidebar__menu-icon el-icon-s-home"></i>
<!-- 菜单项标题使用slot="title"指定但在这里直接写了Element UI内部会处理 -->
<span>首页</span>
<span slot="title">首页</span>
</el-menu-item>
<!-- 遍历menuList为每个菜单项渲染一个sub-menu组件 -->
<sub-menu v-for="menu in menuList" :key="menu.menuId" :menu="menu" :dynamicMenuRoutes="dynamicMenuRoutes"></sub-menu>
<sub-menu v-for="menu in menuList" :key="menu.menuId" :menu="menu" :dynamicMenuRoutes="dynamicMenuRoutes">
</sub-menu>
</el-menu>
</div>
</aside>
</template>
<script>
import SubMenu from './main-sidebar-sub-menu' // SubMenu
import { isURL } from '@/utils/validate' // isURL
import SubMenu from './main-sidebar-sub-menu'
import { isURL } from '@/utils/validate'
export default {
data() {
return {
dynamicMenuRoutes: [] //
dynamicMenuRoutes: []
}
},
components: {
SubMenu // SubMenu
SubMenu
},
computed: {
// Vuex store
sidebarLayoutSkin: {
get() { return this.$store.state.common.sidebarLayoutSkin } //
get() { return this.$store.state.common.sidebarLayoutSkin }
},
sidebarFold: {
get() { return this.$store.state.common.sidebarFold } //
get() { return this.$store.state.common.sidebarFold }
},
menuList: {
get() { return this.$store.state.common.menuList }, //
set(val) { this.$store.commit('common/updateMenuList', val) } //
get() { return this.$store.state.common.menuList },
set(val) { this.$store.commit('common/updateMenuList', val) }
},
menuActiveName: {
get() { return this.$store.state.common.menuActiveName }, //
set(val) { this.$store.commit('common/updateMenuActiveName', val) } //
get() { return this.$store.state.common.menuActiveName },
set(val) { this.$store.commit('common/updateMenuActiveName', val) }
},
mainTabs: {
get() { return this.$store.state.common.mainTabs }, //
set(val) { this.$store.commit('common/updateMainTabs', val) } //
get() { return this.$store.state.common.mainTabs },
set(val) { this.$store.commit('common/updateMainTabs', val) }
},
mainTabsActiveName: {
get() { return this.$store.state.common.mainTabsActiveName }, //
set(val) { this.$store.commit('common/updateMainTabsActiveName', val) } //
get() { return this.$store.state.common.mainTabsActiveName },
set(val) { this.$store.commit('common/updateMainTabsActiveName', val) }
}
},
watch: {
// routeHandle
$route: 'routeHandle'
},
created() {
// sessionStoragerouteHandle
this.menuList = JSON.parse(sessionStorage.getItem('menuList') || '[]')
this.dynamicMenuRoutes = JSON.parse(sessionStorage.getItem('dynamicMenuRoutes') || '[]')
this.routeHandle(this.$route)
},
methods: {
//
//
routeHandle(route) {
if (route.meta.isTab) {
// tabtab
// tab,
var tab = this.mainTabs.filter(item => item.name === route.name)[0]
if (!tab) {
// tabtab
if (route.meta.isDynamic) {
route = this.dynamicMenuRoutes.filter(item => item.name === route.name)[0]
if (!route) {
@ -84,18 +74,17 @@ export default {
menuId: route.meta.menuId || route.name,
name: route.name,
title: route.meta.title,
type: isURL(route.meta.iframeUrl) ? 'iframe' : 'module', // iframeUrltab
type: isURL(route.meta.iframeUrl) ? 'iframe' : 'module',
iframeUrl: route.meta.iframeUrl || '',
params: route.params,
query: route.query
}
this.mainTabs = this.mainTabs.concat(tab) // tabmainTabs
this.mainTabs = this.mainTabs.concat(tab)
}
//
this.menuActiveName = tab.menuId + ''
this.mainTabsActiveName = tab.name
}
}
}
}
</script>
</script>

@ -1,57 +1,41 @@
<template>
<!-- 站点包装器根据sidebarFold状态添加类名同时绑定全屏加载指示器 -->
<div class="site-wrapper" :class="{ 'site-sidebar--fold': sidebarFold }"
v-loading.fullscreen.lock="loading" element-loading-text="拼命加载中">
<!-- 当loading为false时显示以下内容 -->
<div class="site-wrapper" :class="{ 'site-sidebar--fold': sidebarFold }" v-loading.fullscreen.lock="loading" element-loading-text="">
<template v-if="!loading">
<!-- 导航栏组件 -->
<main-navbar />
<!-- 侧边栏组件 -->
<main-sidebar />
<!-- 站点内容包装器动态设置最小高度 -->
<div class="site-content__wrapper" :style="{ 'min-height': documentClientHeight + 'px' }">
<!-- 如果内容不需要刷新则显示主要内容组件 -->
<main-content v-if="!$store.state.common.contentIsNeedRefresh" />
</div>
</template>
</div>
</template>
</template>
<script>
//
import MainNavbar from './main-navbar'
import MainSidebar from './main-sidebar'
import MainContent from './main-content'
export default {
// refresh
provide() {
return {
//
refresh() {
//
this.$store.commit('common/updateContentIsNeedRefresh', true)
// DOM
this.$nextTick(() => {
this.$store.commit('common/updateContentIsNeedRefresh', false)
})
}
}
},
//
data() {
return {
loading: true //
loading: true
}
},
//
components: {
MainNavbar,
MainSidebar,
MainContent
},
// Vuex store
computed: {
documentClientHeight: {
get() { return this.$store.state.common.documentClientHeight },
@ -69,38 +53,29 @@ export default {
set(val) { this.$store.commit('user/updateName', val) }
}
},
//
created() {
this.getUserInfo() //
this.getUserInfo()
},
//
mounted() {
this.resetDocumentClientHeight() //
this.resetDocumentClientHeight()
},
//
methods: {
//
//
resetDocumentClientHeight() {
//
this.documentClientHeight = document.documentElement['clientHeight']
//
window.onresize = () => {
this.documentClientHeight = document.documentElement['clientHeight']
}
},
//
//
getUserInfo() {
// HTTP GET
this.$http({
url: this.$http.adornUrl('/sys/user/info'),
method: 'get',
params: this.$http.adornParams()
}).then(({ data }) => {
// 200
if (data && data.code === 200) {
//
this.loading = false
// ID
this.userId = data.user.userId
this.userName = data.user.username
}
@ -108,4 +83,4 @@ export default {
}
}
}
</script>
</script>

@ -0,0 +1,132 @@
<template>
<div class="mod-role">
<el-form :inline="true" :model="dataForm" @keyup.enter.native="getDataList()">
<el-form-item>
<el-input v-model="dataForm.roleName" placeholder="角色名称" clearable></el-input>
</el-form-item>
<el-form-item>
<el-button @click="getDataList()"></el-button>
<el-button v-if="isAuth('sys:role:save')" type="primary" @click="addOrUpdateHandle()"></el-button>
<el-button v-if="isAuth('sys:role:delete')" type="danger" @click="deleteHandle()" :disabled="dataListSelections.length <= 0"></el-button>
</el-form-item>
</el-form>
<el-table :data="dataList" border v-loading="dataListLoading" @selection-change="selectionChangeHandle" style="width: 100%;">
<el-table-column type="selection" header-align="center" align="center" width="50">
</el-table-column>
<el-table-column prop="roleId" header-align="center" align="center" width="80" label="ID">
</el-table-column>
<el-table-column prop="roleName" header-align="center" align="center" label="角色名称">
</el-table-column>
<el-table-column prop="remark" header-align="center" align="center" label="备注">
</el-table-column>
<el-table-column fixed="right" header-align="center" align="center" width="150" label="操作">
<template slot-scope="scope">
<el-button v-if="isAuth('sys:role:update')" type="text" size="small" @click="addOrUpdateHandle(scope.row.roleId)"></el-button>
<el-button v-if="isAuth('sys:role:delete')" type="text" size="small" @click="deleteHandle(scope.row.roleId)"></el-button>
</template>
</el-table-column>
</el-table>
<el-pagination @size-change="sizeChangeHandle" @current-change="currentChangeHandle" :current-page="pageIndex" :page-sizes="[10, 20, 50, 100]" :page-size="pageSize" :total="totalCount" layout="total, sizes, prev, pager, next, jumper">
</el-pagination>
<!-- 弹窗, 新增 / 修改 -->
<add-or-update v-if="addOrUpdateVisible" ref="addOrUpdate" @refreshDataList="getDataList"></add-or-update>
</div>
</template>
<script>
import AddOrUpdate from './role-add-or-update'
export default {
data() {
return {
dataForm: {
roleName: ''
},
dataList: [],
pageIndex: 1,
pageSize: 10,
totalCount: 0,
dataListLoading: false,
dataListSelections: [],
addOrUpdateVisible: false
}
},
components: {
AddOrUpdate
},
activated() {
this.getDataList()
},
methods: {
//
getDataList() {
this.dataListLoading = true
this.$http({
url: this.$http.adornUrl('/sys/role/list'),
method: 'get',
params: this.$http.adornParams({
'page': this.pageIndex,
'limit': this.pageSize,
'roleName': this.dataForm.roleName
})
}).then(({ data }) => {
if (data && data.code === 200) {
this.dataList = data.page.list
this.totalCount = data.page.totalCount
} else {
this.dataList = []
this.totalCount = 0
}
this.dataListLoading = false
})
},
//
sizeChangeHandle(val) {
this.pageSize = val
this.pageIndex = 1
this.getDataList()
},
//
currentChangeHandle(val) {
this.pageIndex = val
this.getDataList()
},
//
selectionChangeHandle(val) {
this.dataListSelections = val
},
// /
addOrUpdateHandle(id) {
this.addOrUpdateVisible = true
this.$nextTick(() => {
this.$refs.addOrUpdate.init(id)
})
},
//
deleteHandle(id) {
var ids = id ? [id] : this.dataListSelections.map(item => item.roleId)
this.$confirm(`确定对[id=${ids.join(',')}]进行[${id ? '删除' : '批量删除'}]操作?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
this.$http({
url: this.$http.adornUrl('/sys/role/delete'),
method: 'post',
data: this.$http.adornData(ids, false)
}).then(({ data }) => {
if (data && data.code === 200) {
this.$message({
message: '操作成功',
type: 'success',
duration: 1500,
onClose: () => this.getDataList()
})
} else {
this.$message.error(data.msg)
}
})
}).catch(() => { })
}
}
}
</script>

@ -0,0 +1,177 @@
<template>
<el-dialog :title="!dataForm.id ? '新增' : '修改'" :close-on-click-modal="false" :visible.sync="visible">
<el-form :model="dataForm" :rules="dataRule" ref="dataForm" @keyup.enter.native="dataFormSubmit()" label-width="80px">
<el-form-item label="用户名" prop="userName">
<el-input v-model="dataForm.userName" placeholder="登录帐号"></el-input>
</el-form-item>
<el-form-item label="密码" prop="password" :class="{ 'is-required': !dataForm.id }">
<el-input v-model="dataForm.password" type="password" placeholder="密码"></el-input>
</el-form-item>
<el-form-item label="确认密码" prop="comfirmPassword" :class="{ 'is-required': !dataForm.id }">
<el-input v-model="dataForm.comfirmPassword" type="password" placeholder="确认密码"></el-input>
</el-form-item>
<el-form-item label="邮箱" prop="email">
<el-input v-model="dataForm.email" placeholder="邮箱"></el-input>
</el-form-item>
<el-form-item label="手机号" prop="mobile">
<el-input v-model="dataForm.mobile" placeholder="手机号"></el-input>
</el-form-item>
<el-form-item label="角色" size="mini" prop="roleIdList">
<el-checkbox-group v-model="dataForm.roleIdList">
<el-checkbox v-for="role in roleList" :key="role.roleId" :label="role.roleId">{{ role.roleName }}</el-checkbox>
</el-checkbox-group>
</el-form-item>
<el-form-item label="状态" size="mini" prop="status">
<el-radio-group v-model="dataForm.status">
<el-radio :label="0">禁用</el-radio>
<el-radio :label="1">正常</el-radio>
</el-radio-group>
</el-form-item>
</el-form>
<span slot="footer" class="dialog-footer">
<el-button @click="visible = false">取消</el-button>
<el-button type="primary" @click="dataFormSubmit()"></el-button>
</span>
</el-dialog>
</template>
<script>
import { isEmail, isMobile } from '@/utils/validate'
export default {
data() {
var validatePassword = (rule, value, callback) => {
if (!this.dataForm.id && !/\S/.test(value)) {
callback(new Error('密码不能为空'))
} else {
callback()
}
}
var validateComfirmPassword = (rule, value, callback) => {
if (!this.dataForm.id && !/\S/.test(value)) {
callback(new Error('确认密码不能为空'))
} else if (this.dataForm.password !== value) {
callback(new Error('确认密码与密码输入不一致'))
} else {
callback()
}
}
var validateEmail = (rule, value, callback) => {
if (!isEmail(value)) {
callback(new Error('邮箱格式错误'))
} else {
callback()
}
}
var validateMobile = (rule, value, callback) => {
if (!isMobile(value)) {
callback(new Error('手机号格式错误'))
} else {
callback()
}
}
return {
visible: false,
roleList: [],
dataForm: {
id: 0,
userName: '',
password: '',
comfirmPassword: '',
salt: '',
email: '',
mobile: '',
roleIdList: [],
status: 1
},
dataRule: {
userName: [
{ required: true, message: '用户名不能为空', trigger: 'blur' }
],
password: [
{ validator: validatePassword, trigger: 'blur' }
],
comfirmPassword: [
{ validator: validateComfirmPassword, trigger: 'blur' }
],
email: [
{ required: true, message: '邮箱不能为空', trigger: 'blur' },
{ validator: validateEmail, trigger: 'blur' }
],
mobile: [
{ required: true, message: '手机号不能为空', trigger: 'blur' },
{ validator: validateMobile, trigger: 'blur' }
]
}
}
},
methods: {
init(id) {
this.dataForm.id = id || 0
this.$http({
url: this.$http.adornUrl('/sys/role/select'),
method: 'get',
params: this.$http.adornParams()
}).then(({ data }) => {
this.roleList = data && data.code === 200 ? data.list : []
}).then(() => {
this.visible = true
this.$nextTick(() => {
this.$refs['dataForm'].resetFields()
})
}).then(() => {
if (this.dataForm.id) {
this.$http({
url: this.$http.adornUrl(`/sys/user/info/${this.dataForm.id}`),
method: 'get',
params: this.$http.adornParams()
}).then(({ data }) => {
if (data && data.code === 200) {
this.dataForm.userName = data.user.username
this.dataForm.salt = data.user.salt
this.dataForm.email = data.user.email
this.dataForm.mobile = data.user.mobile
this.dataForm.roleIdList = data.user.roleIdList
this.dataForm.status = data.user.status
}
})
}
})
},
//
dataFormSubmit() {
this.$refs['dataForm'].validate((valid) => {
if (valid) {
this.$http({
url: this.$http.adornUrl(`/sys/user/${!this.dataForm.id ? 'save' : 'update'}`),
method: 'post',
data: this.$http.adornData({
'userId': this.dataForm.id || undefined,
'username': this.dataForm.userName,
'password': this.dataForm.password,
'salt': this.dataForm.salt,
'email': this.dataForm.email,
'mobile': this.dataForm.mobile,
'status': this.dataForm.status,
'roleIdList': this.dataForm.roleIdList
})
}).then(({ data }) => {
if (data && data.code === 200) {
this.$message({
message: '操作成功',
type: 'success',
duration: 1500,
onClose: () => {
this.visible = false
this.$emit('refreshDataList')
}
})
} else {
this.$message.error(data.msg)
}
})
}
})
}
}
}
</script>

@ -0,0 +1,140 @@
<template>
<div class="mod-user">
<el-form :inline="true" :model="dataForm" @keyup.enter.native="getDataList()">
<el-form-item>
<el-input v-model="dataForm.userName" placeholder="用户名" clearable></el-input>
</el-form-item>
<el-form-item>
<el-button @click="getDataList()"></el-button>
<el-button v-if="isAuth('sys:user:save')" type="primary" @click="addOrUpdateHandle()"></el-button>
<el-button v-if="isAuth('sys:user:delete')" type="danger" @click="deleteHandle()" :disabled="dataListSelections.length <= 0"></el-button>
</el-form-item>
</el-form>
<el-table :data="dataList" border v-loading="dataListLoading" @selection-change="selectionChangeHandle" style="width: 100%;">
<el-table-column type="selection" header-align="center" align="center" width="50">
</el-table-column>
<el-table-column prop="userId" header-align="center" align="center" width="80" label="ID">
</el-table-column>
<el-table-column prop="username" header-align="center" align="center" label="用户名">
</el-table-column>
<el-table-column prop="email" header-align="center" align="center" label="邮箱">
</el-table-column>
<el-table-column prop="mobile" header-align="center" align="center" label="手机号">
</el-table-column>
<el-table-column prop="status" header-align="center" align="center" label="状态">
<template slot-scope="scope">
<el-tag v-if="scope.row.status === 0" size="small" type="danger"></el-tag>
<el-tag v-else size="small">正常</el-tag>
</template>
</el-table-column>
<el-table-column fixed="right" header-align="center" align="center" width="150" label="操作">
<template slot-scope="scope">
<el-button v-if="isAuth('sys:user:update')" type="text" size="small" @click="addOrUpdateHandle(scope.row.userId)"></el-button>
<el-button v-if="isAuth('sys:user:delete')" type="text" size="small" @click="deleteHandle(scope.row.userId)"></el-button>
</template>
</el-table-column>
</el-table>
<el-pagination @size-change="sizeChangeHandle" @current-change="currentChangeHandle" :current-page="pageIndex" :page-sizes="[10, 20, 50, 100]" :page-size="pageSize" :total="totalCount" layout="total, sizes, prev, pager, next, jumper">
</el-pagination>
<!-- 弹窗, 新增 / 修改 -->
<add-or-update v-if="addOrUpdateVisible" ref="addOrUpdate" @refreshDataList="getDataList"></add-or-update>
</div>
</template>
<script>
import AddOrUpdate from './user-add-or-update'
export default {
data() {
return {
dataForm: {
userName: ''
},
dataList: [],
pageIndex: 1,
pageSize: 10,
totalCount: 0,
dataListLoading: false,
dataListSelections: [],
addOrUpdateVisible: false
}
},
components: {
AddOrUpdate
},
activated() {
this.getDataList()
},
methods: {
//
getDataList() {
this.dataListLoading = true
this.$http({
url: this.$http.adornUrl('/sys/user/list'),
method: 'get',
params: this.$http.adornParams({
'page': this.pageIndex,
'limit': this.pageSize,
'username': this.dataForm.userName
})
}).then(({ data }) => {
if (data && data.code === 200) {
this.dataList = data.page.list
this.totalCount = data.page.totalCount
} else {
this.dataList = []
this.totalCount = 0
}
this.dataListLoading = false
})
},
//
sizeChangeHandle(val) {
this.pageSize = val
this.pageIndex = 1
this.getDataList()
},
//
currentChangeHandle(val) {
this.pageIndex = val
this.getDataList()
},
//
selectionChangeHandle(val) {
this.dataListSelections = val
},
// /
addOrUpdateHandle(id) {
this.addOrUpdateVisible = true
this.$nextTick(() => {
this.$refs.addOrUpdate.init(id)
})
},
//
deleteHandle(id) {
var userIds = id ? [id] : this.dataListSelections.map(item => item.userId)
this.$confirm(`确定对[id=${userIds.join(',')}]进行[${id ? '删除' : '批量删除'}]操作?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
this.$http({
url: this.$http.adornUrl('/sys/user/delete'),
method: 'post',
data: this.$http.adornData(userIds, false)
}).then(({ data }) => {
if (data && data.code === 200) {
this.$message({
message: '操作成功',
type: 'success',
duration: 1500,
onClose: () => this.getDataList()
})
} else {
this.$message.error(data.msg)
}
})
}).catch(() => { })
}
}
}
</script>

@ -0,0 +1,54 @@
<template>
<el-dialog title="开发接入信息" :close-on-click-modal="false" :visible.sync="visible">
<div>
<div class="list-item"><span class="label">公众号:</span>{{account.name}}</div>
<div class="list-item"><span class="label">token:</span>{{account.token}}</div>
<div class="list-item"><span class="label">aesKey:</span>{{account.aesKey}}</div>
<div class="list-item">
<span class="label">接入链接:</span>
<span v-html="accessUrl"></span>
</div>
</div>
</el-dialog>
</template>
<script>
export default {
data() {
return {
visible: false,
account: {},
}
},
computed: {
accessUrl() {
let host = location.host;
if(/^(\d(.\d){3})|localhost/.test(host)){
host='<span class="text-red">正式域名</span>'
}
return location.protocol + '//' + host + '/wx/wx/msg/' + this.account.appid
}
},
methods: {
init(item) {
this.visible = true
if (item && item.appid) {
this.account = item
}
},
}
}
</script>
<style scoped>
.list-item{
line-height: 30px;
}
.label{
width: 100px;
display: inline-block;
margin-right: 10px;
font-weight: bold;
text-align: right;
}
</style>

@ -0,0 +1,118 @@
<template>
<el-dialog
title="新增/修改"
:close-on-click-modal="false"
:visible.sync="visible">
<el-form :model="dataForm" :rules="dataRule" ref="dataForm" @keyup.enter.native="dataFormSubmit()" label-width="100px">
<el-form-item label="公众号名称" prop="name">
<el-input v-model="dataForm.name" placeholder="公众号名称"></el-input>
</el-form-item>
<div class="padding text-gray">测试号可选择服务号不同类型账号是否认证可使用功能权限不同<a href="https://developers.weixin.qq.com/doc/offiaccount/Getting_Started/Explanation_of_interface_privileges.html">参考文档</a></div>
<el-row>
<el-col :span="12">
<el-form-item label="公众号类型" prop="type">
<el-select v-model="dataForm.type" placeholder="公众号类型">
<el-option v-for="(name,key) in ACCOUNT_TYPES" :key="name" :label="name" :value="key"></el-option>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="是否认证" prop="verified">
<el-switch v-model="dataForm.verified" placeholder="是否认证"></el-switch>
</el-form-item>
</el-col>
</el-row>
<el-form-item label="appid" prop="appid">
<el-input v-model="dataForm.appid" placeholder="appid"></el-input>
</el-form-item>
<el-form-item label="appsecret" prop="secret">
<el-input v-model="dataForm.secret" placeholder="appsecret"></el-input>
</el-form-item>
<el-form-item label="token" prop="token">
<el-input v-model="dataForm.token" placeholder="token"></el-input>
</el-form-item>
<el-form-item label="aesKey" prop="aesKey">
<el-input v-model="dataForm.aesKey" placeholder="aesKey可为空"></el-input>
</el-form-item>
</el-form>
<span slot="footer" class="dialog-footer">
<el-button @click="visible = false">取消</el-button>
<el-button type="primary" @click="dataFormSubmit()"></el-button>
</span>
</el-dialog>
</template>
<script>
import { mapState } from 'vuex'
export default {
data () {
return {
visible: false,
dataForm: {
appid: '',
name: '',
type:'2',
verified:true,
secret: '',
token: 'my_weixin_token_',
aesKey: ''
},
dataRule: {
name: [
{ required: true, message: '公众号名称不能为空', trigger: 'blur' }
],
appid: [
{ required: true, message: 'appid不能为空', trigger: 'blur' }
],
secret: [
{ required: true, message: 'appsecret不能为空', trigger: 'blur' }
]
}
}
},
computed: mapState({
ACCOUNT_TYPES: state=>state.wxAccount.ACCOUNT_TYPES
}),
methods: {
init (item) {
this.visible = true
if(item && item.appid){
this.dataForm = item
this.dataForm.type = item.type+''
}else{
this.$nextTick(() => {
this.$refs['dataForm'].resetFields()
})
}
},
//
dataFormSubmit () {
this.$refs['dataForm'].validate((valid) => {
if (valid) {
this.$http({
url: this.$http.adornUrl(`/manage/wxAccount/save`),
method: 'post',
data: this.$http.adornData(this.dataForm)
}).then(({data}) => {
if (data && data.code === 200) {
this.$message({
message: '操作成功',
type: 'success',
duration: 1500,
onClose: () => {
this.visible = false
this.$emit('refreshDataList')
}
})
} else {
this.$message.error(data.msg)
}
})
}
})
}
}
}
</script>

@ -1,101 +1,68 @@
<template>
<!-- 根据visible属性的值控制整个组件内容的显示与隐藏当visible为true时显示 -->
<div v-show="visible">
<!-- 定义一个表单绑定了dataForm数据模型使用了dataRule验证规则设置了表单尺寸为迷你型标签宽度为80px -->
<el-form :model="dataForm" :rules="dataRule" ref="dataForm" size="mini" label-width="80px">
<!-- 第一行布局使用el-row包裹 -->
<el-row>
<!-- 占12列用于放置文章标题相关的表单元素 -->
<el-col :span="12">
<!-- 文章标题的表单项设置了必填验证对应dataForm中的title属性 -->
<el-form-item label="文章标题" prop="title" required>
<!-- 使用el-input组件实现输入框双向绑定dataForm.title设置了最大长度为1024有占位提示文字 -->
<el-input v-model="dataForm.title" :maxlength="1024" placeholder="标题"></el-input>
</el-form-item>
</el-col>
<!-- 同样占12列用于放置文章类型相关的表单元素 -->
<el-col :span="12">
<!-- 文章类型的表单项设置了必填验证对应dataForm中的type属性 -->
<el-form-item label="文章类型" prop="type" required>
<!-- 使用el-select组件实现下拉选择框双向绑定dataForm.type有占位提示文字 -->
<el-select v-model="dataForm.type" placeholder="选择文章类型">
<!-- 循环遍历ARTICLE_TYPES对象生成下拉选项选项的标签显示为name值为key允许用户创建新的选项 -->
<el-option v-for="(name,key) in ARTICLE_TYPES" :key="name" :label="name" :value="key" allow-create></el-option>
</el-select>
</el-form-item>
</el-col>
</el-row>
<!-- 第二行布局 -->
<el-row>
<!-- 占12列用于放置一级目录相关的表单元素 -->
<el-col :span="12">
<!-- 一级目录的表单项对应dataForm中的category属性 -->
<el-form-item label="一级目录" prop="category">
<!-- 使用el-input组件实现输入框双向绑定dataForm.category设置了最大长度为50有占位提示文字 -->
<el-input :maxlength="50" v-model="dataForm.category" placeholder="一级目录"></el-input>
</el-form-item>
</el-col>
<!-- 占12列用于放置二级分类相关的表单元素 -->
<el-col :span="12">
<!-- 二级分类的表单项对应dataForm中的subCategory属性 -->
<el-form-item label="二级分类" prop="subCategory">
<!-- 使用el-input组件实现输入框双向绑定dataForm.subCategory设置了最大长度为50有占位提示文字 -->
<el-input :maxlength="50" v-model="dataForm.subCategory" placeholder="二级目录"></el-input>
</el-form-item>
</el-col>
</el-row>
<!-- 指向外链的表单项对应dataForm中的targetLink属性 -->
<el-form-item label="指向外链" prop="targetLink">
<!-- 使用el-input组件实现输入框双向绑定dataForm.targetLink有占位提示文字 -->
<el-input v-model="dataForm.targetLink" placeholder="指向外链"></el-input>
</el-form-item>
<!-- 摘要的表单项对应dataForm中的summary属性 -->
<el-form-item label="摘要" prop="summary">
<!-- 使用el-input组件实现文本域输入框双向绑定dataForm.summary设置了行数为3最大长度为512显示字数限制提示 -->
<el-input v-model="dataForm.summary" placeholder="摘要" type="textarea" rows="3" maxlength="512" show-word-limit></el-input>
</el-form-item>
<!-- 标签的表单项使用了自定义的tags-editor组件双向绑定dataForm.tags -->
<el-form-item label="标签" prop="tags">
<tags-editor v-model="dataForm.tags"></tags-editor>
</el-form-item>
<!-- 封面图的表单项对应dataForm中的image属性 -->
<el-form-item label="封面图" prop="image">
<!-- 使用el-input组件实现输入框双向绑定dataForm.image有占位提示文字 -->
<el-input v-model="dataForm.image" placeholder="图片链接">
<!-- 插入一个自定义的OssUploader组件用于上传图片上传成功后将返回值赋给dataForm.image -->
<OssUploader slot="append" @uploaded="dataForm.image=$event"></OssUploader>
</el-input>
</el-form-item>
<!-- 引入一个富文本编辑器组件双向绑定dataForm.content -->
<tinymce-editor ref="editor" v-model="dataForm.content"></tinymce-editor>
</el-form>
<!-- 距离顶部有一定间距靠右对齐的按钮区域 -->
<div class="margin-top text-right">
<!-- 取消按钮点击时通过$emit触发父组件的hide事件 -->
<el-button @click="$emit('hide')"></el-button>
<!-- 确定按钮类型为主要按钮点击时调用dataFormSubmit方法进行表单提交 -->
<el-button type="primary" @click="dataFormSubmit()"></el-button>
</div>
</div>
</template>
<script>
// VuexmapStateVuex
import { mapState } from 'vuex'
export default {
name: 'article-add-or-update',
name:'article-add-or-update',
components: {
// TinymceEditor
TinymceEditor: () => import("@/components/tinymce-editor"),
// tags-editor
tagsEditor: () => import("@/components/tags-editor"),
// OssUploaderOSS
OssUploader: () => import('../oss/oss-uploader')
},
props: {
visible: {
type: Boolean,
default: false
props:{
visible:{
type:Boolean,
default:false
}
},
data() {
@ -127,45 +94,37 @@ export default {
};
},
computed: mapState({
// VuexstatearticleARTICLE_TYPES
ARTICLE_TYPES: state => state.article.ARTICLE_TYPES
ARTICLE_TYPES: state=>state.article.ARTICLE_TYPES
}),
methods: {
init(id) {
// dataFormidid使
this.dataForm.id = id || "";
this.$nextTick(() => {
// DOM
this.$refs["dataForm"].resetFields();
if (id > 0) {
// id0HTTP
this.$http({
url: this.$http.adornUrl(`/manage/article/info/${this.dataForm.id}`),
method: "get",
params: this.$http.adornParams()
}).then(({ data }) => {
if (data && data.code === 200) {
// dataForm
this.dataForm = data.article;
this.dataForm=data.article;
this.dataForm.type = data.article.type + "";
}
});
}
});
},
//
//
dataFormSubmit() {
// valid
this.$refs["dataForm"].validate(valid => {
if (valid) {
// HTTP
this.$http({
url: this.$http.adornUrl(`/manage/article/save`),
method: "post",
data: this.$http.adornData(this.dataForm)
}).then(({ data }) => {
if (data && data.code === 200) {
// $emitrefreshDataListhide
this.$message({
message: "操作成功",
type: "success",
@ -176,7 +135,6 @@ export default {
}
});
} else {
//
this.$message.error(data.msg);
}
});
@ -186,14 +144,12 @@ export default {
imgUploadSuccess(response, file, fileList) {
console.log(response);
if (response.code == 200) {
// dataForm.image
this.dataForm.image = response.data;
console.log("this.article", this.article);
} else {
//
this.$message.warning(response.msg);
}
}
}
};
</script>
</script>

@ -1,56 +1,37 @@
<template>
<!-- 外层容器div -->
<div>
<!-- 根据条件控制显示内容当addOrUpdateVisible为false时显示以下部分 -->
<div v-show="!addOrUpdateVisible">
<!-- 内联表单绑定了dataForm数据模型监听回车键native修饰符表示监听原生的键盘事件按下时调用getDataList方法 -->
<el-form :inline="true" :model="dataForm" @keyup.enter.native="getDataList()">
<!-- 文章类型选择的表单项 -->
<el-form-item>
<!-- 使用el-select组件实现下拉选择框双向绑定dataForm.type有占位提示文字选项通过循环ARTICLE_TYPES生成 -->
<el-select v-model="dataForm.type" placeholder="选择文章类型">
<el-option v-for="(name,key) in ARTICLE_TYPES" :key="key" :label="name" :value="key" allow-create></el-option>
</el-select>
</el-form-item>
<!-- 文章标题输入的表单项使用el-input组件双向绑定dataForm.title有占位提示文字且可清空输入内容 -->
<el-form-item>
<el-input v-model="dataForm.title" placeholder="标题" clearable></el-input>
</el-form-item>
<!-- 操作按钮所在的表单项 -->
<el-form-item>
<!-- 查询按钮点击时先将当前页码设置为1然后调用getDataList方法获取数据 -->
<el-button @click="pageIndex=1;getDataList()"></el-button>
<!-- 新增按钮根据权限调用isAuth方法判断决定是否显示类型为主要按钮点击时调用addOrUpdateHandle方法 -->
<el-button v-if="isAuth('wx:article:save')" type="primary" @click="addOrUpdateHandle()"></el-button>
<!-- 批量删除按钮根据权限决定是否显示类型为危险按钮点击时调用deleteHandle方法当没有选中的数据项时禁用按钮 -->
<el-button v-if="isAuth('wx:article:delete')" type="danger" @click="deleteHandle()" :disabled="dataListSelections.length <= 0"></el-button>
</el-form-item>
</el-form>
<!-- el-table组件用于展示数据列表绑定了dataList数据显示边框加载数据时显示加载提示监听选择项变化事件 -->
<el-table :data="dataList" border v-loading="dataListLoading" @selection-change="selectionChangeHandle" style="width: 100%;">
<!-- 选择列用于多选操作设置了表头和内容的对齐方式以及宽度 -->
<el-table-column type="selection" header-align="center" align="center" width="50">
</el-table-column>
<!-- ID列对应dataList中数据项的id属性设置了表头和内容的对齐方式以及列标题 -->
<el-table-column prop="id" header-align="center" align="center" label="ID">
</el-table-column>
<!-- 文章类型列对应dataList中数据项的type属性使用formatter函数格式化显示内容设置了表头和内容的对齐方式以及列标题 -->
<el-table-column prop="type" header-align="center" align="center" :formatter="articleTypeFormat" label="文章类型">
</el-table-column>
<!-- 标题列对应dataList中数据项的title属性显示溢出提示设置了表头和内容的对齐方式以及列标题内容用a标签包裹可点击跳转 -->
<el-table-column prop="title" header-align="center" align="center" show-overflow-tooltip label="标题">
<a :href="scope.row.targetLink" slot-scope="scope">{{scope.row.title}}</a>
</el-table-column>
<!-- 一级分类列对应dataList中数据项的category属性设置了表头和内容的对齐方式以及列标题 -->
<el-table-column prop="category" header-align="center" align="center" label="一级分类">
</el-table-column>
<!-- 二级分类列对应dataList中数据项的subCategory属性设置了表头和内容的对齐方式以及列标题 -->
<el-table-column prop="subCategory" header-align="center" align="center" label="二级分类">
</el-table-column>
<!-- 打开次数列对应dataList中数据项的openCount属性设置了表头和内容的对齐方式以及列标题 -->
<el-table-column prop="openCount" header-align="center" align="center" label="打开次数">
</el-table-column>
<!-- 操作列固定在右侧设置了表头和内容的对齐方式宽度以及列标题通过插槽定义了修改和删除按钮 -->
<el-table-column fixed="right" header-align="center" align="center" width="150" label="操作">
<template slot-scope="scope">
<el-button type="text" size="small" @click="addOrUpdateHandle(scope.row.id)"></el-button>
@ -58,19 +39,16 @@
</template>
</el-table-column>
</el-table>
<!-- 分页组件绑定了相关的分页事件和属性用于切换每页显示数量当前页码等操作 -->
<el-pagination @size-change="sizeChangeHandle" @current-change="currentChangeHandle" :current-page="pageIndex" :page-sizes="[10, 20, 50, 100]" :page-size="pageSize" :total="totalCount" layout="total, sizes, prev, pager, next, jumper">
</el-pagination>
</div>
<!-- 新增/修改组件通过属性和事件与父组件进行交互 -->
<add-or-update :visible="addOrUpdateVisible" ref="addOr-update" @refreshDataList="getDataList" @hide="addOrUpdateVisible=false"></add-or-update>
<!-- 新增 / 修改 -->
<add-or-update :visible="addOrUpdateVisible" ref="addOrUpdate" @refreshDataList="getDataList" @hide="addOrUpdateVisible=false"></add-or-update>
</div>
</template>
<script>
// /
import AddOrUpdate from './article-add-or-update'
// VuexmapStateVuex
import { mapState } from 'vuex'
export default {
components: {
@ -92,19 +70,15 @@ export default {
}
},
computed: mapState({
// VuexstatearticleARTICLE_TYPES
ARTICLE_TYPES: state => state.article.ARTICLE_TYPES
ARTICLE_TYPES: state=>state.article.ARTICLE_TYPES
}),
mounted() {
// getDataList
this.getDataList()
},
methods: {
//
//
getDataList() {
// true
this.dataListLoading = true
// HTTPURL
this.$http({
url: this.$http.adornUrl('/manage/article/list'),
method: 'get',
@ -117,60 +91,52 @@ export default {
'order': 'desc'
})
}).then(({ data }) => {
//
if (data && data.code === 200) {
//
this.dataList = data.page.list
this.totalCount = data.page.totalCount
} else {
//
this.dataList = []
this.totalCount = 0
}
// false
this.dataListLoading = false
})
},
// 1
//
sizeChangeHandle(val) {
this.pageSize = val
this.pageIndex = 1
this.getDataList()
},
//
//
currentChangeHandle(val) {
this.pageIndex = val
this.getDataList()
},
//
//
selectionChangeHandle(val) {
this.dataListSelections = val
},
// //trueDOM/id
// /
addOrUpdateHandle(id) {
this.addOrUpdateVisible = true
this.$nextTick(() => {
this.$refs.addOrUpdate.init(id)
})
},
// idHTTP
//
deleteHandle(id) {
// id使id
var ids = id? [id] : this.dataListSelections.map(item => item.id)
this.$confirm(`确定对[id=${ids.join(',')}]进行[${id? '删除' : '批量删除'}]操作?`, '提示', {
var ids = id ? [id] : this.dataListSelections.map(item => item.id)
this.$confirm(`确定对[id=${ids.join(',')}]进行[${id ? '删除' : '批量删除'}]操作?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
// HTTP
this.$http({
url: this.$http.adornUrl('/manage/article/delete'),
method: 'post',
data: this.$http.adornData(ids, false)
}).then(({ data }) => {
//
if (data && data.code === 200) {
//
this.$message({
message: '操作成功',
type: 'success',
@ -178,16 +144,14 @@ export default {
onClose: () => this.getDataList()
})
} else {
//
this.$message.error(data.msg)
}
})
})
},
// ARTICLE_TYPES
articleTypeFormat(row, column, cellValue) {
return this.ARTICLE_TYPES[cellValue];
}
}
}
</script>
</script>

@ -0,0 +1,38 @@
<template>
<el-dialog title="选择素材" :visible.sync="dataVisible" :modal="true" append-to-body @close="onClose">
<material-news v-if="selectType=='news'" @selected="onSelect" selectMode></material-news>
<material-file v-else :fileType="selectType" @selected="onSelect" selectMode></material-file>
</el-dialog>
</template>
<script>
export default {
name:"assets-selector",
data:function (){
return {
dataVisible : this.visible
}
},
components:{
MaterialFile:()=>import('./material-file'),
MaterialNews:()=>import('./material-news')
},
props:{
selectType:{// imagevoicevideonews
type:String,
default:'image'
},
visible:{
type:Boolean,
default:false
}
},
methods:{
onSelect(itemInfo){
this.$emit('selected', itemInfo)
},
onClose(){
this.$emit('onClose')
}
}
}
</script>

@ -0,0 +1,103 @@
<template>
<el-dialog :title="!dataForm.id ? '新增' : '修改'" :close-on-click-modal="false" :visible.sync="visible">
<el-form :model="dataForm" :rules="dataRule" ref="dataForm" @keyup.enter.native="dataFormSubmit()" label-width="80px">
<el-form-item label="媒体文件">
<el-button type="primary">
选择文件
<input type="file" style="opacity: 0;height: 100%;position: absolute;left: 0;top: 0;" @change="onFileChange" />
</el-button>
<div>{{dataForm.file.name}}</div>
</el-form-item>
<el-form-item label="媒体类型" prop="mediaType">
<el-select v-model="dataForm.mediaType" placeholder="媒体类型" style="width:100%">
<el-option label="图片2M以内支持PNG\JPEG\JPG\GIF" value="image"></el-option>
<el-option label="视频10M以内只支持MP4" value="video"></el-option>
<el-option label="语音2M、60s以内支持AMR\MP3" value="voice"></el-option>
<el-option label="缩略图64K以内JPG" value="thumb"></el-option>
</el-select>
</el-form-item>
<el-form-item label="素材名称" prop="fileName">
<el-input v-model="dataForm.fileName" placeholder="为便于管理建议按用途分类+素材内容命名"></el-input>
</el-form-item>
</el-form>
<span slot="footer" class="dialog-footer">
<el-button @click="visible = false">取消</el-button>
<el-button type="primary" @click="dataFormSubmit()" :disabled="uploading">{{uploading?'提交中...':'提交'}}</el-button>
</span>
</el-dialog>
</template>
<script>
export default {
data() {
return {
visible: false,
uploading:false,
dataForm: {
mediaId: '',
file: '',
fileName: '',
mediaType: 'image'
},
dataRule: {
fileName: [
{ required: true, message: '素材名称不能为空', trigger: 'blur' }
],
mediaType: [
{ required: true, message: '素材类型不能为空', trigger: 'blur' }
]
}
}
},
methods: {
init(fileType) {
if(fileType)this.dataForm.mediaType=fileType
this.visible = true
},
//
dataFormSubmit() {
if(this.uploading)return
this.$refs['dataForm'].validate((valid) => {
if (valid) {
this.uploading=true
console.log(this.dataForm)
let form = new FormData();
form.append('mediaId', this.dataForm.mediaId || '')
form.append('file', this.dataForm.file)
form.append('fileName', this.dataForm.fileName)
form.append('mediaType', this.dataForm.mediaType)
this.$http({
url: this.$http.adornUrl(`/manage/wxAssets/materialFileUpload`),
method: 'post',
data: form,
headers: { 'Content-Type': 'multipart/form-data' }
}).then(({ data }) => {
if (data && data.code === 200) {
this.$message({
message: '操作成功',
type: 'success',
duration: 1500,
onClose: () => {
this.visible = false
this.$emit('refreshDataList')
}
})
} else {
this.$message.error(data.msg)
}
this.uploading=false
})
}
})
},
onFileChange(e) {
let file = event.currentTarget.files[0]
this.dataForm.file = file;
this.dataForm.fileName = file.name.substring(0, file.name.lastIndexOf('.'))
let mediaType = file.type.substring(0, file.type.lastIndexOf('/'))
if (mediaType == 'audio') mediaType = 'voice'
this.dataForm.mediaType = mediaType
}
}
}
</script>

@ -0,0 +1,185 @@
<template>
<div class="mod-menu">
<el-form :inline="true" :model="dataForm">
<el-form-item v-show="!selectMode">
<el-button size="mini" v-if="isAuth('wx:wxassets:save')" type="primary" @click="addOrUpdateHandle()"></el-button>
</el-form-item>
</el-form>
<div v-loading="dataListLoading">
<div class="card" v-for="item in dataList" :key="item.mediaId" @click="onSelect(item)">
<el-image v-if="fileType=='image'" class="card-image" :src="item.url" fit="contain" lazy></el-image>
<div v-else class="card-preview">
<div v-if="fileType=='voice'" class="card-preview-icon el-icon-microphone"></div>
<div v-if="fileType=='video'" class="card-preview-icon el-icon-video-camera-solid"></div>
<div class="card-preview-text">管理后台不支持预览<br/>微信中可正常播放</div>
</div>
<div class="card-footer">
<div class="text-cut-name">{{item.name}}</div>
<div>{{$moment(item.updateTime).calendar()}}</div>
<div class="flex justify-between align-center" v-show="!selectMode">
<el-button size="mini" type="text" icon="el-icon-copy-document" v-clipboard:copy="item.mediaId" v-clipboard:success="onCopySuccess" v-clipboard:error="onCopyError">复制media_id</el-button>
<el-button size="mini" type="text" icon="el-icon-delete" @click="deleteHandle(item.mediaId)" >删除</el-button>
</div>
</div>
</div>
</div>
<el-pagination @current-change="currentChangeHandle" :current-page="pageIndex" :page-sizes="[20]" :page-size="20" :total="totalCount" layout="total, prev,pager, next, jumper">
</el-pagination>
<!-- 弹窗, 新增 / 修改 -->
<add-or-update v-if="addOrUpdateVisible" ref="addOrUpdate" @refreshDataList="onChange"></add-or-update>
</div>
</template>
<script>
import AddOrUpdate from './material-file-add-or-update'
export default {
name:'material-file',
props:{
fileType:{// imagevoicevideo
type:String,
default:'image'
},
selectMode:{//
type:Boolean,
default:false
}
},
components: {
AddOrUpdate
},
data() {
return {
dataForm: {},
addOrUpdateVisible: false,
dataList: [],
pageIndex: 1,
pageSize: 20,
totalCount: 0,
dataListLoading: false,
}
},
mounted(){
this.init()
},
methods: {
init(){
if(!this.dataList.length){
this.getDataList()
}
},
getDataList() {
if(this.dataListLoading) return
this.dataListLoading = true
this.$http({
url: this.$http.adornUrl('/manage/wxAssets/materialFileBatchGet'),
params: this.$http.adornParams({
'page': this.pageIndex,
'type': this.fileType
})
}).then(({ data }) => {
if (data && data.code == 200) {
this.dataList = data.data.items
this.totalCount = data.data.totalCount
this.pageIndex++;
} else {
this.$message.error(data.msg);
}
this.dataListLoading = false
})
},
// /
addOrUpdateHandle() {
this.addOrUpdateVisible = true
this.$nextTick(() => {
this.$refs.addOrUpdate.init(this.fileType)
})
},
onSelect(itemInfo){
if(!this.selectMode)return
this.$emit('selected',itemInfo)
},
//
deleteHandle(id) {
this.$confirm(`确定对[mediaId=${id}]进行删除操作?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
this.$http({
url: this.$http.adornUrl('/manage/wxAssets/materialDelete'),
method: 'post',
data: { mediaId: id }
}).then(({ data }) => {
if (data && data.code === 200) {
this.$message({
message: '操作成功',
type: 'success',
duration: 1500,
onClose: () => this.onChange()
})
} else {
this.$message.error(data.msg)
}
})
})
},
//
currentChangeHandle(val) {
this.pageIndex = val
this.getDataList()
},
onCopySuccess(){
this.$message.success('已复制')
},
onCopyError(err){
this.$message.error('复制失败,可能是此浏览器不支持复制')
},
onChange(){
this.pageIndex=1
this.getDataList()
this.$emit('change')
}
}
}
</script>
<style scoped>
.card{
width: 170px;
display: inline-block;
background: #FFFFFF;
border: 1px solid #EBEEF5;
box-shadow: 1px 1px 20px 0 rgba(0, 0, 0, 0.1);
margin: 0 10px 10px 0;
vertical-align: top;
border-radius: 5px;
box-sizing: border-box;
}
.card:hover{
border: 2px solid #66b1ff;
margin-bottom: 6px;
}
.card-image{
line-height: 170px;
max-height: 170px;
width: 100%;
}
.card-preview{
padding: 20px 0;
color: #d9d9d9;
display: flex;
justify-content: center;
align-items: center;
}
.card-preview-icon{
font-size: 30px;
margin-right: 5px;
}
.card-preview-text{
font-size: 12px;
}
.card-footer{
color: #ccc;
font-size: 12px;
padding: 15px 10px;
}
</style>

@ -0,0 +1,221 @@
<template>
<div v-show="visible">
<div class="flex">
<div class="card-list">
<div class="text-center margin-bottom">图文列表</div>
<div class="card-item" :class="{'selected':selectedIndex==index}" v-for="(item,index) in articles" :key="index" @click="selectedIndex=index">
<div class="text-cut-name">{{item.title}}</div>
</div>
<div v-show="articles.length<8 && !mediaId" class="card-add el-icon-plus" @click="addArticle()"></div>
</div>
<el-form size="mini" v-if="articles.length" :model="articles[selectedIndex]" :rules="dataRule" ref="dataForm" label-width="100px">
<el-form-item label="标题" prop="title">
<el-input v-model="articles[selectedIndex].title" placeholder="标题"></el-input>
</el-form-item>
<el-form-item label="封面图" prop="thumbMediaId">
<el-input v-model="articles[selectedIndex].thumbMediaId" placeholder="封面图media_id">
<div slot="append" @click="assetsSelectorVisible=true"></div>
</el-input>
</el-form-item>
<el-form-item label="摘要" prop="digest">
<el-input v-model="articles[selectedIndex].digest" placeholder="摘要"></el-input>
</el-form-item>
<el-form-item label="原文地址" prop="contentSourceUrl">
<el-input v-model="articles[selectedIndex].contentSourceUrl" placeholder="阅读原文链接"></el-input>
</el-form-item>
<el-row>
<el-col :span="9">
<el-form-item label="作者" prop="author">
<el-input v-model="articles[selectedIndex].author" placeholder="作者"></el-input>
</el-form-item>
</el-col>
<el-col :span="5">
<el-form-item label="显示封面" prop="showCoverPic">
<el-switch v-model="articles[selectedIndex].showCoverPic"></el-switch>
</el-form-item>
</el-col>
<el-col :span="5">
<el-form-item label="允许评论" prop="needOpenComment">
<el-switch v-model="articles[selectedIndex].needOpenComment"></el-switch>
</el-form-item>
</el-col>
<el-col :span="5">
<el-form-item label="仅粉丝可评论" prop="onlyFansCanComment">
<el-switch v-model="articles[selectedIndex].onlyFansCanComment"></el-switch>
</el-form-item>
</el-col>
</el-row>
<el-form-item label="内容" prop="content">
<tinymce-editor ref="editor" v-model="articles[selectedIndex].content"> </tinymce-editor>
</el-form-item>
</el-form>
</div>
<div class="dialog-footer">
<el-button @click="$emit('hide')"></el-button>
<el-button type="primary" @click="dataFormSubmit()" :disabled="uploading">{{this.mediaId?'修改此篇':'全部提交(共'+articles.length+'篇)'}}</el-button>
</div>
<assets-selector v-if="assetsSelectorVisible" :visible="assetsSelectorVisible" selectType="image" @selected="onAssetsSelect"></assets-selector>
</div>
</template>
<script>
const articleTemplate={
templateId: 0,
title: '',
content: '',
author: '',
showCoverPic: true,
contentSourceUrl: '',
digest: '',
thumbMediaId: '',
needOpenComment: false,
onlyFansCanComment: false
}
export default {
components: {
TinymceEditor: () => import('@/components/tinymce-editor'),
AssetsSelector:()=>import('./assets-selector')
},
props:{
visible:{
type:Boolean,
default:false
}
},
data() {
return {
assetsSelectorVisible:false,
mediaId:'',
selectedIndex:0,
articles:[],
uploading:false,
dataRule: {
title: [
{ required: true, message: '标题不能为空', trigger: 'blur' }
],
content: [
{ required: true, message: '内容不能为空', trigger: 'blur' }
],
thumbMediaId: [
{ required: true, message: '封面图media_id不能为空', trigger: 'blur' }
],
contentSourceUrl: [
{ required: true, message: '原文地址不得为空', trigger: 'blur' }
]
}
}
},
methods: {
init(news){
if(news && news.mediaId){
this.mediaId=news.mediaId
this.articles = news.content.articles
}else{
this.mediaId=''
this.articles=[{...articleTemplate}]
}
},
//
dataFormSubmit() {
if(this.uploading)return
this.$refs['dataForm'].validate((valid) => {
if (valid) {
if(this.mediaId){//
this.materialArticleUpdate();
}else{ //
this.materialNewsUpload();
}
}
})
},
materialNewsUpload(){
this.uploading=true
this.$http({
url: this.$http.adornUrl(`/manage/wxAssets/materialNewsUpload`),
method: 'post',
data: this.$http.adornData(this.articles,false)
}).then(({ data }) => {
if (data && data.code === 200) {
this.$message({
message: "操作成功",
type: "success",
duration: 1500,
onClose: () => {
this.$emit("refreshDataList");
this.emit('hide')
}
});
} else {
this.$message.error(data.msg)
}
this.uploading=false
})
},
materialArticleUpdate(){
this.uploading=true
this.$http({
url: this.$http.adornUrl(`/manage/wxAssets/materialArticleUpdate`),
method: 'post',
data: this.$http.adornData({
'mediaId':this.mediaId,
'index':this.selectedIndex,
'articles':this.articles[this.selectedIndex]
})
}).then(({ data }) => {
if (data && data.code === 200) {
this.$message.success('操作成功')
} else {
this.$message.error(data.msg)
}
this.uploading=false
})
},
addArticle(){
this.articles.push({...articleTemplate})
this.selectedIndex=this.articles.length-1
},
onAssetsSelect(assetsInfo){
Vue.set(this.articles[this.selectedIndex], 'thumbMediaId', assetsInfo.mediaId)
this.assetsSelectorVisible=false
}
}
}
</script>
<style scoped>
.card-list{
width: 300px;
padding-right: 10px;
border-right: 1px solid #eeeeee;
}
.card-item{
margin-top: 2px;
padding: 20px 5px;
border: 1px solid #ddd;
font-size: 12px;
line-height: 15px;
}
.card-item.selected{
border: 2px solid #409EFF;
}
.text-cut-name{
display: -webkit-box;
word-wrap:break-word;
word-break:break-all;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
overflow: hidden;
}
.card-add{
margin-top: 2px;
display: block;
border: 1px dotted #ddd;
color: #ddd;
text-align: center;
font-size: 30px;
line-height: 50px;
}
.dialog-footer {
margin-top: 20px;
text-align: right;
}
</style>

@ -1,45 +1,26 @@
<template>
<!-- 整个页面的面板容器 -->
<div class="panel">
<!-- 根据条件控制显示内容当addOrUpdateVisible为false时显示以下内容 -->
<div v-show="!addOrUpdateVisible">
<!-- 内联表单绑定了dataForm数据模型 -->
<el-form :inline="true" :model="dataForm">
<!-- 根据selectMode的值决定是否显示该表单元素当不是选择模式时显示 -->
<el-form-item v-show="!selectMode">
<!-- 按钮根据权限判断是否显示调用isAuth方法判断类型为主要按钮点击时调用addOrUpdateHandle方法按钮尺寸为迷你型 -->
<el-button size="mini" v-if="isAuth('wx:wxassets:save')" type="primary" @click="addOrUpdateHandle()"></el-button>
</el-form-item>
</el-form>
<!-- 加载提示容器根据dataListLoading的值显示加载状态 -->
<div class="flex justify-start" v-loading="dataListLoading">
<!-- 循环生成行n表示行号这里的rows应该是控制每行显示的元素数量相关 -->
<div v-for="n in rows" :key="n">
<!-- 循环遍历dataList数据列表用于展示具体的数据项 -->
<template v-for="(item,i) in dataList">
<!-- 根据条件显示卡片元素当满足i%rows==n-1时显示点击卡片会调用onSelect方法传递当前项数据 -->
<div class="card" :key="item.mediaId" v-if="i%rows==n-1" @click="onSelect(item)">
<!-- 卡片的预览部分用于展示文章相关信息 -->
<div class="card-preview">
<!-- 循环遍历文章数组展示每篇文章的信息点击文章链接会在新标签页打开target="_blank" -->
<a v-for="(article,k) in item.content.articles" :key="k" :href="article.url" class="article-item" target="_blank">
<!-- 文章标题做了多行文本截断显示处理 -->
<div class="article-title">{{article.title}}</div>
<!-- 文章缩略图通过绑定src属性显示图片 -->
<el-image class="article-thumb" :src="article.thumbUrl"></el-image>
</a>
</div>
<!-- 卡片的底部部分用于展示更新时间以及操作按钮等 -->
<div class="card-footer">
<!-- 显示更新时间使用了moment.js库通过$moment方法调用将时间格式化为日历格式 -->
<div>{{$moment(item.updateTime).calendar()}}</div>
<!-- 根据是否是选择模式决定是否显示以下操作按钮 -->
<div class="flex justify-between align-center" v-show="!selectMode">
<!-- 复制按钮点击复制media_id复制成功和失败分别调用对应的方法 -->
<el-button size="mini" type="text" icon="el-icon-copy-document" v-clipboard:copy="item.mediaId" v-clipboard:success="onCopySuccess" v-clipboard:error="onCopyError">复制media_id</el-button>
<!-- 编辑按钮点击调用addOrUpdateHandle方法传入当前项数据按钮类型为文本样式 -->
<el-button size="mini" type="text" icon="el-icon-edit" @click="addOrUpdateHandle(item)"></el-button>
<!-- 删除按钮点击调用deleteHandle方法传入mediaId进行删除操作按钮类型为文本样式 -->
<el-button size="mini" type="text" icon="el-icon-delete" @click="deleteHandle(item.mediaId)"></el-button>
</div>
</div>
@ -47,23 +28,20 @@
</template>
</div>
</div>
<!-- 分页组件绑定了相关的分页事件和属性用于切换页面获取不同页的数据 -->
<el-pagination @current-change="currentChangeHandle" :current-page="pageIndex" :page-size="pageSize" :total="totalCount" layout="total, prev,pager, next, jumper">
</el-pagination>
</div>
<!-- 新增/修改组件通过属性和事件与父组件进行交互 -->
<!-- 新增 / 修改 -->
<add-or-update :visible="addOrUpdateVisible" ref="addOrUpdate" @refreshDataList="onChange" @hide="addOrUpdateVisible=false"></add-or-update>
</div>
</template>
<script>
// /
import AddOrUpdate from './material-news-add-or-update'
export default {
name: 'material-news',
components: {
AddOrUpdate
},
//
props: {
selectMode: {//
type: Boolean,
@ -86,63 +64,50 @@ export default {
}
},
mounted(){
// init
this.init();
},
methods: {
init() {
// getDataList
if (!this.dataList.length) {
this.getDataList()
}
},
getDataList() {
//
if (this.dataListLoading) return
// true
this.dataListLoading = true
// HTTPURL
this.$http({
url: this.$http.adornUrl('/manage/wxAssets/materialNewsBatchGet'),
params: this.$http.adornParams({
'page': this.pageIndex
})
}).then(({ data }) => {
//
if (data.code == 200) {
//
this.dataList = data.data.items
this.totalCount = data.data.totalCount
} else {
//
this.$message.error(data.msg);
}
// false
this.dataListLoading = false
})
},
onSelect(itemInfo) {
// $emitselected
if (!this.selectMode) return
this.$emit('selected', itemInfo)
},
//
//
deleteHandle(id) {
//
this.$confirm(`确定对[mediaId=${id}]进行删除操作?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
// HTTP
this.$http({
url: this.$http.adornUrl('/manage/wxAssets/materialDelete'),
method: 'post',
data: { mediaId: id }
}).then(({ data }) => {
//
if (data && data.code === 200) {
// onChange
this.$message({
message: '操作成功',
type: 'success',
@ -150,18 +115,17 @@ export default {
onClose: () => this.onChange()
})
} else {
//
this.$message.error(data.msg)
}
})
})
},
//
//
currentChangeHandle(val) {
this.pageIndex = val
this.getDataList()
},
// //trueDOM/
// /
addOrUpdateHandle(news) {
this.addOrUpdateVisible = true
this.$nextTick(() => {
@ -169,15 +133,12 @@ export default {
})
},
onCopySuccess() {
//
this.$message.success('已复制')
},
onCopyError(err) {
//
this.$message.error('复制失败,可能是此浏览器不支持复制')
},
onChange() {
// 1$emitchange
this.pageIndex=1
this.getDataList()
this.$emit('change')

@ -1,22 +1,14 @@
<template>
<!-- el-dialog组件用于弹出对话框根据dataForm中是否有id来设置标题为新增修改点击模态框背景不关闭对话框通过visible属性双向绑定控制显示隐藏 -->
<el-dialog :title="!dataForm.id? '新增' : '修改'" :close-on-click-modal="false" :visible.sync="visible" >
<!-- el-form组件定义表单绑定dataForm数据模型应用dataRule验证规则设置标签宽度为80px -->
<el-dialog :title="!dataForm.id ? '新增' : '修改'" :close-on-click-modal="false" :visible.sync="visible" >
<el-form :model="dataForm" :rules="dataRule" ref="dataForm" label-width="80px">
<!-- 规则名称的表单项对应dataForm中的ruleName属性设置了必填验证 -->
<el-form-item label="规则名称" prop="ruleName">
<!-- el-input组件实现输入框双向绑定dataForm.ruleName有占位提示文字 -->
<el-input v-model="dataForm.ruleName" placeholder="规则名称"></el-input>
</el-form-item>
<!-- 匹配词的表单项使用自定义的tags-editor组件双向绑定dataForm.matchValue用于输入或编辑匹配词可能是多个类似标签形式 -->
<el-form-item label="匹配词" prop="matchValue">
<tags-editor v-model="dataForm.matchValue"></tags-editor>
</el-form-item>
<!-- 第一行布局使用el-row包裹 -->
<el-row>
<!-- 占12列用于放置作用范围相关的表单元素 -->
<el-col :span="12">
<!-- 作用范围的表单项对应dataForm中的appid属性通过下拉选择框选择有全部公众号和当前公众号两个选项 -->
<el-form-item label="作用范围" prop="appid">
<el-select v-model="dataForm.appid" placeholder="作用范围">
<el-option label="全部公众号" value=""></el-option>
@ -24,91 +16,71 @@
</el-select>
</el-form-item>
</el-col>
<!-- 同样占12列用于放置精确匹配相关的表单元素使用el-switch组件实现开关切换功能双向绑定dataForm.exactMatch -->
<el-col :span="12">
<el-form-item label="精确匹配" prop="exactMatch">
<el-switch v-model="dataForm.exactMatch" :active-value="true" :inactive-value="false"></el-switch>
</el-form-item>
</el-col>
</el-row>
<!-- 第二行布局 -->
<el-row>
<!-- 占12列用于放置回复类型相关的表单元素 -->
<el-col :span="12">
<!-- 回复类型的表单项对应dataForm中的replyType属性通过下拉选择框选择选项通过循环KefuMsgType生成选择改变时触发onReplyTypeChange方法 -->
<el-form-item label="回复类型" prop="replyType">
<el-select v-model="dataForm.replyType" @change="onReplyTypeChange">
<el-option v-for="(name,key) in KefuMsgType" :key="key" :value="key" :label="name"></el-option>
</el-select>
</el-form-item>
</el-col>
<!-- 占12列用于放置是否启用相关的表单元素使用el-switch组件实现开关切换功能双向绑定dataForm.status -->
<el-col :span="12">
<el-form-item label="是否启用" prop="status">
<el-switch v-model="dataForm.status" :active-value="true" :inactive-value="false"></el-switch>
</el-form-item>
</el-col>
</el-row>
<!-- 第三行布局 -->
<el-row>
<!-- 占12列用于放置生效时间相关的表单元素使用el-time-picker组件实现时间选择功能绑定dataForm.effectTimeStart设置时间格式 -->
<el-col :span="12">
<el-form-item label="生效时间" prop="effectTimeStart">
<el-time-picker v-model="dataForm.effectTimeStart" value-format="HH:mm:ss"></el-time-picker>
</el-form-item>
</el-col>
<!-- 占12列用于放置失效时间相关的表单元素使用el-time-picker组件实现时间选择功能绑定dataForm.effectTimeEnd设置时间格式 -->
<el-col :span="12">
<el-form-item label="失效时间" prop="effectTimeEnd">
<el-time-picker v-model="dataForm.effectTimeEnd" value-format="HH:mm:ss"></el-time-picker>
</el-form-item>
</el-col>
</el-row>
<!-- 回复内容的表单项对应dataForm中的replyContent属性使用el-input组件实现文本域输入框双向绑定dataForm.replyContent有行数设置和占位提示文字 -->
<el-form-item label="回复内容" prop="replyContent">
<el-input v-model="dataForm.replyContent" type="textarea" :rows="5" placeholder="文本、图文ID、media_id、json配置"></el-input>
<!-- 当回复类型为文本时显示插入链接按钮点击调用addLink方法在回复内容中添加链接 -->
<el-button type="text" v-show="'text'==dataForm.replyType" @click="addLink"></el-button>
<!-- 根据assetsType的值决定是否显示按钮点击打开素材选择器通过控制assetsSelectorVisible属性按钮文字根据回复类型有所不同 -->
<el-button type="text" v-show="assetsType" @click="assetsSelectorVisible=true">
从素材库中选择<span v-if="'miniprogrampage'==dataForm.replyType || 'music'==dataForm.replyType"></span>
</el-button>
</el-form-item>
<!-- 备注说明的表单项对应dataForm中的desc属性使用el-input组件实现输入框双向绑定dataForm.desc有占位提示文字 -->
<el-form-item label="备注说明" prop="desc">
<el-input v-model="dataForm.desc" placeholder="备注说明"></el-input>
</el-form-item>
</el-form>
<!-- 对话框底部的按钮区域通过插槽定义 -->
<span slot="footer" class="dialog-footer">
<!-- 取消按钮点击时设置visible为false关闭对话框 -->
<el-button @click="visible = false">取消</el-button>
<!-- 确定按钮类型为主要按钮点击时调用dataFormSubmit方法进行表单提交 -->
<el-button type="primary" @click="dataFormSubmit()"></el-button>
</span>
<!-- 素材选择器组件根据条件控制显示通过属性和事件与当前组件进行交互用于选择素材相关操作 -->
<assets-selector v-if="assetsSelectorVisible && assetsType" :visible="assetsSelectorVisible" :selectType="assetsType" @selected="onAssetsSelect" @onClose="assetsSelectorVisible=false"></assets-selector>
</el-dialog>
</template>
<script>
// VuexmapStateVuex
import { mapState } from 'vuex'
export default {
components: {
// tags-editor
tagsEditor: () => import('@/components/tags-editor'),
// AssetsSelector
AssetsSelector: () => import('./assets/assets-selector')
AssetsSelector:()=>import('./assets/assets-selector')
},
data() {
return {
visible: false,
assetsSelectorVisible: false,
assetsSelectorVisible:false,
dataForm: {
// ID0
ruleId: 0,
appid: '',
appid:'',
ruleName: "",
exactMatch: false,
matchValue: "",
@ -120,31 +92,24 @@ export default {
effectTimeEnd: "23:59:59"
},
dataRule: {
// blur
ruleName: [
{ required: true, message: "规则名称不能为空", trigger: "blur" }
],
//
matchValue: [
{ required: true, message: "匹配的关键词、事件等不能为空", trigger: "blur" }
],
//
replyType: [
{ required: true, message: "回复类型1:文本2:图文3媒体不能为空", trigger: "blur" }
],
//
replyContent: [
{ required: true, message: "回复内容不能为空", trigger: "blur" }
],
//
status: [
{ required: true, message: "是否有效不能为空", trigger: "blur" }
],
//
effectTimeStart: [
{ required: true, message: "生效起始时间不能为空", trigger: "blur" }
],
//
effectTimeEnd: [
{ required: true, message: "生效结束时间不能为空", trigger: "blur" }
]
@ -152,60 +117,49 @@ export default {
};
},
computed: mapState({
// VuexstatemessageKefuMsgType
KefuMsgType: state => state.message.KefuMsgType,
// VuexstatewxAccountselectedAppid
selectedAppid: state => state.wxAccount.selectedAppid,
assetsType() {
const config = {
//
'image': 'image',
'voice': 'voice',
'video': 'video',
'mpnews': 'news',
'miniprogrampage': 'image', //
'music': 'image'
};
return config[this.dataForm.replyType] || '';
KefuMsgType: state=>state.message.KefuMsgType,
selectedAppid:state=>state.wxAccount.selectedAppid,
assetsType(){
const config={//
'image':'image',
'voice':'voice',
'video':'video',
'mpnews':'news',
'miniprogrampage':'image',//
'music':'image'
}
return config[this.dataForm.replyType] || ''
}
}),
methods: {
init(id) {
// dataFormruleIdid0
this.dataForm.ruleId = id || 0;
// true
this.visible = true;
this.$nextTick(() => {
// DOM
this.$refs["dataForm"].resetFields();
if (this.dataForm.ruleId) {
// ruleIdHTTP
this.$http({
url: this.$http.adornUrl(`/manage/msgReplyRule/info/${this.dataForm.ruleId}`),
url: this.$http.adornUrl( `/manage/msgReplyRule/info/${this.dataForm.ruleId}` ),
method: "get",
params: this.$http.adornParams()
}).then(({ data }) => {
if (data && data.code === 200) {
// dataForm
this.dataForm = data.msgReplyRule;
}
});
}
});
},
//
//
dataFormSubmit() {
// valid
this.$refs["dataForm"].validate(valid => {
if (valid) {
// HTTPruleId
this.$http({
url: this.$http.adornUrl(`/manage/msgReplyRule/${!this.dataForm.ruleId? "save" : "update"}`),
url: this.$http.adornUrl(`/manage/msgReplyRule/${!this.dataForm.ruleId ? "save" : "update"}`),
method: "post",
data: this.$http.adornData(this.dataForm)
}).then(({ data }) => {
if (data && data.code === 200) {
// $emitrefreshDataList
this.$message({
message: "操作成功",
type: "success",
@ -216,7 +170,6 @@ export default {
}
});
} else {
//
this.$message.error(data.msg);
}
});
@ -224,11 +177,9 @@ export default {
});
},
addLink() {
//
this.dataForm.replyContent += '<a href="链接地址">链接文字</a>'
},
onReplyTypeChange(value) {
// JSON便使
if ("miniprogrampage" == value) {
let demo = { title: "标题", appid: "小程序APPID", pagepath: "页面地址", thumb_media_id: "缩略图media_id" };
this.dataForm.replyContent = JSON.stringify(demo, null, 4)
@ -239,22 +190,21 @@ export default {
let demo = { head_content: "开头文字", list: [{ id: "菜单1ID", content: "菜单2内容" }, { id: "菜单2ID", content: "菜单2内容" }, { id: "菜单nID", content: "菜单n内容" }], tail_content: "结尾文字" }
this.dataForm.replyContent = JSON.stringify(demo, null, 4)
} else if ("news" == value) {
let demo = { title: "文章标题", description: "文章简介", url: "链接URL", picUrl: "缩略图URL" }
let demo={title:"文章标题",description:"文章简介",url:"链接URL",picUrl:"缩略图URL"}
this.dataForm.replyContent = JSON.stringify(demo, null, 4)
} else {
this.dataForm.replyContent = '媒体素材media_id'
}
},
onAssetsSelect(assetsInfo) {
// media_idmediaId
if (this.dataForm.replyType =='miniprogrampage' || this.dataForm.replyType =='music') {
let data = JSON.parse(this.dataForm.replyContent);
if (data && data.thumb_media_id) data.thumb_media_id = assetsInfo.mediaId;
this.dataForm.replyContent = JSON.stringify(data, null, 4);
} else {
this.dataForm.replyContent = assetsInfo.mediaId;
onAssetsSelect(assetsInfo){
if(this.dataForm.replyType=='miniprogrampage' || this.dataForm.replyType=='music'){
let data = JSON.parse(this.dataForm.replyContent)
if(data && data.thumb_media_id)data.thumb_media_id=assetsInfo.mediaId
this.dataForm.replyContent = JSON.stringify(data, null, 4)
}else{
this.dataForm.replyContent = assetsInfo.mediaId
}
this.assetsSelectorVisible = false;
this.assetsSelectorVisible=false
}
}
};

@ -1,72 +1,50 @@
<template>
<!-- 整个模块的容器类用于包裹后续的表单表格分页等组件 -->
<div class="mod-config">
<!-- 内联表单绑定了dataForm数据模型监听回车键native修饰符表示监听原生的键盘事件按下时调用getDataList方法 -->
<el-form :inline="true" :model="dataForm" @keyup.enter.native="getDataList()">
<!-- 匹配关键词输入框所在的表单项 -->
<el-form-item>
<!-- 使用el-input组件实现输入框双向绑定dataForm.matchValue有占位提示文字且可清空输入内容 -->
<el-input v-model="dataForm.matchValue" placeholder="匹配关键词" clearable></el-input>
</el-form-item>
<!-- 操作按钮所在的表单项 -->
<el-form-item>
<!-- 查询按钮点击时调用getDataList方法获取符合条件的数据列表 -->
<el-button @click="getDataList()"></el-button>
<!-- 新增按钮根据权限调用isAuth方法判断决定是否显示类型为主要按钮点击时调用addOrUpdateHandle方法 -->
<el-button v-if="isAuth('wx:msgreplyrule:save')" type="primary" @click="addOrUpdateHandle()"></el-button>
<!-- 批量删除按钮根据权限决定是否显示类型为危险按钮点击时调用deleteHandle方法当没有选中的数据项时禁用按钮 -->
<el-button v-if="isAuth('wx:msgreplyrule:delete')" type="danger" @click="deleteHandle()" :disabled="dataListSelections.length <= 0"></el-button>
</el-form-item>
</el-form>
<!-- el-table组件用于展示数据列表绑定了dataList数据显示边框可展开行type="expand"加载数据时显示加载提示监听选择项变化事件 -->
<el-table :data="dataList" border type="expand" v-loading="dataListLoading" @selection-change="selectionChangeHandle" style="width: 100%;">
<!-- 展开列通过插槽定义展开内容这里展示了更多规则相关详细信息的表单 -->
<el-table-column type="expand">
<template slot-scope="props">
<el-form label-position="left" inline class="demo-table-expand">
<!-- 作用范围信息展示的表单项根据数据项中的appid值判断显示当前公众号全部公众号 -->
<el-form-item label="作用范围">
<span>{{ props.row.appid? '当前公众号' : '全部公众号' }}</span>
<span>{{ props.row.appid?'当前公众号':'全部公众号' }}</span>
</el-form-item>
<!-- 精确匹配信息展示的表单项根据数据项中的exactMatch值判断显示 -->
<el-form-item label="精确匹配">
<span>{{ props.row.exactMatch? '是' : '否' }}</span>
<span>{{ props.row.exactMatch?'是':'否' }}</span>
</el-form-item>
<!-- 是否有效信息展示的表单项根据数据项中的status值判断显示 -->
<el-form-item label="是否有效">
<span>{{ props.row.status? '是' : '否' }}</span>
<span>{{ props.row.status?'是':'否' }}</span>
</el-form-item>
<!-- 备注说明信息展示的表单项直接显示数据项中的desc值 -->
<el-form-item label="备注说明">
<span>{{ props.row.desc }}</span>
</el-form-item>
<!-- 生效时间信息展示的表单项直接显示数据项中的effectTimeStart值 -->
<el-form-item label="生效时间">
<span>{{ props.row.effectTimeStart }}</span>
</el-form-item>
<!-- 失效时间信息展示的表单项直接显示数据项中的effectTimeEnd值 -->
<el-form-item label="失效时间">
<span>{{ props.row.effectTimeEnd }}</span>
</el-form-item>
</el-form>
</template>
</el-table-column>
<!-- 选择列用于多选操作设置了表头和内容的对齐方式以及宽度 -->
<el-table-column type="selection" header-align="center" align="center" width="50">
</el-table-column>
<!-- 规则名称列对应dataList中数据项的ruleName属性显示溢出提示设置了表头和内容的对齐方式以及列标题 -->
<el-table-column prop="ruleName" header-align="center" align="center" show-overflow-tooltip label="规则名称">
</el-table-column>
<!-- 匹配关键词列对应dataList中数据项的matchValue属性显示溢出提示设置了表头和内容的对齐方式以及列标题 -->
<el-table-column prop="matchValue" header-align="center" align="center" show-overflow-tooltip label="匹配关键词">
</el-table-column>
<!-- 消息类型列对应dataList中数据项的replyType属性使用formatter函数格式化显示内容设置了表头和内容的对齐方式以及列标题 -->
<el-table-column prop="replyType" header-align="center" align="center" :formatter="replyTypeFormat" label="消息类型">
</el-table-column>
<!-- 回复内容列对应dataList中数据项的replyContent属性显示溢出提示设置了表头和内容的对齐方式以及列标题 -->
<el-table-column prop="replyContent" header-align="center" align="center" show-overflow-tooltip label="回复内容">
</el-table-column>
<!-- 操作列固定在右侧设置了表头和内容的对齐方式宽度以及列标题通过插槽定义了修改和删除按钮 -->
<el-table-column fixed="right" header-align="center" align="center" width="150" label="操作">
<template slot-scope="scope">
<el-button type="text" size="small" @click="addOrUpdateHandle(scope.row.ruleId)"></el-button>
@ -74,18 +52,15 @@
</template>
</el-table-column>
</el-table>
<!-- 分页组件绑定了相关的分页事件和属性用于切换每页显示数量当前页码等操作 -->
<el-pagination @size-change="sizeChangeHandle" @current-change="currentChangeHandle" :current-page="pageIndex" :page-sizes="[10, 20, 50, 100]" :page-size="pageSize" :total="totalCount" layout="total, sizes, prev, pager, next, jumper">
</el-pagination>
<!-- 新增/修改组件通过属性和事件与父组件进行交互根据addOrUpdateVisible的值决定是否显示 -->
<!-- 弹窗, 新增 / 修改 -->
<add-or-update v-if="addOrUpdateVisible" ref="addOrUpdate" @refreshDataList="getDataList"></add-or-update>
</div>
</template>
<script>
// /
import AddOrUpdate from './msg-reply-rule-add-or-update'
// VuexmapStateVuex
import { mapState } from 'vuex'
export default {
components: {
@ -106,20 +81,16 @@ export default {
}
},
computed: mapState({
// VuexstatemessageKefuMsgType
KefuMsgType: state => state.message.KefuMsgType
KefuMsgType: state=>state.message.KefuMsgType
}),
activated() {
// getDataList
this.getDataList()
},
methods: {
//
//
getDataList() {
// true
this.dataListLoading = true
// HTTPURL
this.$http({
url: this.$http.adornUrl('/manage/msgReplyRule/list'),
method: 'get',
@ -129,60 +100,52 @@ export default {
'matchValue': this.dataForm.matchValue
})
}).then(({ data }) => {
//
if (data && data.code === 200) {
//
this.dataList = data.page.list
this.totalCount = data.page.totalCount
} else {
//
this.dataList = []
this.totalCount = 0
}
// false
this.dataListLoading = false
})
},
// 1
//
sizeChangeHandle(val) {
this.pageSize = val
this.pageIndex = 1
this.getDataList()
},
//
//
currentChangeHandle(val) {
this.pageIndex = val
this.getDataList()
},
//
//
selectionChangeHandle(val) {
this.dataListSelections = val
},
// //trueDOM/id
// /
addOrUpdateHandle(id) {
this.addOrUpdateVisible = true
this.$nextTick(() => {
this.$refs.addOrUpdate.init(id)
})
},
// idHTTP
//
deleteHandle(id) {
// id使ruleId
var ids = id? [id] : this.dataListSelections.map(item => item.ruleId)
this.$confirm(`确定对[id=${ids.join(',')}]进行[${id? '删除' : '批量删除'}]操作?`, '提示', {
var ids = id ? [id] : this.dataListSelections.map(item => item.ruleId)
this.$confirm(`确定对[id=${ids.join(',')}]进行[${id ? '删除' : '批量删除'}]操作?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
// HTTP
this.$http({
url: this.$http.adornUrl('/manage/msgReplyRule/delete'),
method: 'post',
data: this.$http.adornData(ids, false)
}).then(({ data }) => {
//
if (data && data.code === 200) {
//
this.$message({
message: '操作成功',
type: 'success',
@ -190,16 +153,14 @@ export default {
onClose: () => this.getDataList()
})
} else {
//
this.$message.error(data.msg)
}
})
})
},
// KefuMsgType
replyTypeFormat(row, column, cellValue) {
return this.KefuMsgType[cellValue];
}
}
}
</script>
</script>

@ -0,0 +1,165 @@
<template>
<el-dialog title="模板配置" :close-on-click-modal="false" :visible.sync="visible">
<el-form :model="dataForm" :rules="dataRule" ref="dataForm" label-width="100px" size="mini">
<el-form-item label="标题" prop="title">
<el-input v-model="dataForm.title" placeholder="标题"></el-input>
</el-form-item>
<el-form-item label="链接" prop="url">
<el-input v-model="dataForm.url" placeholder="跳转链接"></el-input>
</el-form-item>
<div>
<el-form-item label="小程序appid" prop="miniprogram.appid">
<el-input v-model="dataForm.miniprogram.appid" placeholder="小程序appid"></el-input>
</el-form-item>
<el-form-item label="小程序路径" prop="miniprogram.pagePath">
<el-input v-model="dataForm.miniprogram.pagePath" placeholder="小程序pagePath"></el-input>
</el-form-item>
</div>
<el-row>
<el-col :span="16">
<el-form-item label="模版名称" prop="name">
<el-input v-model="dataForm.name" placeholder="模版名称"></el-input>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="有效" prop="status">
<el-switch v-model="dataForm.status" placeholder="是否有效" :active-value="true" :inactive-value="false"></el-switch>
</el-form-item>
</el-col>
</el-row>
<div class="form-group-area">
<el-form-item class="form-group-title">消息填充数据请对照模板内容填写</el-form-item>
<el-form-item>
<el-input type="textarea" disabled autosize v-model="dataForm.content" placeholder="模版"></el-input>
</el-form-item>
<el-row v-for="(item,index) in dataForm.data" :key="item.name">
<el-col :span="16">
<el-form-item :label="item.name" :prop="'data.'+index+'.value'" :rules="[{required: true,message: '填充内容不得为空', trigger: 'blur' }]">
<el-input type="textarea" autosize rows="1" v-model="item.value" placeholder="填充内容" ></el-input>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="颜色" >
<el-input type="color" v-model="item.color" placeholder="颜色"></el-input>
</el-form-item>
</el-col>
</el-row>
</div>
</el-form>
<span slot="footer" class="dialog-footer">
<el-button @click="visible = false">取消</el-button>
<el-button type="primary" @click="dataFormSubmit()"></el-button>
</span>
</el-dialog>
</template>
<script>
export default {
data() {
return {
visible: false,
dataForm: {
id: 0,
templateId: '',
title: '',
data: [],
url: '',
miniprogram:{appid:'',pagePath:''},
content: '',
status: true,
name: ''
},
dataRule: {
title: [
{ required: true, message: '标题不能为空', trigger: 'blur' }
],
data: [
{ required: true, message: '内容不能为空', trigger: 'blur' }
],
name: [
{ required: true, message: '模版名称不能为空', trigger: 'blur' }
]
}
}
},
methods: {
init(id) {
console.log('init',id)
this.dataForm.id = id || 0
this.visible = true
this.$nextTick(() => {
this.$refs['dataForm'].resetFields()
if (this.dataForm.id) {
this.$http({
url: this.$http.adornUrl(`/manage/msgTemplate/info/${this.dataForm.id}`),
method: 'get',
params: this.$http.adornParams()
}).then(({ data }) => {
if (data && data.code === 200) {
this.transformTemplate(data.msgTemplate)
}else{
this.$message.error(data.msg)
}
})
}
})
},
/**
* 根据content信息展开data配置项(content为微信公众平台后台配置的模板)
* 如content='{{first.DATA}} ↵商品名称:{{keyword1.DATA}} ↵购买时间:{{keyword2.DATA}} ↵{{remark.DATA}}'
* 则生成data=[{name:'first',value:'',color:''},{name:'first',value:'',color:''},{name:'first',value:'',color:''}]
* 展示表单让管理员给对应的字段填充内容
*/
transformTemplate(template){
if(!template.miniprogram)template.miniprogram={appid:'',pagePath:''}
if(template.data instanceof Array) {//
this.dataForm = template
return
}
template.data=[]
let keysArray = template.content.match(/\{\{(\w*)\.DATA\}\}/g) || [] // ["{{first.DATA}}", "{{keyword1.DATA}}", "{{keyword2.DATA}}", "{{remark.DATA}}"]
keysArray.map(item=>{
name=item.replace('{{','').replace('.DATA}}','')
template.data.push({"name":name,"value":"",color:"#000000"})
})
this.dataForm = template
},
//
dataFormSubmit() {
this.$refs['dataForm'].validate((valid) => {
if (valid) {
this.$http({
url: this.$http.adornUrl(`/manage/msgTemplate/${!this.dataForm.id ? 'save' : 'update'}`),
method: 'post',
data: this.$http.adornData(this.dataForm)
}).then(({ data }) => {
if (data && data.code === 200) {
this.$message({
message: '操作成功',
type: 'success',
duration: 1500,
onClose: () => {
this.visible = false
this.$emit('refreshDataList')
}
})
} else {
this.$message.error(data.msg)
}
})
}
})
}
}
}
</script>
<style scoped>
.form-group-area{
border:1px dotted gray;
}
.form-group-title{
color: gray;
font-size: 12px;
}
</style>

@ -0,0 +1,215 @@
<template>
<div class="mod-config">
<el-form :inline="true" :model="dataForm" @keyup.enter.native="getDataList()">
<el-form-item>
<el-input v-model="dataForm.title" placeholder="标题" clearable></el-input>
</el-form-item>
<el-form-item>
<el-button @click="getDataList()"></el-button>
<el-button v-if="isAuth('wx:msgtemplate:save')" type="success" @click="copyHandle()" :disabled="dataListSelections.length <= 0"></el-button>
<el-button v-if="isAuth('wx:msgtemplate:save')" type="success" @click="templateMsgTaskHandle()" :disabled="dataListSelections.length!=1"></el-button>
<el-button v-if="isAuth('wx:msgtemplate:delete')" type="danger" @click="deleteHandle()" :disabled="dataListSelections.length <= 0"></el-button>
</el-form-item>
<el-form-item class="fr">
<el-button v-if="isAuth('wx:msgtemplate:save')" icon="el-icon-sort" type="success" @click="syncWxTemplate()" :disabled="synchonizingWxTemplate">{{synchonizingWxTemplate?'...':''}}</el-button>
<el-button><el-link type="primary" icon="el-icon-link" target="_blank" href="https://kf.qq.com/faq/170209E3InyI170209nIF7RJ.html">模板管理指引</el-link></el-button>
</el-form-item>
</el-form>
<el-table :data="dataList" border v-loading="dataListLoading" @selection-change="selectionChangeHandle" style="width: 100%;">
<el-table-column type="selection" header-align="center" align="center" width="50">
</el-table-column>
<el-table-column prop="templateId" show-overflow-tooltip header-align="center" align="center" label="模板ID">
</el-table-column>
<el-table-column prop="title" header-align="center" align="center" label="标题">
<a :href="scope.row.url" slot-scope="scope">{{scope.row.title}}</a>
</el-table-column>
<el-table-column prop="name" header-align="center" align="center" label="模版名称">
</el-table-column>
<el-table-column prop="content" show-overflow-tooltip header-align="center" align="center" label="模版字段" width="200">
</el-table-column>
<el-table-column prop="status" header-align="center" align="center" label="是否有效">
<span slot-scope="scope">{{scope.row.status?"是":"否"}}</span>
</el-table-column>
<el-table-column fixed="right" header-align="center" align="center" width="150" label="操作">
<template slot-scope="scope">
<el-button type="text" size="small" @click="addOrUpdateHandle(scope.row.id)"></el-button>
<el-button type="text" size="small" @click="deleteHandle(scope.row.id)"></el-button>
</template>
</el-table-column>
</el-table>
<el-pagination @size-change="sizeChangeHandle" @current-change="currentChangeHandle" :current-page="pageIndex" :page-sizes="[10, 20, 50, 100]" :page-size="pageSize" :total="totalCount" layout="total, sizes, prev, pager, next, jumper">
</el-pagination>
<!-- 弹窗, 新增 / 修改 -->
<add-or-update v-if="addOrUpdateVisible" ref="addOrUpdate" @refreshDataList="getDataList"></add-or-update>
<template-msg-task v-if="templateMsgTaskVisible" ref="templateMsgTask"></template-msg-task>
</div>
</template>
<script>
import AddOrUpdate from './msg-template-add-or-update'
import TemplateMsgTask from '@/components/template-msg-task'
export default {
data() {
return {
dataForm: {
title: ''
},
dataList: [],
pageIndex: 1,
pageSize: 10,
totalCount: 0,
dataListLoading: false,
dataListSelections: [],
addOrUpdateVisible: false,
templateMsgTaskVisible:false,
synchonizingWxTemplate:false
}
},
components: {
AddOrUpdate,TemplateMsgTask
},
activated() {
this.getDataList()
},
methods: {
//
getDataList() {
this.dataListLoading = true
this.$http({
url: this.$http.adornUrl('/manage/msgTemplate/list'),
method: 'get',
params: this.$http.adornParams({
'page': this.pageIndex,
'limit': this.pageSize,
'title': this.dataForm.title,
'sidx': 'id',
'order': 'desc'
})
}).then(({ data }) => {
if (data && data.code === 200) {
this.dataList = data.page.list
this.totalCount = data.page.totalCount
} else {
this.dataList = []
this.totalCount = 0
}
this.dataListLoading = false
})
},
//
sizeChangeHandle(val) {
this.pageSize = val
this.pageIndex = 1
this.getDataList()
},
//
currentChangeHandle(val) {
this.pageIndex = val
this.getDataList()
},
//
selectionChangeHandle(val) {
this.dataListSelections = val
},
// /
addOrUpdateHandle(id) {
this.addOrUpdateVisible = true
this.$nextTick(() => {
this.$refs.addOrUpdate.init(id)
})
},
//
deleteHandle(id) {
var ids = id ? [id] : this.dataListSelections.map(item => item.id)
this.$confirm(`确定对[id=${ids.join(',')}]进行[${id ? '删除' : '批量删除'}]操作?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
this.$http({
url: this.$http.adornUrl('/manage/msgTemplate/delete'),
method: 'post',
data: this.$http.adornData(ids, false)
}).then(({ data }) => {
if (data && data.code === 200) {
this.$message({
message: '操作成功',
type: 'success',
duration: 1500,
onClose: () => {
this.getDataList()
}
})
} else {
this.$message.error(data.msg)
}
})
})
},
syncWxTemplate(){
if(this.synchonizingWxTemplate)return
this.synchonizingWxTemplate=true
this.$http({
url: this.$http.adornUrl('/manage/msgTemplate/syncWxTemplate'),
method: 'post',
}).then(({ data }) => {
this.synchonizingWxTemplate=false
if (data && data.code === 200) {
this.$message({
message: '同步完成',
type: 'success',
duration: 1500,
onClose: () => {
this.getDataList()
}
})
} else {
this.$message.error(data.msg)
}
}).catch(()=>this.synchonizingWxTemplate=false)
},
templateMsgTaskHandle(){
this.templateMsgTaskVisible = true
this.$nextTick(() => {
this.$refs.templateMsgTask.init(this.dataListSelections[0])
})
},
async copyHandle(){
let loading;
for (let i = 0; i < this.dataListSelections.length; i++) {
let item = this.dataListSelections[i];
loading=this.$loading({
lock: true,
text: "复制模板:"+item.title,
spinner: 'el-icon-loading',
background: 'rgba(0, 0, 0, 0.7)'
});
item.id='';
item.updateTime=new Date()
item.name+='_COPY'
await this.addMsgTemplate(item).catch(()=>loading.close())
loading.close()
}
loading.close()
this.getDataList()
},
addMsgTemplate(msgTemplate){
return new Promise((resolve, reject) => {
this.$http({
url: this.$http.adornUrl('/manage/msgTemplate/save'),
method: 'post',
data: this.$http.adornData(msgTemplate)
}).then(({ data }) => {
if (data && data.code === 200) {
resolve()
} else {
this.$message.error(data.msg)
reject(data.msg)
}
}).catch(err=>reject(err))
})
}
}
}
</script>

@ -0,0 +1,135 @@
<template>
<div class="mod-config">
<el-form :inline="true" :model="dataForm" @keyup.enter.native="getDataList()">
<el-form-item>
<el-input v-model="dataForm.touser" placeholder="openid" clearable></el-input>
</el-form-item>
<el-form-item>
<el-button @click="getDataList()"></el-button>
<el-button v-if="isAuth('wx:templatemsglog:delete')" type="danger" @click="deleteHandle()" :disabled="dataListSelections.length <= 0"></el-button>
</el-form-item>
</el-form>
<el-table :data="dataList" border v-loading="dataListLoading" @selection-change="selectionChangeHandle" style="width: 100%;">
<el-table-column type="selection" header-align="center" align="center" width="50">
</el-table-column>
<el-table-column prop="touser" header-align="center" align="center" label="openid" width="100">
</el-table-column>
<el-table-column prop="data" header-align="center" align="center" :formatter="tableJsonFormat" label="内容" width="300">
</el-table-column>
<el-table-column prop="sendResult" header-align="center" align="center" show-overflow-tooltip label="发送结果" width="150">
</el-table-column>
<el-table-column prop="sendTime" header-align="center" align="center" width="100" label="发送时间">
</el-table-column>
<el-table-column prop="url" header-align="center" align="center" show-overflow-tooltip label="链接">
</el-table-column>
<el-table-column prop="miniprogram" header-align="center" align="center" :formatter="tableJsonFormat" show-overflow-tooltip label="小程序">
</el-table-column>
<el-table-column prop="templateId" header-align="center" align="center" label="模板ID" width="150">
</el-table-column>
<el-table-column fixed="right" header-align="center" align="center" width="150" label="操作">
<template slot-scope="scope">
<el-button type="text" size="small" @click="deleteHandle(scope.row.logId)"></el-button>
</template>
</el-table-column>
</el-table>
<el-pagination @size-change="sizeChangeHandle" @current-change="currentChangeHandle" :current-page="pageIndex" :page-sizes="[10, 20, 50, 100]" :page-size="pageSize" :total="totalCount" layout="total, sizes, prev, pager, next, jumper">
</el-pagination>
</div>
</template>
<script>
export default {
data() {
return {
dataForm: {
touser: ''
},
dataList: [],
pageIndex: 1,
pageSize: 10,
totalCount: 0,
dataListLoading: false,
dataListSelections: [],
addOrUpdateVisible: false
}
},
activated() {
this.getDataList()
},
methods: {
//
getDataList() {
this.dataListLoading = true
this.$http({
url: this.$http.adornUrl('/manage/templateMsgLog/list'),
method: 'get',
params: this.$http.adornParams({
'page': this.pageIndex,
'limit': this.pageSize,
'touser': this.dataForm.touser,
'sidx': 'send_time',
'order': 'desc'
})
}).then(({ data }) => {
if (data && data.code === 200) {
this.dataList = data.page.list
this.totalCount = data.page.totalCount
} else {
this.dataList = []
this.totalCount = 0
}
this.dataListLoading = false
})
},
//
sizeChangeHandle(val) {
this.pageSize = val
this.pageIndex = 1
this.getDataList()
},
//
currentChangeHandle(val) {
this.pageIndex = val
this.getDataList()
},
//
selectionChangeHandle(val) {
this.dataListSelections = val
},
//
deleteHandle(id) {
var ids = id ? [id] : this.dataListSelections.map(item => item.logId)
this.$confirm(`确定对[id=${ids.join(',')}]进行[${id ? '删除' : '批量删除'}]操作?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
this.$http({
url: this.$http.adornUrl('/manage/templateMsgLog/delete'),
method: 'post',
data: this.$http.adornData(ids, false)
}).then(({ data }) => {
if (data && data.code === 200) {
this.$message({
message: '操作成功',
type: 'success',
duration: 1500,
onClose: () => {
this.getDataList()
}
})
} else {
this.$message.error(data.msg)
}
})
})
},
tableJsonFormat(row, column, cellValue){
if (!cellValue) {
return '';
}
return JSON.stringify(cellValue)
}
}
}
</script>

@ -1,44 +1,28 @@
<template>
<!-- 组件的HTML模板 -->
<div class="mod-config">
<!-- 搜索表单 -->
<el-form :inline="true" :model="dataForm" @keyup.enter.native="getDataList()">
<!-- 输入框用于输入搜索关键词 -->
<el-form-item>
<el-input v-model="dataForm.key" placeholder="参数名" clearable></el-input>
</el-form-item>
<!-- 操作按钮 -->
<el-form-item>
<!-- 查询按钮触发数据列表的获取 -->
<el-button @click="getDataList()"></el-button>
<!-- 新增按钮根据权限显示 -->
<el-button v-if="isAuth('wx:wxaccount:save')" type="primary" @click="addOrUpdateHandle()"></el-button>
<!-- 批量删除按钮根据权限和选中项显示 -->
<el-button v-if="isAuth('wx:wxaccount:delete')" type="danger" @click="deleteHandle()" :disabled="dataListSelections.length <= 0"></el-button>
</el-form-item>
</el-form>
<!-- 数据表格 -->
<el-table :data="dataList" border v-loading="dataListLoading" @selection-change="selectionChangeHandle" style="width: 100%;">
<!-- 多选框 -->
<el-table-column type="selection" header-align="center" align="center" width="50">
</el-table-column>
<!-- 展示appid -->
<el-table-column prop="appid" header-align="center" align="center" label="appid">
</el-table-column>
<!-- 展示公众号名称 -->
<el-table-column prop="name" header-align="center" align="center" label="公众号名称">
</el-table-column>
<!-- 展示公众号类型通过formatter格式化显示 -->
<el-table-column prop="type" header-align="center" align="center" label="类型" :formatter="accountTypeFormat">
</el-table-column>
<!-- 展示是否认证 -->
<el-table-column prop="verified" header-align="center" align="center" label="是否认证">
<!-- 使用插槽自定义显示内容 -->
<span slot-scope="scope">{{scope.row.verified?"是":"否"}}</span>
</el-table-column>
<!-- 操作列 -->
<el-table-column fixed="right" header-align="center" align="center" width="150" label="操作">
<!-- 使用插槽添加按钮 -->
<template slot-scope="scope">
<el-button type="text" size="small" @click="accessInfo(scope.row)"></el-button>
<el-button type="text" size="small" @click="addOrUpdateHandle(scope.row)"></el-button>
@ -46,48 +30,35 @@
</template>
</el-table-column>
</el-table>
<!-- 新增/修改弹窗 -->
<!-- 弹窗, 新增 / 修改 -->
<add-or-update v-if="addOrUpdateVisible" ref="addOrUpdate" @refreshDataList="getDataList"></add-or-update>
<!-- 接入信息弹窗 -->
<account-access v-if="accountAccessVisible" ref="accountAccessDialog"></account-access>
</div>
</template>
<script>
//
import AddOrUpdate from './account/wx-account-add-or-update'
import AccountAccess from './account/wx-account-access-info'
import { mapState } from 'vuex'
export default {
data() {
return {
//
dataForm: {
key: ''
},
//
dataList: [],
//
dataListLoading: false,
//
dataListSelections: [],
// /
addOrUpdateVisible: false,
//
accountAccessVisible: false
accountAccessVisible:false
}
},
//
components: {
AddOrUpdate,
AccountAccess
AddOrUpdate,AccountAccess
},
// Vuex
computed: mapState({
ACCOUNT_TYPES: state => state.wxAccount.ACCOUNT_TYPES
ACCOUNT_TYPES: state=>state.wxAccount.ACCOUNT_TYPES
}),
//
activated() {
this.getDataList()
},
@ -95,55 +66,53 @@ export default {
//
getDataList() {
this.dataListLoading = true
// HTTP
this.$http({
url: this.$http.adornUrl('/manage/wxAccount/list'), // URL
url: this.$http.adornUrl('/manage/wxAccount/list'),
method: 'get',
params: this.$http.adornParams({ //
params: this.$http.adornParams({
'key': this.dataForm.key
})
}).then(({ data }) => {
if (data && data.code === 200) {
this.dataList = data.list //
this.$store.commit('wxAccount/updateAccountList', data.list) // Vuex
this.dataList = data.list
this.$store.commit('wxAccount/updateAccountList', data.list)
} else {
this.dataList = [] //
this.dataList = []
}
this.dataListLoading = false //
this.dataListLoading = false
})
},
//
//
selectionChangeHandle(val) {
this.dataListSelections = val
},
// /
// /
addOrUpdateHandle(item) {
this.addOrUpdateVisible = true
this.$nextTick(() => {
this.$refs.addOrUpdate.init(item) //
this.$refs.addOrUpdate.init(item)
})
},
//
accessInfo(item) {
accessInfo(item){
this.accountAccessVisible = true
this.$nextTick(() => {
this.$refs.accountAccessDialog.init(item) //
this.$refs.accountAccessDialog.init(item)
})
},
//
//
deleteHandle(appid) {
// appid
var ids = appid ? [appid] : this.dataListSelections.map(item => item.appid)
var ids = appid ? [appid] : this.dataListSelections.map(item => {
return item.appid
})
this.$confirm(`确定对[appid=${ids.join(',')}]进行[${appid ? '删除' : '批量删除'}]操作?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
// HTTP
this.$http({
url: this.$http.adornUrl('/manage/wxAccount/delete'),
method: 'post',
data: this.$http.adornData(ids, false) //
data: this.$http.adornData(ids, false)
}).then(({ data }) => {
if (data && data.code === 200) {
this.$message({
@ -151,19 +120,18 @@ export default {
type: 'success',
duration: 1500,
onClose: () => {
this.getDataList() //
this.getDataList()
}
})
} else {
this.$message.error(data.msg) //
this.$message.error(data.msg)
}
})
})
},
//
accountTypeFormat(row, column, cellValue) {
return this.ACCOUNT_TYPES[cellValue]
return this.ACCOUNT_TYPES[cellValue];
}
}
}
</script>
</script>

@ -0,0 +1,57 @@
<template>
<el-tabs v-model="activeTab" @tab-click="handleTabClick">
<el-tab-pane :label="'图片素材('+assetsCount.imageCount+')'" name="image" lazy>
<material-file fileType="image" ref="imagePanel" @change="materialCount"></material-file>
</el-tab-pane>
<el-tab-pane :label="'语音素材('+assetsCount.voiceCount+')'" name="voice" lazy>
<material-file fileType="voice" ref="voicePanel" @change="materialCount"></material-file>
</el-tab-pane>
<el-tab-pane :label="'视频素材('+assetsCount.videoCount+')'" name="video" lazy>
<material-file fileType="video" ref="videoPanel" @change="materialCount"></material-file>
</el-tab-pane>
<el-tab-pane :label="'图文素材('+assetsCount.newsCount+')'" name="news" lazy>
<material-news ref="newsPanel" @change="materialCount"></material-news>
</el-tab-pane>
</el-tabs>
</template>
<script>
export default {
data() {
return {
activeTab: 'image',
assetsCount:{
imageCount:'..',
videoCount:'..',
voiceCount:'..',
newsCount:'..'
}
};
},
components: {
MaterialFile:()=>import('./assets/material-file'),
MaterialNews:()=>import('./assets/material-news')
},
mounted(){
this.materialCount();
},
methods: {
handleTabClick(tab, event) {
this.$nextTick(()=>{
this.$refs[tab.name+'Panel'].init()
})
},
materialCount(){
this.$http({
url: this.$http.adornUrl('/manage/wxAssets/materialCount')
}).then(({ data }) => {
if (data && data.code == 200) {
this.assetsCount=data.data
} else {
this.$message.error(data.msg);
}
})
}
}
};
</script>

@ -0,0 +1,125 @@
<template>
<div>
<div class="menu-input-group" style="border-bottom: 2px #e8e8e8 solid;">
<div class="menu-name">{{button.name}}</div>
<div class="menu-del" @click="$emit('delMenu')"></div>
</div>
<div class="menu-input-group">
<div class="menu-label">菜单名称</div>
<div class="menu-input">
<input type="text" name="name" placeholder="请输入菜单名称" class="menu-input-text" v-model="button.name" @input="checkMenuName(button.name)">
<p class="menu-tips" style="color:#e15f63" v-show="menuNameBounds"></p>
<p class="menu-tips">字数不超过{{selectedMenuLevel==1?'5':'8'}}个汉字</p>
</div>
</div>
<div v-show="!button.subButtons || button.subButtons.length==0">
<div class="menu-input-group">
<div class="menu-label">菜单内容</div>
<div class="menu-input">
<select v-model="button.type" name="type" class="menu-input-text">
<option value="view">跳转网页(view)</option>
<option value="media_id">发送消息(media_id)</option>
<!--<option value="view_limited">跳转公众号图文消息链接(view_limited)</option>-->
<option value="miniprogram">打开指定小程序(miniprogram)</option>
<option value="click">自定义点击事件(click)</option>
<option value="scancode_push">扫码上传消息(scancode_push)</option>
<option value="scancode_waitmsg">扫码提示下发(scancode_waitmsg)</option>
<option value="pic_sysphoto">系统相机拍照(pic_sysphoto)</option>
<option value="pic_photo_or_album">弹出拍照或者相册(pic_photo_or_album)</option>
<option value="pic_weixin">弹出微信相册(pic_weixin)</option>
<option value="location_select">弹出地理位置选择器(location_select)</option>
</select>
</div>
</div>
<div class="menu-content" v-if="button.type=='view'">
<div class="menu-input-group">
<p class="menu-tips">订阅者点击该子菜单会跳到以下链接</p>
<div class="menu-label">页面地址</div>
<div class="menu-input">
<input type="text" placeholder="" class="menu-input-text" v-model="button.url">
</div>
</div>
</div>
<div class="menu-content" v-else-if="button.type=='media_id'">
<div class="menu-input-group">
<p class="menu-tips">订阅者点击该菜单会收到以下图文消息</p>
<div class="menu-label">media_id</div>
<div class="menu-input">
<input type="text" placeholder="图文消息media_id" class="menu-input-text" v-model="button.mediaId">
</div>
</div>
</div>
<div class="menu-content" v-else-if="button.type=='miniprogram'">
<div class="menu-input-group">
<p class="menu-tips">订阅者点击该子菜单会跳到以下小程序</p>
<div class="menu-label">小程序appId</div>
<div class="menu-input">
<input type="text" placeholder="小程序的appId仅认证公众号可配置" class="menu-input-text" v-model="button.appId">
</div>
</div>
<div class="menu-input-group">
<div class="menu-label">小程序路径</div>
<div class="menu-input">
<input type="text" placeholder="小程序的页面路径 pages/index/index" class="menu-input-text" v-model="button.pagePath">
</div>
</div>
<div class="menu-input-group">
<div class="menu-label">备用网页</div>
<div class="menu-input">
<input type="text" placeholder="" class="menu-input-text" v-model="button.url">
<p class="menu-tips">旧版微信客户端无法支持小程序用户点击菜单时将会打开备用网页</p>
</div>
</div>
</div>
<div class="menu-content" v-else>
<div class="menu-input-group">
<p class="menu-tips">用于消息接口推送不超过128字节</p>
<div class="menu-label">菜单KEY值</div>
<div class="menu-input">
<input type="text" placeholder="" class="menu-input-text" v-model="button.key">
</div>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
props: {
selectedMenuLevel: {
type: Number,
default: 1
},
button: {
type: Object,
required: true
}
},
data() {
return {
menuNameBounds: false,//
}
},
methods: {
//
checkMenuName: function (val) {
if (this.selectedMenuLevel == 1 && this.getMenuNameLen(val) <= 10) {
this.menuNameBounds = false
} else if (this.selectedMenuLevel == 2 && this.getMenuNameLen(val) <= 16) {
this.menuNameBounds = false
} else {
this.menuNameBounds = true
}
},
//
getMenuNameLen: function (val) {
var len = 0;
for (var i = 0; i < val.length; i++) {
var a = val.charAt(i);
a.match(/[^\x00-\xff]/ig) != null ? len += 2 : len += 1;
}
return len;
}
}
}
</script>

@ -0,0 +1,159 @@
<template>
<div>
<div id="app-menu">
<!-- 预览窗 -->
<div class="weixin-preview">
<div class="weixin-bd">
<div class="weixin-header">公众号菜单</div>
<ul class="weixin-menu" id="weixin-menu">
<li v-for="(btn,i) in menu.buttons" :key="i" class="menu-item" :class="{'current':selectedMenuIndex===i&&selectedMenuLevel==1}" @click="selectMenu(i)">
<div class="menu-item-title">
<span>{{ btn.name }}</span>
</div>
<ul class="weixin-sub-menu">
<li v-for="(sub,i2) in btn.subButtons" :key="i2" class="menu-sub-item" :class="{'current':selectedMenuIndex===i&&selectedSubMenuIndex===i2&&selectedMenuLevel==2,'on-drag-over':onDragOverMenu==(i+'_'+i2)}" @click.stop="selectSubMenu(i,i2)" draggable="true" @dragstart="selectSubMenu(i,i2)" @dragover.prevent="onDragOverMenu=(i+'_'+i2)" @drop="onDrop(i,i2)">
<div class="menu-item-title">
<span>{{sub.name}}</span>
</div>
</li>
<li v-if="btn.subButtons.length<5" class="menu-sub-item" :class="{'on-drag-over':onDragOverMenu==(i+'_'+btn.subButtons.length)}" @click.stop="addMenu(2,i)" @dragover.prevent="onDragOverMenu=(i+'_'+btn.subButtons.length)" @drop="onDrop(i,btn.subButtons.length)">
<div class="menu-item-title">
<i class="el-icon-plus"></i>
</div>
</li>
<i class="menu-arrow arrow_out"></i>
<i class="menu-arrow arrow_in"></i>
</ul>
</li>
<li class="menu-item" v-if="menu.buttons.length<3" @click="addMenu(1)"> <i class="el-icon-plus"></i></li>
</ul>
</div>
</div>
<!-- 菜单编辑器 -->
<div class="weixin-menu-detail" v-if="selectedMenuLevel>0">
<wx-menu-button-editor :button="selectedButton" :selectedMenuLevel="selectedMenuLevel" @delMenu="delMenu"></wx-menu-button-editor>
</div>
</div>
<div class="weixin-btn-group" v-if="isAuth('wx:menu:save')" @click="updateWxMenu">
<el-button type="success" icon="el-icon-upload">发布</el-button>
<el-button type="warning" icon="el-icon-delete" @click="delMenu"></el-button>
</div>
</div>
</template>
<script>
export default {
components: {
wxMenuButtonEditor: () => import('./wx-menu-button-editor')
},
data() {
return {
menu: { 'buttons': [] },//
selectedMenuIndex: '',//
selectedSubMenuIndex: '',//
selectedMenuLevel: 0,//
selectedButton: '',//
onDragOverMenu:'' //
}
},
mounted() {
this.getWxMenu();
},
methods: {
getWxMenu() {
this.$http({
url: this.$http.adornUrl('/manage/wxMenu/getMenu')
}).then(({ data }) => {
if (data.code == 200) {
this.menu = data.data.menu;
} else {
this.$message({
type: 'error',
message: data.msg
});
}
});
},
//
selectMenu(i) {
this.selectedMenuLevel = 1
this.selectedSubMenuIndex = ''
this.selectedMenuIndex = i
this.selectedButton = this.menu.buttons[i]
},
//
selectSubMenu(i,i2) {
this.selectedMenuLevel = 2
this.selectedMenuIndex = i
this.selectedSubMenuIndex = i2
this.selectedButton = this.menu.buttons[i].subButtons[i2]
},
//
addMenu(level,i) {
if (level == 1 && this.menu.buttons.length < 3) {
this.menu.buttons.push({
"type": "view",
"name": "菜单名称",
"subButtons": [],
"url": ""
})
this.selectMenu(this.menu.buttons.length - 1)
}
if (level == 2 && this.menu.buttons[i].subButtons.length < 5) {
this.menu.buttons[i].subButtons.push({
"type": "view",
"name": "子菜单名称",
"url": ""
})
this.selectSubMenu(i,this.menu.buttons[i].subButtons.length - 1)
}
},
//
delMenu() {
if (this.selectedMenuLevel == 1 && confirm('删除后菜单下设置的内容将被删除')) {
this.menu.buttons.splice(this.selectedMenuIndex, 1);
this.unSelectMenu()
} else if (this.selectedMenuLevel == 2) {
this.menu.buttons[this.selectedMenuIndex].subButtons.splice(this.selectedSubMenuIndex, 1);
this.unSelectMenu()
}
},
unSelectMenu(){//
this.selectedMenuLevel = 0
this.selectedMenuIndex = ''
this.selectedSubMenuIndex = ''
this.selectedButton = ''
},
updateWxMenu() {
this.$http({
url: this.$http.adornUrl('/manage/wxMenu/updateMenu'),
data: this.menu,
method: 'post'
}).then(({ data }) => {
if (data.code == 200) {
this.$message.success('操作成功')
} else {
this.$message.error(data.msg);
}
});
},
onDrop(i,i2){//
this.onDragOverMenu='';
if(i==this.selectedMenuIndex && i2==this.selectedSubMenuIndex) //
return
if(i!=this.selectedMenuIndex && this.menu.buttons[i].subButtons.length>=5){
this.$message.error('目标组已满');
return
}
this.menu.buttons[i].subButtons.splice(i2,0,this.selectedButton)
let delSubIndex = this.selectedSubMenuIndex
if(i==this.selectedMenuIndex && i2<this.selectedSubMenuIndex)
delSubIndex++
this.menu.buttons[this.selectedMenuIndex].subButtons.splice(delSubIndex, 1);
this.unSelectMenu()
}
}
}
</script>
<style src="@/assets/css/wx-menu.css"></style>

@ -0,0 +1,84 @@
<template>
<el-dialog title="消息回复" :close-on-click-modal="false" :visible.sync="visible">
<el-form :model="dataForm" :rules="dataRule" ref="dataForm">
<el-form-item prop="replyContent">
<el-input v-model="dataForm.replyContent" type="textarea" :rows="5" placeholder="回复内容" maxlength="600" show-word-limit :autosize="{ minRows: 5, maxRows: 30 }" autocomplete></el-input>
<el-button type="text" v-show="'text'==dataForm.replyType" @click="addLink"></el-button>
</el-form-item>
</el-form>
<span slot="footer" class="dialog-footer">
<el-button @click="visible = false">取消</el-button>
<el-button type="success" @click="dataFormSubmit()" :disabled="uploading">{{uploading?'发送中...':'发送'}}</el-button>
</span>
</el-dialog>
</template>
<script>
export default {
data() {
return {
visible: false,
uploading: false,
dataForm: {
openid:'',
replyType:'text',
replyContent:''
},
dataRule: {
replyContent: [
{ required: true, message: "回复内容不能为空", trigger: "blur" }
]
}
}
},
components:{
WxMsgPreview:()=>import('@/components/wx-msg-preview')
},
methods: {
init(openid) {
if(!openid)throw '参数异常'
this.dataForm.openid=openid
this.visible = true
},
//
dataFormSubmit() {
if(this.uploading)return
this.uploading=true
this.$refs['dataForm'].validate((valid) => {
if (valid) {
this.$http({
url: this.$http.adornUrl(`/manage/wxMsg/reply`),
method: 'post',
data: this.$http.adornData(this.dataForm)
}).then(({ data }) => {
if (data && data.code === 200) {
this.$message({
message: '回复成功',
type: 'success',
duration: 1500,
onClose: () => {
this.visible = false
}
})
this.$emit("success",{...this.dataForm});
this.dataForm.replyContent=''
} else {
this.$message.error(data.msg)
}
this.uploading=false
})
}
})
},
addLink() {
this.dataForm.replyContent += '<a href="链接地址">链接文字</a>'
}
}
}
</script>
<style scoped>
.msg-container{
background: #eee;
}
</style>

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save