feat(demo): 恢复后台登录鉴权并新增模板配置管理页面

- 恢复 permission.js 路由守卫,未登录访问自动跳转登录页
- 登录、用户信息、注销接口由 mock 路径改为后端 /auth 接口
- 请求拦截器恢复 X-Token 请求头注入,并增加 40101 登录失效处理
- 登录页移除模板自带的演示账号提示与默认填充值,标题改为千帆公众号管理后台
- 用户名校验由白名单 admin/editor 改为非空校验
- 移除生产环境构建时挂载的 mockXHR,避免劫持真实请求
- 新增 api/template.js 封装模板配置的查询、保存、删除、刷新缓存与微信模板拉取接口
- 新增模板管理页面,支持关键字筛选、新增编辑、删除、刷新缓存及从微信拉取模板自动解析关键字
- 路由新增模板管理菜单项
qianfan/wechat/gzh/fwh/hq
soul2 4 days ago
parent 89eaec3795
commit 4b56e05536
  1. 4
      .env.development
  2. 62
      src/api/template.js
  3. 11
      src/api/user.js
  4. 13
      src/main.js
  5. 128
      src/permission.js
  6. 11
      src/router/index.js
  7. 19
      src/utils/request.js
  8. 3
      src/utils/validate.js
  9. 15
      src/views/login/index.vue
  10. 256
      src/views/template/index.vue

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

@ -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) { export function login(data) {
return request({ return request({
url: '/vue-admin-template/user/login', url: '/auth/login',
method: 'post', method: 'post',
data data
}) })
} }
export function getInfo(token) { export function getInfo() {
return request({ return request({
url: '/vue-admin-template/user/info', url: '/auth/info',
method: 'get', method: 'get'
params: { token }
}) })
} }
export function logout() { export function logout() {
return request({ return request({
url: '/vue-admin-template/user/logout', url: '/auth/logout',
method: 'post' method: 'post'
}) })
} }

@ -12,19 +12,6 @@ import '@/icons' // icon
import '@/permission' // permission control import '@/permission' // permission control
import elementGlobal from './element-ui-global' 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 // set ElementUI lang to EN
// Vue.use(ElementUI, { locale }) // Vue.use(ElementUI, { locale })
// 如果想要中文版 element-ui,按如下方式声明 // 如果想要中文版 element-ui,按如下方式声明

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

@ -54,6 +54,17 @@ export const constantRoutes = [
}] }]
}, },
{
path: '/template',
component: Layout,
children: [{
path: 'index',
name: 'Template',
component: () => import('@/views/template/index'),
meta: { title: '模板管理', icon: 'form' }
}]
},
// { // {
// path: '/dfadfa', // path: '/dfadfa',
// component: Layout, // component: Layout,

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

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

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