Compare commits

..

68 Commits

Author SHA1 Message Date
peyqfw698 c086ce336f branch_hxd
6 months ago
1234567 327565db82
6 months ago
yue 5e703caeae
6 months ago
mkaoj697q f39e625538 李1
6 months ago
李嫚嫚 cc2f535039
6 months ago
李嫚嫚 553be20612
6 months ago
mkaoj697q 358d3c8148
6 months ago
李嫚嫚 9f5300ba0f
6 months ago
李嫚嫚 14d39c39ba
6 months ago
mkaoj697q 394fc16642
6 months ago
李嫚嫚 31c104bb15
6 months ago
李嫚嫚 ab3fcaf7ac
6 months ago
mkaoj697q 58923e7719
6 months ago
yue 10dc8a1cb2 y
6 months ago
mkaoj697q 94569c5d24
6 months ago
李嫚嫚 de89a408d0
6 months ago
mkaoj697q f32a8a1754
6 months ago
李嫚嫚 1345f361e0
6 months ago
李嫚嫚 163ee83e2d l
6 months ago
mkaoj697q 0bd0611eb9
6 months ago
李嫚嫚 c646dd6aed
6 months ago
mkaoj697q efb9f7f925
6 months ago
李嫚嫚 7a68f52121
6 months ago
mkaoj697q 883b1cafea
6 months ago
李嫚嫚 56abc227f8
6 months ago
李嫚嫚 99140563fc 1
6 months ago
mkaoj697q b0df4a4c31
6 months ago
李嫚嫚 d6092929b5
6 months ago
mkaoj697q 139f0ff9d5
6 months ago
李嫚嫚 addd9458eb
6 months ago
李嫚嫚 f1f7901ec7 Merge branch 'main' of https://bdgit.educoder.net/mkaoj697q/WeChat
6 months ago
李嫚嫚 f1f0b80ec1 1
6 months ago
mkaoj697q a84c219650 岳惠莹
6 months ago
lx 0a822834de tijiao
7 months ago
李嫚嫚 da15d8fd44 1
7 months ago
李嫚嫚 de832ea347
7 months ago
李嫚嫚 179167caff 1
7 months ago
李嫚嫚 2e5b1f43c1
7 months ago
李嫚嫚 3ad4b38143 2
7 months ago
14879940+orange-jam-1@user.noreply.gitee.com f3ffe327e1 1
7 months ago
14879940+orange-jam-1@user.noreply.gitee.com ebbeeab675 1
7 months ago
14879940+orange-jam-1@user.noreply.gitee.com 4f146ddbec Merge branch 'main' of https://bdgit.educoder.net/mkaoj697q/WeChat
7 months ago
mkaoj697q e4dd7b86ae Delete 'wx-manage-master/LICENSE'
7 months ago
mkaoj697q 8f4be8a6fe Delete 'wx-manage-master/public/tinymce/zh_CN.js'
7 months ago
mkaoj697q 3804d3da9d Delete 'wx-manage-master/README.md'
7 months ago
mkaoj697q 43dd68ba88 Delete 'wx-manage-master/babel.config.js'
7 months ago
mkaoj697q d782549f8c Delete 'wx-manage-master/package-lock.json'
7 months ago
mkaoj697q 04f87e1db5 Delete 'wx-manage-master/package.json'
7 months ago
mkaoj697q 55f1dded15 Delete 'wx-manage-master/vue.config.js'
7 months ago
mkaoj697q 128e92dcfb Delete 'wx-manage-master/public/index.html'
7 months ago
mkaoj697q 5b4d63196a Delete 'wx-manage-master/.editorconfig'
7 months ago
mkaoj697q ce9f49b6b2 Delete '._wx-manage-master'
7 months ago
mkaoj697q 4e64f10f8b Delete '1.txt'
7 months ago
mkaoj697q 54a1086c7b 1
7 months ago
mkaoj697q dc28e8902d 1
7 months ago
14879940+orange-jam-1@user.noreply.gitee.com 02a46f9a54 Merge branch 'branch1'
7 months ago
longen0707 bde2e0d62b 11
7 months ago
longen0707 4400ac9176 Merge branch 'main' of bdgit.educoder.net:mkaoj697q/WeChat
7 months ago
longen0707 ce25b3dfc7 kuaidian
7 months ago
longen0707 95ac4c3289 ceshi
7 months ago
14879940+orange-jam-1@user.noreply.gitee.com 60fbbf4501 7
7 months ago
mkaoj697q 3034063279 Delete '1.txt'
7 months ago
mkaoj697q 9d2de8e5bd 1
7 months ago
14879940+orange-jam-1@user.noreply.gitee.com fc3296fa48 Merge branch 'branch2'
7 months ago
14879940+orange-jam-1@user.noreply.gitee.com 678fd05167 1
7 months ago
14879940+orange-jam-1@user.noreply.gitee.com a92fe24fe1 1
7 months ago
mkaoj697q 68e9417b7a Delete 'main.txt'
7 months ago
mkaoj697q 47f050fdc0 Delete 'ttt.txt'
7 months ago

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

108
.gitignore vendored

@ -0,0 +1,108 @@
# 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,201 @@
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,2 +1,45 @@
# WeChat
# 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 +0,0 @@
b1

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

18266
package-lock.json generated

File diff suppressed because it is too large Load Diff

@ -0,0 +1,37 @@
{
"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"
]
}

@ -0,0 +1,19 @@
<!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>

@ -0,0 +1,96 @@
/* 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;
}

@ -0,0 +1,389 @@
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"
});

@ -0,0 +1,61 @@
<!-- Vue组件的模板部分开始 -->
<template>
<!-- 定义一个div元素作为Vue应用的根容器并赋予其id为"app" -->
<div id="app">
<!-- 使用Vue Router的<router-view>组件来显示当前路由匹配的组件 -->
<!-- <transition>组件用于在路由切换时添加过渡效果name属性指定了过渡效果的名称 -->
<transition name="fade">
<router-view /> <!-- 此处将渲染与当前URL匹配的组件 -->
</transition>
</div>
</template>
<!-- Vue组件的模板部分结束 -->
<style>
/* 为类名为image-sm的img元素设置最大宽度和高度 */
img.image-sm {
max-width: 80px;
/* 最大宽度为80像素 */
max-height: 80px;
/* 最大高度为80像素 */
}
/* 为el-col类下的el-select和el-date-editor元素设置宽度为100% */
/* 这通常用于Element UI框架中的布局和表单组件 */
.el-col .el-select,
.el-col .el-date-editor {
width: 100%;
/* 宽度占满父容器 */
}
/* 为类名为demo-table-expand的元素设置字体大小为0 */
/* 这可能是一个技巧,用于消除内部元素的默认间距 */
.demo-table-expand {
font-size: 0;
/* 字体大小设置为0 */
}
/* 为demo-table-expand类下的label元素设置宽度和颜色 */
.demo-table-expand label {
width: 90px;
/* 宽度为90像素 */
color: #99a9bf;
/* 颜色为深灰色 */
}
/* 为demo-table-expand类下的el-form-item元素设置外边距和宽度 */
/* 这通常用于表单布局,确保表单项正确对齐和分布 */
.demo-table-expand .el-form-item {
margin-right: 0;
/* 右侧外边距为0 */
margin-bottom: 0;
/* 底部外边距为0 */
width: 50%;
/* 宽度为父容器的50% */
}
/* 为类名为text-warning的元素设置文本颜色为警告色 */
.text-warning {
color: #e6a23c;
/* 颜色为橙色系的警告色 */
}
</style>

@ -0,0 +1,933 @@
/* 常用辅助css */
/* ==================
==================== */
/* -- flex弹性布局 -- */
.flex {
display: flex; /* 设置元素为弹性盒子模型 */
}
.basis-xs {
flex-basis: 20%; /* 设置基础宽度为容器的20% */
}
.basis-sm {
flex-basis: 40%; /* 设置基础宽度为容器的40% */
}
.basis-df {
flex-basis: 50%; /* 设置基础宽度为容器的50% */
}
.basis-lg {
flex-basis: 60%; /* 设置基础宽度为容器的60% */
}
.basis-xl {
flex-basis: 80%; /* 设置基础宽度为容器的80% */
}
.flex-sub {
flex: 1; /* 设置弹性比例为1 */
}
.flex-twice {
flex: 2; /* 设置弹性比例为2 */
}
.flex-treble {
flex: 3; /* 设置弹性比例为3 */
}
.flex-direction {
flex-direction: column; /* 设置主轴方向为纵向 */
}
.flex-wrap {
flex-wrap: wrap; /* 允许子元素换行 */
}
.align-start {
align-items: flex-start; /* 子元素在交叉轴上靠起始位置对齐 */
}
.align-end {
align-items: flex-end; /* 子元素在交叉轴上靠结束位置对齐 */
}
.align-center {
align-items: center; /* 子元素在交叉轴上居中对齐 */
}
.align-stretch {
align-items: stretch; /* 子元素在交叉轴上拉伸以填充容器 */
}
.self-start {
align-self: flex-start; /* 单个子元素在交叉轴上靠起始位置对齐 */
}
.self-center {
align-self: center; /* 单个子元素在交叉轴上居中对齐 */
}
.self-end {
align-self: flex-end; /* 单个子元素在交叉轴上靠结束位置对齐 */
}
.self-stretch {
align-self: stretch; /* 单个子元素在交叉轴上拉伸以填充容器 */
}
.align-stretch {
align-items: stretch; /* 子元素在交叉轴上拉伸以填充容器 */
}
.justify-start {
justify-content: flex-start; /* 子元素在主轴上靠起始位置对齐 */
}
.justify-end {
justify-content: flex-end; /* 子元素在主轴上靠结束位置对齐 */
}
.justify-center {
justify-content: center; /* 子元素在主轴上居中对齐 */
}
.justify-between {
justify-content: space-between; /* 子元素在主轴上两端对齐,间隔均匀分布 */
}
.justify-around {
justify-content: space-around; /* 子元素在主轴上均匀分布,每个子元素两侧间隔相等 */
}
/* -- 内外边距 -- */
.margin-0 {
margin: 0; /* 设置外边距为0 */
}
.margin-xs {
margin: 5px; /* 设置外边距为5px */
}
.margin-sm {
margin: 10px; /* 设置外边距为10px */
}
.margin {
margin: 15px; /* 设置外边距为15px */
}
.margin-lg {
margin: 20px; /* 设置外边距为20px */
}
.margin-xl {
margin: 25px; /* 设置外边距为25px */
}
.margin-top-xs {
margin-top: 5px; /* 设置上外边距为5px */
}
.margin-top-sm {
margin-top: 10px; /* 设置上外边距为10px */
}
.margin-top {
margin-top: 15px; /* 设置上外边距为15px */
}
.margin-top-lg {
margin-top: 20px; /* 设置上外边距为20px */
}
.margin-top-xl {
margin-top: 25px; /* 设置上外边距为25px */
}
.margin-right-xs {
margin-right: 5px; /* 设置右外边距为5px */
}
.margin-right-sm {
margin-right: 10px; /* 设置右外边距为10px */
}
.margin-right {
margin-right: 15px; /* 设置右外边距为15px */
}
.margin-right-lg {
margin-right: 20px; /* 设置右外边距为20px */
}
.margin-right-xl {
margin-right: 25px; /* 设置右外边距为25px */
}
.margin-bottom-xs {
margin-bottom: 5px; /* 设置下外边距为5px */
}
.margin-bottom-sm {
margin-bottom: 10px; /* 设置下外边距为10px */
}
.margin-bottom {
margin-bottom: 15px; /* 设置下外边距为15px */
}
.margin-bottom-lg {
margin-bottom: 20px; /* 设置下外边距为20px */
}
.margin-bottom-xl {
margin-bottom: 25px; /* 设置下外边距为25px */
}
.margin-left-xs {
margin-left: 5px; /* 设置左外边距为5px */
}
.margin-left-sm {
margin-left: 10px; /* 设置左外边距为10px */
}
.margin-left {
margin-left: 15px; /* 设置左外边距为15px */
}
.margin-left-lg {
margin-left: 20px; /* 设置左外边距为20px */
}
.margin-left-xl {
margin-left: 25px; /* 设置左外边距为25px */
}
.margin-lr-xs {
margin-left: 5px; /* 设置左右外边距为5px */
margin-right: 5px; /* 设置左右外边距为5px */
}
.margin-lr-sm {
margin-left: 10px; /* 设置左右外边距为10px */
margin-right: 10px; /* 设置左右外边距为10px */
}
.margin-lr {
margin-left: 15px; /* 设置左右外边距为15px */
margin-right: 15px; /* 设置左右外边距为15px */
}
.margin-lr-lg {
margin-left: 20px; /* 设置左右外边距为20px */
margin-right: 20px; /* 设置左右外边距为20px */
}
.margin-lr-xl {
margin-left: 25px; /* 设置左右外边距为25px */
margin-right: 25px; /* 设置左右外边距为25px */
}
.margin-tb-xs {
margin-top: 5px; /* 设置上下外边距为5px */
margin-bottom: 5px; /* 设置上下外边距为5px */
}
.margin-tb-sm {
margin-top: 10px; /* 设置上下外边距为10px */
margin-bottom: 10px; /* 设置上下外边距为10px */
}
.margin-tb {
margin-top: 15px; /* 设置上下外边距为15px */
margin-bottom: 15px; /* 设置上下外边距为15px */
}
.margin-tb-lg {
margin-top: 20px; /* 设置上下外边距为20px */
margin-bottom: 20px; /* 设置上下外边距为20px */
}
.margin-tb-xl {
margin-top: 25px; /* 设置上下外边距为25px */
margin-bottom: 25px; /* 设置上下外边距为25px */
}
.padding-0 {
padding: 0; /* 设置内边距为0 */
}
.padding-xs {
padding: 5px; /* 设置内边距为5px */
}
.padding-sm {
padding: 10px; /* 设置内边距为10px */
}
.padding {
padding: 15px; /* 设置内边距为15px */
}
.padding-lg {
padding: 20px; /* 设置内边距为20px */
}
.padding-xl {
padding: 25px; /* 设置内边距为25px */
}
.padding-top-xs {
padding-top: 5px; /* 设置上内边距为5px */
}
.padding-top-sm {
padding-top: 10px; /* 设置上内边距为10px */
}
.padding-top {
padding-top: 15px; /* 设置上内边距为15px */
}
.padding-top-lg {
padding-top: 20px; /* 设置上内边距为20px */
}
.padding-top-xl {
padding-top: 25px; /* 设置上内边距为25px */
}
.padding-right-xs {
padding-right: 5px; /* 设置右内边距为5px */
}
.padding-right-sm {
padding-right: 10px; /* 设置右内边距为10px */
}
.padding-right {
padding-right: 15px; /* 设置右内边距为15px */
}
.padding-right-lg {
padding-right: 20px; /* 设置右内边距为20px */
}
.padding-right-xl {
padding-right: 25px; /* 设置右内边距为25px */
}
.padding-bottom-xs {
padding-bottom: 5px; /* 设置下内边距为5px */
}
.padding-bottom-sm {
padding-bottom: 10px; /* 设置下内边距为10px */
}
.padding-bottom {
padding-bottom: 15px; /* 设置下内边距为15px */
}
.padding-bottom-lg {
padding-bottom: 20px; /* 设置下内边距为20px */
}
.padding-bottom-xl {
padding-bottom: 25px; /* 设置下内边距为25px */
}
.padding-left-xs {
padding-left: 5px; /* 设置左内边距为5px */
}
.padding-left-sm {
padding-left: 10px; /* 设置左内边距为10px */
}
.padding-left {
padding-left: 15px; /* 设置左内边距为15px */
}
.padding-left-lg {
padding-left: 20px; /* 设置左内边距为20px */
}
.padding-left-xl {
padding-left: 25px; /* 设置左内边距为25px */
}
.padding-lr-xs {
padding-left: 5px; /* 设置左右内边距为5px */
padding-right: 5px; /* 设置左右内边距为5px */
}
.padding-lr-sm {
padding-left: 10px; /* 设置左右内边距为10px */
padding-right: 10px; /* 设置左右内边距为10px */
}
.padding-lr {
padding-left: 15px; /* 设置左右内边距为15px */
padding-right: 15px; /* 设置左右内边距为15px */
}
.padding-lr-lg {
padding-left: 20px; /* 设置左右内边距为20px */
padding-right: 20px; /* 设置左右内边距为20px */
}
.padding-lr-xl {
padding-left: 25px; /* 设置左右内边距为25px */
padding-right: 25px; /* 设置左右内边距为25px */
}
.padding-tb-xs {
padding-top: 5px; /* 设置上下内边距为5px */
padding-bottom: 5px; /* 设置上下内边距为5px */
}
.padding-tb-sm {
padding-top: 10px; /* 设置上下内边距为10px */
padding-bottom: 10px; /* 设置上下内边距为10px */
}
.padding-tb {
padding-top: 15px; /* 设置上下内边距为15px */
padding-bottom: 15px; /* 设置上下内边距为15px */
}
.padding-tb-lg {
padding-top: 20px; /* 设置上下内边距为20px */
padding-bottom: 20px; /* 设置上下内边距为20px */
}
.padding-tb-xl {
padding-top: 25px; /* 设置上下内边距为25px */
padding-bottom: 25px; /* 设置上下内边距为25px */
}
/* -- 浮动 -- */
/* 清除浮动的伪元素,用于清除浮动影响 */
.cf::after,
.cf::before {
content: " "; /* 设置内容为一个空格 */
display: table; /* 将伪元素设置为表格布局 */
}
/* 清除浮动的具体实现 */
.cf::after {
clear: both; /* 清除左右两侧的浮动 */
}
/* 左浮动类 */
.fl {
float: left; /* 元素向左浮动 */
}
/* 右浮动类 */
.fr {
float: right; /* 元素向右浮动 */
}
/* ==================
==================== */
/* 设置红色线条的伪元素边框颜色 */
.line-red::after,
.lines-red::after {
border-color: #e54d42;
}
/* 设置橙色线条的伪元素边框颜色 */
.line-orange::after,
.lines-orange::after {
border-color: #f37b1d;
}
/* 设置黄色线条的伪元素边框颜色 */
.line-yellow::after,
.lines-yellow::after {
border-color: #fbbd08;
}
/* 设置橄榄色线条的伪元素边框颜色 */
.line-olive::after,
.lines-olive::after {
border-color: #8dc63f;
}
/* 设置绿色线条的伪元素边框颜色 */
.line-green::after,
.lines-green::after {
border-color: #39b54a;
}
/* 设置青色线条的伪元素边框颜色 */
.line-cyan::after,
.lines-cyan::after {
border-color: #1cbbb4;
}
/* 设置蓝色线条的伪元素边框颜色 */
.line-blue::after,
.lines-blue::after {
border-color: #0081ff;
}
/* 设置紫色线条的伪元素边框颜色 */
.line-purple::after,
.lines-purple::after {
border-color: #6739b6;
}
/* 设置紫罗兰色线条的伪元素边框颜色 */
.line-mauve::after,
.lines-mauve::after {
border-color: #9c26b0;
}
/* 设置粉色线条的伪元素边框颜色 */
.line-pink::after,
.lines-pink::after {
border-color: #e03997;
}
/* 设置棕色线条的伪元素边框颜色 */
.line-brown::after,
.lines-brown::after {
border-color: #a5673f;
}
/* 设置灰色线条的伪元素边框颜色 */
.line-grey::after,
.lines-grey::after {
border-color: #8799a3;
}
/* 设置深灰色线条的伪元素边框颜色 */
.line-gray::after,
.lines-gray::after {
border-color: #aaaaaa;
}
/* 设置黑色线条的伪元素边框颜色 */
.line-black::after,
.lines-black::after {
border-color: #333333;
}
/* 设置白色线条的伪元素边框颜色 */
.line-white::after,
.lines-white::after {
border-color: #ffffff;
}
/* 设置红色背景和文字颜色 */
.bg-red {
background-color: #e54d42;
color: #ffffff;
}
/* 设置橙色背景和文字颜色 */
.bg-orange {
background-color: #f37b1d;
color: #ffffff;
}
/* 设置黄色背景和文字颜色 */
.bg-yellow {
background-color: #fbbd08;
color: #333333;
}
/* 设置橄榄色背景和文字颜色 */
.bg-olive {
background-color: #8dc63f;
color: #ffffff;
}
/* 设置绿色背景和文字颜色 */
.bg-green {
background-color: #39b54a;
color: #ffffff;
}
/* 设置青色背景和文字颜色 */
.bg-cyan {
background-color: #1cbbb4;
color: #ffffff;
}
/* 设置蓝色背景和文字颜色 */
.bg-blue {
background-color: #0081ff;
color: #ffffff;
}
/* 设置紫色背景和文字颜色 */
.bg-purple {
background-color: #6739b6;
color: #ffffff;
}
/* 设置紫罗兰色背景和文字颜色 */
.bg-mauve {
background-color: #9c26b0;
color: #ffffff;
}
/* 设置粉色背景和文字颜色 */
.bg-pink {
background-color: #e03997;
color: #ffffff;
}
/* 设置棕色背景和文字颜色 */
.bg-brown {
background-color: #a5673f;
color: #ffffff;
}
/* 设置灰色背景和文字颜色 */
.bg-grey {
background-color: #8799a3;
color: #ffffff;
}
/* 设置深灰色背景和文字颜色 */
.bg-gray {
background-color: #f0f0f0;
color: #333333;
}
/* 设置黑色背景和文字颜色 */
.bg-black {
background-color: #333333;
color: #ffffff;
}
/* 设置白色背景和文字颜色 */
.bg-white {
background-color: #ffffff;
color: #666666;
}
/* 设置红色背景的浅色版本 */
.bg-red.light {
color: #e54d42;
background-color: #fadbd9;
}
/* 设置橙色背景的浅色版本 */
.bg-orange.light {
color: #f37b1d;
background-color: #fde6d2;
}
/* 设置黄色背景的浅色版本 */
.bg-yellow.light {
color: #fbbd08;
background-color: #fef2ced2;
}
/* 设置橄榄色背景的浅色版本 */
.bg-olive.light {
color: #8dc63f;
background-color: #e8f4d9;
}
/* 设置绿色背景的浅色版本 */
.bg-green.light {
color: #39b54a;
background-color: #d7f0dbff;
}
/* 设置青色背景的浅色版本 */
.bg-cyan.light {
color: #1cbbb4;
background-color: #d2f1f0;
}
/* 设置蓝色背景的浅色版本 */
.bg-blue.light {
color: #0081ff;
background-color: #cce6ff;
}
/* 设置紫色背景的浅色版本 */
.bg-purple.light {
color: #6739b6;
background-color: #e1d7f0;
}
/* 设置紫罗兰色背景的浅色版本 */
.bg-mauve.light {
color: #9c26b0;
background-color: #ebd4ef;
}
/* 设置粉色背景的浅色版本 */
.bg-pink.light {
color: #e03997;
background-color: #f9d7ea;
}
/* 设置棕色背景的浅色版本 */
.bg-brown.light {
color: #a5673f;
background-color: #ede1d9;
}
/* 设置灰色背景的浅色版本 */
.bg-grey.light {
color: #8799a3;
background-color: #e7ebed;
}
/* 设置渐变红色背景 */
.bg-gradual-red {
background-image: linear-gradient(45deg, #f43f3b, #ec008c);
color: #ffffff;
}
/* 设置渐变橙色背景 */
.bg-gradual-orange {
background-image: linear-gradient(45deg, #ff9900, #ed1c24);
color: #ffffff;
}
/* 设置渐变绿色背景 */
.bg-gradual-green {
background-image: linear-gradient(45deg, #39b54a, #8dc63f);
color: #ffffff;
}
/* 设置渐变紫色背景 */
.bg-gradual-purple {
background-image: linear-gradient(45deg, #9000ff, #5e00ff);
color: #ffffff;
}
/* 设置渐变粉色背景 */
.bg-gradual-pink {
background-image: linear-gradient(45deg, #ec008c, #6739b6);
color: #ffffff;
}
/* 设置渐变蓝色背景 */
.bg-gradual-blue {
background-image: linear-gradient(45deg, #0081ff, #1cbbb4);
color: #ffffff;
}
/* ==================
==================== */
/* 超小号字体 */
.text-xs {
font-size: 10px;
}
/* 小号字体 */
.text-sm {
font-size: 12px;
}
/* 默认字体大小 */
.text-df {
font-size: 14px;
}
/* 大号字体 */
.text-lg {
font-size: 16px;
}
/* 超大号字体 */
.text-xl {
font-size: 18px;
}
/* 特大号字体 */
.text-xxl {
font-size: 22px;
}
/* 超大号字体 */
.text-sl {
font-size: 40px;
}
/* 极大号字体 */
.text-xsl {
font-size: 60px;
}
/* 首字母大写 */
.text-Abc {
text-transform: Capitalize;
}
/* 全部大写 */
.text-ABC {
text-transform: Uppercase;
}
/* 全部小写 */
.text-abc {
text-transform: Lowercase;
}
/* 文字溢出省略显示 */
.text-cut {
text-overflow: ellipsis; /* 使用省略号表示溢出的文本 */
white-space: nowrap; /* 禁止换行 */
overflow: hidden; /* 隐藏溢出内容 */
}
/* 加粗字体 */
.text-bold {
font-weight: bold; /* 设置字体为加粗 */
}
/* 文本居中对齐 */
.text-center {
text-align: center; /* 设置文本居中对齐 */
}
/* 文本内容行高 */
.text-content {
line-height: 1.6; /* 设置行高为1.6倍字体高度 */
}
/* 文本左对齐 */
.text-left {
text-align: left; /* 设置文本左对齐 */
}
/* 文本右对齐 */
.text-right {
text-align: right; /* 设置文本右对齐 */
}
/* 红色文本和线条 */
.text-red,
.line-red,
.lines-red {
color: #e54d42; /* 设置颜色为红色 */
}
/* 橙色文本和线条 */
.text-orange,
.line-orange,
.lines-orange {
color: #f37b1d; /* 设置颜色为橙色 */
}
/* 黄色文本和线条 */
.text-yellow,
.line-yellow,
.lines-yellow {
color: #fbbd08; /* 设置颜色为黄色 */
}
/* 橄榄色文本和线条 */
.text-olive,
.line-olive,
.lines-olive {
color: #8dc63f; /* 设置颜色为橄榄色 */
}
/* 绿色文本和线条 */
.text-green,
.line-green,
.lines-green {
color: #39b54a; /* 设置颜色为绿色 */
}
/* 青色文本和线条 */
.text-cyan,
.line-cyan,
.lines-cyan {
color: #1cbbb4; /* 设置颜色为青色 */
}
/* 蓝色文本和线条 */
.text-blue,
.line-blue,
.lines-blue {
color: #0081ff; /* 设置颜色为蓝色 */
}
/* 紫色文本和线条 */
.text-purple,
.line-purple,
.lines-purple {
color: #6739b6; /* 设置颜色为紫色 */
}
/* 紫罗兰色文本和线条 */
.text-mauve,
.line-mauve,
.lines-mauve {
color: #9c26b0; /* 设置颜色为紫罗兰色 */
}
/* 粉色文本和线条 */
.text-pink,
.line-pink,
.lines-pink {
color: #e03997; /* 设置颜色为粉色 */
}
/* 棕色文本和线条 */
.text-brown,
.line-brown,
.lines-brown {
color: #a5673f; /* 设置颜色为棕色 */
}
/* 灰色文本和线条 */
.text-grey,
.line-grey,
.lines-grey {
color: #8799a3; /* 设置颜色为灰色 */
}
/* 深灰色文本和线条 */
.text-gray,
.line-gray,
.lines-gray {
color: #aaaaaa; /* 设置颜色为深灰色 */
}
/* 黑色文本和线条 */
.text-black,
.line-black,
.lines-black {
color: #333333; /* 设置颜色为黑色 */
}
/* 白色文本和线条 */
.text-white,
.line-white,
.lines-white {
color: #ffffff; /* 设置颜色为白色 */
}

@ -0,0 +1,364 @@
@charset "utf-8";
* {
box-sizing: border-box; /* 设置所有元素的盒子模型为border-box */
}
#app-menu ul {
padding: 0; /* 移除列表的默认内边距 */
}
#app-menu li {
list-style: none; /* 移除列表项的默认样式 */
}
#app-menu {
overflow: hidden; /* 隐藏溢出的内容 */
width: 100%; /* 设置宽度为100% */
}
.weixin-preview {
position: relative; /* 相对定位 */
width: 320px; /* 设置宽度 */
height: 540px; /* 设置高度 */
float: left; /* 左浮动 */
margin-right: 10px; /* 右边距 */
border: 1px solid #e7e7eb; /* 边框颜色和宽度 */
}
.weixin-preview a {
text-decoration: none; /* 移除链接下划线 */
color: #616161; /* 文字颜色 */
}
.weixin-preview .weixin-hd .weixin-title {
color: #fff; /* 文字颜色 */
font-size: 15px; /* 字体大小 */
width: 100%; /* 宽度 */
text-align: center; /* 文字居中对齐 */
position: absolute; /* 绝对定位 */
top: 33px; /* 顶部位置 */
left: 0px; /* 左侧位置 */
}
.weixin-preview .weixin-header{
text-align: center; /* 文字居中对齐 */
padding: 10px 0; /* 上下内边距 */
background-color: #616161; /* 背景颜色 */
color: #ffffff; /* 文字颜色 */
}
.weixin-preview .weixin-menu {
position: absolute; /* 绝对定位 */
bottom: 0; /* 底部位置 */
left: 0; /* 左侧位置 */
right: 0; /* 右侧位置 */
border-top: 1px solid #e7e7e7; /* 上边框颜色和宽度 */
background-position: 0 0; /* 背景图像位置 */
background-repeat: no-repeat; /* 背景图像不重复 */
margin-bottom: 0px; /* 底部外边距 */
}
/*一级*/
.weixin-preview .weixin-menu .menu-item {
position: relative; /* 相对定位 */
float: left; /* 左浮动 */
line-height: 50px; /* 行高 */
height: 50px; /* 高度 */
text-align: center; /* 文字居中对齐 */
width: 33.33%; /* 宽度 */
border-left: 1px solid #e7e7e7; /* 左边框颜色和宽度 */
cursor: pointer; /* 鼠标指针变为手型 */
color: #616161; /* 文字颜色 */
}
/*二级*/
.weixin-preview .weixin-sub-menu {
position: absolute; /* 绝对定位 */
bottom: 60px; /* 底部位置 */
left: 0; /* 左侧位置 */
right: 0; /* 右侧位置 */
border-top: 1px solid #d0d0d0; /* 上边框颜色和宽度 */
margin-bottom: 0px; /* 底部外边距 */
background: #fafafa; /* 背景颜色 */
display: block; /* 块级显示 */
padding: 0; /* 内边距 */
}
.weixin-preview .weixin-sub-menu .menu-sub-item {
line-height: 50px; /* 行高 */
height: 50px; /* 高度 */
text-align: center; /* 文字居中对齐 */
width: 100%; /* 宽度 */
border: 1px solid #d0d0d0; /* 边框颜色和宽度 */
border-top-width: 0px; /* 顶部边框宽度 */
cursor: pointer; /* 鼠标指针变为手型 */
position: relative; /* 相对定位 */
color: #616161; /* 文字颜色 */
}
.weixin-preview .weixin-sub-menu .menu-sub-item.on-drag-over{
border-top: 2px solid #44b549; /* 顶部边框颜色和宽度 */
}
.weixin-preview .menu-arrow {
position: absolute; /* 绝对定位 */
left: 50%; /* 左侧位置 */
margin-left: -6px; /* 左边距调整 */
}
.weixin-preview .arrow_in {
bottom: -4px; /* 底部位置 */
display: inline-block; /* 行内块级显示 */
width: 0px; /* 宽度 */
height: 0px; /* 高度 */
border-width: 6px 6px 0px; /* 边框宽度 */
border-style: solid dashed dashed; /* 边框样式 */
border-color: #fafafa transparent transparent; /* 边框颜色 */
}
.weixin-preview .arrow_out {
bottom: -5px; /* 底部位置 */
display: inline-block; /* 行内块级显示 */
width: 0px; /* 宽度 */
height: 0px; /* 高度 */
border-width: 6px 6px 0px; /* 边框宽度 */
border-style: solid dashed dashed; /* 边框样式 */
border-color: #d0d0d0 transparent transparent; /* 边框颜色 */
}
.weixin-preview .menu-item .menu-item-title, .weixin-preview .menu-sub-item .menu-item-title {
width: 100%; /* 宽度 */
overflow: hidden; /* 内容溢出时隐藏 */
white-space: nowrap; /* 不换行 */
text-overflow: ellipsis; /* 文本溢出时显示省略号 */
box-sizing: border-box; /* 盒子模型为border-box */
}
.weixin-preview .menu-item.current, .weixin-preview .menu-sub-item.current {
border: 1px solid #44b549; /* 边框颜色和宽度 */
background: #fff; /* 背景颜色 */
color: #44b549; /* 文字颜色 */
}
.weixin-preview .weixin-sub-menu.show {
display: block; /* 块级显示 */
}
.weixin-preview .icon_menu_dot {
/* background: url(../images/index_z354723.png) 0px -36px no-repeat; */
width: 7px; /* 宽度 */
height: 7px; /* 高度 */
vertical-align: middle; /* 垂直居中对齐 */
display: inline-block; /* 行内块级显示 */
margin-right: 2px; /* 右边距 */
margin-top: -2px; /* 顶部外边距调整 */
}
.weixin-preview .icon14_menu_add {
/* background: url(../images/index_z354723.png) 0px 0px no-repeat; */
width: 14px; /* 宽度 */
height: 14px; /* 高度 */
vertical-align: middle; /* 垂直居中对齐 */
display: inline-block; /* 行内块级显示 */
margin-top: -2px; /* 顶部外边距调整 */
}
.weixin-preview li:hover .icon14_menu_add {
/* background: url(../images/index_z354723.png) 0px -18px no-repeat; */
}
.weixin-preview .menu-item:hover {
color: #000; /* 文字颜色 */
}
.weixin-preview .menu-sub-item:hover {
background: #eee; /* 背景颜色 */
}
.weixin-preview li.current:hover {
background: #fff; /* 背景颜色 */
color: #44b549; /* 文字颜色 */
}
/*菜单内容*/
.weixin-menu-detail {
width: 600px; /* 宽度 */
padding: 0px 20px 5px; /* 内边距 */
background-color: #f4f5f9; /* 背景颜色 */
border: 1px solid #e7e7eb; /* 边框颜色和宽度 */
float: left; /* 左浮动 */
min-height: 540px; /* 最小高度 */
}
.weixin-menu-detail .menu-name {
float: left; /* 左浮动 */
height: 40px; /* 高度 */
line-height: 40px; /* 行高 */
font-size: 18px; /* 字体大小 */
}
.weixin-menu-detail .menu-del {
float: right; /* 右浮动 */
height: 40px; /* 高度 */
line-height: 40px; /* 行高 */
color: #459ae9; /* 文字颜色 */
cursor: pointer; /* 鼠标指针变为手型 */
}
.weixin-menu-detail .menu-input-group {
width: 540px; /* 宽度 */
margin: 10px 0 30px 0; /* 外边距 */
overflow: hidden; /* 隐藏溢出的内容 */
}
.weixin-menu-detail .menu-label {
float: left; /* 左浮动 */
height: 30px; /* 高度 */
line-height: 30px; /* 行高 */
width: 80px; /* 宽度 */
text-align: right; /* 文字右对齐 */
}
.weixin-menu-detail .menu-input {
float: left; /* 左浮动 */
width: 380px; /* 宽度 */
}
.weixin-menu-detail .menu-input-text {
border: 0px; /* 边框宽度 */
outline: 0px; /* focus时的轮廓宽度 */
background: #fff; /* 背景颜色 */
width: 300px; /* 宽度 */
padding: 5px 0px 5px 0px; /* 内边距 */
margin-left: 10px; /* 左边距 */
text-indent: 10px; /* text缩进 */
height: 35px; /* 高度 */
}
.weixin-menu-detail .menu-tips {
color: #8d8d8d; /* 文字颜色 */
padding-top: 4px; /* top方向的内边距 */
margin: 0; /* margin属性值设置为0 */
}
.weixin-menu-detail .menu-tips.cursor {
color: #459ae9; /* 文字颜色 */
cursor: pointer; /* mouse指针变为手型图标 */
}
.weixin-menu-detail .menu-input .menu-tips {
margin: 0 0 0 10px; /* margin属性值设置为0最后一个参数为10px */
}
.weixin-menu-detail .menu-content {
padding: 16px 20px; /* Padding属性值设置为16px和20px */
border: 1px solid #e7e7eb; /* Border属性值设置为1px和#e7e7eb颜色值 */
background-color: #fff; /* background-color属性值设置为白色 */
}
.weixin-menu-detail .menu-content .menu-input-group {
margin: 0px 0 10px 0; /* margin属性值设置为0px,10px和0px */
}
.weixin-menu-detail .menu-content .menu-label {
text-align: left; /* text对齐方式设置为左对齐 */
width: 100px; /* width属性值设置为100px */
}
.weixin-menu-detail .menu-content .menu-input-text {
border: 1px solid #e7e7eb; /* border属性值设置为1px和#e7e7eb颜色值 */
}
.weixin-menu-detail .menu-content .menu-tips {
padding-bottom: 10px; /* bottom方向的内边距设置为10px */
}
.weixin-menu-detail .menu-msg-content {
padding: 0; /* Padding属性值设置为0 */
border: 1px solid #e7e7eb; /* Border属性值设置为1px和#e7e7eb颜色值 */
background-color: #fff; /* background-color属性值设置为白色 */
}
.weixin-menu-detail .menu-msg-content .menu-msg-head {
overflow: hidden; /* Overflow属性值设置为hidden */
border-bottom: 1px solid #e7e7eb; /* Border属性值设置为1px和#e7e7eb颜色值 */
line-height: 38px; /* Line height属性值设置为38px */
height: 38px; /* height属性值设置为38px */
padding: 0 20px; /* Padding属性值设置为0和20px */
}
.weixin-menu-detail .menu-msg-content .menu-msg-panel {
padding: 30px 50px; /* Padding属性值设置为30px和50px */
}
.weixin-menu-detail .menu-msg-content .menu-msg-select {
padding: 40px 20px; /* Padding属性值设置为40px和20px */
border: 2px dotted #d9dadc; /* Border属性值设置为2px虚线和#d9dadc颜色值 */
text-align: center; /* text对齐方式设置为居中对齐 */
}
.weixin-menu-detail .menu-msg-content .menu-msg-select:hover {
border-color: #b3b3b3; /* border颜色值设置为#b3b3b3 */
}
.weixin-menu-detail .menu-msg-content strong {
display: block; /* Display属性值设置为block */
padding-top: 3px; /* Top方向的内边距设置为3px */
font-weight: 400; /* font weight属性值设置为400 */
font-style: normal; /* font style属性值设置为normal */
}
.weixin-menu-detail .menu-msg-content .menu-msg-title {
float: left; /* float属性值设置为left */
width: 310px; /* width属性值设置为310px */
height: 30px; /* height属性值设置为30px */
line-height: 30px; /* line height属性值设置为30px */
overflow: hidden; /* Overflow属性值设置为hidden */
text-overflow: ellipsis; /* TextOverflow属性值设置为ellipsis */
white-space: nowrap; /* WhiteSpace属性值设置为nowrap */
}
.icon36_common {
width: 36px; /* width属性值设置为36px */
height: 36px; /* height属性值设置为36px */
vertical-align: middle; /* vertical align属性值设置为middle */
display: inline-block; /* Display属性值设置为inline block */
}
.icon_msg_sender {
margin-right: 3px; /* Right方向的外边距设置为3px */
margin-top: -2px; /* Top方向的外边距设置为负2px */
width: 20px; /* width属性值设置为20px */
height: 20px; /* height属性值设置为20px */
vertical-align: middle; /* vertical align属性值设置为middle */
display: inline-block; /* Display属性值设置为inline block */
/* background: url(../images/msg_tab_z25df2d.png) 0 -270px no-repeat; */
}
.weixin-btn-group {
text-align: center; /* text对齐方式设置为居中对齐 */
width: 100%; /* width属性值设置为100% */
margin: 30px 0px; /* margin属性值设置为30px和0px */
overflow: hidden; /* Overflow属性值设置为hidden */
}
.weixin-btn-group .btn {
width: 100px; /* width属性值设置为100px */
border-radius: 0px; /* border radius属性值设置为0px */
}
#material-list {
padding: 20px; /* Padding属性值设置为20px */
overflow-y: scroll; /* Overflow y属性值设置为scroll使内容可滚动 */
height: 558px; /* height属性值设置为558px */
}
#news-list {
padding: 20px; /* Padding属性值设置为20px */
overflow-y: scroll; /* Overflow y属性值设置为scroll使内容可滚动 */
height: 558px; /* height属性值设置为558px */
}
#material-list table {
width: 100%; /* width属性值设置为100% */
}

@ -0,0 +1,422 @@
/* 设置所有元素和伪元素的 box-sizing 为 border-box */
*,
*:before,
*:after {
box-sizing: border-box;
}
/* 设置 body 的字体、大小、行高和颜色 */
body {
font-family: "Helvetica Neue", Helvetica, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "微软雅黑", Arial, sans-serif;
font-size: 14px;
line-height: 1.15;
color: #303133;
background-color: #fff;
}
/* 设置链接的颜色和文本装饰 */
a {
color: mix(#fff, $--color-primary, 20%); /* 使用混合色函数设置颜色 */
text-decoration: none; /* 去掉下划线 */
/* 当链接获得焦点或悬停时,改变颜色并添加下划线 */
&:focus,
&:hover {
color: $--color-primary;
text-decoration: underline;
}
}
/* 设置图片垂直对齐方式为中间 */
img {
vertical-align: middle;
}
/* Utils
------------------------------ */
/* 清除浮动工具类 */
.clearfix:before,
.clearfix:after {
content: " "; /* 生成内容以触发 clearfix */
display: table; /* 设置为表格布局 */
}
.clearfix:after {
clear: both; /* 清除浮动 */
}
/* Animation
------------------------------ */
/* 淡入淡出动画过渡效果 */
.fade-enter-active,
.fade-leave-active {
transition: opacity .5s; /* 设置透明度变化的过渡时间 */
}
.fade-enter,
.fade-leave-to {
opacity: 0; /* 初始透明度为 0 */
}
/* Reset element-ui
------------------------------ */
/* 重置 element-ui 样式 */
.site-wrapper {
.el-pagination {
margin-top: 15px; /* 设置分页组件的上边距 */
text-align: right; /* 设置分页组件的文本对齐方式 */
}
}
/* Layout
------------------------------ */
/* 设置布局相关样式 */
.site-wrapper {
position: relative; /* 相对定位 */
min-width: 1180px; /* 最小宽度 */
}
/* Sidebar fold
------------------------------ */
/* 侧边栏折叠样式 */
.site-sidebar--fold {
/* 设置导航栏头部、品牌、侧边栏及其内部元素的宽度 */
.site-navbar__header,
.site-navbar__brand,
.site-sidebar,
.site-sidebar__inner,
.el-menu.site-sidebar__menu {
width: 64px; /* 宽度为 64px */
}
/* 设置导航栏主体和内容包装器的左边距 */
.site-navbar__body,
.site-content__wrapper {
margin-left: 64px; /* 左边距为 64px */
}
/* 设置品牌图标在不同状态下的显示方式 */
.site-navbar__brand {
&-lg {
display: none; /* 大尺寸时隐藏 */
}
&-mini {
display: inline-block; /* 小尺寸时内联显示 */
}
}
/* 设置侧边栏和其内部元素的溢出处理 */
.site-sidebar,
.site-sidebar__inner {
overflow: initial; /* 初始溢出处理 */
}
/* 设置侧边栏菜单图标的右边距和字体大小 */
.site-sidebar__menu-icon {
margin-right: 0; /* 右边距为 0 */
font-size: 20px; /* 字体大小为 20px */
}
/* 设置标签页内容的左边距 */
.site-content--tabs > .el-tabs > .el-tabs__header {
left: 64px; /* 左边距为 64px */
}
}
//* animation */
.site-navbar__header,
.site-navbar__brand,
.site-navbar__body,
.site-sidebar,
.site-sidebar__inner,
.site-sidebar__menu.el-menu,
.site-sidebar__menu-icon,
.site-content__wrapper,
.site-content--tabs > .el-tabs .el-tabs__header {
//
transition: inline-block .3s, left .3s, width .3s, margin-left .3s, font-size .3s;
}
/* Navbar */
.site-navbar {
//
position: fixed;
top: 0;
right: 0;
left: 0;
z-index: 1030; //
height: 50px; // 50px
box-shadow: 0 2px 4px rgba(0, 0, 0, .08); //
background-color: $navbar--background-color; //
&--inverse {
.site-navbar__body {
background-color: transparent; //
}
.el-menu {
> .el-menu-item,
> .el-submenu > .el-submenu__title {
color: #fff; //
&:focus,
&:hover {
color: #fff; //
background-color: mix(#000, $navbar--background-color, 15%); //
}
}
> .el-menu-item.is-active,
> .el-submenu.is-active > .el-submenu__title {
border-bottom-color: mix(#fff, $navbar--background-color, 85%); //
}
.el-menu-item i,
.el-submenu__title i,
.el-dropdown {
color: #fff; //
}
}
.el-menu--popup-bottom-start {
background-color: $navbar--background-color; //
}
}
&__header {
position: relative; //
float: left; //
width: 230px; // 230px
height: 50px; // 50px
overflow: hidden; //
}
&__brand {
display: table-cell; //
vertical-align: middle; //
width: 230px; // 230px
height: 50px; // 50px
margin: 0; //
line-height: 50px; // 50px
font-size: 20px; // 20px
text-align: center; //
text-transform: uppercase; //
white-space: nowrap; //
color: #fff; //
&-lg,
&-mini {
margin: 0 5px; // 5px
color: #fff; //
&:focus,
&:hover {
color: #fff; //
text-decoration: none; // 线
}
}
&-mini {
display: none; //
}
}
&__switch {
font-size: 18px; // 18px
border-bottom: none !important; //
}
&__avatar {
border-bottom: none !important; //
* {
vertical-align: inherit; //
}
.el-dropdown-link {
> img {
width: 36px; // 36px
height: auto; //
margin-right: 5px; // 5px
border-radius: 100%; //
vertical-align: middle; //
}
}
}
&__body {
position: relative; //
margin-left: 230px; // 230px
padding-right: 15px; // 15px
background-color: #fff; //
}
&__menu {
float: left; //
background-color: transparent; //
border-bottom: 0; //
&--right {
float: right; //
}
a:focus,
a:hover {
text-decoration: none; // 线
}
.el-menu-item,
.el-submenu > .el-submenu__title {
height: 50px; // 50px
line-height: 50px; // 50px
}
.el-submenu > .el-menu {
top: 55px; // 55px
}
.el-badge {
display: inline; //
z-index: 2; // z2
&__content {
line-height: 16px; // 16px
}
}
}
}
/* Sidebar
------------------------------ */
.site-sidebar {
position: fixed; //
top: 50px; // 50px
left: 0; // 0
bottom: 0; //
z-index: 1020; // z
width: 230px; // 230px
overflow: hidden; //
&--dark,
&--dark-popper {
background-color: $sidebar--background-color-dark; //
.site-sidebar__menu.el-menu,
> .el-menu--popup {
background-color: $sidebar--background-color-dark; //
.el-menu-item,
.el-submenu > .el-submenu__title {
color: $sidebar--color-text-dark; //
&:focus,
&:hover {
color: mix(#fff, $sidebar--color-text-dark, 50%); //
background-color: mix(#fff, $sidebar--background-color-dark, 2.5%); //
}
}
.el-menu,
.el-submenu.is-opened {
background-color: mix(#000, $sidebar--background-color-dark, 15%); //
}
.el-menu-item.is-active,
.el-submenu.is-active > .el-submenu__title {
color: mix(#fff, $sidebar--color-text-dark, 80%); //
}
}
}
&__inner {
position: relative; //
z-index: 1; // z
width: 250px; // 250px
height: 100%; // 100%
padding-bottom: 15px; // 15px
overflow-y: scroll; // y
}
&__menu.el-menu {
width: 230px; // 230px
border-right: 0; //
}
&__menu-icon {
width: 24px; // 24px
margin-right: 5px; // 5px
text-align: center; //
font-size: 16px; // 16px
color: inherit !important; //
}
}
/* Content
------------------------------ */
.site-content {
position: relative; //
padding: 15px; // 15px
&__wrapper {
position: relative; //
padding-top: 50px; // 50px
margin-left: 230px; // 230px
min-height: 100%; // 100%
background: $content--background-color; //
}
&--tabs {
padding: 55px 0 0; // 55px00
}
> .el-tabs {
> .el-tabs__header {
position: fixed; //
top: 50px; // 50px
left: 230px; // 230px
right: 0; // 0
z-index: 930; // z
padding: 0 55px 0 15px; //
box-shadow: 0 2px 4px 0 rgba(0, 0, 0, .12), 0 0 6px 0 rgba(0, 0, 0, .04); //
background-color: #fff; //
> .el-tabs__nav-wrap {
margin-bottom: 0; // 0
&:after {
display: none; //
}
}
}
> .el-tabs__content {
padding: 0 15px 15px; //
> .site-tabs__tools {
position: fixed; //
top: 50px; // 50px
right: 0; // 0
z-index: 931; // z
height: 40px; // 40px
padding: 0 12px; //
font-size: 16px; // 16px
line-height: 40px; // 40px
background-color: $content--background-color; //
cursor: pointer; //
.el-icon--right {
margin-left: 0; // 0
}
}
}
}
}
.el-table__expand-icon {
display: inline-block; //
width: 14px; // 14px
vertical-align: middle; //
margin-right: 5px; // 5px
}

@ -0,0 +1,443 @@
/*! normalize.css v7.0.0 | MIT License | github.com/necolas/normalize.css */
//* Document
========================================================================== */
/**
* 1.
* 2. Windows Phone iOS IE
*/
html {
line-height: 1.15; /* 1 */
-ms-text-size-adjust: 100%; /* 2 */
-webkit-text-size-adjust: 100%; /* 2 */
}
/* Sections
========================================================================== */
/**
*
*/
body {
margin: 0;
}
/**
* IE 9-
*/
article,
aside,
footer,
header,
nav,
section {
display: block;
}
/**
* ChromeFirefox Safari `h1` `section` `article`
*/
h1 {
font-size: 2em;
margin: 0.67em 0;
}
/* Grouping content
========================================================================== */
/**
* IE 9-
* 1. IE
*/
figcaption,
figure,
main { /* 1 */
display: block;
}
/**
* IE 8
*/
figure {
margin: 1em 40px;
}
/**
* 1. Firefox
* 2. Edge IE
*/
hr {
box-sizing: content-box; /* 1 */
height: 0; /* 1 */
overflow: visible; /* 2 */
}
/**
* 1.
* 2. `em`
*/
pre {
font-family: monospace, monospace; /* 1 */
font-size: 1em; /* 2 */
}
/* Text-level semantics
========================================================================== */
/**
* 1. IE 10
* 2. iOS 8+ Safari 8+ 线
*/
a {
background-color: transparent; /* 1 */
-webkit-text-decoration-skip: objects; /* 2 */
}
/**
* 1. Chrome 57- Firefox 39-
* 2. ChromeEdgeIEOpera Safari
*/
abbr[title] {
border-bottom: none; /* 1 */
text-decoration: underline; /* 2 */
text-decoration: underline dotted; /* 2 */
}
/**
* Safari 6 `bolder`
*/
b,
strong {
font-weight: inherit;
}
/**
* ChromeEdge Safari
*/
b,
strong {
font-weight: bolder;
}
/**
* 1.
* 2. `em`
*/
code,
kbd,
samp {
font-family: monospace, monospace; /* 1 */
font-size: 1em; /* 2 */
}
/**
* Android 4.3-
*/
dfn {
font-style: italic;
}
/**
* IE 9-
*/
mark {
background-color: #ff0;
color: #000;
}
/**
*
*/
small {
font-size: 80%;
}
/**
* `sub` `sup`
*/
sub,
sup {
font-size: 75%;
line-height: 0;
position: relative;
vertical-align: baseline;
}
sub {
bottom: -0.25em;
}
sup {
top: -0.5em;
}
/* Embedded content
========================================================================== */
/**
* IE 9-
*/
audio,
video {
display: inline-block;
}
/**
* iOS 4-7
*/
audio:not([controls]) {
display: none;
height: 0;
}
/**
* IE 10-
*/
img {
border-style: none;
}
/**
* IE
*/
svg:not(:root) {
overflow: hidden;
}
/* Forms
========================================================================== */
/**
* 1.
* 2. Firefox Safari
*/
button,
input,
optgroup,
select,
textarea {
font-family: sans-serif; /* 1 */
font-size: 100%; /* 1 */
line-height: 1.15; /* 1 */
margin: 0; /* 2 */
}
/**
* IE
* 1. Edge
*/
button,
input { /* 1 */
overflow: visible;
}
/**
* EdgeFirefox IE
* 1. Firefox
*/
button,
select { /* 1 */
text-transform: none;
}
/**
* 1. WebKit bug bug Android 4 `audio` `video`
* 2. iOS Safari
*/
button,
html [type="button"], /* 1 */
[type="reset"],
[type="submit"] {
-webkit-appearance: button; /* 2 */
}
/**
* Firefox
*/
button::-moz-focus-inner,
[type="button"]::-moz-focus-inner,
[type="reset"]::-moz-focus-inner,
[type="submit"]::-moz-focus-inner {
border-style: none;
padding: 0;
}
/**
*
*/
button:-moz-focusring,
[type="button"]:-moz-focusring,
[type="reset"]:-moz-focusring,
[type="submit"]:-moz-focusring {
outline: 1px dotted ButtonText;
}
/**
* Firefox
*/
fieldset {
padding: 0.35em 0.75em 0.625em;
}
/**
* 1. Edge IE
* 2. IE `fieldset`
* 3. `fieldset`
*/
legend {
box-sizing: border-box; /* 1 */
color: inherit; /* 2 */
display: table; /* 1 */
max-width: 100%; /* 1 */
padding: 0; /* 3 */
white-space: normal; /* 1 */
}
/**
* 1. IE 9+
* 2. ChromeFirefox Opera
*/
progress {
display: inline-block; /* 1 */
vertical-align: baseline; /* 2 */
}
/**
* IE
*/
textarea {
overflow: auto;
}
/**
* 1. IE 10+
* 2. IE 10+
*/
[type="checkbox"],
[type="radio"] {
box-sizing: border-box; /* 1 */
padding: 0; /* 2 */
}
/**
* Chrome
*/
[type="number"]::-webkit-inner-spin-button,
[type="number"]::-webkit-outer-spin-button {
height: auto;
}
/**
* 1. Chrome Safari
* 2. Safari
*/
[type="search"] {
-webkit-appearance: textfield; /* 1 */
outline-offset: -2px; /* 2 */
}
/**
* Chrome Safari on macOS
*/
[type="search"]::-webkit-search-cancel-button,
[type="search"]::-webkit-search-decoration {
-webkit-appearance: none;
}
/**
* 1. iOS Safari
* 2. Safari `inherit`
*/
::-webkit-file-upload-button {
-webkit-appearance: button; /* 1 */
font: inherit; /* 2 */
}
/* Interactive
========================================================================== */
/*
* IE 9+
* 1. EdgeIE Firefox
*/
details, /* 1 */
menu {
display: block;
}
/*
*
*/
summary {
display: list-item;
}
/* Scripting
========================================================================== */
/**
* IE 9+
*/
canvas {
display: inline-block;
}
/**
* IE
*/
template {
display: none;
}
/* Hidden
========================================================================== */
/**
* IE 10+
*/
[hidden] {
display: none;
}

@ -0,0 +1,17 @@
//
// tips: , [$--color-primary][/src/element-ui-theme/index.js][import './element-[#17B3A3]/index.css']
$--color-primary: #409EFF;
// Navbar
//
$navbar--background-color: $--color-primary;
// Sidebar
//
$sidebar--background-color-dark: #263238;
//
$sidebar--color-text-dark: #8a979e;
// Content
//
$content--background-color: #f1f4f5;

@ -0,0 +1,7 @@
@import "normalize";
// normalize.css
// api: https://github.com/necolas/normalize.css/
@import "variables";
// 使
@import "base";
//

@ -0,0 +1,52 @@
<template>
<!-- SVG元素使用动态绑定的类名宽度和高度属性 -->
<svg :class="getClassName" :width="width" :height="height" aria-hidden="true">
<!-- 使用xlink:href属性引用外部定义的SVG符号 -->
<use :xlink:href="getName"></use>
</svg>
</template>
<script>
export default {
name: 'icon-svg', // 'icon-svg'
props: {
name: {
type: String, // 'name'
required: true // 'name'
},
className: {
type: String // 'className'
},
width: {
type: String // 'width'
},
height: {
type: String // 'height'
}
},
computed: {
// 'getName'ID
getName() {
return `#icon-${this.name}`
},
// 'getClassName'
getClassName() {
return [
'icon-svg', //
`icon-svg__${this.name}`, // 'name'
this.className && /\S/.test(this.className) ? `${this.className}` : '' // 'className'
]
}
}
}
</script>
<style>
/* 样式定义 */
.icon-svg {
width: 1em; // 1em
height: 1em; // 1em
fill: currentColor; //
overflow: hidden; //
}
</style>

@ -0,0 +1,95 @@
<template>
<!-- 定义一个表格列绑定属性和插槽 -->
<el-table-column :prop="prop" v-bind="$attrs">
<!-- 自定义单元格内容 -->
<template slot-scope="scope">
<!-- 点击时触发toggleHandle方法并应用样式 -->
<span @click.prevent="toggleHandle(scope.$index, scope.row)" :style="childStyles(scope.row)">
<!-- 根据行数据动态设置图标类和样式 -->
<i :class="iconClasses(scope.row)" :style="iconStyles(scope.row)"></i>
{{ scope.row[prop] }}
</span>
</template>
</el-table-column>
</template>
<script>
// lodashisArray
import isArray from 'lodash/isArray'
export default {
name: 'table-tree-column', //
props: {
prop: {
type: String //
},
treeKey: {
type: String,
default: 'id' // 'id'
},
parentKey: {
type: String,
default: 'parentId' // 'parentId'
},
levelKey: {
type: String,
default: '_level' // '_level'
},
childKey: {
type: String,
default: 'children' // 'children'
}
},
methods: {
//
childStyles(row) {
return { 'padding-left': (row[this.levelKey] > 1 ? row[this.levelKey] * 7 : 0) + 'px' }
},
//
iconClasses(row) {
return [!row._expanded ? 'el-icon-caret-right' : 'el-icon-caret-bottom']
},
//
iconStyles(row) {
return { 'visibility': this.hasChild(row) ? 'visible' : 'hidden' }
},
//
hasChild(row) {
return (isArray(row[this.childKey]) && row[this.childKey].length >= 1) || false
},
//
toggleHandle(index, row) {
if (this.hasChild(row)) {
var data = this.$parent.store.states.data.slice(0) //
data[index]._expanded = !data[index]._expanded //
if (data[index]._expanded) {
//
data = data.splice(0, index + 1).concat(row[this.childKey]).concat(data)
} else {
//
data = this.removeChildNode(data, row[this.treeKey])
}
this.$parent.store.commit('setData', data) //
this.$nextTick(() => {
this.$parent.doLayout() //
})
}
},
//
removeChildNode(data, parentId) {
var parentIds = isArray(parentId) ? parentId : [parentId] // parentId
if (parentId.length <= 0) {
return data // parentId
}
var ids = [] // ID
for (var i = 0; i < data.length; i++) {
if (parentIds.indexOf(data[i][this.parentKey]) !== -1 && parentIds.indexOf(data[i][this.treeKey]) === -1) {
data[i]._expanded = false //
ids.push(data.splice(i, 1)[0][this.treeKey]) // ID
i-- //
}
}
return this.removeChildNode(data, ids) //
}
}
}
</script>

@ -0,0 +1,83 @@
<template>
<!-- 面板容器使用flex布局 -->
<div class="panel flex flex-wrap">
<!-- 动态生成的标签列表 -->
<el-tag v-for="tag in dynamicTags" closable @close="handleClose(tag)" :disable-transitions="false" :key="tag">
{{tag}}
</el-tag>
<!-- 输入框用于添加新标签 -->
<el-input class="input-new-tag" v-if="inputVisible" v-model="inputValue" ref="saveTagInput" size="small" @keyup.enter.native="handleInputConfirm" @blur="handleInputConfirm">
</el-input>
<!-- 按钮点击后显示输入框 -->
<el-button v-else class="button-new-tag" size="small" @click="showInput">+ </el-button>
</div>
</template>
/**
* 标签编辑器组件
*/
let touchMoved = false; //
export default {
name: 'tags-editor', //
props: {
value: { //
type: String,
required: true,
default: ""
},
size: { // [small:,large:]
type: String,
default: 'small'
}
},
data() {
return {
inputVisible: false, //
inputValue: '' //
}
},
computed: {
dynamicTags() { // value
if (this.value != "") return this.value.split(',')
return []
}
},
methods: {
handleClose(tag) { //
let newTags = this.dynamicTags; //
newTags.splice(newTags.indexOf(tag), 1); //
this.$emit('input', newTags.join(",")); //
},
showInput() { //
this.inputVisible = true; //
this.$nextTick(_ => { // DOM
this.$refs.saveTagInput.$refs.input.focus(); //
});
},
handleInputConfirm() { //
let inputValue = this.inputValue; //
let newTags = this.dynamicTags; //
if (inputValue && newTags.indexOf(inputValue) < 0) { //
newTags.push(inputValue); //
}
this.inputVisible = false; //
this.inputValue = ''; //
this.$emit('input', newTags.join(",")); //
}
}
}
<style scoped>
/* 面板样式 */
.panel {
flex: 1; /* 弹性布局,占满剩余空间 */
}
/* 标签和按钮样式 */
.el-tag,.button-new-tag{
margin: 5px; /* 外边距 */
}
/* 输入框样式 */
.input-new-tag {
width: inherit; /* 继承父元素的宽度 */
}
</style>

@ -0,0 +1,211 @@
<template>
<!-- 模态对话框用于筛选模板消息目标用户 -->
<el-dialog title="筛选模板消息目标用户" :close-on-click-modal="false" :visible.sync="visible">
<!-- 表单布局为行内绑定数据模型dataForm清除按钮可清空输入框 -->
<el-form :inline="true" :model="dataForm" ref="dataForm" clearable @keyup.enter.native="getWxUsers()">
<!-- 用户标签选择框 -->
<el-form-item>
<el-select v-model="dataForm.tagid" filterable placeholder="用户标签" @change="getWxUsers()">
<!-- 遍历用户标签列表生成选项 -->
<el-option v-for="item in wxUserTags" :key="item.id" :label="item.name" :value="item.id+''"></el-option>
</el-select>
</el-form-item>
<!-- 昵称输入框 -->
<el-form-item>
<el-input v-model="dataForm.nickname" placeholder="昵称" @change="getWxUsers()" clearable></el-input>
</el-form-item>
<!-- 省份输入框 -->
<el-form-item>
<el-input v-model="dataForm.province" placeholder="省份" @change="getWxUsers()" clearable></el-input>
</el-form-item>
<!-- 城市输入框 -->
<el-form-item>
<el-input v-model="dataForm.city" placeholder="城市" @change="getWxUsers()" clearable></el-input>
</el-form-item>
<!-- 备注输入框 -->
<el-form-item>
<el-input v-model="dataForm.remark" placeholder="备注" @change="getWxUsers()" clearable></el-input>
</el-form-item>
<!-- 扫码场景值输入框 -->
<el-form-item>
<el-input v-model="dataForm.qrScene" placeholder="扫码场景值" @change="getWxUsers()" clearable></el-input>
</el-form-item>
</el-form>
<!-- 显示将发送的消息预览 -->
<div class="text-bold">本消息将发送给</div>
<!-- 用户列表区域加载状态时显示加载动画 -->
<div class="user-list" v-loading="wxUsersLoading">
<!-- 遍历用户列表生成用户卡片 -->
<div class="user-card" v-for="item in wxUserList" :key="item.openid">
<!-- 用户头像 -->
<el-avatar :src="item.headimgurl"></el-avatar>
<!-- 用户昵称 -->
<div class="nickname">{{item.nickname}}</div>
</div>
<!-- 当用户数量超过10个时显示省略号和总用户数 -->
<div class="text-bold">
<span v-show="totalCount>10">...</span>
等共<span class="text-success">{{totalCount}}</span>个用户
</div>
</div>
<!-- 消息预览标题 -->
<div class="margin-top text-bold">消息预览</div>
<!-- 消息预览内容禁用编辑且自动调整大小 -->
<div class="margin-top-xs">
<el-input type="textarea" disabled autosize v-model="msgReview" placeholder="模版"></el-input>
</div>
<!-- 底部操作栏 -->
<span slot="footer" class="dialog-footer">
<!-- 发送按钮根据发送状态显示不同文本 -->
<el-button @click="send" type="success" :disabled="totalCount<=0 || sending">{{sending?'发送中...':'发送'}}</el-button>
<!-- 关闭按钮 -->
<el-button @click="visible=false"></el-button>
</span>
</div>
</template>
<script>
import { mapState } from 'vuex'
export default {
name: 'template-msg', //
props: {
//
wxUserTagName: String,
},
data() {
return {
visible: false, //
wxUsersLoading: false, //
dataForm: { //
page: 1, //
sidx: 'subscribe_time', //
order: 'desc', //
tagid: '', // ID
nickname: '', //
province: '', //
city: '', //
remark: '', //
qrScene: '', //
},
msgTemplate: {}, //
wxUserList: [], //
totalCount: 0, //
}
},
computed: mapState({
wxUserTags: state => state.wxUserTags.tags, // Vuex
msgReview(): string { //
if (!this.msgTemplate.data) return "" //
let content = this.msgTemplate.content //
this.msgTemplate.data.forEach(item => { //
content = content.replace("{{" + item.name + ".DATA}}", item.value) //
})
return content //
}
}),
mounted() {
this.getWxUserTags().then((taglist) => { //
if (this.wxUserTagName) { //
let tagItem = taglist.find(tag => tag.name === this.wxUserTagName) //
if (tagItem) { //
this.dataForm.tagid = tagItem.id + '' // IDID
}
}
this.getWxUsers(); //
});
},
methods: {
init(msgTemplate) { //
if (!msgTemplate || !msgTemplate.templateId) { // ID
this.$message.error('消息模板无效') //
return //
}
if (!msgTemplate.data || !(msgTemplate.data instanceof Array)) { //
this.$message.error('请现配置此模板填充数据') //
return //
}
this.msgTemplate = msgTemplate //
this.visible = true; //
},
getWxUserTags() { //
return new Promise((resolve, reject) => { // Promise
this.$http({ // HTTP
url: this.$adorn.url('/manage/wxUserTags/list'), // URL
method: 'get', // HTTPGET
params: this.$adorn.params({}) //
}).then((response) => { //
if (response.data.code == 200) { // 200
this.$store.dispatch('wxUserTags/updateTags', response.data.list) // Vuex
resolve(response.data.list) // Promise
} else {
this.$message.error(response.data.msg) //
reject(response.data.msg) // Promise
}
}).catch(err=>reject(err)) // Promise
});
},
getWxUsers() { //
this.wxUsersLoading = true // true
this.$http({ // HTTP
url: this.$adorn.url('/manage/wxUser/list'), // URL
method: 'get', // HTTPGET
params: this.$adorn.params(this.dataForm) //
}).then((response) => { //
if (response.data.code == 200) { // 200
this.wxUserList = response.data.page.list //
this.totalCount = response.data.page.totalCount //
} else {
this.$message.error(response.data.msg) //
}
this.wxUsersLoading = false // false
}).catch(err=>reject(err)) // Promise
},
send() { //
if (this.sending) return //
this.sending = true // true
this.$http({ // HTTP
url: this.$adorn.url('/manage/msgTemplate/sendMsgBatch'), // URL
method: 'post', // HTTPPOST
data: this.$adorn.data({ //
wxUserFilterParams: this.dataForm, //
templateId: this.msgTemplate.templateId, // IDID
url: this.msgTemplate.url, // URLURL
miniprogram: this.msgTemplate.miniprogram, // miniprogram
data: this.msgTemplate.data, // data
})
}).then((response) => { //
this.sending = false // false
if (response.data.code == 200) { // 200
this.$message.success("消息将在后台发送") //
this.visible = false; //
} else {
this.$message.error(response.data.msg) //
}
}).catch(err=>reject(err)) // Promise
}
}
}
</script>
<style scoped>
/* 用户列表样式 */
.user-list{
display: flex;
flex-wrap: wrap;
align-items: center;
}
/* 用户卡片样式 */
.user-card{
overflow: hidden;
max-width: 60px;
margin: 5px;
text-align: center;
}
/* 昵称样式 */
.nickname{
color: #999999;
overflow: hidden;
text-overflow:ellipsis;
white-space: nowrap;
}
</style>

@ -0,0 +1,102 @@
<template>
<!-- 定义一个包含TinyMCE编辑器的div容器 -->
<div class="tinymce-editor">
<!-- 使用TinyMCE组件绑定v-model为myValue初始化配置为init监听onExecCommand事件 -->
<editor v-model="myValue" :init="init" @onExecCommand="onExecCommand"></editor>
</div>
</template>
import Editor from "@tinymce/tinymce-vue"; // TinyMCE Vue
var cos; // cos
export default {
name: "tinymce-editor", //
components: {
Editor // TinyMCE
},
props: {
value: {
type: String, // value
default: "" //
}
},
data() {
return {
// TinyMCE
init: {
language_url: "./tinymce/zh_CN.js", //
language: "zh_CN", //
height: 500, //
plugins: "lists image media table paste link searchreplace anchor code preview pagebreak importcss", //
toolbar: "undo redo searchreplace | formatselect pagebreak | bold italic forecolor backcolor | alignleft aligncenter alignright alignjustify | bullist numlist outdent indent | lists link anchor image media table | removeformat code preview", //
toolbar_drawer: false, //
image_advtab: true, //
object_resizing: false, //
paste_data_images: true, //
content_css: "./tinymce/article.css", //
images_upload_handler: (blobInfo, success, failure) => {
//
this.uploadFile(blobInfo.blob()).then(fileUrl => success(fileUrl)).catch(err => failure(err))
}
},
myValue: this.value, // valuemyValue
uploading: false, //
cosConfig: [] //
};
},
mounted() {
// console.log('tinymce-editor mounted:',this.value) // value
tinymce.init({}); // TinyMCE
this.cosInit(); // cosInit
},
methods: {
cosInit() {
// HTTP
this.$http({
url: this.$http.adornUrl("/sys/oss/config"), // URL
method: "get", // HTTPGET
params: this.$http.adornParams() //
}).then(({ data }) => {
if (data && data.code === 200) {
this.cosConfig = data.config; // cosConfig
} else {
this.$message.error("请先配置云存储相关信息!"); //
}
});
},
onExecCommand(e) {
//console.log(e) //
},
uploadFile(file) {
this.uploading = true; // true
return new Promise((resolve, reject) => {
let formData = new FormData(); // FormData
formData.append("file", file); // FormData
// HTTP
this.$http({
url: this.$http.adornUrl('/sys/oss/upload'), // URL
method: 'post', // HTTPPOST
data: formData // FormData
}).then(({ data }) => {
console.log(data) //
if (data && data.code === 200) {
this.$emit('uploaded', data.url) // uploadedURL
resolve(data.url) // PromiseURL
} else {
this.$message.error("文件上传失败:" + data.msg) //
reject(data.msg) // Promise
}
this.uploading = false; // false
}).catch(err=>reject(err)) // Promise
});
}
},
watch: {
value(newValue) {
this.myValue = newValue; // valuemyValue
},
myValue(newValue) {
this.$emit("input", newValue); // myValueinput
}
}
};

@ -0,0 +1,46 @@
<template>
<!-- 使用Element UI的el-select组件绑定v-model为selectedAppid设置size为small并添加v-loading指令来控制加载状态 -->
<el-select v-model="selectedAppid" size="small" v-loading="dataListLoading" @change="selectAccount" filterable>
<!-- 遍历accountList数组生成el-option选项 -->
<el-option v-for="item in accountList" :key="item.appid" :label="item.name+''+ACCOUNT_TYPES[item.type]+''" :value="item.appid"></el-option>
</el-select>
</template>
import { mapState } from 'vuex'
export default {
data() {
return {
dataListLoading: false // dataListLoading
}
},
computed: mapState({
accountList: state=>state.wxAccount.accountList, // VuexaccountList
ACCOUNT_TYPES: state=>state.wxAccount.ACCOUNT_TYPES, // VuexACCOUNT_TYPES
selectedAppid:state=>state.wxAccount.selectedAppid // VuexselectedAppid
}),
mounted(){
this.getDataList() // getDataList
},
methods:{
getDataList() {
this.dataListLoading = true // dataListLoadingtrue
this.$http({
url: this.$http.adornUrl('/manage/wxAccount/list'), // URL
method: 'get' // HTTPGET
}).then(({ data }) => {
if (data && data.code === 200) { // 200
this.$store.commit('wxAccount/updateAccountList', data.list) // mutationaccountList
if(!data.list.length){ //
this.$message.info("公众号列表为空,请先添加") //
}
}
this.dataListLoading = false // dataListLoadingfalse
})
},
selectAccount(appid){
if(this.selectedAppid!=appid){ // appidappid
this.$store.commit('wxAccount/selectAccount', appid) // mutationselectedAppid
}
}
}
}

@ -0,0 +1,54 @@
<template>
<!-- 消息预览面板 -->
<div class="panel">
<!-- 工具提示显示消息方向 -->
<el-tooltip class="item" effect="dark" :content="msg.inOut?'公众号发出的消息':'来自用户的消息'" placement="right">
<!-- 根据消息方向显示不同的图标和标签 -->
<el-tag size="mini" v-if="msg.inOut" class="margin-right el-icon-upload2" type="info"></el-tag>
<el-tag size="mini" v-else class="margin-right el-icon-download"></el-tag>
</el-tooltip>
<!-- 消息内容展示区域 -->
<span class="panel-content">
<!-- 文本消息类型 -->
<span v-if="msg.msgType=='text'" v-html="msg.detail.content"></span>
<!-- 事件消息类型 -->
<span v-else-if="msg.msgType=='event'">
<el-tag size="mini" type="warning" effect="plain">事件</el-tag>
<el-tag size="mini" type="info" effect="plain">{{msg.detail.event}}</el-tag>
{{msg.detail.eventKey}}
</span>
<!-- 转客服事件 -->
<span v-else-if="msg.msgType=='transfer_customer_service'">
<el-tag size="mini" type="warning" effect="plain">事件</el-tag>
<el-tag size="mini" type="info" effect="plain">消息转客服</el-tag>
</span>
<!-- 其他消息类型 -->
<span v-else>
<el-tag size="mini" effect="plain">{{XmlMsgType[msg.msgType]}}</el-tag>
后台不支持预览
</span>
</span>
</div>
</template>
<script>
import { mapState } from 'vuex'
export default {
name:'wx-msg-preview', //
props:{
msg:Object //
},
computed:mapState({
// VuexXmlMsgType
XmlMsgType:state=>state.message.XmlMsgType,
})
}
</script>
<style scoped>
/* 面板样式 */
.panel,.panel a{
color: #999; //
word-break: break-all; //
}
</style>

@ -0,0 +1,157 @@
<template>
<!-- 模态框用于显示公众号用户标签管理界面 -->
<el-dialog title="公众号用户标签管理" :close-on-click-modal="false" :visible.sync="dialogVisible">
<!-- 面板容器包含标签和输入框 -->
<div class="panel flex flex-wrap" v-loading="submitting">
<!-- 遍历并显示所有标签 -->
<el-tag v-for="tag in wxUserTags" closable @click="editTag(tag.id,tag.name)" @close="deleteTag(tag.id)" :disable-transitions="false" :key="tag.id">
{{tag.id}} {{tag.name}}
</el-tag>
<!-- 当inputVisible为true时显示输入框否则显示添加按钮 -->
<el-input class="input-new-tag" v-if="inputVisible" placeholder="回车确认" v-model="inputValue" ref="saveTagInput" size="small" @keyup.enter.native="addTag">
</el-input>
<el-button v-else class="button-new-tag" size="small" @click="showInput">+ </el-button>
</div>
<!-- 对话框底部的关闭按钮 -->
<span slot="footer" class="dialog-footer">
<el-button @click="dialogVisible=false"></el-button>
</span>
</el-dialog>
</template>
import { mapState } from 'vuex'
export default {
name: 'wx-user-tags-manager', //
props: {
visible: {
type: Boolean, //
default: true // true
}
},
data() {
return {
dialogVisible: false, //
inputVisible: false, //
inputValue: '', //
submitting: false, //
}
},
computed: mapState({
// VuexwxUserTags
wxUserTags: state => state.wxUserTags.tags
}),
mounted() {
//
this.getWxUserTags();
},
methods: {
//
show(){
this.dialogVisible = true;
},
//
getWxUserTags() {
this.$http({
url: this.$http.adornUrl('/manage/wxUserTags/list'), // URL
method: 'get', // HTTPGET
}).then(({ data }) => {
if (data && data.code === 200) {
// Vuex
this.$store.commit('wxUserTags/updateTags', data.list)
} else {
//
this.$message.error(data.msg)
}
})
},
//
deleteTag(tagid) {
if(this.submitting){
return //
}
this.$confirm(`确定删除标签?`, '提示', {
confirmButtonText: '确定', //
cancelButtonText: '取消', //
type: 'warning' //
}).then(() => {
this.submitting = true // true
this.$http({
url: this.$http.adornUrl('/manage/wxUserTags/delete/' + tagid), // URL
method: 'post', // HTTPPOST
}).then(({ data }) => {
if (data && data.code === 200) {
this.getWxUserTags(); //
this.$emit('change'); // change
} else {
this.$message.error(data.msg) //
}
this.submitting = false; //
})
})
},
//
showInput() {
this.inputVisible = true; //
this.$nextTick(_ => {
this.$refs.saveTagInput.$refs.input.focus(); //
});
},
//
addTag() {
let newTagName = this.inputValue; //
this.saveTag(newTagName) //
this.inputVisible = false; //
this.inputValue = ''; //
},
//
editTag(tagid, orignName=''){
this.$prompt('请输入新标签名称', '提示', {
confirmButtonText: '确定', //
cancelButtonText: '取消', //
inputValue: orignName, //
inputPattern: /^.{1,30}$/, // 1-30
inputErrorMessage: '名称1-30字符' //
}).then(({ value }) => {
this.saveTag(value, tagid) //
})
},
//
saveTag(name, tagid){
if(this.submitting){
return //
}
this.submitting = true // true
this.$http({
url: this.$http.adornUrl('/manage/wxUserTags/save'), // URL
method: 'post', // HTTPPOST
data: this.$http.adornData({
id: tagid ? tagid : undefined, // tagid
name: name //
})
}).then(({ data }) => {
if (data && data.code === 200) {
this.getWxUserTags(); //
this.$emit('change'); // change
} else {
this.$message.error(data.msg) //
}
this.submitting = false; //
})
}
}
}
<style scoped>
/* 面板样式 */
.panel {
flex: 1; /* 弹性布局,占满剩余空间 */
}
/* 标签和按钮样式 */
.el-tag, .button-new-tag {
margin: 5px; /* 外边距 */
}
/* 输入框样式 */
.input-new-tag {
width: inherit; /* 宽度继承父元素 */
}
</style>

@ -0,0 +1,70 @@
// 从node_modules中引入Vue框架
import Vue from 'vue';
// 引入根组件App.vue
import App from './App.vue';
// 引入Vue Router实例用于管理前端路由
import router from './router';
// 引入Vuex状态管理实例
import store from './store';
// 引入vue-cookie插件用于在Vue应用中方便地操作cookie
import VueCookie from 'vue-cookie';
// 引入ElementUI组件库这是一个基于Vue的桌面端组件库
import ElementUI from 'element-ui';
// 引入moment.js库用于日期和时间的处理
import moment from 'moment';
// 引入ElementUI的样式文件这是ElementUI组件的默认样式
import 'element-ui/lib/theme-chalk/index.css';
// 引入项目中自定义的common.css样式文件
import './assets/css/common.css';
// 引入项目中自定义的index.scss样式文件注意.scss文件需要sass-loader来编译
import './assets/scss/index.scss';
// 引入项目中自定义的httpRequest模块这个模块封装了axios用于发起HTTP请求
import httpRequest from '@/utils/httpRequest'; // @代表src目录的别名在webpack配置中定义
// 从utils模块中引入isAuth函数这个函数可能用于检查用户的登录状态或权限
import { isAuth } from '@/utils';
// 引入vue-clipboard2插件用于在Vue应用中方便地实现复制文本到剪贴板的功能
import VueClipboard from 'vue-clipboard2';
// 使用ElementUI组件库
Vue.use(ElementUI);
// 使用vue-clipboard2插件
Vue.use(VueClipboard);
// 使用vue-cookie插件
Vue.use(VueCookie);
// 关闭Vue在启动时生成的生产提示
Vue.config.productionTip = false;
// 将httpRequest挂载到Vue的原型上这样在Vue组件中就可以通过this.$http来访问了
Vue.prototype.$http = httpRequest;
// 将isAuth函数挂载到Vue的原型上这样在Vue组件中就可以通过this.isAuth来访问了
Vue.prototype.isAuth = isAuth;
// 设置moment.js的语言环境为中文中国
moment.locale('zh-cn');
// 将moment挂载到Vue的原型上这样在Vue组件中就可以通过this.$moment来访问了
Vue.prototype.$moment = moment;
// 创建Vue实例并传入router、store和渲染函数
new Vue({
router, // 路由实例
store, // 状态管理实例
// 渲染函数h是createElement函数的别名用于创建虚拟DOM节点
render: h => h(App) // 将App组件渲染为根节点
}).$mount('#app'); // 将Vue实例挂载到HTML中的id为app的元素上

@ -0,0 +1,3 @@
// 导出一个函数,该函数接收一个文件名作为参数
// 返回一个动态导入模块的函数
module.exports = file => () => import('@/views/' + file + '.vue')

@ -0,0 +1,152 @@
/**
* 全站路由配置
*
* 建议:
* 1. 代码中路由统一使用name属性跳转(不使用path属性)
*/
import Vue from 'vue'
import VueRouter from 'vue-router'
import http from '@/utils/httpRequest'
import { isURL } from '@/utils/validate'
import { clearLoginInfo } from '@/utils'
// 使用VueRouter插件
Vue.use(VueRouter)
// 动态导入视图组件的函数
const _import = require('./import-views')
// 全局路由(无需嵌套上左右整体布局)
const globalRoutes = [
{ path: '/404', component: () => import('@/views/common/404'), name: '404', meta: { title: '404未找到' } }, // 404页面路由
{ path: '/login', component: () => import('@/views/common/login'), name: 'login', meta: { title: '登录' } } // 登录页面路由
]
// 主入口路由(需嵌套上左右整体布局)
const mainRoutes = {
path: '/',
component: () => import('@/views/main'),
name: 'main',
redirect: { name: 'home' },
meta: { title: '主入口整体布局' },
children: [
// 通过meta对象设置路由展示方式
// 1. isTab: 是否通过tab展示内容, true: 是, false: 否
// 2. iframeUrl: 是否通过iframe嵌套展示内容, '以http[s]://开头': 是, '': 否
{ path: '/home', component: () => import('@/views/common/home'), name: 'home', meta: { title: '首页' } }, // 首页路由
{ path: '/theme', component: () => import('@/views/common/theme'), name: 'theme', meta: { title: '主题' } } // 主题页面路由
],
beforeEnter(to, from, next) {
// 检查用户是否已登录,如果未登录则重定向到登录页
let token = Vue.cookie.get('token')
if (!token || !/\S/.test(token)) {
clearLoginInfo()
next({ name: 'login' })
}
next()
}
}
// 创建VueRouter实例
const router = new VueRouter({
mode: 'hash', // 使用hash模式
scrollBehavior: () => ({ y: 0 }), // 滚动行为:切换路由时滚动到顶部
isAddDynamicMenuRoutes: false, // 是否已经添加动态(菜单)路由
routes: globalRoutes.concat(mainRoutes) // 合并全局路由和主入口路由
})
// 全局前置守卫,用于处理动态菜单路由的添加
router.beforeEach((to, from, next) => {
// 如果已经添加了动态(菜单)路由或当前路由是全局路由,直接放行
if (router.options.isAddDynamicMenuRoutes || fnCurrentRouteType(to, globalRoutes) === 'global') {
next()
} else {
// 否则,请求菜单列表并添加动态(菜单)路由
http({
url: http.adornUrl('/sys/menu/nav'),
method: 'get',
params: http.adornParams()
}).then(({ data }) => {
if (data && data.code === 200) {
fnAddDynamicMenuRoutes(data.menuList) // 添加动态(菜单)路由
router.options.isAddDynamicMenuRoutes = true // 标记为已添加动态(菜单)路由
next({ ...to, replace: true }) // 重新导航到当前路由,确保添加完动态路由后能正确显示页面
} else {
console.log(`%c${e} 请求菜单列表和权限失败,跳转至登录页!!`, 'color:blue') // 打印错误信息
router.push({ name: 'login' }) // 跳转到登录页
}
}).catch((e) => {
console.log(`%c${e} 请求菜单列表和权限失败,跳转至登录页!!`, 'color:blue') // 打印错误信息
router.push({ name: 'login' }) // 跳转到登录页
})
}
})
/**
* 判断当前路由类型, global: 全局路由, main: 主入口路由
* @param {*} route 当前路由
*/
function fnCurrentRouteType(route, globalRoutes = []) {
var temp = []
for (var i = 0; i < globalRoutes.length; i++) {
if (route.path === globalRoutes[i].path) {
return 'global' // 如果是全局路由,返回'global'
} else if (globalRoutes[i].children && globalRoutes[i].children.length >= 1) {
temp = temp.concat(globalRoutes[i].children) // 如果有子路由,添加到临时数组中
}
}
return temp.length >= 1 ? fnCurrentRouteType(route, temp) : 'main' // 递归判断,直到找到匹配的路由类型
}
/**
* 添加动态(菜单)路由
* @param {*} menuList 菜单列表
* @param {*} routes 递归创建的动态(菜单)路由
*/
function fnAddDynamicMenuRoutes(menuList = [], routes = []) {
var temp = []
for (var i = 0; i < menuList.length; i++) {
if (menuList[i].list && menuList[i].list.length >= 1) {
temp = temp.concat(menuList[i].list) // 如果菜单项有子菜单,添加到临时数组中
} else if (menuList[i].url && /\S/.test(menuList[i].url)) {
menuList[i].url = menuList[i].url.replace(/^\//, '') // 去掉URL前的斜杠
var route = {
path: menuList[i].url.replace('/', '-'), // 将URL中的斜杠替换为短横线作为路径的一部分
component: null, // 初始化组件为空
name: menuList[i].url.replace('/', '-'), // 将URL中的斜杠替换为短横线作为路由名称的一部分
meta: {
menuId: menuList[i].menuId, // 菜单ID
title: menuList[i].name, // 菜单名称
isDynamic: true, // 标记为动态路由
isTab: true, // 标记为标签页
iframeUrl: '' // 初始化iframeUrl为空
}
}
// 如果URL以http[s]://开头通过iframe展示
if (isURL(menuList[i].url)) {
route['path'] = `i-${menuList[i].menuId}` // 修改路径为i-加上菜单ID
route['name'] = `i-${menuList[i].menuId}` // 修改名称为i-加上菜单ID
route['meta']['iframeUrl'] = menuList[i].url // 设置iframeUrl为菜单URL
} else {
try {
route['component'] = _import(`modules/${menuList[i].url}`) || null // 动态导入组件
} catch (e) { } // 如果导入失败,捕获异常但不做处理
}
routes.push(route) // 将路由添加到routes数组中
}
}
if (temp.length >= 1) {
fnAddDynamicMenuRoutes(temp, routes) // 如果临时数组中有数据,递归调用自身继续处理
} else {
mainRoutes.name = 'main-dynamic' // 修改主入口路由的名称为main-dynamic
mainRoutes.children = routes // 将动态生成的路由设置为主入口路由的子路由
router.addRoutes([mainRoutes, { path: '*', redirect: { name: '404' } }]) // 添加动态路由和404重定向规则到路由器中
sessionStorage.setItem('dynamicMenuRoutes', JSON.stringify(mainRoutes.children || '')) // 将动态路由存储到sessionStorage中以便后续使用
console.log('\n')
console.log('%c!<-------------------- 动态(菜单)路由 s -------------------->', 'color:blue') // 打印动态路由开始标志
console.log(mainRoutes.children) // 打印动态生成的路由信息
console.log('%c!<-------------------- 动态(菜单)路由 e -------------------->', 'color:blue') // 打印动态路由结束标志
console.log('\n')
}
}
export default router // 导出路由器实例

@ -0,0 +1,34 @@
import Vue from 'vue'
import Vuex from 'vuex'
// 引入各个模块的 Vuex 状态管理文件
import common from './modules/common'
import user from './modules/user'
import article from './modules/article'
import message from './modules/message'
import wxUserTags from './modules/wxUserTags'
import wxAccount from './modules/wxAccount'
// 注册 Vuex 插件,使其可用于 Vue 中
Vue.use(Vuex)
export default new Vuex.Store({
// 使用 modules 来组织不同的子模块
modules: {
// 引入并注册各个模块
common, // 公共模块,可能用于存储一些通用的状态
user, // 用户模块,存储用户信息相关的状态
article, // 文章模块,存储与文章相关的状态
message, // 消息模块,存储消息相关的状态
wxUserTags, // 微信用户标签模块,管理微信用户标签的状态
wxAccount // 微信账号模块,管理微信账号相关的状态
},
// mutations 用于同步修改状态,这里没有定义任何 mutations可根据需求进行扩展
mutations: {
// 这里可以添加全局的 mutation但目前没有定义
},
// 启用严格模式,开发环境下会对状态的修改进行检查,确保只能通过 mutation 修改状态
strict: true
})

@ -0,0 +1,14 @@
export default {
// 启用命名空间使得该模块的状态和getters、actions、mutations是注册在全局命名空间下的子模块
namespaced: true,
state: {
// 定义文章类型常量对象
ARTICLE_TYPES: {
1: '普通文章', // 普通文章类型
5: '帮助中心', // 帮助中心类型
}
},
mutations: {
// 目前没有定义任何mutation
}
}

@ -0,0 +1,86 @@
import router from '@/router'
export default {
// 启用命名空间使得该模块的状态和getters、actions、mutations是注册在全局命名空间下的子模块
namespaced: true,
state: {
// 页面文档可视高度(随窗口改变大小)
documentClientHeight: 0,
// 导航条, 布局风格, defalut(默认) / inverse(反向)
navbarLayoutType: 'default',
// 侧边栏, 布局皮肤, light(浅色) / dark(黑色)
sidebarLayoutSkin: 'dark',
// 侧边栏, 折叠状态
sidebarFold: false,
// 侧边栏, 菜单列表
menuList: [],
// 当前激活的菜单项名称
menuActiveName: '',
// 内容区域是否需要刷新
contentIsNeedRefresh: false,
// 主入口标签页数组
mainTabs: [],
// 当前激活的标签页名称
mainTabsActiveName: ''
},
mutations: {
// 更新文档可视高度
updateDocumentClientHeight(state, height) {
state.documentClientHeight = height
},
// 更新导航条布局类型
updateNavbarLayoutType(state, type) {
state.navbarLayoutType = type
},
// 更新侧边栏布局皮肤
updateSidebarLayoutSkin(state, skin) {
state.sidebarLayoutSkin = skin
},
// 更新侧边栏折叠状态
updateSidebarFold(state, fold) {
state.sidebarFold = fold
},
// 更新菜单列表
updateMenuList(state, list) {
state.menuList = list
},
// 更新当前激活的菜单项名称
updateMenuActiveName(state, name) {
state.menuActiveName = name
},
// 更新内容区域是否需要刷新
updateContentIsNeedRefresh(state, status) {
state.contentIsNeedRefresh = status
},
// 更新主入口标签页数组
updateMainTabs(state, tabs) {
state.mainTabs = tabs
},
// 更新当前激活的标签页名称
updateMainTabsActiveName(state, name) {
state.mainTabsActiveName = name
},
// 移除指定的标签页
removeTab(state, tabName) {
// 过滤掉要删除的标签页
state.mainTabs = state.mainTabs.filter(item => item.name !== tabName)
if (state.mainTabs.length >= 1) {
// 如果当前选中的标签页被删除,则跳转到最后一个标签页
if (tabName === state.mainTabsActiveName) {
var tab = state.mainTabs[state.mainTabs.length - 1]
router.push({ name: tab.name, query: tab.query, params: tab.params }, () => {
state.mainTabsActiveName = tab.name
})
}
} else {
// 如果没有剩余的标签页,重置菜单并跳转到首页
state.menuActiveName = ''
router.push({ name: 'home' })
}
},
// 关闭当前激活的标签页
closeCurrentTab(state) {
this.commit('common/removeTab', state.mainTabsActiveName)
}
}
}

@ -0,0 +1,40 @@
// 导出一个默认的模块对象
export default {
// 启用命名空间使得该模块的状态和getters、actions、mutations是注册在全局命名空间下的子模块
namespaced: true,
// 定义模块的初始状态
state: {
// 定义微信消息类型的映射关系
XmlMsgType: {
"text": "文字", // 文本消息类型
"image": "图片", // 图片消息类型
"voice": "语音", // 语音消息类型
"shortvideo": "短视频", // 短视频消息类型
"video": "视频", // 视频消息类型
"news": "图文", // 图文消息类型
"music": "音乐", // 音乐消息类型
"location": "位置", // 位置消息类型
"link": "链接", // 链接消息类型
"event": "事件", // 事件消息类型
"transfer_customer_service": "转客服" // 转接客服消息类型
},
// 定义客服消息类型的映射关系
KefuMsgType: {
"text": "文本消息", // 文本消息类型
"image": "图片消息", // 图片消息类型
"voice": "语音消息", // 语音消息类型
"video": "视频消息", // 视频消息类型
"music": "音乐消息", // 音乐消息类型
"news": "文章链接", // 文章链接消息类型
"mpnews": "公众号图文消息", // 公众号图文消息类型
"wxcard": "卡券消息", // 卡券消息类型
"miniprogrampage": "小程序消息", // 小程序消息类型
"msgmenu": "菜单消息" // 菜单消息类型
}
},
// 定义用于修改状态的 mutations目前为空
mutations: {
}
}

@ -0,0 +1,23 @@
// 导出一个默认的模块对象
export default {
// 启用命名空间使得该模块的状态和getters、actions、mutations是注册在全局命名空间下的子模块
namespaced: true,
// 定义模块的初始状态
state: {
id: 0, // 初始化 id 为 0
name: '' // 初始化 name 为空字符串
},
// 定义用于修改状态的 mutations
mutations: {
// 更新 id 的 mutation
updateId(state, id) {
state.id = id; // 将传入的 id 赋值给 state 中的 id
},
// 更新 name 的 mutation
updateName(state, name) {
state.name = name; // 将传入的 name 赋值给 state 中的 name
}
}
}

@ -0,0 +1,59 @@
import Vue from 'vue';
export default {
// Vuex模块启用命名空间
namespaced: true,
// Vuex的state用来存储应用状态
state: {
// 账户类型映射数字ID到账户类型名称
ACCOUNT_TYPES: {
1: '订阅号',
2: '服务号'
},
// 存储账户列表
accountList: [],
// 当前选中的Appid用来标识选择的账号
selectedAppid: ''
},
// Vuex的mutations用来修改state
mutations: {
// 更新账户列表
updateAccountList (state, list) {
// 更新state中的accountList
state.accountList = list;
// 如果列表为空,直接返回
if (!list.length) return;
// 如果当前没有选中的Appid则从cookie或列表中选择一个默认Appid
if (!state.selectedAppid) {
let appidCookie = Vue.cookie.get('appid');
// 获取cookie中的appid如果有则使用cookie中的appid否则使用列表中的第一个Appid
let selectedAppid = appidCookie ? appidCookie : list[0].appid;
// 通过commit调用mutation更新选中的账号
this.commit('wxAccount/selectAccount', selectedAppid);
}
},
// 选择某个账号切换Appid
selectAccount (state, appid) {
// 更新cookie中的appid保存选中的Appid
Vue.cookie.set('appid', appid);
// 记录上一个选中的Appid
let oldAppid = state.selectedAppid;
// 更新当前选中的Appid
state.selectedAppid = appid;
// 如果选中的Appid发生变化则刷新页面
if (oldAppid) {
location.reload();
}
}
}
};

@ -0,0 +1,20 @@
export default {
// 启用 Vuex 模块的命名空间,避免命名冲突
namespaced: true,
// state 存储模块的状态
state: {
// tags 用来存储标签数据的数组
tags: []
},
// mutations 用来修改 state 中的状态
mutations: {
// 更新 tags 数组的内容
updateTags (state, tags) {
// 将传入的 tags 更新到 state 中
state.tags = tags;
}
}
};

@ -0,0 +1,84 @@
import Vue from 'vue'
import axios from 'axios'
import router from '@/router'
import qs from 'qs'
import merge from 'lodash/merge'
import { clearLoginInfo } from '@/utils'
const baseUrl = '/wx' // 设置请求的基础路径
// 创建axios实例
const http = axios.create({
timeout: 1000 * 30, // 设置请求超时为30秒
withCredentials: true, // 允许携带跨域请求的cookie
headers: {
'Content-Type': 'application/json; charset=utf-8' // 默认请求头为json格式
}
})
/**
* 请求拦截器
* 在每个请求发送之前加入token从cookie中获取
*/
http.interceptors.request.use(config => {
config.headers['token'] = Vue.cookie.get('token') // 在请求头中加入token
return config // 返回请求配置
}, error => {
return Promise.reject(error) // 请求出错时返回Promise拒绝
})
/**
* 响应拦截器
* 对响应数据进行拦截处理
* 如果返回的状态码为401未授权则清除登录信息并跳转到登录页
*/
http.interceptors.response.use(response => {
if (response.data && response.data.code === 401) { // 判断返回的code是否为401代表token失效
clearLoginInfo() // 清除登录信息
router.push({ name: 'login' }) // 跳转到登录页面
}
return response // 返回响应数据
}, error => {
return Promise.reject(error) // 响应出错时返回Promise拒绝
})
/**
* 请求地址处理函数
* @param {*} actionName 接口的名称拼接成完整的URL
* @returns {string} 拼接后的完整URL
*/
http.adornUrl = (actionName) => {
// 在开发环境下,如果开启了代理,则请求路径会带上代理前缀
return baseUrl + actionName // 返回完整的请求URL
}
/**
* get请求的参数处理
* @param {*} params 请求的参数对象
* @param {*} openDefultParams 是否开启默认参数
* @returns {object} 处理后的参数对象
*/
http.adornParams = (params = {}, openDefultParams = true) => {
const defaults = {
't': new Date().getTime() // 添加时间戳参数,防止缓存
}
return openDefultParams ? merge(defaults, params) : params // 合并默认参数和传入的参数
}
/**
* post请求的数据处理
* @param {*} data 请求的数据对象
* @param {*} openDefultdata 是否开启默认数据
* @param {*} contentType 数据格式类型'json''form'
* @returns {string} 处理后的数据
*/
http.adornData = (data = {}, openDefultdata = true, contentType = 'json') => {
const defaults = {
't': new Date().getTime() // 添加时间戳参数,防止缓存
}
data = openDefultdata ? merge(defaults, data) : data // 合并默认数据和传入的数据
// 根据不同的contentType处理数据格式
return contentType === 'json' ? JSON.stringify(data) : qs.stringify(data)
}
export default http // 导出axios实例供其他模块使用

@ -0,0 +1,78 @@
import Vue from 'vue'
import router from '@/router'
import store from '@/store'
/**
* 获取UUID
* 生成一个标准的UUID例如xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx
* 使用随机数和指定格式的规则生成UUID
*/
export function getUUID() {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => {
return (c === 'x' ? (Math.random() * 16 | 0) : ('r&0x3' | '0x8')).toString(16)
})
}
/**
* 检查是否有某个权限
* @param {*} key 权限的标识符
* @returns {boolean} 如果权限列表中包含该权限返回true否则返回false
*/
export function isAuth(key) {
// 从 sessionStorage 中获取权限列表,并转换为数组,如果没有权限列表,默认返回空数组
return JSON.parse(sessionStorage.getItem('permissions') || '[]').indexOf(key) !== -1 || false
}
/**
* 将平面数据转换为树形数据
* @param {*} data 原始平面数据
* @param {*} id 唯一标识符字段默认为'id'
* @param {*} pid 父级标识符字段默认为'parentId'
* @returns {Array} 转换后的树形数据
*/
export function treeDataTranslate(data, id = 'id', pid = 'parentId') {
var res = [] // 存储最终的树形结构
var temp = {} // 临时存储每个节点,以便快速查找父节点
// 将数据转换为临时对象key为节点的id值为节点本身
for (var i = 0; i < data.length; i++) {
temp[data[i][id]] = data[i]
}
// 遍历数据根据pid将节点组织成树形结构
for (var k = 0; k < data.length; k++) {
// 如果节点的父节点存在并且当前节点的id不等于父节点的id
if (temp[data[k][pid]] && data[k][id] !== data[k][pid]) {
// 如果父节点没有'children'属性,则初始化为数组
if (!temp[data[k][pid]]['children']) {
temp[data[k][pid]]['children'] = []
}
// 如果父节点没有'_level'属性则设置为1
if (!temp[data[k][pid]]['_level']) {
temp[data[k][pid]]['_level'] = 1
}
// 当前节点的级别为父节点的级别+1
data[k]['_level'] = temp[data[k][pid]]._level + 1
// 将当前节点推送到父节点的children数组中
temp[data[k][pid]]['children'].push(data[k])
} else {
// 如果当前节点是根节点,直接推送到结果数组中
res.push(data[k])
}
}
return res // 返回转换后的树形数据
}
/**
* 清除登录信息
* 用于用户退出时清理本地存储的登录信息
*/
export function clearLoginInfo() {
// 删除cookie中的'token'
Vue.cookie.delete('token')
// 目前注释掉了重置store的操作若需要可以解除注释
// store.commit('resetStore')
// 重置动态菜单路由标志
router.options.isAddDynamicMenuRoutes = false
}

@ -0,0 +1,36 @@
/**
* 验证邮箱格式
* @param {*} s - 需要验证的邮箱地址
* @returns {boolean} - 返回是否是有效的邮箱地址
*/
export function isEmail(s) {
return /^([a-zA-Z0-9_-])+@([a-zA-Z0-9_-])+((\.[a-zA-Z0-9_-]{2,3}){1,2})$/.test(s)
}
/**
* 验证手机号码格式中国手机号
* @param {*} s - 需要验证的手机号
* @returns {boolean} - 返回是否是有效的手机号码
*/
export function isMobile(s) {
return /^1[0-9]{10}$/.test(s)
}
/**
* 验证固定电话号码格式
* @param {*} s - 需要验证的电话号码
* @returns {boolean} - 返回是否是有效的电话号码包括区号和本地号码
*/
export function isPhone(s) {
return /^([0-9]{3,4}-)?[0-9]{7,8}$/.test(s)
}
/**
* 验证URL地址格式
* @param {*} s - 需要验证的URL地址
* @returns {boolean} - 返回是否是有效的URL地址包括http或https协议
*/
export function isURL(s) {
return /^http[s]?:\/\/.*/.test(s)
}

@ -0,0 +1,81 @@
<template>
<!-- 404 错误页面的模板 -->
<div class="site-wrapper site-page--not-found">
<div class="site-content__wrapper">
<div class="site-content">
<!-- 错误代码 -->
<h2 class="not-found-title">400</h2>
<!-- 错误描述 -->
<p class="not-found-desc">抱歉您访问的页面<em>失联</em> ...</p >
<!-- 返回上一页按钮 -->
<el-button @click="$router.go(-1)"></el-button>
<!-- 返回首页按钮 -->
<el-button type="primary" class="not-found-btn-gohome" @click="$router.push({ name: 'home' })">进入首页</el-button>
</div>
</div>
</div>
</template>
<script>
export default {
// JavaScript
}
</script>
<style lang="scss">
/* 整个404页面的外部容器 */
.site-wrapper.site-page--not-found {
position: absolute; /* 定位到页面的绝对位置 */
top: 0;
right: 0;
bottom: 0;
left: 0;
overflow: hidden; /* 防止内容溢出 */
/* 内容包裹层 */
.site-content__wrapper {
padding: 0;
margin: 0;
background-color: #fff; /* 设置背景色为白色 */
}
/* 页面内容容器 */
.site-content {
position: fixed; /* 固定位置 */
top: 15%; /* 距离顶部15% */
left: 50%; /* 距离左边50% */
z-index: 2; /* 确保内容在其他元素之上 */
padding: 30px;
text-align: center; /* 内容居中 */
transform: translate(-50%, 0); /* 将内容水平居中 */
}
/* 错误标题 */
.not-found-title {
margin: 20px 0 15px;
font-size: 10em; /* 大字体 */
font-weight: 400;
color: rgb(55, 71, 79); /* 文字颜色 */
}
/* 错误描述 */
.not-found-desc {
margin: 0 0 30px;
font-size: 26px;
text-transform: uppercase; /* 将文本转换为大写 */
color: rgb(118, 131, 143); /* 文字颜色 */
/* 强调标签 <em> 样式 */
> em {
font-style: normal;
color: #ee8145; /* 设置颜色 */
}
}
/* 返回首页按钮的左边距 */
.not-found-btn-gohome {
margin-left: 30px;
}
}
</style>

@ -0,0 +1,18 @@
<template>
<!-- 模块容器 div -->
<div class="mod-home">
<!-- 欢迎标题 -->
<h3>欢迎使用微信管理系统</h3>
</div>
</template>
<style>
/* 样式部分 */
.mod-home {
/* 设置行高,增加文本的可读性 */
line-height: 2.5;
/* 使文本水平居中对齐 */
text-align: center;
}
</style>

@ -0,0 +1,220 @@
<template>
<div class="site-wrapper site-page--login">
<!-- 页面容器背景层 -->
<div class="site-content__wrapper">
<!-- 页面内容容器 -->
<div class="site-content">
<!-- 品牌信息部分 -->
<div class="brand-info">
<h2 class="brand-info__text">微信后台管理系统</h2>
<p class="brand-info__intro">微信公众号后台管理系统</p >
</div>
<!-- 登录表单 -->
<div class="login-main">
<h3 class="login-title">管理员登录</h3>
<!-- 表单绑定了 model validation 规则 -->
<el-form :model="dataForm" :rules="dataRule" ref="dataForm" @keyup.enter.native="dataFormSubmit()" status-icon>
<!-- 用户名输入框 -->
<el-form-item prop="userName">
<el-input v-model="dataForm.userName" placeholder="帐号"></el-input>
</el-form-item>
<!-- 密码输入框 -->
<el-form-item prop="password">
<el-input v-model="dataForm.password" type="password" placeholder="密码"></el-input>
</el-form-item>
<!-- 验证码输入框 -->
<el-form-item prop="captcha">
<el-row :gutter="20">
<el-col :span="14">
<el-input v-model="dataForm.captcha" placeholder="验证码"></el-input>
</el-col>
<el-col :span="10" class="login-captcha">
<!-- 验证码图片点击刷新验证码 -->
< img :src="captchaPath" @click="getCaptcha()" alt="">
</el-col>
</el-row>
</el-form-item>
<!-- 登录按钮 -->
<el-form-item>
<el-button class="login-btn-submit" type="primary" @click="dataFormSubmit()"></el-button>
</el-form-item>
</el-form>
</div>
</div>
</div>
</div>
</template>
<script>
import { getUUID } from '@/utils' // UUID
export default {
data() {
return {
//
dataForm: {
userName: '', //
password: '', //
uuid: '', // UUID
captcha: '' //
},
//
dataRule: {
userName: [
{ required: true, message: '帐号不能为空', trigger: 'blur' }
],
password: [
{ required: true, message: '密码不能为空', trigger: 'blur' }
],
captcha: [
{ required: true, message: '验证码不能为空', trigger: 'blur' }
]
},
captchaPath: '' //
}
},
created() {
//
this.getCaptcha()
},
methods: {
//
dataFormSubmit() {
//
this.$refs['dataForm'].validate((valid) => {
if (valid) {
//
this.$http({
url: this.$http.adornUrl('/sys/login'), //
method: 'post',
data: this.$http.adornData({
'username': this.dataForm.userName,
'password': this.dataForm.password,
'uuid': this.dataForm.uuid,
'captcha': this.dataForm.captcha
})
}).then(({ data }) => {
if (data && data.code === 200) {
// token
this.$cookie.set('token', data.token)
this.$router.replace({ name: 'home' })
} else {
//
this.getCaptcha()
this.$message.error(data.msg)
}
})
}
})
},
// UUID
getCaptcha() {
this.dataForm.uuid = getUUID() // UUID
//
this.captchaPath = this.$http.adornUrl(`/captcha?uuid=${this.dataForm.uuid}`)
}
}
}
</script>
<style lang="scss">
.site-wrapper.site-page--login {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
background-color: rgba(38, 50, 56, 0.5); /* 半透明背景 */
overflow: hidden;
/* 页面背景渐变效果 */
&:before {
position: fixed;
top: 0;
left: 0;
z-index: -1; /* 确保背景层在最底层 */
width: 100%;
height: 100%;
content: "";
background-color: #fa8bff;
background-image: linear-gradient(
45deg,
#fa8bff 0%,
#2bd2ff 52%,
#2bff88 90%
);
background-size: cover;
}
.site-content__wrapper {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
padding: 0;
margin: 0;
overflow-x: hidden;
overflow-y: auto;
background-color: transparent;
}
.site-content {
min-height: 100%;
padding: 30px 500px 30px 30px;
}
/* 品牌信息样式 */
.brand-info {
margin: 220px 100px 0 90px;
color: #fff;
}
.brand-info__text {
margin: 0 0 22px 0;
font-size: 48px;
font-weight: 400;
text-transform: uppercase;
}
.brand-info__intro {
margin: 10px 0;
font-size: 16px;
line-height: 1.58;
opacity: 0.6;
}
/* 登录表单样式 */
.login-main {
position: absolute;
top: 0;
right: 0;
padding: 150px 60px 180px;
width: 470px;
min-height: 100%;
background-color: #fff;
}
.login-title {
font-size: 16px;
}
/* 验证码图片样式 */
.login-captcha {
overflow: hidden;
> img {
width: 100%;
cursor: pointer; /* 鼠标指针变成手形,表示可以点击 */
}
}
/* 登录按钮样式 */
.login-btn-submit {
width: 100%;
margin-top: 38px;
}
}
</style>

@ -0,0 +1,53 @@
<template>
<el-form>
<!-- 布局设置表单标题 -->
<h2>布局设置</h2>
<!-- 导航条类型设置项 -->
<el-form-item label="导航条类型">
<!-- 导航条类型选择框使用 radio 按钮选择 'default' 'inverse' -->
<el-radio-group v-model="navbarLayoutType">
<el-radio label="default" border>default</el-radio>
<el-radio label="inverse" border>inverse</el-radio>
</el-radio-group>
</el-form-item>
<!-- 侧边栏皮肤设置项 -->
<el-form-item label="侧边栏皮肤">
<!-- 侧边栏皮肤选择框使用 radio 按钮选择 'light' 'dark' -->
<el-radio-group v-model="sidebarLayoutSkin">
<el-radio label="light" border>light</el-radio>
<el-radio label="dark" border>dark</el-radio>
</el-radio-group>
</el-form-item>
</el-form>
</template>
<script>
export default {
computed: {
//
navbarLayoutType: {
// getter Vuex navbarLayoutType
get() {
return this.$store.state.common.navbarLayoutType
},
// setter Vuex navbarLayoutType
set(val) {
this.$store.commit('common/updateNavbarLayoutType', val)
}
},
//
sidebarLayoutSkin: {
// getter Vuex sidebarLayoutSkin
get() {
return this.$store.state.common.sidebarLayoutSkin
},
// setter Vuex sidebarLayoutSkin
set(val) {
this.$store.commit('common/updateSidebarLayoutSkin', val)
}
}
}
}
</script>

@ -0,0 +1,116 @@
<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">
<!-- 标签页工具菜单包含关闭当前关闭其他关闭全部和刷新功能 -->
<el-dropdown class="site-tabs__tools" :show-timeout="0">
<i class="el-icon-arrow-down el-icon--right"></i>
<el-dropdown-menu slot="dropdown">
<el-dropdown-item @click.native="tabsCloseCurrentHandle">关闭当前标签页</el-dropdown-item>
<el-dropdown-item @click.native="tabsCloseOtherHandle">关闭其它标签页</el-dropdown-item>
<el-dropdown-item @click.native="tabsCloseAllHandle">关闭全部标签页</el-dropdown-item>
<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>
<!-- 否则渲染router-view并使用keep-alive缓存组件 -->
<keep-alive v-else>
<router-view v-if="item.name === mainTabsActiveName" />
</keep-alive>
</el-card>
</el-tab-pane>
</el-tabs>
<!-- 如果当前路由元信息中不包含isTab则直接渲染router-view -->
<el-card v-else :body-style="siteContentViewHeight">
<keep-alive>
<router-view />
</keep-alive>
</el-card>
</main>
</template>
<script>
import { isURL } from '@/utils/validate'
export default {
// 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: {
//
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 })
}
},
// Vuex
removeTabHandle(tabName) {
this.$store.commit('common/removeTab', tabName)
},
//
tabsCloseCurrentHandle() {
this.removeTabHandle(this.mainTabsActiveName)
},
//
tabsCloseOtherHandle() {
this.mainTabs = this.mainTabs.filter(item => item.name === this.mainTabsActiveName)
},
//
tabsCloseAllHandle() {
this.mainTabs = []
this.menuActiveName = ''
this.$router.push({ name: 'home' })
},
// tabsRefreshCurrentHandlerefresh
tabsRefreshCurrentHandle() {
var tab = this.$route
this.removeTabHandle(tab.name)
this.$nextTick(() => {
this.$router.push({ name: tab.name, query: tab.query, params: tab.params })
})
}
// refresh
}
}
</script>

@ -0,0 +1,126 @@
<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>
</span>
</el-dialog>
</template>
<script>
import { clearLoginInfo } from '@/utils' //
export default {
data() {
//
var validateConfirmPassword = (rule, value, callback) => {
if (this.dataForm.newPassword !== value) {
callback(new Error('确认密码与新密码不一致'))
} else {
callback()
}
}
return {
//
visible: false,
//
dataForm: {
password: '',
newPassword: '',
confirmPassword: ''
},
//
dataRule: {
password: [
{ required: true, message: '原密码不能为空', trigger: 'blur' }
],
newPassword: [
{ required: true, message: '新密码不能为空', trigger: 'blur' }
],
confirmPassword: [
{ required: true, message: '确认密码不能为空', 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'), // URL
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)
}
})
}
})
}
}
}
</script>

@ -0,0 +1,136 @@
<template>
<!-- 导航栏组件其类名根据navbarLayoutType动态设置 -->
<nav class="site-navbar" :class="'site-navbar--' + navbarLayoutType">
<!-- 导航栏头部 -->
<div class="site-navbar__header">
<!-- 点击h1元素将导航到首页 -->
<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'
export default {
data() {
return {
//
updatePassowrdVisible: false
}
},
components: {
//
UpdatePassword, WxAccountSelector
},
computed: {
//
navbarLayoutType: {
get() { return this.$store.state.common.navbarLayoutType }
},
//
sidebarFold: {
get() { return this.$store.state.common.sidebarFold },
set(val) { this.$store.commit('common/updateSidebarFold', val) }
},
//
mainTabs: {
get() { return this.$store.state.common.mainTabs },
set(val) { this.$store.commit('common/updateMainTabs', val) }
},
//
userName: {
get() { return this.$store.state.user.name }
}
},
methods: {
//
updatePasswordHandle() {
//
this.updatePassowrdVisible = true
// DOM
this.$nextTick(() => {
this.$refs.updatePassowrd.init()
})
},
// 退
logoutHandle() {
//
this.$confirm(`确定进行[退出]操作?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
// 退
this.$http({
url: this.$http.adornUrl('/sys/logout'), // URL
method: 'post',
data: this.$http.adornData() //
}).then(({ data }) => {
//
if (data && data.code === 200) {
//
clearLoginInfo()
//
this.$router.push({ name: 'login' })
}
})
}).catch(() => { }) //
}
}
}
</script>

@ -0,0 +1,69 @@
<!-- 组件模板开始 -->
<template>
<!-- 如果menu对象有list属性且list数组长度大于等于1则渲染el-submenu -->
<el-submenu v-if="menu.list && menu.list.length >= 1" :index="menu.menuId + ''"
:popper-class="'site-sidebar--' + sidebarLayoutSkin + '-popper'">
<!-- 子菜单标题 -->
<template slot="title">
<!-- 图标使用menu对象的icon属性作为类名 -->
<i class="site-sidebar__menu-icon" :class="menu.icon"></i>
<!-- 菜单名称 -->
<span>{{ menu.name }}</span>
</template>
<!-- 遍历menu的list属性为每个子菜单项渲染sub-menu组件 -->
<sub-menu v-for="item in menu.list" :key="item.menuId" :menu="item" :dynamicMenuRoutes="dynamicMenuRoutes">
</sub-menu>
</el-submenu>
<!-- 如果没有子菜单则渲染el-menu-item -->
<el-menu-item v-else :index="menu.menuId + ''" @click="gotoRouteHandle(menu)">
<!-- 图标 -->
<i class="site-sidebar__menu-icon fa" :class="menu.icon"></i>
<!-- 菜单名称 -->
<span>{{ menu.name }}</span>
</el-menu-item>
</template>
<!-- 组件模板结束 -->
<script>
// SubMenu
import SubMenu from './main-sidebar-sub-menu'
export default {
//
name: 'sub-menu',
// props
props: {
menu: {
type: Object, // menu
required: true //
},
dynamicMenuRoutes: {
type: Array, //
required: true //
}
},
// 使
components: {
SubMenu // 使
},
//
computed: {
// Vuexstate
sidebarLayoutSkin: {
get() { return this.$store.state.common.sidebarLayoutSkin }
}
},
//
methods: {
// menuId
gotoRouteHandle(menu) {
// menu.menuId
var route = this.dynamicMenuRoutes.filter(item => item.meta.menuId === menu.menuId)
//
if (route.length >= 1) {
this.$router.push({ name: route[0].name })
}
}
}
}
</script>

@ -0,0 +1,120 @@
<template>
<!-- 侧边栏容器类名根据sidebarLayoutSkin动态设置 -->
<aside class="site-sidebar" :class="'site-sidebar--' + sidebarLayoutSkin">
<!-- 侧边栏内部容器 -->
<div class="site-sidebar__inner">
<!-- Element UI的菜单组件用于构建导航菜单 -->
<el-menu :default-active="menuActiveName || 'home'" :collapse="sidebarFold" :collapseTransition="false"
class="site-sidebar__menu">
<!-- 首页菜单项 -->
<el-menu-item index="home" @click="$router.push({ name: 'home' })">
<!-- 图标 -->
<i class="site-sidebar__menu-icon el-icon-s-home"></i>
<!-- 菜单标题 -->
<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>
</el-menu>
</div>
</aside>
</template>
<script>
// SubMenu
import SubMenu from './main-sidebar-sub-menu'
// isURL
import { isURL } from '@/utils/validate'
export default {
data() {
return {
//
dynamicMenuRoutes: []
}
},
components: {
SubMenu // SubMenu
},
computed: {
//
sidebarLayoutSkin: {
get() { return this.$store.state.common.sidebarLayoutSkin }
},
//
sidebarFold: {
get() { return this.$store.state.common.sidebarFold }
},
//
menuList: {
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) }
},
//
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) }
}
},
watch: {
// routeHandle
$route: 'routeHandle'
},
created() {
// sessionStorage
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) {
//
var tab = this.mainTabs.filter(item => item.name === route.name)[0]
//
if (!tab) {
//
if (route.meta.isDynamic) {
//
route = this.dynamicMenuRoutes.filter(item => item.name === route.name)[0]
//
if (!route) {
return console.error('未能找到可用标签页!')
}
}
//
tab = {
menuId: route.meta.menuId || route.name, // ID
name: route.name, //
title: route.meta.title, //
type: isURL(route.meta.iframeUrl) ? 'iframe' : 'module', // iframeUrlURL
iframeUrl: route.meta.iframeUrl || '', // iframe
params: route.params, //
query: route.query //
}
//
this.mainTabs = this.mainTabs.concat(tab)
}
//
this.menuActiveName = tab.menuId + ''
this.mainTabsActiveName = tab.name
}
}
}
}
</script>

@ -0,0 +1,120 @@
<!-- Vue组件的模板开始 -->
<template>
<!-- 外层容器根据sidebarFold的状态添加或移除'site-sidebar--fold' -->
<div class="site-wrapper" :class="{ 'site-sidebar--fold': sidebarFold }" <!-- loadingtrue -->
v-loading.fullscreen.lock="loading"
<!-- 自定义加载文本 -->
element-loading-text="拼命加载中">
<!-- 当loading不为true时渲染以下内容 -->
<template v-if="!loading">
<!-- 引入主导航栏组件 -->
<main-navbar />
<!-- 引入主侧边栏组件 -->
<main-sidebar />
<!-- 内容区域容器其最小高度由documentClientHeight决定 -->
<div class="site-content__wrapper" :style="{ 'min-height': documentClientHeight + 'px' }">
<!--
当store中的common.contentIsNeedRefresh为false时
渲染主内容区域组件
-->
<main-content v-if="!$store.state.common.contentIsNeedRefresh" />
</div>
</template>
</div>
<!-- Vue组件的模板结束 -->
</template>
<script>
//
import MainNavbar from './main-navbar';
import MainSidebar from './main-sidebar';
import MainContent from './main-content';
export default {
//
provide() {
return {
//
refresh() {
// contentIsNeedRefreshtrue
this.$store.commit('common/updateContentIsNeedRefresh', true);
// DOMcontentIsNeedRefreshfalse
this.$nextTick(() => {
this.$store.commit('common/updateContentIsNeedRefresh', false);
});
}
};
},
//
data() {
return {
loading: true // true
};
},
//
components: {
MainNavbar,
MainSidebar,
MainContent
},
//
computed: {
// store
documentClientHeight: {
get() { return this.$store.state.common.documentClientHeight; },
set(val) { this.$store.commit('common/updateDocumentClientHeight', val); }
},
// store
sidebarFold: {
get() { return this.$store.state.common.sidebarFold; }
},
// IDstoreuser
userId: {
get() { return this.$store.state.user.id; },
set(val) { this.$store.commit('user/updateId', val); }
},
// storeuser
userName: {
get() { return this.$store.state.user.name; },
set(val) { this.$store.commit('user/updateName', val); }
}
},
//
created() {
this.getUserInfo(); //
},
// DOM
mounted() {
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'), // adornUrlURL
method: 'get',
params: this.$http.adornParams() // adornParams
}).then(({ data }) => {
//
if (data && data.code === 200) {
this.loading = false; //
this.userId = data.user.userId; // ID
this.userName = data.user.username; //
}
});
}
}
};
</script>

@ -0,0 +1,150 @@
<template>
<!-- Dialog弹框用于配置云存储 -->
<el-dialog title="云存储配置" :close-on-click-modal="false" :visible.sync="visible">
<!-- 表单组件model 绑定 dataFormrules 绑定 dataRule -->
<el-form :model="dataForm" :rules="dataRule" ref="dataForm" @keyup.enter.native="dataFormSubmit()" label-width="120px">
<!-- 存储类型选择项 -->
<el-form-item size="mini" label="存储类型">
<el-radio-group v-model="dataForm.type">
<el-radio :label="1">七牛</el-radio>
<el-radio :label="2">阿里云</el-radio>
<el-radio :label="3">腾讯云</el-radio>
</el-radio-group>
</el-form-item>
<!-- 当选择七牛云时显示七牛相关配置项 -->
<template v-if="dataForm.type === 1">
<el-form-item label="域名">
<el-input v-model="dataForm.qiniuDomain" placeholder="七牛绑定的域名"></el-input>
</el-form-item>
<el-form-item label="路径前缀">
<el-input v-model="dataForm.qiniuPrefix" placeholder="不设置默认为空"></el-input>
</el-form-item>
<el-form-item label="AccessKey">
<el-input v-model="dataForm.qiniuAccessKey" placeholder="七牛AccessKey"></el-input>
</el-form-item>
<el-form-item label="SecretKey">
<el-input v-model="dataForm.qiniuSecretKey" placeholder="七牛SecretKey"></el-input>
</el-form-item>
<el-form-item label="空间名">
<el-input v-model="dataForm.qiniuBucketName" placeholder="七牛存储空间名"></el-input>
</el-form-item>
</template>
<!-- 当选择阿里云时显示阿里云相关配置项 -->
<template v-else-if="dataForm.type === 2">
<el-form-item label="域名">
<el-input v-model="dataForm.aliyunDomain" placeholder="阿里云绑定的域名"></el-input>
</el-form-item>
<el-form-item label="路径前缀">
<el-input v-model="dataForm.aliyunPrefix" placeholder="不设置默认为空"></el-input>
</el-form-item>
<el-form-item label="EndPoint">
<el-input v-model="dataForm.aliyunEndPoint" placeholder="阿里云EndPoint"></el-input>
</el-form-item>
<el-form-item label="AccessKeyId">
<el-input v-model="dataForm.aliyunAccessKeyId" placeholder="阿里云AccessKeyId"></el-input>
</el-form-item>
<el-form-item label="AccessKeySecret">
<el-input v-model="dataForm.aliyunAccessKeySecret" placeholder="阿里云AccessKeySecret"></el-input>
</el-form-item>
<el-form-item label="BucketName">
<el-input v-model="dataForm.aliyunBucketName" placeholder="阿里云BucketName"></el-input>
</el-form-item>
</template>
<!-- 当选择腾讯云时显示腾讯云相关配置项 -->
<template v-else-if="dataForm.type === 3">
<el-form-item label="域名">
<el-input v-model="dataForm.qcloudDomain" placeholder="腾讯云绑定的域名"></el-input>
</el-form-item>
<el-form-item label="路径前缀">
<el-input v-model="dataForm.qcloudPrefix" placeholder="不设置默认为空"></el-input>
</el-form-item>
<el-form-item label="AppId">
<el-input v-model="dataForm.qcloudAppId" placeholder="腾讯云AppId"></el-input>
</el-form-item>
<el-form-item label="SecretId">
<el-input v-model="dataForm.qcloudSecretId" placeholder="腾讯云SecretId"></el-input>
</el-form-item>
<el-form-item label="SecretKey">
<el-input v-model="dataForm.qcloudSecretKey" placeholder="腾讯云SecretKey"></el-input>
</el-form-item>
<el-form-item label="BucketName">
<el-input v-model="dataForm.qcloudBucketName" placeholder="腾讯云BucketName"></el-input>
</el-form-item>
<el-form-item label="Bucket所属地区">
<el-input v-model="dataForm.qcloudRegion" placeholder="如sh可选值 华南gz 华北tj 华东sh"></el-input>
</el-form-item>
</template>
</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: {},
//
dataRule: {}
}
},
methods: {
// id
init(id) {
this.visible = true //
//
this.$http({
url: this.$http.adornUrl('/sys/oss/config'), //
method: 'get', // GET
params: this.$http.adornParams() //
}).then(({ data }) => {
//
this.dataForm = data && data.code === 200 ? data.config : []
})
},
//
dataFormSubmit() {
//
this.$refs['dataForm'].validate((valid) => {
if (valid) {
//
this.$http({
url: this.$http.adornUrl('/sys/oss/saveConfig'), //
method: 'post', // 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 //
}
})
} else {
//
this.$message.error(data.msg)
}
})
}
})
}
}
}
</script>

@ -0,0 +1,105 @@
<template>
<!-- 触发文件选择的区域点击时触发 selectFile 方法 -->
<div @click="selectFile">
<!-- 隐藏的文件输入框通过 click 触发文件选择 -->
<input type="file" ref="fileInput" v-show="false" @change="onFileChange" />
<!-- 显示上传状态文本如果正在上传则显示 infoText否则显示 "上传文件" -->
<div>{{uploading ? infoText : '上传文件'}}</div>
</div>
</template>
<script>
// 使
// 使 SDKhttps://unpkg.com/cos-js-sdk-v5@0.5.23/dist/cos-js-sdk-v5.min.js
var cos;
export default {
name: "oss-uploader", //
data() {
return {
uploading: false, //
infoText: "上传中...", //
cosConfig: [] // COS
}
},
mounted() {
// COS
this.$http({
url: this.$http.adornUrl('/sys/oss/config'),
method: 'get',
params: this.$http.adornParams() //
}).then(({ data }) => {
if (data && data.code === 200) {
//
this.cosConfig = data.config;
// COS
cos = new COS({
SecretId: data.config.qcloudSecretId,
SecretKey: data.config.qcloudSecretKey,
});
} else {
//
this.$message.error('请先配置云存储相关信息!');
}
})
},
methods: {
//
selectFile() {
//
if (!this.uploading) {
this.$refs.fileInput.click();
}
},
//
onFileChange() {
let file = this.$refs.fileInput.files[0]; //
this.uploading = true; // true
let now = new Date();
//
let path = now.toISOString().slice(0, 10) + '/' + now.getTime() + file.name.substr(file.name.lastIndexOf('.'));
// COS
cos.putObject({
Bucket: this.cosConfig.qcloudBucketName, //
Region: this.cosConfig.qcloudRegion, //
Key: path, //
Body: file, //
onProgress: (progressData) => { //
//
this.infoText = '上传中:' + (progressData.percent * 100).toFixed(2) + '%';
}
}, (err, data) => {
//
console.log(err || data);
this.uploading = false; // false
if (data) {
//
this.infoText = '上传文件';
// 访 URL
let fileUrl = 'https://' + this.cosConfig.qcloudBucketName + '.cos.' + this.cosConfig.qcloudRegion + '.myqcloud.com/' + path;
this.saveUploadResult(fileUrl); //
} else {
//
this.$message.error('文件上传失败', err);
}
});
},
// 访 URL
saveUploadResult(url) {
this.$http({
url: this.$http.adornUrl('/sys/oss/upload'),
method: 'post',
data: { url: url } // URL
}).then(({ data }) => {
// `uploaded` URL
this.$emit('uploaded', url);
})
}
}
}
</script>
<style scoped>
/* 样式部分为空 */
</style>

@ -0,0 +1,73 @@
<template>
<!-- 点击外部div触发文件选择操作 -->
<div @click="selectFile">
<!-- 隐藏的文件上传input选择文件后触发onFileChange方法 -->
<input type="file" ref="fileInput" v-show="false" @change="onFileChange" />
<!-- 显示上传中的状态或默认的上传文件文本 -->
<div>{{uploading ? infoText : '上传文件'}}</div>
</div>
</template>
<script>
export default {
name: "oss-uploader", // "oss-uploader"
data() {
return {
uploading: false, // false
infoText: "上传中...", //
cosConfig: [] //
}
},
mounted() {
//
this.$http({
url: this.$http.adornUrl('/sys/oss/config'), // API
method: 'get', // 使GET
params: this.$http.adornParams() //
}).then(({ data }) => {
//
if (data && data.code === 200 && data.config.type) {
this.cosConfig = data.config //
} else {
//
this.$message.error('请先配置云存储相关信息!')
}
})
},
methods: {
//
selectFile() {
if (!this.uploading) {
// input
this.$refs.fileInput.click();
}
},
//
onFileChange() {
let file = this.$refs.fileInput.files[0]; //
this.uploading = true; // true
let formData = new FormData();
formData.append("file", file); // formData
// POST
this.$http({
url: this.$http.adornUrl('/sys/oss/upload'), // API
method: 'post', // 使POST
data: formData //
}).then(({ data }) => {
//
console.log(data); // 便
if (data && data.code === 200) {
// 'uploaded'url
this.$emit('uploaded', data.url)
} else {
//
this.$message.error("文件上传失败:" + data.msg)
}
this.uploading = false; // false
})
}
}
}
</script>

@ -0,0 +1,117 @@
<template>
<div class="mod-oss">
<el-form :inline="true" :model="dataForm">
<el-form-item>
<el-button type="primary" @click="configHandle()"></el-button>
<el-button type="primary">
<OssUploader @uploaded="getDataList"></OssUploader>
</el-button>
<el-button 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="id" header-align="center" align="center" width="80" label="ID">
</el-table-column>
<el-table-column prop="url" header-align="center" align="center" label="URL地址">
<div slot-scope="scope">
<img class="image-sm" v-if="isImageUrl(scope.row.url)" :src="scope.row.url" />
<a :href="scope.row.url" target="_blank" v-else>{{scope.row.url}}</a>
</div>
</el-table-column>
<el-table-column prop="createDate" header-align="center" align="center" width="180" 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="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>
<!-- 弹窗, 云存储配置 -->
<config v-show="configVisible" ref="config"></config>
<!-- 弹窗, 上传文件 -->
<upload v-show="uploadVisible" ref="upload" @refreshDataList="getDataList"></upload>
</div>
</template>
<script>
export default {
data() {
return {
//
dataForm: {},
//
dataList: [],
//
pageIndex: 1,
//
pageSize: 10,
//
totalCount: 0,
//
dataListLoading: false,
//
dataListSelections: [],
//
configVisible: false,
//
uploadVisible: false
}
},
components: {
//
Config: () => import('./oss-config'),
//
OssUploader: () => import('./oss-uploader')
},
//
activated() {
this.getDataList();
},
methods: {
//
getDataList() {
this.dataListLoading = true;
this.$http({
url: this.$http.adornUrl('/sys/oss/list'),
method: 'get',
params: this.$http.adornParams({
'page': this.pageIndex, //
'limit': this.pageSize, //
'sidx': 'id', //
'order': 'desc' //
})
}).then(({ data }) => {
// dataListtotalCount
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;
},
}
}
</script>

@ -0,0 +1,117 @@
<template>
<el-dialog :title="!dataForm.id ? '新增' : '修改'" :close-on-click-modal="false" :visible.sync="visible">
<!-- 弹窗组件显示标题是新增修改取决于dataForm.id -->
<el-form :model="dataForm" :rules="dataRule" ref="dataForm" @keyup.enter.native="dataFormSubmit()" label-width="80px">
<!-- 表单组件绑定数据模型为dataForm表单校验规则为dataRule按回车键提交表单 -->
<el-form-item label="参数名" prop="paramKey">
<!-- 参数名输入框验证参数名 -->
<el-input v-model="dataForm.paramKey" placeholder="参数名"></el-input>
</el-form-item>
<el-form-item label="参数值" prop="paramValue">
<!-- 参数值输入框验证参数值 -->
<el-input v-model="dataForm.paramValue" placeholder="参数值"></el-input>
</el-form-item>
<el-form-item label="备注" prop="remark">
<!-- 备注输入框 -->
<el-input v-model="dataForm.remark" 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()"></el-button>
<!-- 确定按钮点击时提交表单 -->
</span>
</el-dialog>
</template>
<script>
export default {
data() {
return {
visible: false, //
dataForm: {
id: 0, // ID0ID
paramKey: '', //
paramValue: '',//
remark: '' //
},
dataRule: {
paramKey: [
{ required: true, message: '参数名不能为空', trigger: 'blur' }
],
paramValue: [
{ required: true, message: '参数值不能为空', trigger: 'blur' }
]
}
}
},
methods: {
// ID
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(`/sys/config/info/${this.dataForm.id}`),
method: 'get',
params: this.$http.adornParams()
}).then(({ data }) => {
if (data && data.code === 200) {
this.dataForm.paramKey = data.config.paramKey
this.dataForm.paramValue = data.config.paramValue
this.dataForm.remark = data.config.remark
}
})
}
})
},
//
dataFormSubmit() {
//
this.$refs['dataForm'].validate((valid) => {
if (valid) {
//
this.$http({
url: this.$http.adornUrl(`/sys/config/${!this.dataForm.id ? 'save' : 'update'}`),
method: 'post',
data: this.$http.adornData({
'id': this.dataForm.id || undefined, // IDID
'paramKey': this.dataForm.paramKey,
'paramValue': this.dataForm.paramValue,
'remark': this.dataForm.remark
})
}).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,172 @@
<template>
<div class="mod-config">
<!-- 表单部分包含参数名的查询框和操作按钮 -->
<el-form :inline="true" :model="dataForm" @keyup.enter.native="getDataList()">
<!-- 表单项使用 inline 布局按回车触发查询 -->
<el-form-item>
<el-input v-model="dataForm.paramKey" placeholder="参数名" clearable></el-input>
<!-- 输入框绑定 model dataForm.paramKey允许清除内容 -->
</el-form-item>
<el-form-item>
<!-- 查询按钮点击时触发 getDataList 方法 -->
<el-button @click="getDataList()"></el-button>
<!-- 新增按钮点击时触发 addOrUpdateHandle 方法 -->
<el-button type="primary" @click="addOrUpdateHandle()"></el-button>
<!-- 批量删除按钮点击时触发 deleteHandle 方法只有当有选中的数据时可用 -->
<el-button 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="id" header-align="center" align="center" width="80" label="ID"></el-table-column>
<!-- 参数名列 -->
<el-table-column prop="paramKey" header-align="center" align="center" label="参数名"></el-table-column>
<!-- 参数值列 -->
<el-table-column prop="paramValue" 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">
<!-- 修改按钮点击时触发 addOrUpdateHandle 方法传入当前行的 id -->
<el-button type="text" size="small" @click="addOrUpdateHandle(scope.row.id)"></el-button>
<!-- 删除按钮点击时触发 deleteHandle 方法传入当前行的 id -->
<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>
</div>
</template>
<script>
import AddOrUpdate from './config-add-or-update'
export default {
data() {
return {
//
dataForm: {
paramKey: ''
},
//
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/config/list'),
method: 'get',
params: this.$http.adornParams({
'page': this.pageIndex,
'limit': this.pageSize,
'paramKey': this.dataForm.paramKey
})
}).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('/sys/config/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,107 @@
<template>
<div class="mod-log">
<!-- 搜索表单部分 -->
<el-form :inline="true" :model="dataForm" @keyup.enter.native="getDataList()">
<!-- 输入框绑定到 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>
</el-form-item>
</el-form>
<!-- 表格显示数据 -->
<el-table :data="dataList" border v-loading="dataListLoading" style="width: 100%">
<!-- 表格的列定义 -->
<el-table-column prop="id" 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="operation" header-align="center" align="center" label="用户操作"></el-table-column>
<el-table-column prop="method" header-align="center" align="center" width="150" :show-overflow-tooltip="true" label="请求方法"></el-table-column>
<el-table-column prop="params" header-align="center" align="center" width="150" :show-overflow-tooltip="true" label="请求参数"></el-table-column>
<el-table-column prop="time" header-align="center" align="center" label="执行时长(毫秒)"></el-table-column>
<el-table-column prop="ip" header-align="center" align="center" width="150" label="IP地址"></el-table-column>
<el-table-column prop="createDate" header-align="center" align="center" width="180" label="创建时间"></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 {
// 'key'
dataForm: {
key: ''
},
//
dataList: [],
//
pageIndex: 1,
//
pageSize: 10,
//
totalCount: 0,
//
dataListLoading: false,
// 使
selectionDataList: []
}
},
created() {
//
this.getDataList()
},
methods: {
//
getDataList() {
//
this.dataListLoading = true
this.$http({
url: this.$http.adornUrl('/sys/log/list'), //
method: 'get',
params: this.$http.adornParams({
//
'page': this.pageIndex,
'limit': this.pageSize,
'key': this.dataForm.key, //
'sidx': 'id', // 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() //
}
}
}
</script>

@ -0,0 +1,253 @@
<template>
<!-- 弹窗组件标题根据dataForm.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="类型" prop="type">
<el-radio-group v-model="dataForm.type">
<!-- 循环渲染可选择的菜单类型 -->
<el-radio v-for="(type, index) in dataForm.typeList" :label="index" :key="index">{{ type }}</el-radio>
</el-radio-group>
</el-form-item>
<!-- 菜单名称输入框 -->
<el-form-item :label="dataForm.typeList[dataForm.type] + '名称'" prop="name">
<el-input v-model="dataForm.name" :placeholder="dataForm.typeList[dataForm.type] + '名称'"></el-input>
</el-form-item>
<!-- 上级菜单选择 -->
<el-form-item label="上级菜单" prop="parentName">
<!-- 弹出菜单树选择 -->
<el-popover ref="menuListPopover" placement="bottom-start" trigger="click">
<el-tree :data="menuList" :props="menuListTreeProps" node-key="menuId" ref="menuListTree"
@current-change="menuListTreeCurrentChangeHandle" :default-expand-all="true"
:highlight-current="true" :expand-on-click-node="false">
</el-tree>
</el-popover>
<!-- 显示上级菜单选择 -->
<el-input v-model="dataForm.parentName" v-popover:menuListPopover :readonly="true"
placeholder="点击选择上级菜单" class="menu-list__input"></el-input>
</el-form-item>
<!-- 菜单路由仅在选择菜单类型时显示 -->
<el-form-item v-if="dataForm.type === 1" label="菜单路由" prop="url">
<el-input v-model="dataForm.url" placeholder="菜单路由"></el-input>
</el-form-item>
<!-- 授权标识仅在类型不为目录时显示 -->
<el-form-item v-if="dataForm.type !== 0" label="授权标识" prop="perms">
<el-input v-model="dataForm.perms" placeholder="多个用逗号分隔, 如: user:list,user:create"></el-input>
</el-form-item>
<!-- 菜单图标仅在类型不为按钮时显示 -->
<el-form-item v-if="dataForm.type !== 2" label="菜单图标" prop="icon">
<el-row>
<el-col :span="12">
<el-input v-model="dataForm.icon" placeholder="菜单图标名称" class="icon-list__input"></el-input>
</el-col>
<el-col :span="12" class="icon-list__tips">
<!-- 排序号输入框仅在类型不为按钮时显示 -->
<el-form-item v-if="dataForm.type !== 2" label="排序号" prop="orderNum">
<el-input-number v-model="dataForm.orderNum" controls-position="right" :min="0" label="排序号"></el-input-number>
</el-form-item>
</el-col>
</el-row>
</el-form-item>
<!-- 提示用户参考图标库 -->
<div>参考ElementUI图标库, 找图标</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>
//
import { treeDataTranslate } from '@/utils'
export default {
data() {
// URL
var validateUrl = (rule, value, callback) => {
// URL
if (this.dataForm.type === 1 && !/\S/.test(value)) {
callback(new Error('菜单URL不能为空'))
} else {
callback() //
}
}
return {
visible: false, //
dataForm: {
id: 0, // ID0
type: 1, // 0-1-2-
typeList: ['目录', '菜单', '按钮'], //
name: '', //
parentId: 0, // ID
parentName: '', //
url: '', //
perms: '', //
orderNum: 0, //
icon: '', //
},
//
dataRule: {
name: [
{ required: true, message: '菜单名称不能为空', trigger: 'blur' } //
],
parentName: [
{ required: true, message: '上级菜单不能为空', trigger: 'change' } //
],
url: [
{ validator: validateUrl, trigger: 'blur' } // URL
]
},
menuList: [], // 使
menuListTreeProps: {
label: 'name', //
children: 'children' //
}
}
},
methods: {
//
init(id) {
this.dataForm.id = id || 0 // ID
this.$http({
url: this.$http.adornUrl('/sys/menu/select'),
method: 'get',
params: this.$http.adornParams()
}).then(({ data }) => {
//
this.menuList = treeDataTranslate(data.menuList, 'menuId')
}).then(() => {
this.visible = true //
this.$nextTick(() => {
this.$refs['dataForm'].resetFields() //
})
}).then(() => {
if (!this.dataForm.id) {
//
this.menuListTreeSetCurrentNode()
} else {
//
this.$http({
url: this.$http.adornUrl(`/sys/menu/info/${this.dataForm.id}`),
method: 'get',
params: this.$http.adornParams()
}).then(({ data }) => {
//
this.dataForm.id = data.menu.menuId
this.dataForm.type = data.menu.type
this.dataForm.name = data.menu.name
this.dataForm.parentId = data.menu.parentId
this.dataForm.url = data.menu.url
this.dataForm.perms = data.menu.perms
this.dataForm.orderNum = data.menu.orderNum
this.dataForm.icon = data.menu.icon
this.menuListTreeSetCurrentNode() //
})
}
})
},
//
menuListTreeCurrentChangeHandle(data, node) {
this.dataForm.parentId = data.menuId // ID
this.dataForm.parentName = data.name //
},
//
menuListTreeSetCurrentNode() {
this.$refs.menuListTree.setCurrentKey(this.dataForm.parentId) //
this.dataForm.parentName = (this.$refs.menuListTree.getCurrentNode() || {})['name'] //
},
//
dataFormSubmit() {
this.$refs['dataForm'].validate((valid) => {
if (valid) {
//
this.$http({
url: this.$http.adornUrl(`/sys/menu/${!this.dataForm.id ? 'save' : 'update'}`),
method: 'post',
data: this.$http.adornData({
'menuId': this.dataForm.id || undefined,
'type': this.dataForm.type,
'name': this.dataForm.name,
'parentId': this.dataForm.parentId,
'url': this.dataForm.url,
'perms': this.dataForm.perms,
'orderNum': this.dataForm.orderNum,
'icon': this.dataForm.icon
})
}).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 lang="scss">
.mod-menu {
.menu-list__input,
.icon-list__input {
> .el-input__inner {
cursor: pointer; //
}
}
&__icon-popover {
width: 458px; //
overflow: hidden; //
}
&__icon-inner {
width: 478px; //
max-height: 258px; //
overflow-x: hidden; //
overflow-y: auto; //
}
&__icon-list {
width: 458px; //
padding: 0; //
margin: -8px 0 0 -8px; //
> .el-button {
padding: 8px; //
margin: 8px 0 0 8px; //
> span {
display: inline-block; // 使
vertical-align: middle; //
width: 18px; //
height: 18px; //
font-size: 18px; //
}
}
}
.icon-list__tips {
font-size: 18px; //
text-align: center; //
color: #e6a23c; //
cursor: pointer; //
}
}
</style>

@ -0,0 +1,148 @@
<template>
<!-- 菜单管理模块 -->
<div class="mod-menu">
<!-- 表单组件内联布局 -->
<el-form :inline="true" :model="dataForm">
<el-form-item>
<!-- 如果有新增权限显示新增按钮 -->
<el-button v-if="isAuth('sys:menu:save')" type="primary" @click="addOrUpdateHandle()"></el-button>
</el-form-item>
</el-form>
<!-- 菜单数据表格 -->
<el-table :data="dataList" row-key="menuId" border style="width: 100%;">
<!-- 菜单名称列 -->
<el-table-column prop="name" header-align="center" min-width="150" label="名称">
</el-table-column>
<!-- 上级菜单列 -->
<el-table-column prop="parentName" header-align="center" align="center" width="120" label="上级菜单">
</el-table-column>
<!-- 菜单图标列 -->
<el-table-column header-align="center" align="center" label="图标">
<template slot-scope="scope">
<!-- 根据图标类渲染图标 -->
<i :class="scope.row.icon"></i>
</template>
</el-table-column>
<!-- 菜单类型列 -->
<el-table-column prop="type" header-align="center" align="center" label="类型">
<template slot-scope="scope">
<!-- 根据菜单类型渲染不同的标签 -->
<el-tag v-if="scope.row.type === 0" size="small"></el-tag>
<el-tag v-else-if="scope.row.type === 1" size="small" type="success">菜单</el-tag>
<el-tag v-else-if="scope.row.type === 2" size="small" type="info">按钮</el-tag>
</template>
</el-table-column>
<!-- 排序号列 -->
<el-table-column prop="orderNum" header-align="center" align="center" label="排序号">
</el-table-column>
<!-- 菜单URL列支持溢出提示 -->
<el-table-column prop="url" header-align="center" align="center" width="150" :show-overflow-tooltip="true" label="菜单URL">
</el-table-column>
<!-- 授权标识列支持溢出提示 -->
<el-table-column prop="perms" header-align="center" align="center" width="150" :show-overflow-tooltip="true" 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:menu:update')" type="text" size="small" @click="addOrUpdateHandle(scope.row.menuId)"></el-button>
<!-- 如果有删除权限显示删除按钮 -->
<el-button v-if="isAuth('sys:menu:delete')" type="text" size="small" @click="deleteHandle(scope.row.menuId)"></el-button>
</template>
</el-table-column>
</el-table>
<!-- 弹窗组件用于新增或修改菜单 -->
<add-or-update v-if="addOrUpdateVisible" ref="addOrUpdate" @refreshDataList="getDataList"></add-or-update>
</div>
</template>
<script>
// /
import AddOrUpdate from './menu-add-or-update'
import { treeDataTranslate } from '@/utils'
export default {
data() {
return {
dataForm: {}, //
dataList: [], //
dataListLoading: false, //
addOrUpdateVisible: false //
}
},
components: {
AddOrUpdate // /
},
activated() {
//
this.getDataList()
},
methods: {
//
getDataList() {
this.dataListLoading = true //
// HTTP
this.$http({
url: this.$http.adornUrl('/sys/menu/list'),
method: 'get',
params: this.$http.adornParams()
}).then(({ data }) => {
// 使
this.dataList = treeDataTranslate(data, 'menuId')
this.dataListLoading = false //
})
},
//
addOrUpdateHandle(id) {
this.addOrUpdateVisible = true // /
this.$nextTick(() => {
//
this.$refs.addOrUpdate.init(id)
})
},
//
deleteHandle(id) {
//
this.$confirm(`确定对[id=${id}]进行[删除]操作?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning' //
}).then(() => {
//
this.$http({
url: this.$http.adornUrl(`/sys/menu/delete/${id}`),
method: 'post',
data: this.$http.adornData()
}).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,148 @@
<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="roleName">
<el-input v-model="dataForm.roleName" placeholder="角色名称"></el-input>
</el-form-item>
<!-- 备注输入框 -->
<el-form-item label="备注" prop="remark">
<el-input v-model="dataForm.remark" placeholder="备注"></el-input>
</el-form-item>
<!-- 授权菜单树 -->
<el-form-item size="mini" label="授权">
<el-tree :data="menuList" :props="menuListTreeProps" node-key="menuId" ref="menuListTree" :default-expand-all="true" show-checkbox>
</el-tree>
</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 { treeDataTranslate } from '@/utils'
export default {
data() {
return {
//
visible: false,
//
menuList: [],
// el-tree
menuListTreeProps: {
label: 'name', //
children: 'children' //
},
//
dataForm: {
id: 0, // ID0
roleName: '', //
remark: '' //
},
//
dataRule: {
roleName: [
{ required: true, message: '角色名称不能为空', trigger: 'blur' } //
]
}
}
},
methods: {
// ID
init(id) {
this.dataForm.id = id || 0 // ID0
//
this.$http({
url: this.$http.adornUrl('/sys/menu/list'),
method: 'get',
params: this.$http.adornParams()
}).then(({ data }) => {
this.menuList = treeDataTranslate(data, 'menuId') //
}).then(() => {
//
this.visible = true
// DOM
this.$nextTick(() => {
//
this.$refs['dataForm'].resetFields()
//
this.$refs.menuListTree.setCheckedKeys([])
})
}).then(() => {
// ID
if (this.dataForm.id) {
this.$http({
url: this.$http.adornUrl(`/sys/role/info/${this.dataForm.id}`),
method: 'get',
params: this.$http.adornParams()
}).then(({ data }) => {
if (data && data.code === 200) {
//
this.dataForm.roleName = data.role.roleName
this.dataForm.remark = data.role.remark
//
data.role.menuIdList.forEach(item => {
this.$refs.menuListTree.setChecked(item, true)
})
}
})
}
})
},
//
dataFormSubmit() {
//
this.$refs['dataForm'].validate((valid) => {
if (valid) {
//
this.$http({
url: this.$http.adornUrl(`/sys/role/${!this.dataForm.id ? 'save' : 'update'}`), // ID
method: 'post',
data: this.$http.adornData({
'roleId': this.dataForm.id || undefined, // ID
'roleName': this.dataForm.roleName, //
'remark': this.dataForm.remark, //
// ID
'menuIdList': [].concat(this.$refs.menuListTree.getCheckedKeys(), this.$refs.menuListTree.getHalfCheckedKeys())
})
}).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,168 @@
<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>

@ -0,0 +1,202 @@
<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>

@ -0,0 +1,168 @@
<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>

@ -0,0 +1,63 @@
<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>

@ -0,0 +1,145 @@
<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>

@ -0,0 +1,199 @@
<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',
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
}
},
data() {
return {
dataForm: {
id: "",
type: '1',
title: "",
content: "",
category: "",
subCategory: "",
summary: "",
tags: "",
openCount: 0,
targetLink: location.origin + "/client/#/article/${articleId}",
image: ""
},
dataRule: {
type: [
{ required: true, message: "文章类型不能为空", trigger: "blur" }
],
title: [
{ required: true, message: "标题不能为空", trigger: "blur" }
],
category: [
{ required: true, message: "分类不能为空", trigger: "blur" }
]
}
};
},
computed: mapState({
// VuexstatearticleARTICLE_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.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",
duration: 1500,
onClose: () => {
this.$emit("refreshDataList");
this.$emit('hide')
}
});
} else {
//
this.$message.error(data.msg);
}
});
}
});
},
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>

@ -0,0 +1,193 @@
<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>
<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>
</div>
<!-- 新增/修改组件通过属性和事件与父组件进行交互 -->
<add-or-update :visible="addOrUpdateVisible" ref="addOr-update" @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: {
AddOrUpdate
},
data() {
return {
dataForm: {
title: '',
type: ''
},
dataList: [],
pageIndex: 1,
pageSize: 10,
totalCount: 0,
dataListLoading: false,
dataListSelections: [],
addOrUpdateVisible: false
}
},
computed: mapState({
// VuexstatearticleARTICLE_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',
params: this.$http.adornParams({
'page': this.pageIndex,
'limit': this.pageSize,
'title': this.dataForm.title,
'type': this.dataForm.type,
'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/article/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)
}
})
})
},
// ARTICLE_TYPES
articleTypeFormat(row, column, cellValue) {
return this.ARTICLE_TYPES[cellValue];
}
}
}
</script>

@ -0,0 +1,44 @@
<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>

@ -0,0 +1,118 @@
<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>

@ -0,0 +1,207 @@
<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>

@ -0,0 +1,232 @@
<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>

@ -0,0 +1,245 @@
<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>
</div>
</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,
default: false
},
rows: {
type: Number,
default: 4
}
},
data() {
return {
dataForm: {},
addOrUpdateVisible: false,
dataList: [],
pageIndex: 1,
pageSize: 20,
totalCount: 0,
dataListLoading: false
}
},
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',
duration: 1500,
onClose: () => this.onChange()
})
} else {
//
this.$message.error(data.msg)
}
})
})
},
//
currentChangeHandle(val) {
this.pageIndex = val
this.getDataList()
},
// //trueDOM/
addOrUpdateHandle(news) {
this.addOrUpdateVisible = true
this.$nextTick(() => {
this.$refs.addOrUpdate.init(news || '')
})
},
onCopySuccess() {
//
this.$message.success('已复制')
},
onCopyError(err) {
//
this.$message.error('复制失败,可能是此浏览器不支持复制')
},
onChange() {
// 1$emitchange
this.pageIndex=1
this.getDataList()
this.$emit('change')
}
}
}
</script>
<style scoped>
.card {
width: 240px;
min-height: 120px;
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;
border-radius: 5px;
vertical-align: top;
height: fit-content;
}
.card:hover {
border: 2px solid #66b1ff;
margin-bottom: 6px;
}
.card-preview {
color: #d9d9d9;
padding-left: 10px;
padding-top: 15px;
}
.article-item {
display: flex;
justify-content: center;
align-items: flex-start;
padding: 10px 0;
cursor: pointer;
}
.article-item::after{
width: 168px;
border-bottom: 1px solid #eee;
}
.article-title {
display: -webkit-box;
word-wrap: break-word;
word-break: break-all;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
overflow: hidden;
flex: 1;
color: #333333;
padding-right: 10px;
line-height: 20px;
font-size: 13px;
}
.article-thumb {
width: 50px;
height: 50px;
display: inline-block;
}
.card-footer {
font-size: 12px;
color: #ccc;
padding: 15px 10px;
}
</style>

@ -0,0 +1,261 @@
<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-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>
<el-option label="当前公众号" :value="selectedAppid"></el-option>
</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')
},
data() {
return {
visible: false,
assetsSelectorVisible: false,
dataForm: {
// ID0
ruleId: 0,
appid: '',
ruleName: "",
exactMatch: false,
matchValue: "",
replyType: 'text',
replyContent: "",
status: true,
desc: "",
effectTimeStart: "00:00:00",
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" }
]
}
};
},
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] || '';
}
}),
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}`),
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"}`),
method: "post",
data: this.$http.adornData(this.dataForm)
}).then(({ data }) => {
if (data && data.code === 200) {
// $emitrefreshDataList
this.$message({
message: "操作成功",
type: "success",
duration: 1500,
onClose: () => {
this.visible = false;
this.$emit("refreshDataList");
}
});
} else {
//
this.$message.error(data.msg);
}
});
}
});
},
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)
} else if ("music" == value) {
let demo = { musicurl: "音乐链接", hqmusicurl: "高品质链接", title: "标题", description: "描述", thumb_media_id: "缩略图media_id" }
this.dataForm.replyContent = JSON.stringify(demo, null, 4)
} else if ("msgmenu" == value) {
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" }
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;
}
this.assetsSelectorVisible = false;
}
}
};
</script>

@ -0,0 +1,205 @@
<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>
</el-form-item>
<!-- 精确匹配信息展示的表单项根据数据项中的exactMatch值判断显示 -->
<el-form-item label="精确匹配">
<span>{{ props.row.exactMatch? '是' : '否' }}</span>
</el-form-item>
<!-- 是否有效信息展示的表单项根据数据项中的status值判断显示 -->
<el-form-item label="是否有效">
<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>
<el-button type="text" size="small" @click="deleteHandle(scope.row.ruleId)"></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>
</div>
</template>
<script>
// /
import AddOrUpdate from './msg-reply-rule-add-or-update'
// VuexmapStateVuex
import { mapState } from 'vuex'
export default {
components: {
AddOrUpdate
},
data() {
return {
dataForm: {
matchValue: ''
},
dataList: [],
pageIndex: 1,
pageSize: 10,
totalCount: 0,
dataListLoading: false,
dataListSelections: [],
addOrUpdateVisible: false
}
},
computed: mapState({
// VuexstatemessageKefuMsgType
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',
params: this.$http.adornParams({
'page': this.pageIndex,
'limit': this.pageSize,
'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? '删除' : '批量删除'}]操作?`, '提示', {
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',
duration: 1500,
onClose: () => this.getDataList()
})
} else {
//
this.$message.error(data.msg)
}
})
})
},
// KefuMsgType
replyTypeFormat(row, column, cellValue) {
return this.KefuMsgType[cellValue];
}
}
}
</script>

@ -0,0 +1,195 @@
<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>

@ -0,0 +1,274 @@
<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>

@ -0,0 +1,169 @@
<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>

@ -0,0 +1,174 @@
<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>

@ -0,0 +1,81 @@
<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>

@ -0,0 +1,184 @@
<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>

@ -0,0 +1,200 @@
<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>
<!-- 如果子菜单项少于5个显示添加按钮 -->
<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>
<!-- 如果主菜单项少于3个显示添加按钮 -->
<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')">
<!-- 发布按钮 -->
<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 {
// wxMenuButtonEditor
components: {
wxMenuButtonEditor: () => import('./wx-menu-button-editor')
},
data() {
return {
//
menu: { 'buttons': [] },
//
selectedMenuIndex: '',
//
selectedSubMenuIndex: '',
// 12
selectedMenuLevel: 0,
//
selectedButton: '',
//
onDragOverMenu: ''
}
},
mounted() {
//
this.getWxMenu();
},
methods: {
//
getWxMenu() {
this.$http({
url: this.$http.adornUrl('/manage/wxMenu/getMenu') // API
}).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'), // API
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) // +1
delSubIndex++;
this.menu.buttons[this.selectedMenuIndex].subButtons.splice(delSubIndex, 1); //
this.unSelectMenu(); //
}
}
}
</script>
<!-- 引入外部CSS文件 -->
<style src="@/assets/css/wx-menu.css"></style>

@ -0,0 +1,109 @@
<template>
<!-- 使用element-ui的Dialog组件用于显示回复消息的对话框 -->
<el-dialog title="消息回复" :close-on-click-modal="false" :visible.sync="visible">
<!-- 使用element-ui的Form组件用于构建表单 -->
<el-form :model="dataForm" :rules="dataRule" ref="dataForm">
<!-- 表单项用于输入回复内容 -->
<el-form-item prop="replyContent">
<!-- 使用element-ui的Input组件配置为textarea类型用于多行文本输入 -->
<el-input v-model="dataForm.replyContent" type="textarea" :rows="5" placeholder="回复内容" maxlength="600"
show-word-limit :autosize="{ minRows: 5, maxRows: 30 }" autocomplete></el-input>
<!-- 当回复类型为'text'时显示点击插入链接 -->
<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
openid: '',
// 'text'
replyType: 'text',
//
replyContent: ''
},
//
dataRule: {
replyContent: [
{ required: true, message: "回复内容不能为空", trigger: "blur" }
]
}
}
},
// 使
components: {
WxMsgPreview: () => import('@/components/wx-msg-preview')
},
methods: {
// openid
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) { // 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;
}
/* 注意:.msg-container样式在模板中未使用可能是遗留或备用 */
</style>

@ -0,0 +1,215 @@
<template>
<div class="mod-config">
<!-- 内联表单用于查询消息 -->
<el-form :inline="true" :model="dataForm" @keyup.enter.native="getDataList()">
<!-- 时间选择器 -->
<el-form-item>
<el-select v-model="dataForm.startTime" placeholder="时间">
<el-option v-for="(name, key) in timeSelections" :key="key" :value="name" :label="key"></el-option>
</el-select>
</el-form-item>
<!-- 消息类型选择器 -->
<el-form-item>
<el-select v-model="dataForm.msgTypes" placeholder="消息类型">
<el-option value="" label="不限类型"></el-option>
<el-option value="text,image,voice,shortvideo,video,news,music,location,link"
label="消息"></el-option>
<el-option value="event,transfer_customer_service" label="事件"></el-option>
</el-select>
</el-form-item>
<!-- 查询按钮 -->
<el-form-item>
<el-button @click="getDataList()"></el-button>
</el-form-item>
</el-form>
<!-- 提示信息 -->
<div class="text-gray">
24小时内消息可回复此后台展示消息有一分钟左右延迟如需畅聊请使用
<a href="https://mpkf.weixin.qq.com/" target="_blank">公众平台客服</a>
</div>
<!-- 消息列表加载时显示加载动画 -->
<div v-loading="dataListLoading">
<div class="msg-item" v-for="(msg, index) in dataList" :key="index">
<!-- 用户头像 -->
<div class="avatar"><el-avatar shape="square" :size="60"
:src="getUserInfo(msg.openid).headimgurl"></el-avatar></div>
<!-- 消息内容 -->
<div class="item-content">
<div class="flex justify-between margin-bottom">
<div class="text-cut">{{ getUserInfo(msg.openid).nickname || '--' }}</div>
<div>{{ $moment(msg.createTime).calendar() }}</div>
<!-- 回复按钮如果消息在24小时内可点击 -->
<div class="reply-btn">
<div v-if="canReply(msg.createTime)" @click="replyHandle(msg.openid)"
class="el-icon-s-promotion">回复</div>
</div>
</div>
<!-- 消息预览组件 -->
<wx-msg-preview :msg="msg" singleLine></wx-msg-preview>
</div>
</div>
</div>
<!-- 分页组件 -->
<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>
<!-- 消息回复弹窗组件 -->
<wx-msg-reply ref="wxMsgReply" @success="onReplyed"></wx-msg-reply>
</div>
</template>
<script>
const TIME_FORMAT = 'YYYY/MM/DD hh:mm:ss'
export default {
data() {
return {
//
timeSelections: {
'近24小时': this.$moment().subtract(1, 'days').format(TIME_FORMAT),
'近3天': this.$moment().subtract(3, 'days').format(TIME_FORMAT),
'近7天': this.$moment().subtract(7, 'days').format(TIME_FORMAT),
'近30天': this.$moment().subtract(30, 'days').format(TIME_FORMAT),
},
//
dataForm: {
startTime: this.$moment().subtract(1, 'days').format(TIME_FORMAT),
msgTypes: ''
},
//
dataList: [],
//
userDataList: [],
//
pageIndex: 1,
pageSize: 20,
totalCount: 0,
//
dataListLoading: false,
// 使
dataListSelections: []
}
},
//
components: {
WxMsgReply: () => import('./wx-msg-reply'),
WxMsgPreview: () => import('@/components/wx-msg-preview')
},
//
activated() {
this.getDataList()
},
methods: {
//
getDataList() {
this.dataListLoading = true
this.$http({
url: this.$http.adornUrl('/manage/wxMsg/list'),
method: 'get',
params: this.$http.adornParams({
'page': this.pageIndex,
'limit': this.pageSize,
'msgTypes': this.dataForm.msgTypes,
'startTime': this.dataForm.startTime,
'sidx': 'create_time',
'order': 'desc'
})
}).then(({ data }) => {
if (data && data.code === 200) {
this.dataList = data.page.list
this.totalCount = data.page.totalCount
this.refreshUserList(this.dataList)
} else {
this.dataList = []
this.totalCount = 0
}
this.dataListLoading = false
})
},
//
refreshUserList(msgList) {
let openidList = msgList.map(msg => msg.openid).filter(openid => !this.userDataList.some(u => u.openid == openid))
if (!openidList.length) return
openidList = Array.from(new Set(openidList)) //
this.$http({
url: this.$http.adornUrl('/manage/wxUser/listByIds'),
method: 'post',
data: this.$http.adornParams(openidList, false)
}).then(({ data }) => {
if (data && data.code === 200) {
this.userDataList = this.userDataList.concat(data.data)
}
})
},
// openid
getUserInfo(openid) {
return this.userDataList.find(u => u.openid == openid) || { nickname: '--', headimgurl: '' }
},
// 24
canReply(time) {
return new Date(time).getTime() > new Date().getTime() - 24 * 60 * 60 * 1000
},
//
sizeChangeHandle(val) {
this.pageSize = val
this.pageIndex = 1
this.getDataList()
},
//
currentChangeHandle(val) {
this.pageIndex = val
this.getDataList()
},
// 使
selectionChangeHandle(val) {
this.dataListSelections = val
},
//
replyHandle(openid) {
this.$nextTick(() => {
this.$refs.wxMsgReply.init(openid)
})
},
//
onReplyed(replyMsg) {
this.dataList.unshift({
openid: replyMsg.openid,
msgType: replyMsg.replyType,
detail: {
content: replyMsg.replyContent
},
inOut: 1,
createTime: new Date()
})
}
}
}
</script>
<style scoped>
.msg-item {
border: 1px solid #DCDFE6;
display: flex;
justify-content: flex-start;
align-items: top;
margin-top: 20px;
padding: 10px 20px;
}
.avatar {
flex: 0;
display: inline-block;
min-width: 60px;
margin-right: 20px;
}
.item-content {
flex: 1;
line-height: 20px;
max-width: 100%;
overflow: hidden;
}
.reply-btn {
width: 50px;
}
</style>

@ -0,0 +1,102 @@
<template>
<!-- 弹窗对话框标题根据dataForm.id是否存在决定是新增还是修改 -->
<el-dialog :title="!dataForm.id ? '新增' : '修改'" :close-on-click-modal="false" :visible.sync="visible">
<!-- 表单绑定dataForm作为数据模型dataRule作为验证规则监听回车事件触发提交 -->
<el-form :model="dataForm" :rules="dataRule" ref="dataForm" @keyup.enter.native="dataFormSubmit()"
label-width="100px">
<!-- 表单项二维码类型使用单选按钮选择临时或永久 -->
<el-form-item label="二维码类型" prop="isTemp">
<el-radio v-model="dataForm.isTemp" :label="true"></el-radio>
<el-radio v-model="dataForm.isTemp" :label="false"></el-radio>
<!-- 如果不是临时二维码显示一个链接提醒用户注意永久二维码的限制 -->
<div>
<a class="text-warning" v-show="!dataForm.isTemp" target="_blank"
href="https://developers.weixin.qq.com/doc/offiaccount/Account_Management/Generating_a_Parametric_QR_Code.html">注意永久二维码上限10万个且暂时无法删除旧的二维码</a>
</div>
</el-form-item>
<!-- 表单项场景值使用输入框输入任意字符串 -->
<el-form-item label="场景值" prop="sceneStr">
<el-input v-model="dataForm.sceneStr" placeholder="任意字符串" maxlength="64"></el-input>
</el-form-item>
<!-- 表单项失效时间仅当二维码为临时时显示 -->
<el-form-item label="失效时间/秒" prop="expireSeconds" v-if="dataForm.isTemp">
<el-input v-model="dataForm.expireSeconds" placeholder="单位最大259200030天"></el-input>
<!-- 显示当前设置的失效时间转换为天的结果 -->
<div>最大30天当前设置<span class="text-warning">{{ dataForm.expireSeconds / (24 * 3600) }}</span></div>
</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>
export default {
data() {
return {
//
visible: false,
//
dataForm: {
isTemp: true, //
sceneStr: '', //
expireSeconds: 2592000 // 30
},
//
dataRule: {
isTemp: [
{ required: true, message: '二维码类型不能为空', trigger: 'blur' }
],
sceneStr: [
{ required: true, message: '场景值ID不能为空', trigger: 'blur' }
],
expireSeconds: [
{ required: true, message: '该二维码失效时间不能为空', trigger: 'blur' }
]
}
}
},
methods: {
// id
init(id) {
this.dataForm.id = id || 0 // id
this.visible = true //
this.$nextTick(() => {
this.$refs['dataForm'].resetFields() //
})
},
//
dataFormSubmit() {
this.$refs['dataForm'].validate((valid) => {
if (valid) { //
// POST
this.$http({
url: this.$http.adornUrl(`/manage/wxQrCode/createTicket`), //
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>

@ -0,0 +1,170 @@
<template>
<div class="mod-config">
<!-- 表单用于查询二维码信息 -->
<el-form :inline="true" :model="dataForm" @keyup.enter.native="getDataList()">
<el-form-item>
<el-input v-model="dataForm.sceneStr" placeholder="场景值" clearable></el-input>
</el-form-item>
<el-form-item>
<!-- 查询按钮 -->
<el-button @click="getDataList()"></el-button>
<!-- 新增按钮仅当用户有新增权限时显示 -->
<el-button v-if="isAuth('wx:wxqrcode:save')" type="primary" @click="addOrUpdateHandle()"></el-button>
<!-- 批量删除按钮仅当用户有删除权限且至少选中一项时显示 -->
<el-button v-if="isAuth('wx:wxqrcode: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="id" header-align="center" align="center" label="ID">
</el-table-column>
<!-- 类型列显示临时永久 -->
<el-table-column prop="isTemp" header-align="center" align="center" label="类型">
<span slot-scope="scope">{{ scope.row.isTemp ? '临时' : '永久' }}</span>
</el-table-column>
<!-- 场景值列 -->
<el-table-column prop="sceneStr" header-align="center" align="center" label="场景值">
</el-table-column>
<!-- 二维码图片列显示二维码链接 -->
<el-table-column prop="ticket" header-align="center" align="center" show-overflow-tooltip label="二维码图片">
<a :href="'https://mp.weixin.qq.com/cgi-bin/showqrcode?ticket=' + scope.row.ticket"
slot-scope="scope">{{ scope.row.ticket }}</a>
</el-table-column>
<!-- 解析后的地址列显示链接 -->
<el-table-column prop="url" header-align="center" align="center" show-overflow-tooltip label="解析后的地址">
<a :href="scope.row.url" slot-scope="scope">{{ scope.row.url }}</a>
</el-table-column>
<!-- 失效时间列 -->
<el-table-column prop="expireTime" header-align="center" align="center" width="100" 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="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="totalPage"
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 './wx-qrcode-add-or-update' // /
export default {
data() {
return {
//
dataForm: {
sceneStr: ''
},
//
dataList: [],
//
pageIndex: 1,
//
pageSize: 10,
//
totalPage: 0,
//
dataListLoading: false,
//
dataListSelections: [],
// /
addOrUpdateVisible: false
}
},
components: {
AddOrUpdate // /
},
activated() {
this.getDataList() //
},
methods: {
//
getDataList() {
this.dataListLoading = true //
this.$http({ //
url: this.$http.adornUrl('/manage/wxQrCode/list'), //
method: 'get',
params: this.$http.adornParams({ //
'page': this.pageIndex,
'limit': this.pageSize,
'sceneStr': this.dataForm.sceneStr,
'sidx': 'id',
'order': 'desc'
})
}).then(({ data }) => { //
if (data && data.code === 200) {
this.dataList = data.page.list //
this.totalPage = data.page.totalCount //
} else {
this.dataList = [] //
this.totalPage = 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(() => { // DOM
this.$refs.addOrUpdate.init(id) // idnull
})
},
//
deleteHandle(id) {
var ids = id ? [id] : this.dataListSelections.map(item => item.id) // id
this.$confirm(`确定对[id=${ids.join(',')}]进行[${id ? '删除' : '批量删除'}]操作?(仅删存档)`, '提示', { //
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => { //
this.$http({ //
url: this.$http.adornUrl('/manage/wxQrCode/delete'),
method: 'post',
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) //
}
})
})
}
}
}
</script>

@ -0,0 +1,114 @@
<template>
<!-- 使用Element UI的Dialog组件用于显示模态框 -->
<el-dialog :title="modeDesc[mode] + '用户标签'" <!-- mode -->
:close-on-click-modal="false" <!-- 点击模态框外部不关闭模态框 -->
:visible.sync="dialogVisible"> <!-- 双向绑定控制模态框的显示隐藏 -->
<div>
<!-- 使用Element UI的Select组件用于选择标签 -->
<el-select v-model="selectedTagid" <!-- ID -->
filterable <!-- 可过滤选项 -->
placeholder="请选择标签"
style="width:100%">
<!-- 遍历tagidsInOption数组生成下拉选项 -->
<el-option v-for="tagid in tagidsInOption" :key="tagid" :label="getTagName(tagid)" <!--
使用getTagName方法获取标签名称 -->
: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> <!-- submitting -->
</span>
</el-dialog>
</template>
<script>
import { mapState } from 'vuex' // VuexmapState
export default {
name: 'wx-user-tagging', //
props: {
wxUsers: Array, // wxUsersopenidID
},
data() {
return {
mode: 'tagging', //
modeDesc: { //
'tagging': '绑定',
'untagging': '解绑'
},
selectedTagid: '', // ID
dialogVisible: false, //
submitting: false //
}
},
computed: mapState({
wxUserTags: state => state.wxUserTags.tags, // Vuex
//
tagidsInOption() {
let userTags = this.wxUsers.map(u => u.tagidList || []) // ID
if (this.mode == 'tagging') { // -
let all = this.wxUserTags.map(item => item.id) // ID
return all.filter(tagid => !userTags.every(tagsIdArray => tagsIdArray.indexOf(tagid) > -1)) //
} else if (this.mode == 'untagging') { //
let unionSet = new Set(); // 使Set
userTags.forEach(tagsIdArray => {
tagsIdArray.forEach(tagid => unionSet.add(tagid)) // Set
});
return Array.from(unionSet); // Set
}
return [] //
}
}),
methods: {
//
init(mode) {
if ('tagging' == mode || 'untagging' == mode) {
this.mode = mode;
this.dialogVisible = true
} else {
throw ('mode参数有误') // 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 // true
let openidList = this.wxUsers.map(u => u.openid) // openid
this.$http({ // HTTP
url: this.$http.adornUrl(`/manage/wxUserTags/${this.mode == 'tagging' ? 'batchTagging' : 'batchUnTagging'}`), // URL
method: 'post', // 使POST
data: this.$http.adornData({ //
tagid: this.selectedTagid, // ID
openidList: openidList // openid
})
}).then(({ data }) => { //
this.submitting = false // false
if (data && data.code === 200) { //
this.$message({
message: '操作成功,列表数据需稍后刷新查看',
type: 'success',
onClose: () => this.dialogVisible = false //
})
} else { //
this.$message.error(data.msg) //
}
})
}
}
}
</script>

@ -0,0 +1,239 @@
<template>
<div class="mod-config">
<!-- 使用Element UI的表单组件设置为行内表单绑定dataForm数据模型并监听回车事件触发getDataList方法 -->
<el-form :inline="true" :model="dataForm" @keyup.enter.native="getDataList()">
<!-- 用户标签选择器 -->
<el-form-item>
<el-select v-model="dataForm.tagid" filterable clearable placeholder="用户标签">
<!-- 循环渲染标签选项 -->
<el-option v-for="item in wxUserTags" :key="item.id" :label="item.name"
:value="item.id"></el-option>
</el-select>
</el-form-item>
<!-- 昵称输入框 -->
<el-form-item>
<el-input v-model="dataForm.nickname" placeholder="昵称" clearable></el-input>
</el-form-item>
<!-- 城市输入框 -->
<el-form-item>
<el-input v-model="dataForm.city" placeholder="城市" clearable></el-input>
</el-form-item>
<!-- 关注场景值输入框 -->
<el-form-item>
<el-input v-model="dataForm.qrSceneStr" placeholder="关注场景值" clearable></el-input>
</el-form-item>
<!-- 表单操作按钮 -->
<el-form-item>
<el-button @click="getDataList()"></el-button>
<!-- 根据权限显示绑定/解绑标签按钮并设置禁用状态 -->
<el-button v-if="isAuth('wx:wxuser:save')" type="primary" @click="$refs.wxUserTagging.init('tagging')"
:disabled="dataListSelections.length <= 0">绑定标签</el-button>
<el-button v-if="isAuth('wx:wxuser:save')" type="primary" @click="$refs.wxUserTagging.init('untagging')"
:disabled="dataListSelections.length <= 0">解绑标签</el-button>
<!-- 根据权限显示批量删除按钮并设置禁用状态 -->
<el-button v-if="isAuth('wx:wxuser:delete')" type="danger" @click="deleteHandle()"
:disabled="dataListSelections.length <= 0">批量删除</el-button>
</el-form-item>
<!-- 其他操作按钮 -->
<el-form-item class="fr">
<!-- 标签管理按钮 -->
<el-button icon="el-icon-price-tag" type="success"
@click="$refs.wxUserTagsEditor.show()">标签管理</el-button>
<!-- 同步粉丝按钮 -->
<el-button icon="el-icon-sort" type="success" @click="syncWxUsers()"></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>
<!-- openid列 -->
<el-table-column prop="openid" header-align="center" align="center" label="openid"></el-table-column>
<!-- 昵称列 -->
<el-table-column prop="nickname" header-align="center" align="center" label="昵称"></el-table-column>
<!-- 性别列使用formatter格式化显示 -->
<el-table-column prop="sex" header-align="center" align="center" label="性别"
:formatter="sexFormat"></el-table-column>
<!-- 城市列 -->
<el-table-column prop="city" header-align="center" align="center" label="城市"></el-table-column>
<!-- 头像列使用插槽显示图片 -->
<el-table-column prop="headimgurl" header-align="center" align="center" label="头像">
<img class="headimg" slot-scope="scope" v-if="scope.row.headimgurl" :src="scope.row.headimgurl" />
</el-table-column>
<!-- 标签列使用插槽显示多个标签 -->
<el-table-column prop="tagidList" header-align="center" align="center" label="标签" show-overflow-tooltip>
<template slot-scope="scope">
<span v-for="tagid in scope.row.tagidList" :key="tagid">{{ getTagName(tagid) }} </span>
</template>
</el-table-column>
<!-- 订阅时间列使用moment格式化显示 -->
<el-table-column prop="subscribeTime" header-align="center" align="center" label="订阅时间">
<template slot-scope="scope">{{ $moment(scope.row.subscribeTime).calendar() }}</template>
</el-table-column>
<!-- 场景值列 -->
<el-table-column prop="qrSceneStr" header-align="center" align="center" label="场景值"></el-table-column>
<!-- 是否关注列使用插槽显示"是""否" -->
<el-table-column prop="subscribe" header-align="center" align="center" label="是否关注">
<span slot-scope="scope">{{ scope.row.subscribe ? "是" : "否" }}</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="deleteHandle(scope.row.openid)"></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="totalPage"
layout="total, sizes, prev, pager, next, jumper"></el-pagination>
<!-- 标签管理器组件 -->
<wx-user-tags-manager ref="wxUserTagsEditor" :visible="showWxUserTagsEditor"
@close="showWxUserTagsEditor = false"></wx-user-tags-manager>
<!-- 用户标签绑定/解绑组件 -->
<wx-user-tagging ref="wxUserTagging" :wxUsers="dataListSelections"></wx-user-tagging>
</div>
</template>
<script>
import WxUserTagsManager from '@/components/wx-user-tags-manager'
import WxUserTagging from './wx-user-tagging'
import { mapState } from 'vuex'
export default {
data() {
return {
dataForm: {
tagid: '',
nickname: '',
city: '',
qrSceneStr: ''
},
dataList: [],
pageIndex: 1,
pageSize: 10,
totalPage: 0,
showWxUserTagsEditor: false,
dataListLoading: false,
dataListSelections: [],
}
},
components: {
WxUserTagsManager, WxUserTagging
},
activated() {
this.getDataList()
},
computed: mapState({
wxUserTags: state => state.wxUserTags.tags
}),
methods: {
//
getDataList() {
this.dataListLoading = true
this.$http({
url: this.$http.adornUrl('/manage/wxUser/list'),
method: 'get',
params: this.$http.adornParams({
'page': this.pageIndex,
'limit': this.pageSize,
'nickname': this.dataForm.nickname,
'tagid': this.dataForm.tagid,
'city': this.dataForm.city,
'qrSceneStr': this.dataForm.qrSceneStr,
'sidx': 'subscribe_time',
'order': 'desc'
})
}).then(({ data }) => {
if (data && data.code === 200) {
this.dataList = data.page.list
this.totalPage = data.page.totalCount
} else {
this.dataList = []
this.totalPage = 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.openid)
this.$confirm(`确定对[id=${ids.join(',')}]进行[${id ? '删除' : '批量删除'}]操作?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
this.$http({
url: this.$http.adornUrl('/manage/wxUser/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)
}
})
})
},
syncWxUsers() {
this.$http({
url: this.$http.adornUrl('/manage/wxUser/syncWxUsers'),
method: 'post',
}).then(({ data }) => {
if (data && data.code === 200) {
this.$message({
message: '同步任务已建立,请稍候刷新查看列表',
type: 'success',
duration: 1500
})
} else {
this.$message.error(data.msg)
}
})
},
sexFormat(row, column, cellValue) {
let sexType = {
0: '未知',
1: '男',
2: '女'
}
return sexType[cellValue];
},
getTagName(tagid) {
let tag = this.wxUserTags.find(item => item.id == tagid)
return tag ? tag.name : "?"
}
}
}
</script>
<style scoped>
.headimg {
width: 50px;
height: 50px;
border-radius: 8px;
}
</style>

@ -0,0 +1,29 @@
module.exports = {
publicPath: "./",
devServer: {
// 后端请求转发此配置仅开发环境有效生产环境请参考生产环境部署文档配置nginx转发
proxy: {
'/wx': {
target: 'http://localhost:8088/'
}
},
port:8001
},
configureWebpack:{
devServer: {
historyApiFallback: true,
allowedHosts:"all",
}
},
chainWebpack: config => {
// 移除 prefetch 插件
config.plugins.delete('prefetch')
},
outputDir: undefined,
assetsDir: undefined,
runtimeCompiler: undefined,
productionSourceMap: false,
parallel: undefined,
css: undefined
}
Loading…
Cancel
Save