Compare commits

...

7 Commits

Binary file not shown.

@ -1 +0,0 @@
2

@ -0,0 +1 @@
Subproject commit 78f3b98269fb3004a43417c3b26c6e48f013f349

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

@ -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.

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

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

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

@ -0,0 +1,847 @@
/* 常用辅助css */
/* ==================
==================== */
/* -- flex弹性布局 -- */
.flex {
display: flex;
}
.basis-xs {
flex-basis: 20%;
}
.basis-sm {
flex-basis: 40%;
}
.basis-df {
flex-basis: 50%;
}
.basis-lg {
flex-basis: 60%;
}
.basis-xl {
flex-basis: 80%;
}
.flex-sub {
flex: 1;
}
.flex-twice {
flex: 2;
}
.flex-treble {
flex: 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: flex-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;
}
.margin-xs {
margin: 5px;
}
.margin-sm {
margin: 10px;
}
.margin {
margin: 15px;
}
.margin-lg {
margin: 20px;
}
.margin-xl {
margin: 25px;
}
.margin-top-xs {
margin-top: 5px;
}
.margin-top-sm {
margin-top: 10px;
}
.margin-top {
margin-top: 15px;
}
.margin-top-lg {
margin-top: 20px;
}
.margin-top-xl {
margin-top: 25px;
}
.margin-right-xs {
margin-right: 5px;
}
.margin-right-sm {
margin-right: 10px;
}
.margin-right {
margin-right: 15px;
}
.margin-right-lg {
margin-right: 20px;
}
.margin-right-xl {
margin-right: 25px;
}
.margin-bottom-xs {
margin-bottom: 5px;
}
.margin-bottom-sm {
margin-bottom: 10px;
}
.margin-bottom {
margin-bottom: 15px;
}
.margin-bottom-lg {
margin-bottom: 20px;
}
.margin-bottom-xl {
margin-bottom: 25px;
}
.margin-left-xs {
margin-left: 5px;
}
.margin-left-sm {
margin-left: 10px;
}
.margin-left {
margin-left: 15px;
}
.margin-left-lg {
margin-left: 20px;
}
.margin-left-xl {
margin-left: 25px;
}
.margin-lr-xs {
margin-left: 5px;
margin-right: 5px;
}
.margin-lr-sm {
margin-left: 10px;
margin-right: 10px;
}
.margin-lr {
margin-left: 15px;
margin-right: 15px;
}
.margin-lr-lg {
margin-left: 20px;
margin-right: 20px;
}
.margin-lr-xl {
margin-left: 25px;
margin-right: 25px;
}
.margin-tb-xs {
margin-top: 5px;
margin-bottom: 5px;
}
.margin-tb-sm {
margin-top: 10px;
margin-bottom: 10px;
}
.margin-tb {
margin-top: 15px;
margin-bottom: 15px;
}
.margin-tb-lg {
margin-top: 20px;
margin-bottom: 20px;
}
.margin-tb-xl {
margin-top: 25px;
margin-bottom: 25px;
}
.padding-0 {
padding: 0;
}
.padding-xs {
padding: 5px;
}
.padding-sm {
padding: 10px;
}
.padding {
padding: 15px;
}
.padding-lg {
padding: 20px;
}
.padding-xl {
padding: 25px;
}
.padding-top-xs {
padding-top: 5px;
}
.padding-top-sm {
padding-top: 10px;
}
.padding-top {
padding-top: 15px;
}
.padding-top-lg {
padding-top: 20px;
}
.padding-top-xl {
padding-top: 25px;
}
.padding-right-xs {
padding-right: 5px;
}
.padding-right-sm {
padding-right: 10px;
}
.padding-right {
padding-right: 15px;
}
.padding-right-lg {
padding-right: 20px;
}
.padding-right-xl {
padding-right: 25px;
}
.padding-bottom-xs {
padding-bottom: 5px;
}
.padding-bottom-sm {
padding-bottom: 10px;
}
.padding-bottom {
padding-bottom: 15px;
}
.padding-bottom-lg {
padding-bottom: 20px;
}
.padding-bottom-xl {
padding-bottom: 25px;
}
.padding-left-xs {
padding-left: 5px;
}
.padding-left-sm {
padding-left: 10px;
}
.padding-left {
padding-left: 15px;
}
.padding-left-lg {
padding-left: 20px;
}
.padding-left-xl {
padding-left: 25px;
}
.padding-lr-xs {
padding-left: 5px;
padding-right: 5px;
}
.padding-lr-sm {
padding-left: 10px;
padding-right: 10px;
}
.padding-lr {
padding-left: 15px;
padding-right: 15px;
}
.padding-lr-lg {
padding-left: 20px;
padding-right: 20px;
}
.padding-lr-xl {
padding-left: 25px;
padding-right: 25px;
}
.padding-tb-xs {
padding-top: 5px;
padding-bottom: 5px;
}
.padding-tb-sm {
padding-top: 10px;
padding-bottom: 10px;
}
.padding-tb {
padding-top: 15px;
padding-bottom: 15px;
}
.padding-tb-lg {
padding-top: 20px;
padding-bottom: 20px;
}
.padding-tb-xl {
padding-top: 25px;
padding-bottom: 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, #ff9700, #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;
}
.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,365 @@
@charset "utf-8";
* {
box-sizing: border-box;
}
#app-menu ul {
padding: 0;
}
#app-menu li {
list-style: none;
}
#app-menu {
overflow: hidden;
width: 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;
}
.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;
background: #fff;
width: 300px;
padding: 5px 0px 5px 0px;
margin-left: 10px;
text-indent: 10px;
height: 35px;
}
.weixin-menu-detail .menu-tips {
color: #8d8d8d;
padding-top: 4px;
margin: 0;
}
.weixin-menu-detail .menu-tips.cursor {
color: #459ae9;
cursor: pointer;
}
.weixin-menu-detail .menu-input .menu-tips {
margin: 0 0 0 10px;
}
.weixin-menu-detail .menu-content {
padding: 16px 20px;
border: 1px solid #e7e7eb;
background-color: #fff;
}
.weixin-menu-detail .menu-content .menu-input-group {
margin: 0px 0 10px 0;
}
.weixin-menu-detail .menu-content .menu-label {
text-align: left;
width: 100px;
}
.weixin-menu-detail .menu-content .menu-input-text {
border: 1px solid #e7e7eb;
}
.weixin-menu-detail .menu-content .menu-tips {
padding-bottom: 10px;
}
.weixin-menu-detail .menu-msg-content {
padding: 0;
border: 1px solid #e7e7eb;
background-color: #fff;
}
.weixin-menu-detail .menu-msg-content .menu-msg-head {
overflow: hidden;
border-bottom: 1px solid #e7e7eb;
line-height: 38px;
height: 38px;
padding: 0 20px;
}
.weixin-menu-detail .menu-msg-content .menu-msg-panel {
padding: 30px 50px;
}
.weixin-menu-detail .menu-msg-content .menu-msg-select {
padding: 40px 20px;
border: 2px dotted #d9dadc;
text-align: center;
}
.weixin-menu-detail .menu-msg-content .menu-msg-select:hover {
border-color: #b3b3b3;
}
.weixin-menu-detail .menu-msg-content strong {
display: block;
padding-top: 3px;
font-weight: 400;
font-style: normal;
}
.weixin-menu-detail .menu-msg-content .menu-msg-title {
float: left;
width: 310px;
height: 30px;
line-height: 30px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.icon36_common {
width: 36px;
height: 36px;
vertical-align: middle;
display: inline-block;
}
.icon_msg_sender {
margin-right: 3px;
margin-top: -2px;
width: 20px;
height: 20px;
vertical-align: middle;
display: inline-block;
/* background: url(../images/msg_tab_z25df2d.png) 0 -270px no-repeat; */
}
.weixin-btn-group {
text-align: center;
width: 100%;
margin: 30px 0px;
overflow: hidden;
}
.weixin-btn-group .btn {
width: 100px;
border-radius: 0px;
}
#material-list {
padding: 20px;
overflow-y: scroll;
height: 558px;
}
#news-list {
padding: 20px;
overflow-y: scroll;
height: 558px;
}
#material-list table {
width: 100%;
}

@ -0,0 +1,412 @@
*,
*:before,
*:after {
box-sizing: border-box;
}
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: " ";
display: table;
}
.clearfix:after {
clear: both;
}
/* Animation
------------------------------ */
.fade-enter-active,
.fade-leave-active {
transition: opacity .5s;
}
.fade-enter,
.fade-leave-to {
opacity: 0;
}
/* Reset 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;
}
.site-navbar__body,
.site-content__wrapper {
margin-left: 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;
font-size: 20px;
}
.site-content--tabs > .el-tabs > .el-tabs__header {
left: 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;
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;
height: 50px;
overflow: hidden;
}
&__brand {
display: table-cell;
vertical-align: middle;
width: 230px;
height: 50px;
margin: 0;
line-height: 50px;
font-size: 20px;
text-align: center;
text-transform: uppercase;
white-space: nowrap;
color: #fff;
&-lg,
&-mini {
margin: 0 5px;
color: #fff;
&:focus,
&:hover {
color: #fff;
text-decoration: none;
}
}
&-mini {
display: none;
}
}
&__switch {
font-size: 18px;
border-bottom: none !important;
}
&__avatar {
border-bottom: none !important;
* {
vertical-align: inherit;
}
.el-dropdown-link {
> img {
width: 36px;
height: auto;
margin-right: 5px;
border-radius: 100%;
vertical-align: middle;
}
}
}
&__body {
position: relative;
margin-left: 230px;
padding-right: 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;
line-height: 50px;
}
.el-submenu > .el-menu {
top: 55px;
}
.el-badge {
display: inline;
z-index: 2;
&__content {
line-height: 16px;
}
}
}
}
/* Sidebar
------------------------------ */
.site-sidebar {
position: fixed;
top: 50px;
left: 0;
bottom: 0;
z-index: 1020;
width: 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;
width: 250px;
height: 100%;
padding-bottom: 15px;
overflow-y: scroll;
}
&__menu.el-menu {
width: 230px;
border-right: 0;
}
&__menu-icon {
width: 24px;
margin-right: 5px;
text-align: center;
font-size: 16px;
color: inherit !important;
}
}
/* Content
------------------------------ */
.site-content {
position: relative;
padding: 15px;
&__wrapper {
position: relative;
padding-top: 50px;
margin-left: 230px;
min-height: 100%;
background: $content--background-color;
}
&--tabs {
padding: 55px 0 0;
}
> .el-tabs {
> .el-tabs__header {
position: fixed;
top: 50px;
left: 230px;
right: 0;
z-index: 930;
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;
&:after {
display: none;
}
}
}
> .el-tabs__content {
padding: 0 15px 15px;
> .site-tabs__tools {
position: fixed;
top: 50px;
right: 0;
z-index: 931;
height: 40px;
padding: 0 12px;
font-size: 16px;
line-height: 40px;
background-color: $content--background-color;
cursor: pointer;
.el-icon--right {
margin-left: 0;
}
}
}
}
}
.el-table__expand-icon {
display: inline-block;
width: 14px;
vertical-align: middle;
margin-right: 5px;
}

@ -0,0 +1,447 @@
/*! normalize.css v7.0.0 | MIT License | github.com/necolas/normalize.css */
/* Document
========================================================================== */
/**
* 1. Correct the line height in all browsers.
* 2. Prevent adjustments of font size after orientation changes in
* IE on Windows Phone and in iOS.
*/
html {
line-height: 1.15; /* 1 */
-ms-text-size-adjust: 100%; /* 2 */
-webkit-text-size-adjust: 100%; /* 2 */
}
/* Sections
========================================================================== */
/**
* Remove the margin in all browsers (opinionated).
*/
body {
margin: 0;
}
/**
* Add the correct display in IE 9-.
*/
article,
aside,
footer,
header,
nav,
section {
display: block;
}
/**
* Correct the font size and margin on `h1` elements within `section` and
* `article` contexts in Chrome, Firefox, and Safari.
*/
h1 {
font-size: 2em;
margin: 0.67em 0;
}
/* Grouping content
========================================================================== */
/**
* Add the correct display in IE 9-.
* 1. Add the correct display in IE.
*/
figcaption,
figure,
main { /* 1 */
display: block;
}
/**
* Add the correct margin in IE 8.
*/
figure {
margin: 1em 40px;
}
/**
* 1. Add the correct box sizing in Firefox.
* 2. Show the overflow in Edge and IE.
*/
hr {
box-sizing: content-box; /* 1 */
height: 0; /* 1 */
overflow: visible; /* 2 */
}
/**
* 1. Correct the inheritance and scaling of font size in all browsers.
* 2. Correct the odd `em` font sizing in all browsers.
*/
pre {
font-family: monospace, monospace; /* 1 */
font-size: 1em; /* 2 */
}
/* Text-level semantics
========================================================================== */
/**
* 1. Remove the gray background on active links in IE 10.
* 2. Remove gaps in links underline in iOS 8+ and Safari 8+.
*/
a {
background-color: transparent; /* 1 */
-webkit-text-decoration-skip: objects; /* 2 */
}
/**
* 1. Remove the bottom border in Chrome 57- and Firefox 39-.
* 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari.
*/
abbr[title] {
border-bottom: none; /* 1 */
text-decoration: underline; /* 2 */
text-decoration: underline dotted; /* 2 */
}
/**
* Prevent the duplicate application of `bolder` by the next rule in Safari 6.
*/
b,
strong {
font-weight: inherit;
}
/**
* Add the correct font weight in Chrome, Edge, and Safari.
*/
b,
strong {
font-weight: bolder;
}
/**
* 1. Correct the inheritance and scaling of font size in all browsers.
* 2. Correct the odd `em` font sizing in all browsers.
*/
code,
kbd,
samp {
font-family: monospace, monospace; /* 1 */
font-size: 1em; /* 2 */
}
/**
* Add the correct font style in Android 4.3-.
*/
dfn {
font-style: italic;
}
/**
* Add the correct background and color in IE 9-.
*/
mark {
background-color: #ff0;
color: #000;
}
/**
* Add the correct font size in all browsers.
*/
small {
font-size: 80%;
}
/**
* Prevent `sub` and `sup` elements from affecting the line height in
* all browsers.
*/
sub,
sup {
font-size: 75%;
line-height: 0;
position: relative;
vertical-align: baseline;
}
sub {
bottom: -0.25em;
}
sup {
top: -0.5em;
}
/* Embedded content
========================================================================== */
/**
* Add the correct display in IE 9-.
*/
audio,
video {
display: inline-block;
}
/**
* Add the correct display in iOS 4-7.
*/
audio:not([controls]) {
display: none;
height: 0;
}
/**
* Remove the border on images inside links in IE 10-.
*/
img {
border-style: none;
}
/**
* Hide the overflow in IE.
*/
svg:not(:root) {
overflow: hidden;
}
/* Forms
========================================================================== */
/**
* 1. Change the font styles in all browsers (opinionated).
* 2. Remove the margin in Firefox and Safari.
*/
button,
input,
optgroup,
select,
textarea {
font-family: sans-serif; /* 1 */
font-size: 100%; /* 1 */
line-height: 1.15; /* 1 */
margin: 0; /* 2 */
}
/**
* Show the overflow in IE.
* 1. Show the overflow in Edge.
*/
button,
input { /* 1 */
overflow: visible;
}
/**
* Remove the inheritance of text transform in Edge, Firefox, and IE.
* 1. Remove the inheritance of text transform in Firefox.
*/
button,
select { /* 1 */
text-transform: none;
}
/**
* 1. Prevent a WebKit bug where (2) destroys native `audio` and `video`
* controls in Android 4.
* 2. Correct the inability to style clickable types in iOS and Safari.
*/
button,
html [type="button"], /* 1 */
[type="reset"],
[type="submit"] {
-webkit-appearance: button; /* 2 */
}
/**
* Remove the inner border and padding in 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;
}
/**
* Restore the focus styles unset by the previous rule.
*/
button:-moz-focusring,
[type="button"]:-moz-focusring,
[type="reset"]:-moz-focusring,
[type="submit"]:-moz-focusring {
outline: 1px dotted ButtonText;
}
/**
* Correct the padding in Firefox.
*/
fieldset {
padding: 0.35em 0.75em 0.625em;
}
/**
* 1. Correct the text wrapping in Edge and IE.
* 2. Correct the color inheritance from `fieldset` elements in IE.
* 3. Remove the padding so developers are not caught out when they zero out
* `fieldset` elements in all browsers.
*/
legend {
box-sizing: border-box; /* 1 */
color: inherit; /* 2 */
display: table; /* 1 */
max-width: 100%; /* 1 */
padding: 0; /* 3 */
white-space: normal; /* 1 */
}
/**
* 1. Add the correct display in IE 9-.
* 2. Add the correct vertical alignment in Chrome, Firefox, and Opera.
*/
progress {
display: inline-block; /* 1 */
vertical-align: baseline; /* 2 */
}
/**
* Remove the default vertical scrollbar in IE.
*/
textarea {
overflow: auto;
}
/**
* 1. Add the correct box sizing in IE 10-.
* 2. Remove the padding in IE 10-.
*/
[type="checkbox"],
[type="radio"] {
box-sizing: border-box; /* 1 */
padding: 0; /* 2 */
}
/**
* Correct the cursor style of increment and decrement buttons in Chrome.
*/
[type="number"]::-webkit-inner-spin-button,
[type="number"]::-webkit-outer-spin-button {
height: auto;
}
/**
* 1. Correct the odd appearance in Chrome and Safari.
* 2. Correct the outline style in Safari.
*/
[type="search"] {
-webkit-appearance: textfield; /* 1 */
outline-offset: -2px; /* 2 */
}
/**
* Remove the inner padding and cancel buttons in Chrome and Safari on macOS.
*/
[type="search"]::-webkit-search-cancel-button,
[type="search"]::-webkit-search-decoration {
-webkit-appearance: none;
}
/**
* 1. Correct the inability to style clickable types in iOS and Safari.
* 2. Change font properties to `inherit` in Safari.
*/
::-webkit-file-upload-button {
-webkit-appearance: button; /* 1 */
font: inherit; /* 2 */
}
/* Interactive
========================================================================== */
/*
* Add the correct display in IE 9-.
* 1. Add the correct display in Edge, IE, and Firefox.
*/
details, /* 1 */
menu {
display: block;
}
/*
* Add the correct display in all browsers.
*/
summary {
display: list-item;
}
/* Scripting
========================================================================== */
/**
* Add the correct display in IE 9-.
*/
canvas {
display: inline-block;
}
/**
* Add the correct display in IE.
*/
template {
display: none;
}
/* Hidden
========================================================================== */
/**
* Add the correct display in IE 10-.
*/
[hidden] {
display: none;
}

@ -0,0 +1,13 @@
//
// 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,5 @@
@import "normalize";
// api: https://github.com/necolas/normalize.css/
@import "variables";
//
@import "base";

@ -0,0 +1,47 @@
<template>
<svg :class="getClassName" :width="width" :height="height" aria-hidden="true">
<use :xlink:href="getName"></use>
</svg>
</template>
<script>
export default {
name: 'icon-svg',
props: {
name: {
type: String,
required: true
},
className: {
type: String
},
width: {
type: String
},
height: {
type: String
}
},
computed: {
getName() {
return `#icon-${this.name}`
},
getClassName() {
return [
'icon-svg',
`icon-svg__${this.name}`,
this.className && /\S/.test(this.className) ? `${this.className}` : ''
]
}
}
}
</script>
<style>
.icon-svg {
width: 1em;
height: 1em;
fill: currentColor;
overflow: hidden;
}
</style>

@ -0,0 +1,84 @@
<template>
<el-table-column :prop="prop" v-bind="$attrs">
<template slot-scope="scope">
<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>
import isArray from 'lodash/isArray'
export default {
name: 'table-tree-column',
props: {
prop: {
type: String
},
treeKey: {
type: String,
default: 'id'
},
parentKey: {
type: String,
default: 'parentId'
},
levelKey: {
type: String,
default: '_level'
},
childKey: {
type: String,
default: '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]
if (parentId.length <= 0) {
return data
}
var ids = []
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])
i--
}
}
return this.removeChildNode(data, ids)
}
}
}
</script>

@ -0,0 +1,77 @@
<template>
<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>
<script>
/**
* 标签编辑器
*/
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() {
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(_ => {
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(","));
}
}
}
</script>
<style scoped>
.panel {
flex: 1;
}
.el-tag,.button-new-tag{
margin: 5px;
}
.input-new-tag {
width: inherit;
}
</style>

@ -0,0 +1,189 @@
<template>
<el-dialog title="筛选模板消息目标用户" :close-on-click-modal="false" :visible.sync="visible">
<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>
<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>
</el-dialog>
</template>
<script>
import { mapState } from 'vuex'
export default {
name:'template-msg-task',
props:{
wxUserTagName:{
type:String,
required:false
}
},
data(){
return{
visible:false,
wxUsersLoading:false,
sending:false,
msgTemplate:{},
dataForm: {
page:1,
sidx: 'subscribe_time',
order: 'desc',
tagid:'',
nickname: '',
city:'',
province:'',
remark:'',
qrScene:''
},
wxUserList:[],
totalCount:0
}
},
computed: mapState({
wxUserTags:state=>state.wxUserTags.tags,
msgReview(){
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)
console.log(tagItem)
if(tagItem) {
this.dataForm.tagid=tagItem.id+''
}
}
this.getWxUsers()
});
},
methods:{
init(msgTemplate){
if(!msgTemplate || !msgTemplate.templateId){
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)=>{
this.$http({
url: this.$http.adornUrl('/manage/wxUserTags/list'),
method: 'get',
}).then(({ data }) => {
if (data && data.code === 200) {
this.$store.commit('wxUserTags/updateTags', data.list)
resolve(data.list)
} else {
this.$message.error(data.msg)
reject(data.msg)
}
}).catch(err=>reject(err))
})
},
getWxUsers() {
this.wxUsersLoading = true
this.$http({
url: this.$http.adornUrl('/manage/wxUser/list'),
method: 'get',
params: this.$http.adornParams(this.dataForm)
}).then(({ data }) => {
if (data && data.code === 200) {
this.wxUserList = data.page.list
this.totalCount = data.page.totalCount
} else {
this.$message.error(data.msg)
}
this.wxUsersLoading = false
})
},
send(){
if(this.sending)return
this.sending=true
this.$http({
url: this.$http.adornUrl('/manage/msgTemplate/sendMsgBatch'),
method: 'post',
data:this.$http.adornData({
wxUserFilterParams : this.dataForm,
templateId : this.msgTemplate.templateId,
url : this.msgTemplate.url,
miniprogram : this.msgTemplate.miniprogram,
data : this.msgTemplate.data,
})
}).then(({ data }) => {
this.sending = false
if (data && data.code === 200) {
this.$message.success("消息将在后台发送")
this.visible=false
} else {
this.$message.error(data.msg)
}
})
}
}
}
</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,99 @@
<template>
<div class="tinymce-editor">
<editor v-model="myValue" :init="init" @onExecCommand="onExecCommand"></editor>
</div>
</template>
<script>
import Editor from "@tinymce/tinymce-vue";
var cos;
export default {
name: "tinymce-editor",
components: {
Editor
},
props: {
value: {
type: String,
default: ""
}
},
data() {
return {
init: {
language_url: "./tinymce/zh_CN.js", //public
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,
uploading: false,
cosConfig: []
};
},
mounted() {
// console.log('tinymce-editor mounted:',this.value)
tinymce.init({});
this.cosInit();
},
methods: {
cosInit() {
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;
} else {
this.$message.error("请先配置云存储相关信息!");
}
});
},
onExecCommand(e) {
//console.log(e)
},
uploadFile(file) {
this.uploading = true;
return new Promise((resolve, reject) => {
let formData = new FormData();
formData.append("file", file);
this.$http({
url: this.$http.adornUrl('/sys/oss/upload'),
method: 'post',
data: formData
}).then(({ data }) => {
console.log(data)
if (data && data.code === 200) {
this.$emit('uploaded', data.url)
resolve(data.url)
} else {
this.$message.error("文件上传失败:" + data.msg)
reject(data.msg)
}
this.uploading = false;
}).catch(err=>reject(err))
});
}
},
watch: {
value(newValue) {
this.myValue = newValue;
},
myValue(newValue) {
this.$emit("input", newValue);
}
}
};
</script>

@ -0,0 +1,45 @@
<template>
<el-select v-model="selectedAppid" size="small" v-loading="dataListLoading" @change="selectAccount" filterable>
<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>
<script>
import { mapState } from 'vuex'
export default {
data() {
return {
dataListLoading: false
}
},
computed: mapState({
accountList: state=>state.wxAccount.accountList,
ACCOUNT_TYPES: state=>state.wxAccount.ACCOUNT_TYPES,
selectedAppid:state=>state.wxAccount.selectedAppid
}),
mounted(){
this.getDataList()
},
methods:{
getDataList() {
this.dataListLoading = true
this.$http({
url: this.$http.adornUrl('/manage/wxAccount/list'),
method: 'get'
}).then(({ data }) => {
if (data && data.code === 200) {
this.$store.commit('wxAccount/updateAccountList', data.list)
if(!data.list.length){
this.$message.info("公众号列表为空,请先添加")
}
}
this.dataListLoading = false
})
},
selectAccount(appid){
if(this.selectedAppid!=appid){
this.$store.commit('wxAccount/selectAccount', appid)
}
}
}
}
</script>

@ -0,0 +1,42 @@
<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({
XmlMsgType:state=>state.message.XmlMsgType,
})
}
</script>
<style scoped>
.panel,.panel a{
color: #999;
word-break: break-all;
}
</style>

@ -0,0 +1,139 @@
<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>
<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>
<script>
import { mapState } from 'vuex'
export default {
name: 'wx-user-tags-manager',
props: {
visible: {
type: Boolean,
default: true
}
},
data() {
return {
dialogVisible:false,
inputVisible: false,
inputValue: '',
submitting:false,
}
},
computed: mapState({
wxUserTags:state=>state.wxUserTags.tags
}),
mounted() {
this.getWxUserTags();
},
methods: {
show(){
this.dialogVisible=true;
},
getWxUserTags() {
this.$http({
url: this.$http.adornUrl('/manage/wxUserTags/list'),
method: 'get',
}).then(({ data }) => {
if (data && data.code === 200) {
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
this.$http({
url: this.$http.adornUrl('/manage/wxUserTags/delete/'+tagid),
method: 'post',
}).then(({ data }) => {
if (data && data.code === 200) {
this.getWxUserTags();
this.$emit('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}$/,
inputErrorMessage: '名称1-30字符'
}).then(({ value }) => {
console.log(value)
this.saveTag(value,tagid)
})
},
saveTag(name,tagid){
if(this.submitting){
return
}
this.submitting=true
this.$http({
url: this.$http.adornUrl('/manage/wxUserTags/save'),
method: 'post',
data:this.$http.adornData({
id : tagid?tagid:undefined,
name : name
})
}).then(({ data }) => {
if (data && data.code === 200) {
this.getWxUserTags();
this.$emit('change');
} else {
this.$message.error(data.msg)
}
this.submitting=false;
})
}
}
}
</script>
<style scoped>
.panel {
flex: 1;
}
.el-tag,.button-new-tag {
margin: 5px;
}
.input-new-tag {
width: inherit;
}
</style>

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

@ -0,0 +1 @@
module.exports = file => () => import('@/views/' + file + '.vue')

@ -0,0 +1,154 @@
/**
* 全站路由配置
*
* 建议:
* 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'
Vue.use(VueRouter)
const _import = require('./import-views')
// 全局路由(无需嵌套上左右整体布局)
const globalRoutes = [
{ path: '/404', component: () => import('@/views/common/404'), name: '404', meta: { title: '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]://开头': 是, '': 否
// 提示: 如需要通过iframe嵌套展示内容, 但不通过tab打开, 请自行创建组件使用iframe处理!
{ 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()
}
}
const router = new VueRouter({
mode: 'hash',
scrollBehavior: () => ({ y: 0 }),
isAddDynamicMenuRoutes: false, // 是否已经添加动态(菜单)路由
routes: globalRoutes.concat(mainRoutes)
})
router.beforeEach((to, from, next) => {
// 添加动态(菜单)路由
// 1. 已经添加 or 全局路由, 直接访问
// 2. 获取菜单列表, 添加并保存本地存储
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
sessionStorage.setItem('menuList', JSON.stringify(data.menuList || '[]'))
sessionStorage.setItem('permissions', JSON.stringify(data.permissions || '[]'))
next({ ...to, replace: true })
} else {
sessionStorage.setItem('menuList', '[]')
sessionStorage.setItem('permissions', '[]')
next()
}
}).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'
} 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(/^\//, '')
var route = {
path: menuList[i].url.replace('/', '-'),
component: null,
name: menuList[i].url.replace('/', '-'),
meta: {
menuId: menuList[i].menuId,
title: menuList[i].name,
isDynamic: true,
isTab: true,
iframeUrl: ''
}
}
// url以http[s]://开头, 通过iframe展示
if (isURL(menuList[i].url)) {
route['path'] = `i-${menuList[i].menuId}`
route['name'] = `i-${menuList[i].menuId}`
route['meta']['iframeUrl'] = menuList[i].url
} else {
try {
route['component'] = _import(`modules/${menuList[i].url}`) || null
// route['component'] = ()=>import(`@/views/modules/${menuList[i].url}.vue`) || null
} catch (e) { }
}
routes.push(route)
}
}
if (temp.length >= 1) {
fnAddDynamicMenuRoutes(temp, routes)
} else {
mainRoutes.name = 'main-dynamic'
mainRoutes.children = routes
router.addRoutes([
mainRoutes,
{ path: '*', redirect: { name: '404' } }
])
sessionStorage.setItem('dynamicMenuRoutes', JSON.stringify(mainRoutes.children || '[]'))
console.log('\n')
console.log('%c!<-------------------- 动态(菜单)路由 s -------------------->', 'color:blue')
console.log(mainRoutes.children)
console.log('%c!<-------------------- 动态(菜单)路由 e -------------------->', 'color:blue')
}
}
export default router

@ -0,0 +1,24 @@
import Vue from 'vue'
import Vuex from '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'
Vue.use(Vuex)
export default new Vuex.Store({
modules: {
common,
user,
article,
message,
wxUserTags,
wxAccount
},
mutations: {
},
strict: true
})

@ -0,0 +1,12 @@
export default {
namespaced: true,
state: {
ARTICLE_TYPES: {
1: '普通文章',
5: '帮助中心',
}
},
mutations: {
}
}

@ -0,0 +1,70 @@
import router from '@/router'
export default {
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) {
// 当前选中tab被删除
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,33 @@
export default {
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: {
}
}

@ -0,0 +1,15 @@
export default {
namespaced: true,
state: {
id: 0,
name: ''
},
mutations: {
updateId(state, id) {
state.id = id
},
updateName(state, name) {
state.name = name
}
}
}

@ -0,0 +1,32 @@
import Vue from 'vue'
export default {
namespaced: true,
state: {
ACCOUNT_TYPES:{
1:'订阅号',
2:'服务号'
},
accountList:[],
selectedAppid:''
},
mutations: {
updateAccountList (state, list) {
state.accountList = list
if(!list.length)return
if(!state.selectedAppid){
let appidCookie = Vue.cookie.get('appid')
let selectedAppid = appidCookie?appidCookie:list[0].appid
this.commit('wxAccount/selectAccount',selectedAppid)
}
},
selectAccount (state, appid) {
Vue.cookie.set('appid',appid)
let oldAppid = state.selectedAppid
state.selectedAppid = appid
if(oldAppid){//切换账号时刷新网页
location.reload();
}
},
}
}

@ -0,0 +1,12 @@
export default {
namespaced: true,
state: {
tags:[]
},
mutations: {
updateTags (state, tags) {
state.tags = tags
}
}
}

@ -0,0 +1,77 @@
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'
const http = axios.create({
timeout: 1000 * 30,
withCredentials: true,
headers: {
'Content-Type': 'application/json; charset=utf-8'
}
})
/**
* 请求拦截
*/
http.interceptors.request.use(config => {
config.headers['token'] = Vue.cookie.get('token') // 请求头带上token
return config
}, error => {
return Promise.reject(error)
})
/**
* 响应拦截
*/
http.interceptors.response.use(response => {
if (response.data && response.data.code === 401) { // 401, token失效
clearLoginInfo()
router.push({ name: 'login' })
}
return response
}, error => {
return Promise.reject(error)
})
/**
* 请求地址处理
* @param {*} actionName action方法名称
*/
http.adornUrl = (actionName) => {
// 非生产环境 && 开启代理, 接口前缀统一使用[/proxyApi/]前缀做代理拦截!
return baseUrl + actionName
}
/**
* get请求参数处理
* @param {*} params 参数对象
* @param {*} openDefultParams 是否开启默认参数?
*/
http.adornParams = (params = {}, openDefultParams = true) => {
var defaults = {
't': new Date().getTime()
}
return openDefultParams ? merge(defaults, params) : params
}
/**
* post请求数据处理
* @param {*} data 数据对象
* @param {*} openDefultdata 是否开启默认数据?
* @param {*} contentType 数据格式
* json: 'application/json; charset=utf-8'
* form: 'application/x-www-form-urlencoded; charset=utf-8'
*/
http.adornData = (data = {}, openDefultdata = true, contentType = 'json') => {
var defaults = {
't': new Date().getTime()
}
data = openDefultdata ? merge(defaults, data) : data
return contentType === 'json' ? JSON.stringify(data) : qs.stringify(data)
}
export default http

@ -0,0 +1,58 @@
import Vue from 'vue'
import router from '@/router'
import store from '@/store'
/**
* 获取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
*/
export function isAuth(key) {
return JSON.parse(sessionStorage.getItem('permissions') || '[]').indexOf(key) !== -1 || false
}
/**
* 树形数据转换
* @param {*} data
* @param {*} id
* @param {*} pid
*/
export function treeDataTranslate(data, id = 'id', pid = 'parentId') {
var res = []
var temp = {}
for (var i = 0; i < data.length; i++) {
temp[data[i][id]] = data[i]
}
for (var k = 0; k < data.length; k++) {
if (temp[data[k][pid]] && data[k][id] !== data[k][pid]) {
if (!temp[data[k][pid]]['children']) {
temp[data[k][pid]]['children'] = []
}
if (!temp[data[k][pid]]['_level']) {
temp[data[k][pid]]['_level'] = 1
}
data[k]['_level'] = temp[data[k][pid]]._level + 1
temp[data[k][pid]]['children'].push(data[k])
} else {
res.push(data[k])
}
}
return res
}
/**
* 清除登录信息
*/
export function clearLoginInfo() {
Vue.cookie.delete('token')
//store.commit('resetStore')
router.options.isAddDynamicMenuRoutes = false
}

@ -0,0 +1,31 @@
/**
* 邮箱
* @param {*} s
*/
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
*/
export function isMobile(s) {
return /^1[0-9]{10}$/.test(s)
}
/**
* 电话号码
* @param {*} s
*/
export function isPhone(s) {
return /^([0-9]{3,4}-)?[0-9]{7,8}$/.test(s)
}
/**
* URL地址
* @param {*} s
*/
export function isURL(s) {
return /^http[s]?:\/\/.*/.test(s)
}

@ -0,0 +1,61 @@
<template>
<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 {
}
</script>
<style lang="scss">
.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%;
left: 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 {
font-style: normal;
color: #ee8145;
}
}
.not-found-btn-gohome {
margin-left: 30px;
}
}
</style>

@ -0,0 +1,12 @@
<template>
<div class="mod-home">
<h3>欢迎使用微信管理系统</h3>
</div>
</template>
<style>
.mod-home {
line-height: 2.5;
text-align: center;
}
</style>

@ -0,0 +1,184 @@
<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>
<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'
export default {
data() {
return {
dataForm: {
userName: '',
password: '',
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) {
this.$cookie.set('token', data.token)
this.$router.replace({ name: 'home' })
} else {
this.getCaptcha()
this.$message.error(data.msg)
}
})
}
})
},
//
getCaptcha() {
this.dataForm.uuid = getUUID()
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,33 @@
<template>
<el-form>
<h2>布局设置</h2>
<el-form-item label="导航条类型">
<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="侧边栏皮肤">
<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: {
get() { return this.$store.state.common.navbarLayoutType },
set(val) { this.$store.commit('common/updateNavbarLayoutType', val) }
},
sidebarLayoutSkin: {
get() { return this.$store.state.common.sidebarLayoutSkin },
set(val) { this.$store.commit('common/updateSidebarLayoutSkin', val) }
}
}
}
</script>

@ -0,0 +1,103 @@
<template>
<main class="site-content" :class="{ 'site-content--tabs': $route.meta.isTab }">
<!-- 主入口标签页 s -->
<el-tabs v-if="$route.meta.isTab" v-model="mainTabsActiveName" :closable="true" @tab-click="selectedTabHandle" @tab-remove="removeTabHandle">
<el-dropdown class="site-tabs__tools" :show-timeout="0">
<i class="el-icon-arrow-down el-icon--right"></i>
<el-dropdown-menu slot="dropdown">
<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 v-if="item.type === 'iframe'" :src="item.iframeUrl" width="100%" height="100%" frameborder="0" scrolling="yes">
</iframe>
<keep-alive v-else>
<router-view v-if="item.name === mainTabsActiveName" />
</keep-alive>
</el-card>
</el-tab-pane>
</el-tabs>
<!-- 主入口标签页 e -->
<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 {
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
return isURL(this.$route.meta.iframeUrl) ? { height: height + 'px' } : { minHeight: height + 'px' }
}
return { minHeight: height + 'px' }
}
},
methods: {
// tabs, tab
selectedTabHandle(tab) {
tab = this.mainTabs.filter(item => item.name === tab.name)
if (tab.length >= 1) {
this.$router.push({ name: tab[0].name, query: tab[0].query, params: tab[0].params })
}
},
// tabs, tab
removeTabHandle(tabName) {
this.$store.commit('common/removeTab', tabName)
},
// tabs,
tabsCloseCurrentHandle() {
this.removeTabHandle(this.mainTabsActiveName)
},
// tabs,
tabsCloseOtherHandle() {
this.mainTabs = this.mainTabs.filter(item => item.name === this.mainTabsActiveName)
},
// tabs,
tabsCloseAllHandle() {
this.mainTabs = []
this.menuActiveName = ''
this.$router.push({ name: 'home' })
},
// tabs,
tabsRefreshCurrentHandle() {
var tab = this.$route
this.removeTabHandle(tab.name)
this.$nextTick(() => {
this.$router.push({ name: tab.name, query: tab.query, params: tab.params })
})
}
}
}
</script>

@ -0,0 +1,109 @@
<template>
<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'),
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,102 @@
<template>
<nav class="site-navbar" :class="'site-navbar--' + navbarLayoutType">
<div class="site-navbar__header">
<h1 class="site-navbar__brand" @click="$router.push({ name: 'home' })">
<a class="site-navbar__brand-lg" href="javascript:;">微信管理系统</a>
<a class="site-navbar__brand-mini" href="javascript:;">W</a>
</h1>
</div>
<div class="site-navbar__body clearfix">
<el-menu class="site-navbar__menu" mode="horizontal">
<el-menu-item class="site-navbar__switch" index="0" @click="sidebarFold = !sidebarFold">
<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>
<!-- 弹窗, 修改密码 -->
<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
this.$nextTick(() => {
this.$refs.updatePassowrd.init()
})
},
// 退
logoutHandle() {
this.$confirm(`确定进行[退出]操作?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
this.$http({
url: this.$http.adornUrl('/sys/logout'),
method: 'post',
data: this.$http.adornData()
}).then(({ data }) => {
if (data && data.code === 200) {
clearLoginInfo()
this.$router.push({ name: 'login' })
}
})
}).catch(() => { })
}
}
}
</script>

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

@ -0,0 +1,90 @@
<template>
<aside class="site-sidebar" :class="'site-sidebar--' + sidebarLayoutSkin">
<div class="site-sidebar__inner">
<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>
<sub-menu v-for="menu in menuList" :key="menu.menuId" :menu="menu" :dynamicMenuRoutes="dynamicMenuRoutes">
</sub-menu>
</el-menu>
</div>
</aside>
</template>
<script>
import SubMenu from './main-sidebar-sub-menu'
import { isURL } from '@/utils/validate'
export default {
data() {
return {
dynamicMenuRoutes: []
}
},
components: {
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: {
$route: 'routeHandle'
},
created() {
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) {
// tab,
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,
name: route.name,
title: route.meta.title,
type: isURL(route.meta.iframeUrl) ? 'iframe' : 'module',
iframeUrl: route.meta.iframeUrl || '',
params: route.params,
query: route.query
}
this.mainTabs = this.mainTabs.concat(tab)
}
this.menuActiveName = tab.menuId + ''
this.mainTabsActiveName = tab.name
}
}
}
}
</script>

@ -0,0 +1,86 @@
<template>
<div class="site-wrapper" :class="{ 'site-sidebar--fold': sidebarFold }" v-loading.fullscreen.lock="loading" element-loading-text="">
<template v-if="!loading">
<main-navbar />
<main-sidebar />
<div class="site-content__wrapper" :style="{ 'min-height': documentClientHeight + 'px' }">
<main-content v-if="!$store.state.common.contentIsNeedRefresh" />
</div>
</template>
</div>
</template>
<script>
import MainNavbar from './main-navbar'
import MainSidebar from './main-sidebar'
import MainContent from './main-content'
export default {
provide() {
return {
//
refresh() {
this.$store.commit('common/updateContentIsNeedRefresh', true)
this.$nextTick(() => {
this.$store.commit('common/updateContentIsNeedRefresh', false)
})
}
}
},
data() {
return {
loading: true
}
},
components: {
MainNavbar,
MainSidebar,
MainContent
},
computed: {
documentClientHeight: {
get() { return this.$store.state.common.documentClientHeight },
set(val) { this.$store.commit('common/updateDocumentClientHeight', val) }
},
sidebarFold: {
get() { return this.$store.state.common.sidebarFold }
},
userId: {
get() { return this.$store.state.user.id },
set(val) { this.$store.commit('user/updateId', val) }
},
userName: {
get() { return this.$store.state.user.name },
set(val) { this.$store.commit('user/updateName', val) }
}
},
created() {
this.getUserInfo()
},
mounted() {
this.resetDocumentClientHeight()
},
methods: {
//
resetDocumentClientHeight() {
this.documentClientHeight = document.documentElement['clientHeight']
window.onresize = () => {
this.documentClientHeight = document.documentElement['clientHeight']
}
},
//
getUserInfo() {
this.$http({
url: this.$http.adornUrl('/sys/user/info'),
method: 'get',
params: this.$http.adornParams()
}).then(({ data }) => {
if (data && data.code === 200) {
this.loading = false
this.userId = data.user.userId
this.userName = data.user.username
}
})
}
}
}
</script>

@ -0,0 +1,127 @@
<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="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: {
init(id) {
this.visible = true
this.$http({
url: this.$http.adornUrl('/sys/oss/config'),
method: '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',
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,87 @@
<template>
<div @click="selectFile">
<input type="file" ref="fileInput" v-show="false" @change="onFileChange" />
<div>{{uploading?infoText:'上传文件'}}</div>
</div>
</template>
<script>
// 使
// 使 <script src="https://unpkg.com/cos-js-sdk-v5@0.5.23/dist/cos-js-sdk-v5.min.js" async></script>
var cos;
export default {
name: "oss-uploader",
data() {
return {
uploading: false,
infoText:"上传中...",
cosConfig:[]
}
},
mounted(){
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=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;
let now = new Date();
let path=now.toISOString().slice(0,10)+'/'+now.getTime()+file.name.substr(file.name.lastIndexOf('.'))
cos.putObject({
Bucket: this.cosConfig.qcloudBucketName, /* 必须 */
Region: this.cosConfig.qcloudRegion, /* 必须 */
Key: path, /* 必须 */
Body: file, //
onProgress: (progressData)=> {
this.infoText='上传中:'+progressData.percent*100+'%'
}
}, (err, data)=> {
console.log(err || data);
this.uploading = false;
if(data){
this.infoText='上传文件'
let fileUrl='https://'+this.cosConfig.qcloudBucketName+'.cos.'+this.cosConfig.qcloudRegion+'.myqcloud.com/'+path;
this.saveUploadResult(fileUrl)
}else {
this.$message.error('文件上传失败',err)
}
});
},
saveUploadResult(url){
this.$http({
url: this.$http.adornUrl('/sys/oss/upload'),
method: 'post',
data:{
url:url
}
}).then(({data})=>{
this.$emit('uploaded', url)
})
}
}
}
</script>
<style scoped>
</style>

@ -0,0 +1,59 @@
<template>
<div @click="selectFile">
<input type="file" ref="fileInput" v-show="false" @change="onFileChange" />
<div>{{uploading?infoText:'上传文件'}}</div>
</div>
</template>
<script>
export default {
name: "oss-uploader",
data() {
return {
uploading: false,
infoText: "上传中...",
cosConfig: []
}
},
mounted() {
this.$http({
url: this.$http.adornUrl('/sys/oss/config'),
method: '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) {
this.$refs.fileInput.click();
}
},
onFileChange() {
let file = this.$refs.fileInput.files[0];
this.uploading = true;
let formData = new FormData();
formData.append("file", file)
this.$http({
url: this.$http.adornUrl('/sys/oss/upload'),
method: 'post',
data: formData
}).then(({ data }) => {
console.log(data)
if (data && data.code === 200) {
this.$emit('uploaded', data.url)
} else {
this.$message.error("文件上传失败:" + data.msg)
}
this.uploading = false;
})
}
}
}
</script>

@ -0,0 +1,146 @@
<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 }) => {
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
},
//
configHandle() {
this.configVisible = true
this.$nextTick(() => {
this.$refs.config.init()
})
},
//
uploadHandle() {
this.uploadVisible = true
this.$nextTick(() => {
this.$refs.upload.init()
})
},
//
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/oss/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(() => { })
},
isImageUrl(url) {
return url && /.*\.(gif|jpg|jpeg|png|GIF|JPEG|JPG|PNG)/.test(url)
}
}
}
</script>

@ -0,0 +1,96 @@
<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="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,
paramKey: '',
paramValue: '',
remark: ''
},
dataRule: {
paramKey: [
{ required: true, message: '参数名不能为空', trigger: 'blur' }
],
paramValue: [
{ required: true, message: '参数值不能为空', trigger: 'blur' }
]
}
}
},
methods: {
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,
'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,134 @@
<template>
<div class="mod-config">
<el-form :inline="true" :model="dataForm" @keyup.enter.native="getDataList()">
<el-form-item>
<el-input v-model="dataForm.paramKey" placeholder="参数名" clearable></el-input>
</el-form-item>
<el-form-item>
<el-button @click="getDataList()"></el-button>
<el-button type="primary" @click="addOrUpdateHandle()"></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="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">
<el-button type="text" size="small" @click="addOrUpdateHandle(scope.row.id)"></el-button>
<el-button type="text" size="small" @click="deleteHandle(scope.row.id)"></el-button>
</template>
</el-table-column>
</el-table>
<el-pagination @size-change="sizeChangeHandle" @current-change="currentChangeHandle" :current-page="pageIndex" :page-sizes="[10, 20, 50, 100]" :page-size="pageSize" :total="totalCount" layout="total, sizes, prev, pager, next, jumper">
</el-pagination>
<!-- 弹窗, 新增 / 修改 -->
<add-or-update v-if="addOrUpdateVisible" ref="addOrUpdate" @refreshDataList="getDataList"></add-or-update>
</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,90 @@
<template>
<div class="mod-log">
<el-form :inline="true" :model="dataForm" @keyup.enter.native="getDataList()">
<el-form-item>
<el-input v-model="dataForm.key" placeholder="用户名/用户操作" clearable></el-input>
</el-form-item>
<el-form-item>
<el-button @click="getDataList()"></el-button>
</el-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 {
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',
'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,218 @@
<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="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图标库, <a href="https://element.eleme.cn/#/zh-CN/component/icon" target="_blank">找图标</a></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() {
var validateUrl = (rule, value, callback) => {
if (this.dataForm.type === 1 && !/\S/.test(value)) {
callback(new Error('菜单URL不能为空'))
} else {
callback()
}
}
return {
visible: false,
dataForm: {
id: 0,
type: 1,
typeList: ['目录', '菜单', '按钮'],
name: '',
parentId: 0,
parentName: '',
url: '',
perms: '',
orderNum: 0,
icon: '',
},
dataRule: {
name: [
{ required: true, message: '菜单名称不能为空', trigger: 'blur' }
],
parentName: [
{ required: true, message: '上级菜单不能为空', trigger: 'change' }
],
url: [
{ validator: validateUrl, trigger: 'blur' }
]
},
menuList: [],
menuListTreeProps: {
label: 'name',
children: 'children'
}
}
},
methods: {
init(id) {
this.dataForm.id = id || 0
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
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,109 @@
<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>
<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
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,111 @@
<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: [],
menuListTreeProps: {
label: 'name',
children: 'children'
},
dataForm: {
id: 0,
roleName: '',
remark: ''
},
dataRule: {
roleName: [
{ required: true, message: '角色名称不能为空', trigger: 'blur' }
]
}
}
},
methods: {
init(id) {
this.dataForm.id = id || 0
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
this.$nextTick(() => {
this.$refs['dataForm'].resetFields()
this.$refs.menuListTree.setCheckedKeys([])
})
}).then(() => {
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'}`),
method: 'post',
data: this.$http.adornData({
'roleId': this.dataForm.id || undefined,
'roleName': this.dataForm.roleName,
'remark': this.dataForm.remark,
'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,132 @@
<template>
<div class="mod-role">
<el-form :inline="true" :model="dataForm" @keyup.enter.native="getDataList()">
<el-form-item>
<el-input v-model="dataForm.roleName" placeholder="角色名称" clearable></el-input>
</el-form-item>
<el-form-item>
<el-button @click="getDataList()"></el-button>
<el-button v-if="isAuth('sys:role:save')" type="primary" @click="addOrUpdateHandle()"></el-button>
<el-button v-if="isAuth('sys:role:delete')" type="danger" @click="deleteHandle()" :disabled="dataListSelections.length <= 0"></el-button>
</el-form-item>
</el-form>
<el-table :data="dataList" border v-loading="dataListLoading" @selection-change="selectionChangeHandle" style="width: 100%;">
<el-table-column type="selection" header-align="center" align="center" width="50">
</el-table-column>
<el-table-column prop="roleId" header-align="center" align="center" width="80" label="ID">
</el-table-column>
<el-table-column prop="roleName" header-align="center" align="center" label="角色名称">
</el-table-column>
<el-table-column prop="remark" header-align="center" align="center" label="备注">
</el-table-column>
<el-table-column fixed="right" header-align="center" align="center" width="150" label="操作">
<template slot-scope="scope">
<el-button v-if="isAuth('sys:role:update')" type="text" size="small" @click="addOrUpdateHandle(scope.row.roleId)"></el-button>
<el-button v-if="isAuth('sys:role:delete')" type="text" size="small" @click="deleteHandle(scope.row.roleId)"></el-button>
</template>
</el-table-column>
</el-table>
<el-pagination @size-change="sizeChangeHandle" @current-change="currentChangeHandle" :current-page="pageIndex" :page-sizes="[10, 20, 50, 100]" :page-size="pageSize" :total="totalCount" layout="total, sizes, prev, pager, next, jumper">
</el-pagination>
<!-- 弹窗, 新增 / 修改 -->
<add-or-update v-if="addOrUpdateVisible" ref="addOrUpdate" @refreshDataList="getDataList"></add-or-update>
</div>
</template>
<script>
import AddOrUpdate from './role-add-or-update'
export default {
data() {
return {
dataForm: {
roleName: ''
},
dataList: [],
pageIndex: 1,
pageSize: 10,
totalCount: 0,
dataListLoading: false,
dataListSelections: [],
addOrUpdateVisible: false
}
},
components: {
AddOrUpdate
},
activated() {
this.getDataList()
},
methods: {
//
getDataList() {
this.dataListLoading = true
this.$http({
url: this.$http.adornUrl('/sys/role/list'),
method: 'get',
params: this.$http.adornParams({
'page': this.pageIndex,
'limit': this.pageSize,
'roleName': this.dataForm.roleName
})
}).then(({ data }) => {
if (data && data.code === 200) {
this.dataList = data.page.list
this.totalCount = data.page.totalCount
} else {
this.dataList = []
this.totalCount = 0
}
this.dataListLoading = false
})
},
//
sizeChangeHandle(val) {
this.pageSize = val
this.pageIndex = 1
this.getDataList()
},
//
currentChangeHandle(val) {
this.pageIndex = val
this.getDataList()
},
//
selectionChangeHandle(val) {
this.dataListSelections = val
},
// /
addOrUpdateHandle(id) {
this.addOrUpdateVisible = true
this.$nextTick(() => {
this.$refs.addOrUpdate.init(id)
})
},
//
deleteHandle(id) {
var ids = id ? [id] : this.dataListSelections.map(item => item.roleId)
this.$confirm(`确定对[id=${ids.join(',')}]进行[${id ? '删除' : '批量删除'}]操作?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
this.$http({
url: this.$http.adornUrl('/sys/role/delete'),
method: 'post',
data: this.$http.adornData(ids, false)
}).then(({ data }) => {
if (data && data.code === 200) {
this.$message({
message: '操作成功',
type: 'success',
duration: 1500,
onClose: () => this.getDataList()
})
} else {
this.$message.error(data.msg)
}
})
}).catch(() => { })
}
}
}
</script>

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

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

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

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

@ -0,0 +1,155 @@
<template>
<div v-show="visible">
<el-form :model="dataForm" :rules="dataRule" ref="dataForm" size="mini" label-width="80px">
<el-row>
<el-col :span="12">
<el-form-item label="文章标题" prop="title" required>
<el-input v-model="dataForm.title" :maxlength="1024" placeholder="标题"></el-input>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="文章类型" prop="type" required>
<el-select v-model="dataForm.type" placeholder="选择文章类型">
<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>
<el-col :span="12">
<el-form-item label="一级目录" prop="category">
<el-input :maxlength="50" v-model="dataForm.category" placeholder="一级目录"></el-input>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="二级分类" prop="subCategory">
<el-input :maxlength="50" v-model="dataForm.subCategory" placeholder="二级目录"></el-input>
</el-form-item>
</el-col>
</el-row>
<el-form-item label="指向外链" prop="targetLink">
<el-input v-model="dataForm.targetLink" placeholder="指向外链"></el-input>
</el-form-item>
<el-form-item label="摘要" prop="summary">
<el-input v-model="dataForm.summary" placeholder="摘要" type="textarea" rows="3" maxlength="512" show-word-limit></el-input>
</el-form-item>
<el-form-item label="标签" prop="tags">
<tags-editor v-model="dataForm.tags"></tags-editor>
</el-form-item>
<el-form-item label="封面图" prop="image">
<el-input v-model="dataForm.image" placeholder="图片链接">
<OssUploader slot="append" @uploaded="dataForm.image=$event"></OssUploader>
</el-input>
</el-form-item>
<tinymce-editor ref="editor" v-model="dataForm.content"></tinymce-editor>
</el-form>
<div class="margin-top text-right">
<el-button @click="$emit('hide')"></el-button>
<el-button type="primary" @click="dataFormSubmit()"></el-button>
</div>
</div>
</template>
<script>
import { mapState } from 'vuex'
export default {
name:'article-add-or-update',
components: {
TinymceEditor: () => import("@/components/tinymce-editor"),
tagsEditor: () => import("@/components/tags-editor"),
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({
ARTICLE_TYPES: state=>state.article.ARTICLE_TYPES
}),
methods: {
init(id) {
this.dataForm.id = id || "";
this.$nextTick(() => {
this.$refs["dataForm"].resetFields();
if (id > 0) {
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) {
this.dataForm=data.article;
this.dataForm.type = data.article.type + "";
}
});
}
});
},
//
dataFormSubmit() {
this.$refs["dataForm"].validate(valid => {
if (valid) {
this.$http({
url: this.$http.adornUrl(`/manage/article/save`),
method: "post",
data: this.$http.adornData(this.dataForm)
}).then(({ data }) => {
if (data && data.code === 200) {
this.$message({
message: "操作成功",
type: "success",
duration: 1500,
onClose: () => {
this.$emit("refreshDataList");
this.$emit('hide')
}
});
} else {
this.$message.error(data.msg);
}
});
}
});
},
imgUploadSuccess(response, file, fileList) {
console.log(response);
if (response.code == 200) {
this.dataForm.image = response.data;
console.log("this.article", this.article);
} else {
this.$message.warning(response.msg);
}
}
}
};
</script>

@ -0,0 +1,157 @@
<template>
<div>
<div v-show="!addOrUpdateVisible">
<el-form :inline="true" :model="dataForm" @keyup.enter.native="getDataList()">
<el-form-item>
<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-form-item>
<el-input v-model="dataForm.title" placeholder="标题" clearable></el-input>
</el-form-item>
<el-form-item>
<el-button @click="pageIndex=1;getDataList()"></el-button>
<el-button v-if="isAuth('wx:article:save')" type="primary" @click="addOrUpdateHandle()"></el-button>
<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 :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" label="ID">
</el-table-column>
<el-table-column prop="type" header-align="center" align="center" :formatter="articleTypeFormat" label="文章类型">
</el-table-column>
<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>
<el-table-column prop="category" header-align="center" align="center" label="一级分类">
</el-table-column>
<el-table-column prop="subCategory" header-align="center" align="center" label="二级分类">
</el-table-column>
<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="addOrUpdate" @refreshDataList="getDataList" @hide="addOrUpdateVisible=false"></add-or-update>
</div>
</template>
<script>
import AddOrUpdate from './article-add-or-update'
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({
ARTICLE_TYPES: state=>state.article.ARTICLE_TYPES
}),
mounted() {
this.getDataList()
},
methods: {
//
getDataList() {
this.dataListLoading = true
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
}
this.dataListLoading = false
})
},
//
sizeChangeHandle(val) {
this.pageSize = val
this.pageIndex = 1
this.getDataList()
},
//
currentChangeHandle(val) {
this.pageIndex = val
this.getDataList()
},
//
selectionChangeHandle(val) {
this.dataListSelections = val
},
// /
addOrUpdateHandle(id) {
this.addOrUpdateVisible = true
this.$nextTick(() => {
this.$refs.addOrUpdate.init(id)
})
},
//
deleteHandle(id) {
var ids = id ? [id] : this.dataListSelections.map(item => item.id)
this.$confirm(`确定对[id=${ids.join(',')}]进行[${id ? '删除' : '批量删除'}]操作?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
this.$http({
url: this.$http.adornUrl('/manage/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)
}
})
})
},
articleTypeFormat(row, column, cellValue) {
return this.ARTICLE_TYPES[cellValue];
}
}
}
</script>

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

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

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

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

@ -0,0 +1,206 @@
<template>
<div class="panel">
<div v-show="!addOrUpdateVisible">
<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 class="flex justify-start" v-loading="dataListLoading">
<div v-for="n in rows" :key="n">
<template v-for="(item,i) in dataList">
<div class="card" :key="item.mediaId" v-if="i%rows==n-1" @click="onSelect(item)">
<div class="card-preview">
<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>
<el-image class="article-thumb" :src="article.thumbUrl"></el-image>
</a>
</div>
<div class="card-footer">
<div>{{$moment(item.updateTime).calendar()}}</div>
<div class="flex justify-between align-center" v-show="!selectMode">
<el-button size="mini" type="text" icon="el-icon-copy-document" v-clipboard:copy="item.mediaId" v-clipboard:success="onCopySuccess" v-clipboard:error="onCopyError">复制media_id</el-button>
<el-button size="mini" type="text" icon="el-icon-edit" @click="addOrUpdateHandle(item)"></el-button>
<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(){
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/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);
}
this.dataListLoading = false
})
},
onSelect(itemInfo) {
if (!this.selectMode) return
this.$emit('selected', itemInfo)
},
//
deleteHandle(id) {
this.$confirm(`确定对[mediaId=${id}]进行删除操作?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
this.$http({
url: this.$http.adornUrl('/manage/wxAssets/materialDelete'),
method: 'post',
data: { mediaId: id }
}).then(({ data }) => {
if (data && data.code === 200) {
this.$message({
message: '操作成功',
type: 'success',
duration: 1500,
onClose: () => this.onChange()
})
} else {
this.$message.error(data.msg)
}
})
})
},
//
currentChangeHandle(val) {
this.pageIndex = val
this.getDataList()
},
// /
addOrUpdateHandle(news) {
this.addOrUpdateVisible = true
this.$nextTick(() => {
this.$refs.addOrUpdate.init(news || '')
})
},
onCopySuccess() {
this.$message.success('已复制')
},
onCopyError(err) {
this.$message.error('复制失败,可能是此浏览器不支持复制')
},
onChange() {
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,211 @@
<template>
<el-dialog :title="!dataForm.id ? '新增' : '修改'" :close-on-click-modal="false" :visible.sync="visible" >
<el-form :model="dataForm" :rules="dataRule" ref="dataForm" label-width="80px">
<el-form-item label="规则名称" prop="ruleName">
<el-input v-model="dataForm.ruleName" placeholder="规则名称"></el-input>
</el-form-item>
<el-form-item label="匹配词" prop="matchValue">
<tags-editor v-model="dataForm.matchValue"></tags-editor>
</el-form-item>
<el-row>
<el-col :span="12">
<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>
<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>
<el-col :span="12">
<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>
<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>
<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>
<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>
<el-form-item label="回复内容" prop="replyContent">
<el-input v-model="dataForm.replyContent" type="textarea" :rows="5" placeholder="文本、图文ID、media_id、json配置"></el-input>
<el-button type="text" v-show="'text'==dataForm.replyType" @click="addLink"></el-button>
<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>
<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">
<el-button @click="visible = false">取消</el-button>
<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>
import { mapState } from 'vuex'
export default {
components: {
tagsEditor: () => import('@/components/tags-editor'),
AssetsSelector:()=>import('./assets/assets-selector')
},
data() {
return {
visible: false,
assetsSelectorVisible:false,
dataForm: {
ruleId: 0,
appid:'',
ruleName: "",
exactMatch: false,
matchValue: "",
replyType: 'text',
replyContent: "",
status: true,
desc: "",
effectTimeStart: "00:00:00",
effectTimeEnd: "23:59:59"
},
dataRule: {
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({
KefuMsgType: state=>state.message.KefuMsgType,
selectedAppid:state=>state.wxAccount.selectedAppid,
assetsType(){
const config={//
'image':'image',
'voice':'voice',
'video':'video',
'mpnews':'news',
'miniprogrampage':'image',//
'music':'image'
}
return config[this.dataForm.replyType] || ''
}
}),
methods: {
init(id) {
this.dataForm.ruleId = id || 0;
this.visible = true;
this.$nextTick(() => {
this.$refs["dataForm"].resetFields();
if (this.dataForm.ruleId) {
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) {
this.dataForm = data.msgReplyRule;
}
});
}
});
},
//
dataFormSubmit() {
this.$refs["dataForm"].validate(valid => {
if (valid) {
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) {
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) {
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){
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,166 @@
<template>
<div class="mod-config">
<el-form :inline="true" :model="dataForm" @keyup.enter.native="getDataList()">
<el-form-item>
<el-input v-model="dataForm.matchValue" placeholder="匹配关键词" clearable></el-input>
</el-form-item>
<el-form-item>
<el-button @click="getDataList()"></el-button>
<el-button v-if="isAuth('wx:msgreplyrule:save')" type="primary" @click="addOrUpdateHandle()"></el-button>
<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 :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">
<el-form-item label="作用范围">
<span>{{ props.row.appid?'当前公众号':'全部公众号' }}</span>
</el-form-item>
<el-form-item label="精确匹配">
<span>{{ props.row.exactMatch?'是':'否' }}</span>
</el-form-item>
<el-form-item label="是否有效">
<span>{{ props.row.status?'是':'否' }}</span>
</el-form-item>
<el-form-item label="备注说明">
<span>{{ props.row.desc }}</span>
</el-form-item>
<el-form-item label="生效时间">
<span>{{ props.row.effectTimeStart }}</span>
</el-form-item>
<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>
<el-table-column prop="ruleName" header-align="center" align="center" show-overflow-tooltip label="规则名称">
</el-table-column>
<el-table-column prop="matchValue" header-align="center" align="center" show-overflow-tooltip label="匹配关键词">
</el-table-column>
<el-table-column prop="replyType" header-align="center" align="center" :formatter="replyTypeFormat" label="消息类型">
</el-table-column>
<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>
<!-- 弹窗, 新增 / 修改 -->
<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'
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({
KefuMsgType: state=>state.message.KefuMsgType
}),
activated() {
this.getDataList()
},
methods: {
//
getDataList() {
this.dataListLoading = true
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
}
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.ruleId)
this.$confirm(`确定对[id=${ids.join(',')}]进行[${id ? '删除' : '批量删除'}]操作?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
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)
}
})
})
},
replyTypeFormat(row, column, cellValue) {
return this.KefuMsgType[cellValue];
}
}
}
</script>

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

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

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

@ -0,0 +1,137 @@
<template>
<div class="mod-config">
<el-form :inline="true" :model="dataForm" @keyup.enter.native="getDataList()">
<el-form-item>
<el-input v-model="dataForm.key" placeholder="参数名" clearable></el-input>
</el-form-item>
<el-form-item>
<el-button @click="getDataList()"></el-button>
<el-button v-if="isAuth('wx:wxaccount:save')" type="primary" @click="addOrUpdateHandle()"></el-button>
<el-button v-if="isAuth('wx:wxaccount:delete')" type="danger" @click="deleteHandle()" :disabled="dataListSelections.length <= 0"></el-button>
</el-form-item>
</el-form>
<el-table :data="dataList" border v-loading="dataListLoading" @selection-change="selectionChangeHandle" style="width: 100%;">
<el-table-column type="selection" header-align="center" align="center" width="50">
</el-table-column>
<el-table-column prop="appid" header-align="center" align="center" label="appid">
</el-table-column>
<el-table-column prop="name" header-align="center" align="center" label="公众号名称">
</el-table-column>
<el-table-column prop="type" header-align="center" align="center" label="类型" :formatter="accountTypeFormat">
</el-table-column>
<el-table-column prop="verified" header-align="center" align="center" label="是否认证">
<span slot-scope="scope">{{scope.row.verified?"是":"否"}}</span>
</el-table-column>
<el-table-column fixed="right" header-align="center" align="center" width="150" label="操作">
<template slot-scope="scope">
<el-button type="text" size="small" @click="accessInfo(scope.row)"></el-button>
<el-button type="text" size="small" @click="addOrUpdateHandle(scope.row)"></el-button>
<el-button type="text" size="small" @click="deleteHandle(scope.row.appid)"></el-button>
</template>
</el-table-column>
</el-table>
<!-- 弹窗, 新增 / 修改 -->
<add-or-update v-if="addOrUpdateVisible" ref="addOrUpdate" @refreshDataList="getDataList"></add-or-update>
<account-access v-if="accountAccessVisible" ref="accountAccessDialog"></account-access>
</div>
</template>
<script>
import AddOrUpdate from './account/wx-account-add-or-update'
import AccountAccess from './account/wx-account-access-info'
import { mapState } from 'vuex'
export default {
data() {
return {
dataForm: {
key: ''
},
dataList: [],
dataListLoading: false,
dataListSelections: [],
addOrUpdateVisible: false,
accountAccessVisible:false
}
},
components: {
AddOrUpdate,AccountAccess
},
computed: mapState({
ACCOUNT_TYPES: state=>state.wxAccount.ACCOUNT_TYPES
}),
activated() {
this.getDataList()
},
methods: {
//
getDataList() {
this.dataListLoading = true
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) {
this.dataList = data.list
this.$store.commit('wxAccount/updateAccountList', data.list)
} else {
this.dataList = []
}
this.dataListLoading = false
})
},
//
selectionChangeHandle(val) {
this.dataListSelections = val
},
// /
addOrUpdateHandle(item) {
this.addOrUpdateVisible = true
this.$nextTick(() => {
this.$refs.addOrUpdate.init(item)
})
},
accessInfo(item){
this.accountAccessVisible = true
this.$nextTick(() => {
this.$refs.accountAccessDialog.init(item)
})
},
//
deleteHandle(appid) {
var ids = appid ? [appid] : this.dataListSelections.map(item => {
return item.appid
})
this.$confirm(`确定对[appid=${ids.join(',')}]进行[${appid ? '删除' : '批量删除'}]操作?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
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) {
return this.ACCOUNT_TYPES[cellValue];
}
}
}
</script>

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

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

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

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

@ -0,0 +1,184 @@
<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>
<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)
}
})
},
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,86 @@
<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="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
},
dataRule: {
isTemp: [
{ required: true, message: '二维码类型不能为空', trigger: 'blur' }
],
sceneStr: [
{ required: true, message: '场景值ID不能为空', trigger: 'blur' }
],
expireSeconds: [
{ required: true, message: '该二维码失效时间不能为空', trigger: 'blur' }
]
}
}
},
methods: {
init(id) {
this.dataForm.id = id || 0
this.visible = true
this.$nextTick(() => {
this.$refs['dataForm'].resetFields()
})
},
//
dataFormSubmit() {
this.$refs['dataForm'].validate((valid) => {
if (valid) {
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,142 @@
<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>
<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(() => {
this.$refs.addOrUpdate.init(id)
})
},
//
deleteHandle(id) {
var ids = id ? [id] : this.dataListSelections.map(item => item.id)
this.$confirm(`确定对[id=${ids.join(',')}]进行[${id ? '删除' : '批量删除'}]操作?(仅删存档)`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
this.$http({
url: this.$http.adornUrl('/manage/wxQrCode/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)
}
})
})
}
}
}
</script>

@ -0,0 +1,102 @@
<template>
<el-dialog :title="modeDesc[mode]+'用户标签'" :close-on-click-modal="false" :visible.sync="dialogVisible">
<div>
<el-select v-model="selectedTagid" filterable placeholder="请选择标签" style="width:100%">
<el-option v-for="tagid in tagidsInOption" :key="tagid" :label="getTagName(tagid)" :value="tagid"></el-option>
</el-select>
<div style="margin-top:20px;">已选择用户数{{wxUsers.length}}</div>
</div>
<span slot="footer" class="dialog-footer">
<el-button @click="dialogVisible=false"></el-button>
<el-button type="primary" @click="dataFormSubmit()" :disabled="submitting">{{submitting?'保存中...':'确定'}}</el-button>
</span>
</el-dialog>
</template>
<script>
import { mapState } from 'vuex'
export default {
name:'wx-user-tagging',
props:{
wxUsers:Array,
},
data(){
return{
mode:'tagging',//tagging | untagging
modeDesc:{
'tagging':'绑定',
'untagging':'解绑'
},
selectedTagid:'',
dialogVisible:false,
submitting:false
}
},
computed: mapState({
wxUserTags:state=>state.wxUserTags.tags,
/**
* 返回下拉选择框中的选项列表
* 假设 all= 全部标签intersection = 用户标签交集即所有用户都有的 union=用户标签并集即至少一个用户的
* 那么绑定时可选all-intersection的差集即所有用户都有的就不列出来了
* 解绑时可选union 即用户有的标签都列出来
*/
tagidsInOption(){
let userTags=this.wxUsers.map(u=>u.tagidList || [])//[[1,2],[],[1,3]]
if(this.mode=='tagging'){// -
let all = this.wxUserTags.map(item=>item.id)
return all.filter(tagid=>!userTags.every(tagsIdArray=>tagsIdArray.indexOf(tagid)>-1))
}else if(this.mode=='untagging'){//
let unionSet = new Set();
userTags.forEach(tagsIdArray=>{
tagsIdArray.forEach(tagid => unionSet.add(tagid))
});//unionSet
return Array.from(unionSet);//unionSet
}
return []
}
}),
methods:{
init(mode){
if('tagging'==mode || 'untagging'==mode){
this.mode=mode;
this.dialogVisible=true
}else{
throw('mode参数有误')
}
},
getTagName(tagid){
let tag = this.wxUserTags.find(item=>item.id==tagid)
return tag?tag.name : "?"
},
dataFormSubmit(){
if(this.submitting)return
if(!this.selectedTagid){
this.$message.error('未选择标签')
return
}
this.submitting=true
let openidList=this.wxUsers.map(u=>u.openid)
this.$http({
url: this.$http.adornUrl(`/manage/wxUserTags/${this.mode=='tagging'?'batchTagging':'batchUnTagging'}`),
method: 'post',
data:this.$http.adornData({
tagid : this.selectedTagid,
openidList : openidList
})
}).then(({ data }) => {
this.submitting=false
if (data && data.code === 200) {
this.$message({
message: '操作成功,列表数据需稍后刷新查看',
type: 'success',
onClose: () =>this.dialogVisible=false
})
} else {
this.$message.error(data.msg)
}
})
}
}
}
</script>

@ -0,0 +1,209 @@
<template>
<div class="mod-config">
<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>
<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>
<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>
<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