代码注释
pull/19/head
mkaoj697q 2 months ago
commit 883b1cafea

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

108
.gitignore vendored

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

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

@ -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'
}

18266
package-lock.json generated

File diff suppressed because it is too large Load Diff

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

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

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

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

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

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

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

@ -0,0 +1,129 @@
<template>
<!-- 使用Element UI的对话框组件 -->
<el-dialog title="修改密码" :visible.sync="visible" :append-to-body="true">
<!-- 表单用于输入原密码和新密码 -->
<el-form :model="dataForm" :rules="dataRule" ref="dataForm" @keyup.enter.native="dataFormSubmit()" label-width="80px">
<!-- 显示用户名不可编辑 -->
<el-form-item label="账号">
<span>{{ userName }}</span>
</el-form-item>
<!-- 输入原密码 -->
<el-form-item label="原密码" prop="password">
<el-input type="password" v-model="dataForm.password"></el-input>
</el-form-item>
<!-- 输入新密码 -->
<el-form-item label="新密码" prop="newPassword">
<el-input type="password" v-model="dataForm.newPassword"></el-input>
</el-form-item>
<!-- 确认新密码 -->
<el-form-item label="确认密码" prop="confirmPassword">
<el-input type="password" v-model="dataForm.confirmPassword"></el-input>
</el-form-item>
</el-form>
<!-- 对话框底部按钮 -->
<span slot="footer" class="dialog-footer">
<el-button @click="visible = false">取消</el-button>
<el-button type="primary" @click="dataFormSubmit()"></el-button>
</span>
</el-dialog>
</template>
<script>
import { clearLoginInfo } from '@/utils' //
export default {
data() {
//
var validateConfirmPassword = (rule, value, callback) => {
if (this.dataForm.newPassword !== value) {
callback(new Error('确认密码与新密码不一致'))
} else {
callback()
}
}
return {
//
visible: false,
//
dataForm: {
password: '', //
newPassword: '', //
confirmPassword: '' //
},
//
dataRule: {
password: [
{ required: true, message: '原密码不能为空', trigger: 'blur' }
],
newPassword: [
{ required: true, message: '新密码不能为空', trigger: 'blur' }
],
confirmPassword: [
{ required: true, message: '确认密码不能为空', trigger: 'blur' },
{ validator: validateConfirmPassword, trigger: 'blur' } //
]
}
}
},
computed: {
//
userName: {
get() { return this.$store.state.user.name }
},
//
mainTabs: {
get() { return this.$store.state.common.mainTabs },
set(val) { this.$store.commit('common/updateMainTabs', val) }
}
},
methods: {
//
init() {
this.visible = true
this.$nextTick(() => {
this.$refs['dataForm'].resetFields()
})
},
//
dataFormSubmit() {
//
this.$refs['dataForm'].validate((valid) => {
if (valid) {
//
this.$http({
url: this.$http.adornUrl('/sys/user/password'), //
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,120 @@
<template>
<!-- 导航栏组件类名根据navbarLayoutType动态设置 -->
<nav class="site-navbar" :class="'site-navbar--' + navbarLayoutType">
<!-- 导航栏头部包含品牌标识 -->
<div class="site-navbar__header">
<h1 class="site-navbar__brand" @click="$router.push({ name: 'home' })">
<!-- 大尺寸品牌标识点击返回首页 -->
<a class="site-navbar__brand-lg" href="javascript:;">微信管理系统</a>
<!-- 小尺寸品牌标识点击无效果 -->
<a class="site-navbar__brand-mini" href="javascript:;">W</a>
</h1>
</div>
<!-- 导航栏主体包含菜单 -->
<div class="site-navbar__body clearfix">
<!-- 左侧菜单包含侧边栏折叠/展开按钮 -->
<el-menu class="site-navbar__menu" mode="horizontal">
<el-menu-item class="site-navbar__switch" index="0" @click="sidebarFold = !sidebarFold">
<!-- 侧边栏折叠/展开图标根据sidebarFold动态切换 -->
<i :class="sidebarFold?'el-icon-s-unfold':'el-icon-s-fold'"></i>
</el-menu-item>
</el-menu>
<!-- 右侧菜单包含设置微信账号选择用户信息等 -->
<el-menu class="site-navbar__menu site-navbar__menu--right" mode="horizontal">
<!-- 设置菜单项 -->
<el-menu-item index="1" @click="$router.push({ name: 'theme' })">
<template slot="title">
<i class="el-icon-setting"></i>
</template>
</el-menu-item>
<!-- 微信账号选择器需要权限验证 -->
<el-menu-item index="2" v-if="isAuth('wx:wxaccount:list')">
<template slot="title">
<wx-account-selector></wx-account-selector>
</template>
</el-menu-item>
<!-- 用户信息菜单项包含下拉菜单 -->
<el-menu-item class="site-navbar__avatar" index="3">
<el-dropdown :show-timeout="0" placement="bottom">
<span class="el-dropdown-link">
{{ userName }}
</span>
<!-- 下拉菜单内容 -->
<el-dropdown-menu slot="dropdown">
<!-- 修改密码菜单项 -->
<el-dropdown-item @click.native="updatePasswordHandle()">修改密码</el-dropdown-item>
<!-- 退出菜单项 -->
<el-dropdown-item @click.native="logoutHandle()">退出</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
</el-menu-item>
</el-menu>
</div>
<!-- 修改密码弹窗组件根据updatePassowrdVisible显示/隐藏 -->
<update-password v-if="updatePassowrdVisible" ref="updatePassowrd"></update-password>
</nav>
</template>
<script>
import UpdatePassword from './main-navbar-update-password' //
import WxAccountSelector from '@/components/wx-account-selector' //
import { clearLoginInfo } from '@/utils' //
export default {
data() {
return {
updatePassowrdVisible: false // /
}
},
components: {
UpdatePassword, //
WxAccountSelector //
},
computed: {
// navbarLayoutTypesidebarFoldmainTabsuserName
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,38 @@
<script>
// SubMenu
import SubMenu from './main-sidebar-sub-menu'
export default {
name: 'sub-menu', //
props: {
menu: {
type: Object, //
required: true //
},
dynamicMenuRoutes: {
type: Array, //
required: true //
}
},
components: {
SubMenu // SubMenu
},
computed: {
sidebarLayoutSkin() {
// Vuex storesidebarLayoutSkin
return this.$store.state.common.sidebarLayoutSkin
}
},
methods: {
// menuId
gotoRouteHandle(menu) {
// dynamicMenuRoutesmenuIdmenumenuId
var route = this.dynamicMenuRoutes.filter(item => item.meta.menuId === menu.menuId)
// 使$router.push
if (route.length >= 1) {
this.$router.push({ name: route[0].name })
}
}
}
}
</script>

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

@ -0,0 +1,109 @@
<template>
<!-- 站点包装器根据sidebarFold状态添加类名同时绑定全屏加载指示器 -->
<div class="site-wrapper" :class="{ 'site-sidebar--fold': sidebarFold }"
v-loading.fullscreen.lock="loading" element-loading-text="拼命加载中">
<!-- 当loading为false时显示以下内容 -->
<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 {
// refresh
provide() {
return {
refresh() {
//
this.$store.commit('common/updateContentIsNeedRefresh', true)
// DOM
this.$nextTick(() => {
this.$store.commit('common/updateContentIsNeedRefresh', false)
})
}
}
},
//
data() {
return {
loading: true //
}
},
//
components: {
MainNavbar,
MainSidebar,
MainContent
},
// Vuex store
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() {
// HTTP GET
this.$http({
url: this.$http.adornUrl('/sys/user/info'),
method: 'get',
params: this.$http.adornParams()
}).then(({ data }) => {
// 200
if (data && data.code === 200) {
//
this.loading = false
// ID
this.userId = data.user.userId
this.userName = data.user.username
}
})
}
}
}
</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,178 @@
<template>
<div>
<div id="app-menu">
<!-- 预览窗 -->
<div class="weixin-preview">
<div class="weixin-bd">
<div class="weixin-header">公众号菜单</div>
<ul class="weixin-menu" id="weixin-menu">
<!-- 遍历主菜单按钮 -->
<li v-for="(btn, i) in menu.buttons" :key="i" class="menu-item"
:class="{'current': selectedMenuIndex === i && selectedMenuLevel == 1}"
@click="selectMenu(i)">
<div class="menu-item-title">
<span>{{ btn.name }}</span>
</div>
<ul class="weixin-sub-menu">
<!-- 遍历子菜单按钮 -->
<li v-for="(sub, i2) in btn.subButtons" :key="i2" class="menu-sub-item"
:class="{'current': selectedMenuIndex === i && selectedSubMenuIndex === i2 && selectedMenuLevel == 2, 'on-drag-over': onDragOverMenu === (i + '_' + i2)}"
@click.stop="selectSubMenu(i, i2)" draggable="true"
@dragstart="selectSubMenu(i, i2)" @dragover.prevent="onDragOverMenu = (i + '_' + i2)" @drop="onDrop(i, i2)">
<div class="menu-item-title">
<span>{{ sub.name }}</span>
</div>
</li>
<!-- 如果子菜单数量少于5个显示添加按钮 -->
<li v-if="btn.subButtons.length < 5" class="menu-sub-item"
:class="{'on-drag-over': onDragOverMenu === (i + '_' + btn.subButtons.length)}"
@click.stop="addMenu(2, i)" @dragover.prevent="onDragOverMenu = (i + '_' + btn.subButtons.length)" @drop="onDrop(i, btn.subButtons.length)">
<div class="menu-item-title">
<i class="el-icon-plus"></i>
</div>
</li>
<!-- 菜单展开/收起图标 -->
<i class="menu-arrow arrow_out"></i>
<i class="menu-arrow arrow_in"></i>
</ul>
</li>
<!-- 如果主菜单数量少于3个显示添加按钮 -->
<li class="menu-item" v-if="menu.buttons.length < 3" @click="addMenu(1)">
<i class="el-icon-plus"></i>
</li>
</ul>
</div>
</div>
<!-- 菜单编辑器 -->
<div class="weixin-menu-detail" v-if="selectedMenuLevel > 0">
<wx-menu-button-editor :button="selectedButton" :selectedMenuLevel="selectedMenuLevel" @delMenu="delMenu"></wx-menu-button-editor>
</div>
</div>
<!-- 发布和清空菜单的按钮组 -->
<div class="weixin-btn-group" v-if="isAuth('wx:menu:save')" @click="updateWxMenu">
<el-button type="success" icon="el-icon-upload">发布</el-button>
<el-button type="warning" icon="el-icon-delete" @click="delMenuAll"></el-button>
<!-- 注意这里可能是一个小错误因为delMenu方法通常用于删除选中的菜单项而不是清空所有菜单 -->
<!-- 如果要清空所有菜单可能需要一个单独的方法比如delMenuAll -->
</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,107 @@
<template>
<!-- 消息回复对话框 -->
<el-dialog title="消息回复" :close-on-click-modal="false" :visible.sync="visible">
<!-- 表单区域 -->
<el-form :model="dataForm" :rules="dataRule" ref="dataForm">
<!-- 表单项回复内容 -->
<el-form-item prop="replyContent">
<!-- 文本区域输入框用于输入回复内容 -->
<el-input v-model="dataForm.replyContent" type="textarea" :rows="5" placeholder="回复内容" maxlength="600" show-word-limit :autosize="{ minRows: 5, maxRows: 30 }" autocomplete></el-input>
<!-- 当回复类型为文本时显示点击插入链接 -->
<el-button type="text" v-show="'text'==dataForm.replyType" @click="addLink"></el-button>
</el-form-item>
</el-form>
<!-- 对话框底部操作区域 -->
<span slot="footer" class="dialog-footer">
<!-- 取消按钮点击关闭对话框 -->
<el-button @click="visible = false">取消</el-button>
<!-- 发送按钮根据uploading状态显示不同文本并控制是否禁用 -->
<el-button type="success" @click="dataFormSubmit()" :disabled="uploading">{{uploading ? '发送中...' : '发送'}}</el-button>
</span>
</el-dialog>
</template>
<script>
export default {
data() {
return {
//
visible: false,
//
uploading: false,
//
dataForm: {
//
openid: '',
//
replyType: 'text',
//
replyContent: ''
},
//
dataRule: {
//
replyContent: [
{ required: true, message: "回复内容不能为空", trigger: "blur" }
]
}
}
},
// 使
components: {
WxMsgPreview: () => import('@/components/wx-msg-preview')
},
methods: {
// openid
init(openid) {
if (!openid) throw '参数异常'; // openid
this.dataForm.openid = openid; // openid
this.visible = true; //
},
//
dataFormSubmit() {
if (this.uploading) return; //
this.uploading = true; // true
this.$refs['dataForm'].validate((valid) => { //
if (valid) { //
// $http
this.$http({
url: this.$http.adornUrl(`/manage/wxMsg/reply`), //
method: 'post', //
data: this.$http.adornData(this.dataForm) //
}).then(({ data }) => { //
if (data && data.code === 200) { // 200
//
this.$message({
message: '回复成功',
type: 'success',
duration: 1500,
onClose: () => {
this.visible = false;
}
});
this.$emit("success", { ...this.dataForm }); // success
this.dataForm.replyContent = ''; //
} else { //
//
this.$message.error(data.msg);
}
this.uploading = false; //
});
}
});
},
//
addLink() {
this.dataForm.replyContent += '<a href="链接地址">链接文字</a>';
}
}
}
</script>
<style scoped>
/* 样式部分,但.msg-container类在模板中未使用 */
.msg-container {
background: #eee;
}
</style>

@ -0,0 +1,208 @@
<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>
<!-- 消息列表使用v-loading显示加载状态 -->
<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">
<!-- 回复按钮如果消息在24小时内可点击 -->
<div v-if="canReply(msg.createTime)" @click="replyHandle(msg.openid)" class="el-icon-s-promotion"></div>
</div>
</div>
<!-- 消息预览组件 -->
<wx-msg-preview :msg="msg" singleLine></wx-msg-preview>
</div>
</div>
</div>
<!-- 分页组件 -->
<el-pagination @size-change="sizeChangeHandle" @current-change="currentChangeHandle" :current-page="pageIndex" :page-sizes="[10, 20, 50, 100]" :page-size="pageSize" :total="totalCount" layout="total, sizes, prev, pager, next, jumper">
</el-pagination>
<!-- 弹窗, 消息回复 -->
<wx-msg-reply ref="wxMsgReply" @success="onReplyed"></wx-msg-reply>
</div>
</template>
<script>
const TIME_FORMAT = 'YYYY/MM/DD hh:mm:ss'
export default {
data() {
return {
//
timeSelections:{
'近24小时':this.$moment().subtract(1, 'days').format(TIME_FORMAT),
'近3天': this.$moment().subtract(3, 'days').format(TIME_FORMAT),
'近7天': this.$moment().subtract(7, 'days').format(TIME_FORMAT),
'近30天': this.$moment().subtract(30, 'days').format(TIME_FORMAT),
},
//
dataForm: {
startTime: this.$moment().subtract(1, 'days').format(TIME_FORMAT),
msgTypes: ''
},
//
dataList: [],
//
userDataList:[],
//
pageIndex: 1,
pageSize: 20,
totalCount: 0,
//
dataListLoading: false,
// 使
dataListSelections: []
}
},
components: {
//
WxMsgReply:()=>import('./wx-msg-reply'),
WxMsgPreview:()=>import('@/components/wx-msg-preview')
},
//
activated() {
this.getDataList()
},
methods: {
//
getDataList() {
this.dataListLoading = true
this.$http({
url: this.$http.adornUrl('/manage/wxMsg/list'),
method: 'get',
params: this.$http.adornParams({
'page': this.pageIndex,
'limit': this.pageSize,
'msgTypes': this.dataForm.msgTypes,
'startTime':this.dataForm.startTime,
'sidx': 'create_time',
'order': 'desc'
})
}).then(({ data }) => {
if (data && data.code === 200) {
this.dataList = data.page.list
this.totalCount = data.page.totalCount
this.refreshUserList(this.dataList)
} else {
this.dataList = []
this.totalCount = 0
}
this.dataListLoading = false
})
},
//
refreshUserList(msgList){
let openidList=msgList.map(msg=>msg.openid).filter(openid=>!this.userDataList.some(u=>u.openid==openid))
if(!openidList.length)return
openidList = Array.from(new Set(openidList))//
this.$http({
url: this.$http.adornUrl('/manage/wxUser/listByIds'),
method: 'post',
data: this.$http.adornParams(openidList,false)
}).then(({ data }) => {
if (data && data.code === 200) {
this.userDataList = this.userDataList.concat(data.data)
}
})
},
// openid
getUserInfo(openid){
return this.userDataList.find(u=>u.openid==openid) || {nickname:'--',headimgurl:''}
},
// 24
canReply(time){
return new Date(time).getTime()>new Date().getTime()-24*60*60*1000
},
//
sizeChangeHandle(val) {
this.pageSize = val
this.pageIndex = 1
this.getDataList()
},
//
currentChangeHandle(val) {
this.pageIndex = val
this.getDataList()
},
// 使
selectionChangeHandle(val) {
this.dataListSelections = val
},
//
replyHandle(openid) {
this.$nextTick(() => {
this.$refs.wxMsgReply.init(openid)
})
},
//
onReplyed(replyMsg){
this.dataList.unshift({
openid : replyMsg.openid,
msgType : replyMsg.replyType,
detail : {
content : replyMsg.replyContent
},
inOut : 1,
createTime : new Date()
})
}
}
}
</script>
<style scoped>
.msg-item{
border: 1px solid #DCDFE6;
display: flex;
justify-content: flex-start;
align-items: top;
margin-top: 20px;
padding: 10px 20px;
}
.avatar{
flex: 0;
display: inline-block;
min-width: 60px;
margin-right: 20px;
}
.item-content{
flex: 1;
line-height: 20px;
max-width: 100%;
overflow: hidden;
}
.reply-btn{
width: 50px;
}
</style>

@ -0,0 +1,108 @@
<template>
<!-- Element UI的对话框组件根据dataForm.id是否存在来设置标题为新增修改 -->
<el-dialog
:title="!dataForm.id ? '新增' : '修改'"
:close-on-click-modal="false" <!-- 点击遮罩层不关闭对话框 -->
:visible.sync="visible"> <!-- 双向绑定控制对话框的显示隐藏 -->
<!-- 表单部分绑定dataForm作为数据模型dataRule作为验证规则 -->
<el-form
:model="dataForm"
:rules="dataRule"
ref="dataForm"
@keyup.enter.native="dataFormSubmit()" <!-- 按下Enter键提交表单 -->
label-width="100px">
<!-- 表单项二维码类型 -->
<el-form-item label="二维码类型" prop="isTemp">
<el-radio v-model="dataForm.isTemp" :label="true"></el-radio>
<el-radio v-model="dataForm.isTemp" :label="false"></el-radio>
<div>
<!-- 当不是临时二维码时显示链接 -->
<a class="text-warning" v-show="!dataForm.isTemp" target="_blank" href="https://developers.weixin.qq.com/doc/offiaccount/Account_Management/Generating_a_Parametric_QR_Code.html">
注意永久二维码上限10万个且暂时无法删除旧的二维码
</a>
</div>
</el-form-item>
<!-- 表单项场景值 -->
<el-form-item label="场景值" prop="sceneStr">
<el-input v-model="dataForm.sceneStr" placeholder="任意字符串" maxlength="64"></el-input>
</el-form-item>
<!-- 表单项失效时间仅临时二维码显示 -->
<el-form-item label="失效时间/秒" prop="expireSeconds" v-if="dataForm.isTemp">
<el-input v-model="dataForm.expireSeconds" placeholder="单位最大259200030天"></el-input>
<div>最大30天当前设置<span class="text-warning">{{dataForm.expireSeconds/(24*3600)}}</span></div>
</el-form-item>
</el-form>
<!-- 对话框底部按钮 -->
<span slot="footer" class="dialog-footer">
<el-button @click="visible = false">取消</el-button>
<el-button type="primary" @click="dataFormSubmit()"></el-button>
</span>
</el-dialog>
</template>
<script>
export default {
data() {
return {
visible: false, //
dataForm: {
isTemp: true, //
sceneStr: '', //
expireSeconds: 2592000 // 30
},
dataRule: {
isTemp: [
{ required: true, message: '二维码类型不能为空', trigger: 'blur' } //
],
sceneStr: [
{ required: true, message: '场景值ID不能为空', trigger: 'blur' } //
],
expireSeconds: [
{ required: true, message: '该二维码失效时间不能为空', trigger: 'blur' } //
]
}
}
},
methods: {
// id
init(id) {
this.dataForm.id = id || 0 // dataFormid
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`), // URL
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,161 @@
<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="类型">
<!-- 根据isTemp的值显示临时永久 -->
<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) // id
})
},
//
deleteHandle(id) {
var ids = id ? [id] : this.dataListSelections.map(item => item.id) // id
this.$confirm(`确定对[id=${ids.join(',')}]进行[${id ? '删除' : '批量删除'}]操作?(仅删存档)`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
this.$http({
url: this.$http.adornUrl('/manage/wxQrCode/delete'), //
method: 'post',
data: this.$http.adornData(ids, false) // id
}).then(({ data }) => {
if (data && data.code === 200) {
this.$message({
message: '操作成功',
type: 'success',
duration: 1500,
onClose: () => this.getDataList() //
})
} else {
this.$message.error(data.msg) //
}
})
})
}
}
}
</script>

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

@ -0,0 +1,250 @@
<template>
<div class="mod-config">
<!-- 使用内联表单绑定数据模型并在按下Enter键时调用获取数据列表的方法 -->
<el-form :inline="true" :model="dataForm" @keyup.enter.native="getDataList()">
<!-- 用户标签选择器 -->
<el-form-item>
<el-select v-model="dataForm.tagid" filterable clearable placeholder="用户标签">
<!-- 循环遍历wxUserTags数组生成选项 -->
<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>
<!-- 根据权限显示绑定标签按钮如果选中的用户数为0则禁用 -->
<el-button v-if="isAuth('wx:wxuser:save')" type="primary" @click="$refs.wxUserTagging.init('tagging')" :disabled="dataListSelections.length <= 0"></el-button>
<!-- 根据权限显示解绑标签按钮如果选中的用户数为0则禁用 -->
<el-button v-if="isAuth('wx:wxuser:save')" type="primary" @click="$refs.wxUserTagging.init('untagging')" :disabled="dataListSelections.length <= 0"></el-button>
<!-- 根据权限显示批量删除按钮如果选中的用户数为0则禁用 -->
<el-button v-if="isAuth('wx:wxuser:delete')" type="danger" @click="deleteHandle()" :disabled="dataListSelections.length <= 0"></el-button>
</el-form-item>
<!-- 其他操作按钮 -->
<el-form-item class="fr">
<!-- 打开标签管理界面 -->
<el-button icon="el-icon-price-tag" type="success" @click="$refs.wxUserTagsEditor.show()"></el-button>
<!-- 同步粉丝数据 -->
<el-button icon="el-icon-sort" type="success" @click="syncWxUsers()"></el-button>
</el-form-item>
</el-form>
<!-- 数据表格 -->
<el-table :data="dataList" border v-loading="dataListLoading" @selection-change="selectionChangeHandle" style="width: 100%;">
<!-- 多选框 -->
<el-table-column type="selection" header-align="center" align="center" width="50"></el-table-column>
<!-- openid列 -->
<el-table-column prop="openid" header-align="center" align="center" label="openid"></el-table-column>
<!-- 昵称列 -->
<el-table-column prop="nickname" header-align="center" align="center" label="昵称"></el-table-column>
<!-- 性别列使用formatter格式化显示 -->
<el-table-column prop="sex" header-align="center" align="center" label="性别" :formatter="sexFormat"></el-table-column>
<!-- 城市列 -->
<el-table-column prop="city" header-align="center" align="center" label="城市"></el-table-column>
<!-- 头像列使用插槽自定义显示 -->
<el-table-column prop="headimgurl" header-align="center" align="center" label="头像">
<img class="headimg" slot-scope="scope" v-if="scope.row.headimgurl" :src="scope.row.headimgurl" />
</el-table-column>
<!-- 标签列使用插槽和循环显示多个标签 -->
<el-table-column prop="tagidList" header-align="center" align="center" label="标签" show-overflow-tooltip>
<template slot-scope="scope">
<span v-for="tagid in scope.row.tagidList" :key="tagid">{{getTagName(tagid)}} </span>
</template>
</el-table-column>
<!-- 订阅时间列使用moment格式化显示 -->
<el-table-column prop="subscribeTime" header-align="center" align="center" label="订阅时间">
<template slot-scope="scope">{{$moment(scope.row.subscribeTime).calendar()}}</template>
</el-table-column>
<!-- 场景值列 -->
<el-table-column prop="qrSceneStr" header-align="center" align="center" label="场景值"></el-table-column>
<!-- 是否关注列使用插槽自定义显示 -->
<el-table-column prop="subscribe" header-align="center" align="center" label="是否关注">
<span slot-scope="scope">{{scope.row.subscribe?"是":"否"}}</span>
</el-table-column>
<!-- 操作列包含删除按钮 -->
<el-table-column fixed="right" header-align="center" align="center" width="150" label="操作">
<template slot-scope="scope">
<el-button type="text" size="small" @click="deleteHandle(scope.row.openid)"></el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页组件 -->
<el-pagination @size-change="sizeChangeHandle" @current-change="currentChangeHandle" :current-page="pageIndex" :page-sizes="[10, 20, 50, 100]" :page-size="pageSize" :total="totalPage" layout="total, sizes, prev, pager, next, jumper"></el-pagination>
<!-- 标签管理组件 -->
<wx-user-tags-manager ref="wxUserTagsEditor" :visible="showWxUserTagsEditor" @close="showWxUserTagsEditor=false"></wx-user-tags-manager>
<!-- 用户标签绑定/解绑组件 -->
<wx-user-tagging ref="wxUserTagging" :wxUsers="dataListSelections"></wx-user-tagging>
</div>
</template>
<script>
//
import WxUserTagsManager from '@/components/wx-user-tags-manager'
import WxUserTagging from './wx-user-tagging'
// vuexmapState
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
},
// keep-alive
activated() {
this.getDataList() //
},
// vuexstatewxUserTags
computed: mapState({
wxUserTags: state => state.wxUserTags.tags
}),
methods: {
//
getDataList() {
this.dataListLoading = true //
this.$http({ // HTTP
url: this.$http.adornUrl('/manage/wxUser/list'), // URL
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) {
// 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];
},
// tagid
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