Compare commits

...

2 Commits

Author SHA1 Message Date
soul2 4b56e05536 feat(demo): 恢复后台登录鉴权并新增模板配置管理页面 4 days ago
soul2 89eaec3795 feat(demo): 重构微信公众号关注用户管理功能 4 days ago
  1. 3
      .env.development
  2. 2
      .env.production
  3. BIN
      public/favicon.ico
  4. 14
      src/api/demo.js
  5. 62
      src/api/template.js
  6. 11
      src/api/user.js
  7. 13
      src/main.js
  8. 128
      src/permission.js
  9. 33
      src/router/index.js
  10. 2
      src/settings.js
  11. 21
      src/utils/request.js
  12. 3
      src/utils/validate.js
  13. 104
      src/views/demo/index.vue
  14. 15
      src/views/login/index.vue
  15. 256
      src/views/template/index.vue
  16. 2
      vue.config.js

@ -2,4 +2,5 @@
ENV = 'development'
# base api
VUE_APP_BASE_API = 'http://localhost:7301'
VUE_APP_BASE_API = 'http://localhost:7710/wxmessage'
#VUE_APP_BASE_API = 'https://alumni.zdxh.cn/kj/wxmessage'

@ -2,5 +2,5 @@
ENV = 'production'
# base api
VUE_APP_BASE_API = '/prod-api'
VUE_APP_BASE_API = 'https://alumni.zdxh.cn/kj/wxmessage'

Binary file not shown.

Before

Width:  |  Height:  |  Size: 66 KiB

After

Width:  |  Height:  |  Size: 13 KiB

@ -1,22 +1,24 @@
import request from '@/utils/request'
let assew = 'assew'
export function page(data) {
return request({
url: '/demo/page',
url: `/${assew}/page`,
data
})
}
export function save(data) {
export function refreshOpenId(data) {
return request({
url: '/demo/save',
url: `/${assew}/refresh/openid`,
data
})
}
export function remove(data) {
export function initUserList() {
return request({
url: '/demo/remove',
data
url: `/${assew}/init/user-list`,
})
}

@ -0,0 +1,62 @@
import request from '@/utils/request'
/**
* 模板列表
* @param data { onlyEnabled, keyword }
*/
export function page(data) {
return request({
url: '/template/list',
data
})
}
/**
* 模板详情
*/
export function detail(id) {
return request({
url: `/template/detail/${id}`,
method: 'get'
})
}
/**
* 新增或更新模板
*/
export function save(data) {
return request({
url: '/template/save',
data
})
}
/**
* 删除模板
*/
export function remove(id) {
return request({
url: `/template/delete/${id}`,
data: {}
})
}
/**
* 刷新发送缓存
*/
export function refresh() {
return request({
url: '/template/refresh',
data: {}
})
}
/**
* 从微信拉取当前公众号的模板列表
*/
export function wechatList() {
return request({
url: '/template/wechat/list',
data: {}
})
}

@ -2,23 +2,22 @@ import request from '@/utils/request'
export function login(data) {
return request({
url: '/vue-admin-template/user/login',
url: '/auth/login',
method: 'post',
data
})
}
export function getInfo(token) {
export function getInfo() {
return request({
url: '/vue-admin-template/user/info',
method: 'get',
params: { token }
url: '/auth/info',
method: 'get'
})
}
export function logout() {
return request({
url: '/vue-admin-template/user/logout',
url: '/auth/logout',
method: 'post'
})
}

@ -12,19 +12,6 @@ import '@/icons' // icon
import '@/permission' // permission control
import elementGlobal from './element-ui-global'
/**
* If you don't want to use mock-server
* you want to use MockJs for mock api
* you can execute: mockXHR()
*
* Currently MockJs will be used in the production environment,
* please remove it before going online ! ! !
*/
if (process.env.NODE_ENV === 'production') {
const { mockXHR } = require('../mock')
mockXHR()
}
// set ElementUI lang to EN
// Vue.use(ElementUI, { locale })
// 如果想要中文版 element-ui,按如下方式声明

@ -1,64 +1,64 @@
// import router from './router'
// import store from './store'
// import { Message } from 'element-ui'
// import NProgress from 'nprogress' // progress bar
// import 'nprogress/nprogress.css' // progress bar style
// import { getToken } from '@/utils/auth' // get token from cookie
// import getPageTitle from '@/utils/get-page-title'
//
// NProgress.configure({ showSpinner: false }) // NProgress Configuration
//
// const whiteList = ['/login'] // no redirect whitelist
//
// router.beforeEach(async(to, from, next) => {
// // start progress bar
// NProgress.start()
//
// // set page title
// document.title = getPageTitle(to.meta.title)
//
// // determine whether the user has logged in
// const hasToken = getToken()
//
// if (hasToken) {
// if (to.path === '/login') {
// // if is logged in, redirect to the home page
// next({ path: '/' })
// NProgress.done()
// } else {
// const hasGetUserInfo = store.getters.name
// if (hasGetUserInfo) {
// next()
// } else {
// try {
// // get user info
// await store.dispatch('user/getInfo')
//
// next()
// } catch (error) {
// // remove token and go to login page to re-login
// await store.dispatch('user/resetToken')
// Message.error(error || 'Has Error')
// next(`/login?redirect=${to.path}`)
// NProgress.done()
// }
// }
// }
// } else {
// /* has no token*/
//
// if (whiteList.indexOf(to.path) !== -1) {
// // in the free login whitelist, go directly
// next()
// } else {
// // other pages that do not have permission to access are redirected to the login page.
// next(`/login?redirect=${to.path}`)
// NProgress.done()
// }
// }
// })
//
// router.afterEach(() => {
// // finish progress bar
// NProgress.done()
// })
import router from './router'
import store from './store'
import { Message } from 'element-ui'
import NProgress from 'nprogress' // progress bar
import 'nprogress/nprogress.css' // progress bar style
import { getToken } from '@/utils/auth' // get token from cookie
import getPageTitle from '@/utils/get-page-title'
NProgress.configure({ showSpinner: false }) // NProgress Configuration
const whiteList = ['/login'] // no redirect whitelist
router.beforeEach(async(to, from, next) => {
// start progress bar
NProgress.start()
// set page title
document.title = getPageTitle(to.meta.title)
// determine whether the user has logged in
const hasToken = getToken()
if (hasToken) {
if (to.path === '/login') {
// if is logged in, redirect to the home page
next({ path: '/' })
NProgress.done()
} else {
const hasGetUserInfo = store.getters.name
if (hasGetUserInfo) {
next()
} else {
try {
// get user info
await store.dispatch('user/getInfo')
next()
} catch (error) {
// remove token and go to login page to re-login
await store.dispatch('user/resetToken')
Message.error(error || 'Has Error')
next(`/login?redirect=${to.path}`)
NProgress.done()
}
}
}
} else {
/* has no token*/
if (whiteList.indexOf(to.path) !== -1) {
// in the free login whitelist, go directly
next()
} else {
// other pages that do not have permission to access are redirected to the login page.
next(`/login?redirect=${to.path}`)
NProgress.done()
}
}
})
router.afterEach(() => {
// finish progress bar
NProgress.done()
})

@ -49,24 +49,35 @@ export const constantRoutes = [
children: [{
path: 'dashboard',
name: 'Dashboard',
component: () => import('@/views/dashboard/index'),
meta: { title: '欢迎', icon: 'dashboard' }
component: () => import('@/views/demo/index'),
meta: { title: '关注用户', icon: 'dashboard' }
}]
},
{
path: '/demo',
path: '/template',
component: Layout,
children: [
{
path: 'index',
name: 'Demo',
component: () => import('@/views/demo/index'),
meta: { title: '表格示例', icon: 'table' }
}
]
children: [{
path: 'index',
name: 'Template',
component: () => import('@/views/template/index'),
meta: { title: '模板管理', icon: 'form' }
}]
},
// {
// path: '/dfadfa',
// component: Layout,
// children: [
// {
// path: 'index',
// name: 'Dfadfa',
// component: () => import('@/views/demo/index'),
// meta: { title: '关注用户', icon: 'table' }
// }
// ]
// },
// 404 page must be placed at the end !!!
{ path: '*', redirect: '/404', hidden: true }
]

@ -1,6 +1,6 @@
module.exports = {
title: 'Vue Demo For Soul2',
title: '关注用户 - 千帆公众号',
/**
* @type {boolean} true | false

@ -1,6 +1,7 @@
import axios from 'axios'
import { Message, MessageBox } from 'element-ui'
import store from '@/store'
import { getToken } from '@/utils/auth'
// create an axios instance
const service = axios.create({
@ -16,14 +17,11 @@ const service = axios.create({
// request interceptor
service.interceptors.request.use(
config => {
// do something before request is sent
// if (store.getters.token) {
// // let each request carry token
// // ['X-Token'] is a custom headers key
// // please modify it according to the actual situation
// config.headers['X-Token'] = getToken()
// }
// 每个请求携带后台登录令牌
const token = getToken()
if (token) {
config.headers['X-Token'] = token
}
return config
},
error => {
@ -49,15 +47,16 @@ service.interceptors.response.use(
const res = response.data
// if the custom code is not 0, it is judged as an error.
if (res.code !== 0) {
if (res.code !== 200) {
Message({
message: res.message || 'Error',
type: 'error',
duration: 5 * 1000
})
// 40401:非法令牌;40402:其他客户登录;40403:令牌过期;
if (res.code === 40401 || res.code === 40402 || res.code === 40403) {
// 40101:登录信息丢失(MsgPlatform 统一鉴权返回)
// 40401:非法令牌;40402:其他客户登录;40403:令牌过期(历史遗留码,保留兼容)
if (res.code === 40101 || res.code === 40401 || res.code === 40402 || res.code === 40403) {
// to re-login
MessageBox.confirm('当前账户的登录信息已过期,点击“取消”按钮停留在当前页面,或重新登录', '走丢了', {
confirmButtonText: '重新登录',

@ -15,6 +15,5 @@ export function isExternal(path) {
* @returns {Boolean}
*/
export function validUsername(str) {
const valid_map = ['admin', 'editor']
return valid_map.indexOf(str.trim()) >= 0
return typeof str === 'string' && str.trim().length > 0
}

@ -6,8 +6,11 @@
@keyup.enter.native="conditionQuery"
>
<!--查询条件-->
<el-form-item label="姓名" prop="name">
<el-input v-model="condition.name" placeholder="请输入" clearable/>
<el-form-item label="openid" prop="openid">
<el-input v-model="condition.openid" placeholder="openid" clearable/>
</el-form-item>
<el-form-item label="unionid" prop="unionid">
<el-input v-model="condition.unionid" placeholder="unionid" clearable/>
</el-form-item>
<!-- End 查询条件 -->
<el-form-item>
@ -21,8 +24,11 @@
<!-- 表格操作 -->
<el-card class="table-operate-container" shadow="never">
<el-button size="mini" icon="el-icon-plus" type="primary" plain @click="addRow">添加</el-button>
<el-button :disabled="!hasSelection" type="danger" size="mini" @click="removeBatchRows">批量删除
<!-- <el-button size="mini" icon="el-icon-plus" type="primary" plain @click="addRow">添加</el-button>-->
<!-- <el-button :disabled="!hasSelection" type="danger" size="mini" @click="removeBatchRows">批量删除-->
<!-- </el-button>-->
<el-button type="danger" size="mini" @click="initUserList">
初始化
</el-button>
</el-card>
@ -34,15 +40,16 @@
>
<el-table-column type="selection" width="40" align="center"/>
<el-table-column type="index" label="序号" width="60" align="center"/>
<el-table-column property="name" label="姓名" min-width="25%" align="center"/>
<el-table-column property="attr1" label="属性1" min-width="25%" align="center"/>
<el-table-column property="attr2" label="属性2" min-width="25%" align="center"/>
<el-table-column property="updatedTime" label="更新时间" min-width="25%" align="center"/>
<el-table-column property="createdTime" label="创建时间" min-width="25%" align="center"/>
<el-table-column label="操作" width="180" align="center">
<el-table-column property="openid" label="openid" min-width="25%" align="center"/>
<el-table-column property="unionid" label="unionid" min-width="25%" align="center"/>
<el-table-column property="subscribe_scene" label="订阅来源" width="120" align="center"/>
<el-table-column property="updated_time" label="更新时间" width="200" align="center"/>
<el-table-column property="created_time" label="创建时间" width="200" align="center"/>
<el-table-column label="操作" width="210" align="center">
<template slot-scope="scope">
<el-button size="mini" @click.stop="handleEdit(scope.row)">编辑</el-button>
<el-button type="danger" plain size="mini" @click.stop="handleRemove(scope.row)">删除</el-button>
<!-- <el-button size="mini" @click.stop="handleEdit(scope.row)">编辑</el-button>-->
<el-button size="mini" @click.stop="refreshPull(scope.row)">重新拉取</el-button>
<!-- <el-button type="danger" plain size="mini" @click.stop="handleRemove(scope.row)">删除</el-button>-->
</template>
</el-table-column>
</el-table>
@ -60,15 +67,15 @@
</div>
<!-- End 表格 -->
<!-- 编辑弹窗 -->
<edit-dialog v-if="showDialog" :row="editRowData" :show.sync="showDialog" :is-add="isAddDialog"
@saved="conditionQuery"
/>
<!-- &lt;!&ndash; 编辑弹窗 &ndash;&gt;-->
<!-- <edit-dialog v-if="showDialog" :row="editRowData" :show.sync="showDialog" :is-add="isAddDialog"-->
<!-- @saved="conditionQuery"-->
<!-- />-->
</div>
</template>
<script>
import { page, remove } from '@/api/demo'
import {initUserList, page, refreshOpenId} from '@/api/demo'
import editDialog from './edit'
/**
* @version 2.0
@ -76,7 +83,8 @@ import editDialog from './edit'
/* Base */
const default_condition = {
name: '',
openid: '',
unionid: '',
pageNumber: 1,
pageSize: 10
}
@ -122,6 +130,66 @@ export default {
this.conditionQuery()
},
methods: {
initUserList() {
let title = '初始化关注列表?该操作会从公众号读取关注用户覆盖当前数据库'
this.$confirm(title, '提示', {
type: 'warning'
}).then(() => {
const loading = this.$loading({
background: this.$elementGlobal.loadingBackground
})
initUserList().then(r => {
loading.close()
if (r.data) {
this.$message({
type: 'success',
message: '操作成功!'
})
} else {
this.$message({
type: 'error',
message: '操作失败!'
})
}
this.conditionQuery()
}).catch(e => {
loading.close()
console.log(e)
})
}).catch(() => {
})
},
refreshPull(row) {
let title = '重新拉取该用户的数据?该操作会从公众号读取关注用户覆盖当前数据'
this.$confirm(title, '提示', {
type: 'warning'
}).then(() => {
const loading = this.$loading({
background: this.$elementGlobal.loadingBackground
})
refreshOpenId(row.openid).then(r => {
loading.close()
if (r.data) {
this.$message({
type: 'success',
message: '操作成功!'
})
} else {
this.$message({
type: 'error',
message: '操作失败!'
})
}
this.conditionQuery()
}).catch(e => {
loading.close()
console.log(e)
})
}).catch(() => {
})
},
/* Base */
addRow() {
// 表格操作 - 添加

@ -5,7 +5,7 @@
>
<div class="title-container">
<h3 class="title">Login Form</h3>
<h3 class="title">千帆公众号管理后台</h3>
</div>
<el-form-item prop="username">
@ -15,7 +15,7 @@
<el-input
ref="username"
v-model="loginForm.username"
placeholder="Username"
placeholder="用户名"
name="username"
type="text"
tabindex="1"
@ -32,7 +32,7 @@
ref="password"
v-model="loginForm.password"
:type="passwordType"
placeholder="Password"
placeholder="密码"
name="password"
tabindex="2"
auto-complete="on"
@ -48,11 +48,6 @@
>Login
</el-button>
<div class="tips">
<span style="margin-right:20px;">username: admin</span>
<span> password: any</span>
</div>
</el-form>
</div>
</template>
@ -79,8 +74,8 @@ export default {
}
return {
loginForm: {
username: 'admin',
password: '111111'
username: '',
password: ''
},
loginRules: {
username: [{ required: true, trigger: 'blur', validator: validateUsername }],

@ -0,0 +1,256 @@
<template>
<div class="app-container full-height">
<!-- 查询条件 -->
<el-card class="filter-container" shadow="never">
<el-form
label-width="110px"
:inline="true"
:model="condition"
@submit.native.prevent
@keyup.enter.native="conditionQuery"
>
<el-form-item label="关键字" prop="keyword">
<el-input v-model="condition.keyword" placeholder="业务键 / 名称 / 模板ID" clearable />
</el-form-item>
<el-form-item label="只看启用">
<el-switch v-model="condition.onlyEnabled" />
</el-form-item>
<el-form-item>
<el-button type="primary" @click="conditionQuery">查询</el-button>
</el-form-item>
<el-form-item>
<el-button @click="handleResetCondition">重置</el-button>
</el-form-item>
</el-form>
</el-card>
<!-- 表格操作 -->
<el-card class="table-operate-container" shadow="never">
<el-button type="primary" size="mini" icon="el-icon-plus" @click="handleAdd">新增模板</el-button>
<el-button size="mini" icon="el-icon-refresh" @click="handleRefreshCache">刷新缓存</el-button>
<el-button size="mini" icon="el-icon-download" @click="handlePullFromWechat">从微信拉取</el-button>
</el-card>
<!-- 表格 -->
<div class="table-container">
<el-table v-loading="loading" :data="tableData" border style="width: 100%">
<el-table-column type="index" label="序号" width="60" align="center" />
<el-table-column property="biz_key" label="业务键" width="140" align="center">
<template slot-scope="scope">
<span>{{ scope.row.biz_key || '-' }}</span>
</template>
</el-table-column>
<el-table-column property="title" label="名称" width="170" align="center">
<template slot-scope="scope">
<span>{{ scope.row.title || '-' }}</span>
</template>
</el-table-column>
<el-table-column property="template_id" label="模板ID" min-width="30%" align="center" />
<el-table-column property="keywords" label="关键字" min-width="22%" align="center" />
<el-table-column label="状态" width="90" align="center">
<template slot-scope="scope">
<el-tag :type="scope.row.enabled === 1 ? 'success' : 'info'" size="mini">
{{ scope.row.enabled === 1 ? '启用' : '停用' }}
</el-tag>
</template>
</el-table-column>
<el-table-column property="updated_time" label="更新时间" width="170" align="center" />
<el-table-column label="操作" width="150" align="center">
<template slot-scope="scope">
<el-button size="mini" @click.stop="handleEdit(scope.row)">编辑</el-button>
<el-button type="danger" plain size="mini" @click.stop="handleRemove(scope.row)">删除</el-button>
</template>
</el-table-column>
</el-table>
</div>
<!-- 编辑弹窗 -->
<el-dialog :title="dialogTitle" :visible.sync="showDialog" width="600px" @closed="handleDialogClosed">
<el-form ref="editForm" :model="editForm" :rules="editRules" label-width="100px">
<el-form-item label="业务键" prop="biz_key">
<el-input v-model="editForm.biz_key" placeholder="业务代码引用的别名,如 order_paid" />
</el-form-item>
<el-form-item label="名称" prop="title">
<el-input v-model="editForm.title" placeholder="如 订单下单成功通知" />
</el-form-item>
<el-form-item label="模板ID" prop="template_id">
<el-input v-model="editForm.template_id" placeholder="微信侧模板ID" />
</el-form-item>
<el-form-item label="关键字" prop="keywords">
<el-input
v-model="editForm.keywords"
type="textarea"
:rows="2"
placeholder="英文逗号分隔,顺序即组装顺序,如 character_string1,time2,thing5,amount6"
/>
</el-form-item>
<el-form-item label="备注" prop="remark">
<el-input v-model="editForm.remark" type="textarea" :rows="2" />
</el-form-item>
<el-form-item label="启用">
<el-switch v-model="editForm.enabled" :active-value="1" :inactive-value="0" />
</el-form-item>
</el-form>
<div slot="footer">
<el-button @click="showDialog = false">取消</el-button>
<el-button type="primary" :loading="saving" @click="handleSave">保存</el-button>
</div>
</el-dialog>
<!-- 微信模板选择弹窗 -->
<el-dialog title="从微信拉取模板" :visible.sync="showWechatDialog" width="820px">
<el-table v-loading="wechatLoading" :data="wechatTemplates" border max-height="420">
<el-table-column property="title" label="名称" width="180" align="center" />
<el-table-column property="template_id" label="模板ID" min-width="35%" align="center" />
<el-table-column property="keywords" label="解析出的关键字" min-width="25%" align="center" />
<el-table-column label="操作" width="100" align="center">
<template slot-scope="scope">
<el-button size="mini" type="primary" @click="handleUseWechatTemplate(scope.row)">填入</el-button>
</template>
</el-table-column>
</el-table>
</el-dialog>
</div>
</template>
<script>
import { page, save, remove, refresh, wechatList } from '@/api/template'
export default {
name: 'TemplateManage',
data() {
return {
loading: false,
saving: false,
condition: {
keyword: '',
onlyEnabled: false
},
tableData: [],
total: 0,
showDialog: false,
dialogTitle: '新增模板',
editForm: this.getEmptyForm(),
editRules: {
template_id: [{ required: true, message: '请填写模板ID', trigger: 'blur' }],
keywords: [{ required: true, message: '请填写关键字', trigger: 'blur' }]
},
showWechatDialog: false,
wechatLoading: false,
wechatTemplates: []
}
},
created() {
this.conditionQuery()
},
methods: {
getEmptyForm() {
return {
id: null,
biz_key: '',
title: '',
template_id: '',
keywords: '',
remark: '',
enabled: 1
}
},
conditionQuery() {
this.loading = true
page({
keyword: this.condition.keyword,
onlyEnabled: this.condition.onlyEnabled
}).then(res => {
const data = res.data || {}
this.tableData = data.rows || []
this.total = data.total || 0
}).finally(() => {
this.loading = false
})
},
handleResetCondition() {
this.condition.keyword = ''
this.condition.onlyEnabled = false
this.conditionQuery()
},
handleAdd() {
this.dialogTitle = '新增模板'
this.editForm = this.getEmptyForm()
this.showDialog = true
},
handleEdit(row) {
this.dialogTitle = '编辑模板'
this.editForm = Object.assign(this.getEmptyForm(), row)
this.showDialog = true
},
handleDialogClosed() {
if (this.$refs.editForm) {
this.$refs.editForm.clearValidate()
}
},
handleSave() {
this.$refs.editForm.validate(valid => {
if (!valid) return
this.saving = true
save(this.editForm).then(() => {
this.$message.success('保存成功')
this.showDialog = false
this.conditionQuery()
}).finally(() => {
this.saving = false
})
})
},
handleRemove(row) {
this.$confirm(`确认删除模板「${row.title || row.template_id}」?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
remove(row.id).then(() => {
this.$message.success('删除成功')
this.conditionQuery()
})
}).catch(() => {})
},
handleRefreshCache() {
refresh().then(() => {
this.$message.success('缓存已刷新')
})
},
handlePullFromWechat() {
this.showWechatDialog = true
this.wechatLoading = true
wechatList().then(res => {
this.wechatTemplates = res.data || []
}).finally(() => {
this.wechatLoading = false
})
},
handleUseWechatTemplate(row) {
this.showWechatDialog = false
this.dialogTitle = '新增模板'
this.editForm = Object.assign(this.getEmptyForm(), {
title: row.title,
template_id: row.template_id,
keywords: row.keywords
})
this.showDialog = true
}
}
}
</script>
<style lang="scss" scoped>
.app-container {
padding: 16px;
}
.filter-container {
margin-bottom: 12px;
}
.table-operate-container {
margin-bottom: 12px;
}
</style>

@ -24,7 +24,7 @@ module.exports = {
* In most cases please use '/' !!!
* Detail: https://cli.vuejs.org/config/#publicpath
*/
publicPath: '/',
publicPath: '/GaEbgkre9BcJfxTig6bYJBdHw2MJk9W3aGvHQCxGXpXUPBuXD8YC2m8uPTjj4bLa1BsjihAn3AA5UYF4cBE3sj4GKuXdM9muB7Ds/wxmsge/',
outputDir: 'dist',
assetsDir: 'static',
lintOnSave: process.env.NODE_ENV === 'development',

Loading…
Cancel
Save