Merge branch 'main' of bdgit.educoder.net:mkaoj697q/WeChat into branch4

pull/24/head
1234567 4 months ago
commit 65e2715746

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

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

@ -0,0 +1 @@
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,116 @@
<template>
<main class="site-content" :class="{ 'site-content--tabs': $route.meta.isTab }">
<!-- 如果当前路由元信息中包含isTab则显示标签页组件 -->
<el-tabs v-if="$route.meta.isTab" v-model="mainTabsActiveName" :closable="true" @tab-click="selectedTabHandle"
@tab-remove="removeTabHandle">
<!-- 标签页工具菜单包含关闭当前关闭其他关闭全部和刷新功能 -->
<el-dropdown class="site-tabs__tools" :show-timeout="0">
<i class="el-icon-arrow-down el-icon--right"></i>
<el-dropdown-menu slot="dropdown">
<el-dropdown-item @click.native="tabsCloseCurrentHandle">关闭当前标签页</el-dropdown-item>
<el-dropdown-item @click.native="tabsCloseOtherHandle">关闭其它标签页</el-dropdown-item>
<el-dropdown-item @click.native="tabsCloseAllHandle">关闭全部标签页</el-dropdown-item>
<el-dropdown-item @click.native="refresh()">刷新当前标签页</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
<!-- 循环渲染标签页 -->
<el-tab-pane v-for="item in mainTabs" :key="item.name" :label="item.title" :name="item.name">
<el-card :body-style="siteContentViewHeight">
<!-- 如果标签页类型为iframe则渲染iframe -->
<iframe v-if="item.type === 'iframe'" :src="item.iframeUrl" width="100%" height="100%"
frameborder="0" scrolling="yes"></iframe>
<!-- 否则渲染router-view并使用keep-alive缓存组件 -->
<keep-alive v-else>
<router-view v-if="item.name === mainTabsActiveName" />
</keep-alive>
</el-card>
</el-tab-pane>
</el-tabs>
<!-- 如果当前路由元信息中不包含isTab则直接渲染router-view -->
<el-card v-else :body-style="siteContentViewHeight">
<keep-alive>
<router-view />
</keep-alive>
</el-card>
</main>
</template>
<script>
import { isURL } from '@/utils/validate'
export default {
// refresh
inject: ['refresh'],
data() {
return {
//
}
},
computed: {
//
documentClientHeight: {
get() { return this.$store.state.common.documentClientHeight }
},
//
menuActiveName: {
get() { return this.$store.state.common.menuActiveName },
set(val) { this.$store.commit('common/updateMenuActiveName', val) }
},
//
mainTabs: {
get() { return this.$store.state.common.mainTabs },
set(val) { this.$store.commit('common/updateMainTabs', val) }
},
//
mainTabsActiveName: {
get() { return this.$store.state.common.mainTabsActiveName },
set(val) { this.$store.commit('common/updateMainTabsActiveName', val) }
},
//
siteContentViewHeight() {
var height = this.documentClientHeight - 50 - 30 - 2
if (this.$route.meta.isTab) {
height -= 40
// metaiframeUrlURL
return isURL(this.$route.meta.iframeUrl) ? { height: height + 'px' } : { minHeight: height + 'px' }
}
return { minHeight: height + 'px' }
}
},
methods: {
//
selectedTabHandle(tab) {
tab = this.mainTabs.filter(item => item.name === tab.name)
if (tab.length >= 1) {
this.$router.push({ name: tab[0].name, query: tab[0].query, params: tab[0].params })
}
},
// Vuex
removeTabHandle(tabName) {
this.$store.commit('common/removeTab', tabName)
},
//
tabsCloseCurrentHandle() {
this.removeTabHandle(this.mainTabsActiveName)
},
//
tabsCloseOtherHandle() {
this.mainTabs = this.mainTabs.filter(item => item.name === this.mainTabsActiveName)
},
//
tabsCloseAllHandle() {
this.mainTabs = []
this.menuActiveName = ''
this.$router.push({ name: 'home' })
},
// tabsRefreshCurrentHandlerefresh
tabsRefreshCurrentHandle() {
var tab = this.$route
this.removeTabHandle(tab.name)
this.$nextTick(() => {
this.$router.push({ name: tab.name, query: tab.query, params: tab.params })
})
}
// refresh
}
}
</script>

@ -0,0 +1,126 @@
<template>
<!-- Element UI的对话框组件用于显示修改密码的表单 -->
<el-dialog title="修改密码" :visible.sync="visible" :append-to-body="true">
<!-- 表单组件用于数据绑定和验证 -->
<el-form :model="dataForm" :rules="dataRule" ref="dataForm" @keyup.enter.native="dataFormSubmit()"
label-width="80px">
<!-- 账号显示区域使用计算属性获取用户名 -->
<el-form-item label="账号">
<span>{{ userName }}</span>
</el-form-item>
<!-- 原密码输入框 -->
<el-form-item label="原密码" prop="password">
<el-input type="password" v-model="dataForm.password"></el-input>
</el-form-item>
<!-- 新密码输入框 -->
<el-form-item label="新密码" prop="newPassword">
<el-input type="password" v-model="dataForm.newPassword"></el-input>
</el-form-item>
<!-- 确认密码输入框带有自定义验证器以确保与新密码一致 -->
<el-form-item label="确认密码" prop="confirmPassword">
<el-input type="password" v-model="dataForm.confirmPassword"></el-input>
</el-form-item>
</el-form>
<!-- 对话框底部按钮区域 -->
<span slot="footer" class="dialog-footer">
<el-button @click="visible = false">取消</el-button>
<el-button type="primary" @click="dataFormSubmit()"></el-button>
</span>
</el-dialog>
</template>
<script>
import { clearLoginInfo } from '@/utils' //
export default {
data() {
//
var validateConfirmPassword = (rule, value, callback) => {
if (this.dataForm.newPassword !== value) {
callback(new Error('确认密码与新密码不一致'))
} else {
callback()
}
}
return {
//
visible: false,
//
dataForm: {
password: '',
newPassword: '',
confirmPassword: ''
},
//
dataRule: {
password: [
{ required: true, message: '原密码不能为空', trigger: 'blur' }
],
newPassword: [
{ required: true, message: '新密码不能为空', trigger: 'blur' }
],
confirmPassword: [
{ required: true, message: '确认密码不能为空', trigger: 'blur' },
{ validator: validateConfirmPassword, trigger: 'blur' }
]
}
}
},
computed: {
//
userName: {
get() { return this.$store.state.user.name }
},
//
mainTabs: {
get() { return this.$store.state.common.mainTabs },
set(val) { this.$store.commit('common/updateMainTabs', val) }
}
},
methods: {
//
init() {
this.visible = true
this.$nextTick(() => {
this.$refs['dataForm'].resetFields()
})
},
//
dataFormSubmit() {
//
this.$refs['dataForm'].validate((valid) => {
if (valid) {
//
this.$http({
url: this.$http.adornUrl('/sys/user/password'), // URL
method: 'post',
data: this.$http.adornData({ //
'password': this.dataForm.password,
'newPassword': this.dataForm.newPassword
})
}).then(({ data }) => {
if (data && data.code === 200) {
//
this.$message({
message: '操作成功',
type: 'success',
duration: 1500,
onClose: () => {
this.visible = false //
this.$nextTick(() => {
this.mainTabs = [] //
clearLoginInfo() //
this.$router.replace({ name: 'login' }) //
})
}
})
} else {
//
this.$message.error(data.msg)
}
})
}
})
}
}
}
</script>

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

@ -0,0 +1,200 @@
<template>
<div>
<!-- 应用菜单容器 -->
<div id="app-menu">
<!-- 预览窗口 -->
<div class="weixin-preview">
<div class="weixin-bd">
<div class="weixin-header">公众号菜单</div>
<!-- 菜单列表 -->
<ul class="weixin-menu" id="weixin-menu">
<!-- 循环遍历主菜单项 -->
<li v-for="(btn, i) in menu.buttons" :key="i" class="menu-item"
:class="{ 'current': selectedMenuIndex === i && selectedMenuLevel == 1 }"
@click="selectMenu(i)">
<div class="menu-item-title">
<span>{{ btn.name }}</span> <!-- 显示菜单项名称 -->
</div>
<!-- 子菜单列表 -->
<ul class="weixin-sub-menu">
<!-- 循环遍历子菜单项 -->
<li v-for="(sub, i2) in btn.subButtons" :key="i2" class="menu-sub-item" :class="{
'current': selectedMenuIndex === i && selectedSubMenuIndex === i2 &&
selectedMenuLevel == 2, 'on-drag-over': onDragOverMenu == (i + '_' + i2)
}"
@click.stop="selectSubMenu(i, i2)" draggable="true"
@dragstart="selectSubMenu(i, i2)"
@dragover.prevent="onDragOverMenu = (i + '_' + i2)" @drop="onDrop(i, i2)">
<div class="menu-item-title">
<span>{{ sub.name }}</span> <!-- 显示子菜单项名称 -->
</div>
</li>
<!-- 如果子菜单项少于5个显示添加按钮 -->
<li v-if="btn.subButtons.length < 5" class="menu-sub-item"
:class="{ 'on-drag-over': onDragOverMenu == (i + '_' + btn.subButtons.length) }"
@click.stop="addMenu(2, i)"
@dragover.prevent="onDragOverMenu = (i + '_' + btn.subButtons.length)"
@drop="onDrop(i, btn.subButtons.length)">
<div class="menu-item-title">
<i class="el-icon-plus"></i> <!-- 添加图标 -->
</div>
</li>
<!-- 菜单展开/收起箭头 -->
<i class="menu-arrow arrow_out"></i>
<i class="menu-arrow arrow_in"></i>
</ul>
</li>
<!-- 如果主菜单项少于3个显示添加按钮 -->
<li class="menu-item" v-if="menu.buttons.length < 3" @click="addMenu(1)">
<i class="el-icon-plus"></i> <!-- 添加图标 -->
</li>
</ul>
</div>
</div>
<!-- 菜单编辑器 -->
<div class="weixin-menu-detail" v-if="selectedMenuLevel > 0">
<!-- 菜单按钮编辑器组件 -->
<wx-menu-button-editor :button="selectedButton" :selectedMenuLevel="selectedMenuLevel"
@delMenu="delMenu"></wx-menu-button-editor>
</div>
</div>
<!-- 按钮组根据权限显示 -->
<div class="weixin-btn-group" v-if="isAuth('wx:menu:save')">
<!-- 发布按钮 -->
<el-button type="success" icon="el-icon-upload">发布</el-button>
<!-- 清空按钮 -->
<el-button type="warning" icon="el-icon-delete" @click="delMenu"></el-button>
</div>
</div>
</template>
<script>
export default {
// wxMenuButtonEditor
components: {
wxMenuButtonEditor: () => import('./wx-menu-button-editor')
},
data() {
return {
//
menu: { 'buttons': [] },
//
selectedMenuIndex: '',
//
selectedSubMenuIndex: '',
// 12
selectedMenuLevel: 0,
//
selectedButton: '',
//
onDragOverMenu: ''
}
},
mounted() {
//
this.getWxMenu();
},
methods: {
//
getWxMenu() {
this.$http({
url: this.$http.adornUrl('/manage/wxMenu/getMenu') // API
}).then(({ data }) => {
if (data.code == 200) {
this.menu = data.data.menu; //
} else {
this.$message({
type: 'error',
message: data.msg //
});
}
});
},
//
selectMenu(i) {
this.selectedMenuLevel = 1; //
this.selectedSubMenuIndex = ''; //
this.selectedMenuIndex = i; //
this.selectedButton = this.menu.buttons[i]; //
},
//
selectSubMenu(i, i2) {
this.selectedMenuLevel = 2; //
this.selectedMenuIndex = i; //
this.selectedSubMenuIndex = i2; //
this.selectedButton = this.menu.buttons[i].subButtons[i2]; //
},
//
addMenu(level, i) {
if (level == 1 && this.menu.buttons.length < 3) { //
//
this.menu.buttons.push({
"type": "view",
"name": "菜单名称",
"subButtons": [],
"url": ""
});
this.selectMenu(this.menu.buttons.length - 1); //
}
if (level == 2 && this.menu.buttons[i].subButtons.length < 5) { //
//
this.menu.buttons[i].subButtons.push({
"type": "view",
"name": "子菜单名称",
"url": ""
});
this.selectSubMenu(i, this.menu.buttons[i].subButtons.length - 1); //
}
},
//
delMenu() {
if (this.selectedMenuLevel == 1 && confirm('删除后菜单下设置的内容将被删除')) { //
this.menu.buttons.splice(this.selectedMenuIndex, 1); //
this.unSelectMenu(); //
} else if (this.selectedMenuLevel == 2) { //
this.menu.buttons[this.selectedMenuIndex].subButtons.splice(this.selectedSubMenuIndex, 1); //
this.unSelectMenu(); //
}
},
unSelectMenu() { //
this.selectedMenuLevel = 0; //
this.selectedMenuIndex = ''; //
this.selectedSubMenuIndex = ''; //
this.selectedButton = ''; //
},
//
updateWxMenu() {
this.$http({
url: this.$http.adornUrl('/manage/wxMenu/updateMenu'), // API
data: this.menu,
method: 'post'
}).then(({ data }) => {
if (data.code == 200) {
this.$message.success('操作成功'); //
} else {
this.$message.error(data.msg); //
}
});
},
//
onDrop(i, i2) {
this.onDragOverMenu = ''; //
if (i == this.selectedMenuIndex && i2 == this.selectedSubMenuIndex) //
return;
if (i != this.selectedMenuIndex && this.menu.buttons[i].subButtons.length >= 5) { //
this.$message.error('目标组已满'); //
return;
}
//
this.menu.buttons[i].subButtons.splice(i2, 0, this.selectedButton);
let delSubIndex = this.selectedSubMenuIndex; //
if (i == this.selectedMenuIndex && i2 < this.selectedSubMenuIndex) // +1
delSubIndex++;
this.menu.buttons[this.selectedMenuIndex].subButtons.splice(delSubIndex, 1); //
this.unSelectMenu(); //
}
}
}
</script>
<!-- 引入外部CSS文件 -->
<style src="@/assets/css/wx-menu.css"></style>

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

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

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

@ -0,0 +1,170 @@
<template>
<div class="mod-config">
<!-- 表单用于查询二维码信息 -->
<el-form :inline="true" :model="dataForm" @keyup.enter.native="getDataList()">
<el-form-item>
<el-input v-model="dataForm.sceneStr" placeholder="场景值" clearable></el-input>
</el-form-item>
<el-form-item>
<!-- 查询按钮 -->
<el-button @click="getDataList()"></el-button>
<!-- 新增按钮仅当用户有新增权限时显示 -->
<el-button v-if="isAuth('wx:wxqrcode:save')" type="primary" @click="addOrUpdateHandle()"></el-button>
<!-- 批量删除按钮仅当用户有删除权限且至少选中一项时显示 -->
<el-button v-if="isAuth('wx:wxqrcode:delete')" type="danger" @click="deleteHandle()"
:disabled="dataListSelections.length <= 0">批量删除</el-button>
</el-form-item>
</el-form>
<!-- 表格用于显示二维码列表 -->
<el-table :data="dataList" border v-loading="dataListLoading" @selection-change="selectionChangeHandle"
style="width: 100%;">
<!-- 选择框列 -->
<el-table-column type="selection" header-align="center" align="center" width="50">
</el-table-column>
<!-- ID列 -->
<el-table-column prop="id" header-align="center" align="center" label="ID">
</el-table-column>
<!-- 类型列显示临时永久 -->
<el-table-column prop="isTemp" header-align="center" align="center" label="类型">
<span slot-scope="scope">{{ scope.row.isTemp ? '临时' : '永久' }}</span>
</el-table-column>
<!-- 场景值列 -->
<el-table-column prop="sceneStr" header-align="center" align="center" label="场景值">
</el-table-column>
<!-- 二维码图片列显示二维码链接 -->
<el-table-column prop="ticket" header-align="center" align="center" show-overflow-tooltip label="二维码图片">
<a :href="'https://mp.weixin.qq.com/cgi-bin/showqrcode?ticket=' + scope.row.ticket"
slot-scope="scope">{{ scope.row.ticket }}</a>
</el-table-column>
<!-- 解析后的地址列显示链接 -->
<el-table-column prop="url" header-align="center" align="center" show-overflow-tooltip label="解析后的地址">
<a :href="scope.row.url" slot-scope="scope">{{ scope.row.url }}</a>
</el-table-column>
<!-- 失效时间列 -->
<el-table-column prop="expireTime" header-align="center" align="center" width="100" label="失效时间">
</el-table-column>
<!-- 操作列包含删除按钮 -->
<el-table-column fixed="right" header-align="center" align="center" width="150" label="操作">
<template slot-scope="scope">
<el-button type="text" size="small" @click="deleteHandle(scope.row.id)"></el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页组件 -->
<el-pagination @size-change="sizeChangeHandle" @current-change="currentChangeHandle" :current-page="pageIndex"
:page-sizes="[10, 20, 50, 100]" :page-size="pageSize" :total="totalPage"
layout="total, sizes, prev, pager, next, jumper">
</el-pagination>
<!-- 弹窗组件用于新增/修改二维码信息 -->
<add-or-update v-if="addOrUpdateVisible" ref="addOrUpdate" @refreshDataList="getDataList"></add-or-update>
</div>
</template>
<script>
import AddOrUpdate from './wx-qrcode-add-or-update' // /
export default {
data() {
return {
//
dataForm: {
sceneStr: ''
},
//
dataList: [],
//
pageIndex: 1,
//
pageSize: 10,
//
totalPage: 0,
//
dataListLoading: false,
//
dataListSelections: [],
// /
addOrUpdateVisible: false
}
},
components: {
AddOrUpdate // /
},
activated() {
this.getDataList() //
},
methods: {
//
getDataList() {
this.dataListLoading = true //
this.$http({ //
url: this.$http.adornUrl('/manage/wxQrCode/list'), //
method: 'get',
params: this.$http.adornParams({ //
'page': this.pageIndex,
'limit': this.pageSize,
'sceneStr': this.dataForm.sceneStr,
'sidx': 'id',
'order': 'desc'
})
}).then(({ data }) => { //
if (data && data.code === 200) {
this.dataList = data.page.list //
this.totalPage = data.page.totalCount //
} else {
this.dataList = [] //
this.totalPage = 0 //
}
this.dataListLoading = false //
})
},
//
sizeChangeHandle(val) {
this.pageSize = val //
this.pageIndex = 1 //
this.getDataList() //
},
//
currentChangeHandle(val) {
this.pageIndex = val //
this.getDataList() //
},
//
selectionChangeHandle(val) {
this.dataListSelections = val //
},
// /
addOrUpdateHandle(id) {
this.addOrUpdateVisible = true //
this.$nextTick(() => { // DOM
this.$refs.addOrUpdate.init(id) // idnull
})
},
//
deleteHandle(id) {
var ids = id ? [id] : this.dataListSelections.map(item => item.id) // id
this.$confirm(`确定对[id=${ids.join(',')}]进行[${id ? '删除' : '批量删除'}]操作?(仅删存档)`, '提示', { //
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => { //
this.$http({ //
url: this.$http.adornUrl('/manage/wxQrCode/delete'),
method: 'post',
data: this.$http.adornData(ids, false) // id
}).then(({ data }) => { //
if (data && data.code === 200) {
this.$message({ //
message: '操作成功',
type: 'success',
duration: 1500,
onClose: () => this.getDataList() //
})
} else {
this.$message.error(data.msg) //
}
})
})
}
}
}
</script>

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

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