提交 2150dbd6 authored 作者: pengxiaohui's avatar pengxiaohui

update:新增角色管理模块和使用手册模块

上级 9f2b4d6a
......@@ -5,6 +5,7 @@
<link rel="icon" href="https://zws-imgs-pub.ezijing.com/pc/base/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>营销管理系统-紫荆教育</title>
<script type="text/javascript" src="https://webapp-pub.ezijing.com/plugins/tinymce/tinymce.min.js"></script>
</head>
<body>
<div id="app"></div>
......
......@@ -15,15 +15,26 @@ export function getToken() {
return httpRequest.get('/api/usercenter/aliyun/assume-role')
}
/**
* 文件上传
*/
export function fileUpload(formData) {
return httpRequest({
url: 'https://webapp-pub.oss-cn-beijing.aliyuncs.com',
method: 'post',
headers: { 'Content-Type': 'multipart/form-data' },
timeout: 900000,
data: formData,
withCredentials: false
})
}
// 获取oss signature
export function getSignature() {
return httpRequest.get('/api/usercenter/aliyun/get-signature')
}
// 图片上传
export function uploadFile(data) {
return httpRequest.post('https://webapp-pub.oss-cn-beijing.aliyuncs.com', data, {
withCredentials: false,
headers: { 'Content-Type': 'multipart/form-data' }
})
/**
* 获取登录用户的角色
*/
export function getUserRoles() {
return httpRequest.get('/api/marketing/admin/v1/user/roles')
}
<template>
<aside class="app-aside">
<nav class="nav">
<el-menu :default-active="defaultActive" :router="true">
<el-menu :default-active="defaultActive" :router="true" @select="handlleSelect">
<template v-for="item in menuList">
<el-submenu :index="item.path" :key="item.path" v-if="item.children">
<template #title><i :class="item.icon"></i>{{ item.name }}</template>
......@@ -34,12 +34,6 @@ export default {
path: '/tools/sign-in',
icon: 'el-icon-s-grid',
children: [{ name: '打卡签到', path: '/tools/sign-in' }]
},
{
name: '系统管理',
path: '/system/user',
icon: 'el-icon-s-tools',
children: [{ name: '用户管理', path: '/system/user' }]
}
]
}
......@@ -47,6 +41,34 @@ export default {
computed: {
defaultActive() {
return this.$route.path
},
isAdmin() {
const roles = this.$store.state.roles
return roles.includes('administrator')
}
},
watch: {
isAdmin: {
handler(nv) {
if (nv) {
this.menuList.push({
name: '系统管理',
path: '/system/user',
icon: 'el-icon-s-tools',
children: [
{ name: '用户管理', path: '/system/user' },
{ name: '角色管理', path: '/system/role' },
{ name: '使用手册', path: '/system/manual' }
]
})
}
},
immediate: true
}
},
methods: {
handlleSelect(path) {
this.$router.push(path)
}
}
}
......
......@@ -4,6 +4,7 @@
<router-link to="/"><img src="https://zws-imgs-pub.ezijing.com/pc/base/ezijing-logo-white.svg" /></router-link>
</div>
<div class="app-header-right">
<i class="el-icon-question"></i>
<el-dropdown>
<div class="avatar">
<img :src="user.avatar || 'https://zws-imgs-pub.ezijing.com/pc/base/logo.png'" />
......@@ -53,7 +54,11 @@ export default {
}
.app-header-right {
display: flex;
i{
font-size:20px;
line-height:40px;
margin-right:10px;
}
.avatar {
width: 40px;
height: 40px;
......
<template>
<editor :init="init" v-bind="$attrs" v-on="$listeners" @onChange="onChange" @onBlur="onBlur" />
<div :class="{fullscreen:fullscreen}" class="tinymce-container" :style="{width:containerWidth}">
<textarea :id="tinymceId" class="tinymce-textarea" />
</div>
</template>
<script>
import Editor from '@tinymce/tinymce-vue'
import ImageUpload from './imageUpload'
import plugins from './plugins'
import toolbar from './toolbar'
import imageUpload from './plugins/image'
import { mediaUpload, mediaResolver } from './plugins/media'
export default {
components: {
editor: Editor
name: 'Tinymce',
props: {
id: {
type: String,
default: function() {
return 'vue-tinymce-' + +new Date() + ((Math.random() * 1000).toFixed(0) + '')
}
},
value: {
type: String,
default: ''
},
toolbar: {
type: Array,
required: false,
default() {
return []
}
},
menubar: {
type: String,
default: 'file edit insert view format table'
},
height: {
type: [Number, String],
required: false,
default: 360
},
width: {
type: [Number, String],
required: false,
default: 'auto'
},
readonly: {
type: Boolean,
default: false
}
},
data() {
return {
init: {
min_height: 600,
max_height: 600,
menubar: false,
statusbar: false,
plugins: 'table autoresize charmap fullscreen hr lists link code preview quickbars',
toolbar:
'undo redo | fontsizeselect lineheight bold italic underline strikethrough forecolor backcolor | link quickimage image media table | align hangingindent indent outdent numlist bullist | charmap blockquote hr fullscreen | code preview',
// font_formats:
// '微软雅黑=Microsoft YaHei,Helvetica Neue,PingFang SC,sans-serif;苹果苹方=PingFang SC,Microsoft YaHei,sans-serif;宋体=simsun,serif;仿宋体=FangSong,serif;黑体=SimHei,sans-serif;Arial=arial,helvetica,sans-serif;Times New Roman',
fontsize_formats: '8px 10px 12px 14px 15px 16px 17px 18px 20px 24px',
lineheight_formats: '0.5 1 1.2 1.5 2',
images_upload_handler: ImageUpload,
automatic_uploads: true,
quickbars_insert_toolbar: false,
// style_formats: [{ title: '悬挂缩进', block: 'p', styles: { textIndent: '-2em', paddingLeft: '2em' } }],
content_style: 'img {max-width:100%;}'
hasChange: false,
hasInit: false,
tinymceId: this.id,
fullscreen: false
}
},
computed: {
containerWidth() {
const width = this.width
if (/^[\d]+(\.[\d]+)?$/.test(width)) { // matches `100`, `'100'`
return `${width}px`
}
return width
}
},
watch: {
value(val) {
if (!this.hasChange && this.hasInit) {
this.$nextTick(() =>
window.tinymce.get(this.tinymceId).setContent(val || ''))
}
},
readonly(val) {
if (val) {
window.tinymce.editors[this.tinymceId].setMode('readonly')
} else {
window.tinymce.editors[this.tinymceId].setMode('design');
}
}
},
mounted() {
this.init()
},
activated() {
if (window.tinymce) {
this.initTinymce()
}
},
deactivated() {
this.destroyTinymce()
},
destroyed() {
this.destroyTinymce()
},
methods: {
onChange(event, editor) {
this.dispatch('ElFormItem', 'el.form.change', editor.getContent())
init() {
this.initTinymce()
},
onBlur(event, editor) {
this.dispatch('ElFormItem', 'el.form.blur', editor.getContent())
initTinymce() {
const _this = this
window.tinymce.init({
selector: `#${this.tinymceId}`,
language: 'zh_CN',
readonly: this.readonly,
fontsize_formats: '12px 14px 16px 18px 24px 36px 48px 56px 72px',
font_formats: '微软雅黑=Microsoft YaHei,Helvetica Neue,PingFang SC,sans-serif;苹果苹方=PingFang SC,Microsoft YaHei,sans-serif;宋体=simsun,serif;仿宋体=FangSong,serif;黑体=SimHei,sans-serif;Arial=arial,helvetica,sans-serif;Arial Black=arial black,avant garde;Book Antiqua=book antiqua,palatino;',
images_upload_handler: imageUpload,
file_picker_types: 'media',
file_picker_callback: mediaUpload,
media_url_resolver: mediaResolver,
height: this.height,
body_class: 'panel-body ',
object_resizing: false,
toolbar: this.toolbar.length > 0 ? this.toolbar : toolbar,
menubar: this.menubar,
plugins: plugins,
end_container_on_empty_block: true,
powerpaste_word_import: 'clean',
code_dialog_height: 450,
code_dialog_width: 1000,
advlist_bullet_styles: 'default,circle,disc,square',
advlist_number_styles: 'default,lower-alpha,lower-roman,upper-alpha,upper-roman',
imagetools_cors_hosts: ['www.tinymce.com', 'codepen.io'],
default_link_target: '_blank',
link_title: false,
nonbreaking_force_tab: true, // inserting nonbreaking space &nbsp; need Nonbreaking Space Plugin
init_instance_callback: editor => {
if (_this.value) {
editor.setContent(_this.value)
}
_this.hasInit = true
editor.on('NodeChange Change KeyUp SetContent', () => {
this.hasChange = true
this.$emit('input', editor.getContent())
})
},
setup(editor) {
editor.on('FullscreenStateChanged', (e) => {
_this.fullscreen = e.state
});
editor.on('blur', (e) => {
_this.$emit('blur', e)
})
},
// it will try to keep these URLs intact
// https://www.tiny.cloud/docs-3x/reference/configuration/Configuration3x@convert_urls/
// https://stackoverflow.com/questions/5196205/disable-tinymce-absolute-to-relative-url-conversions
convert_urls: false
// 整合七牛上传
// images_dataimg_filter(img) {
// setTimeout(() => {
// const $image = $(img);
// $image.removeAttr('width');
// $image.removeAttr('height');
// if ($image[0].height && $image[0].width) {
// $image.attr('data-wscntype', 'image');
// $image.attr('data-wscnh', $image[0].height);
// $image.attr('data-wscnw', $image[0].width);
// $image.addClass('wscnph');
// }
// }, 0);
// return img
// },
// images_upload_handler(blobInfo, success, failure, progress) {
// progress(0);
// const token = _this.$store.getters.token;
// getToken(token).then(response => {
// const url = response.data.qiniu_url;
// const formData = new FormData();
// formData.append('token', response.data.qiniu_token);
// formData.append('key', response.data.qiniu_key);
// formData.append('file', blobInfo.blob(), url);
// upload(formData).then(() => {
// success(url);
// progress(100);
// })
// }).catch(err => {
// failure('出现未知问题,刷新页面,或者联系程序员')
// console.log(err);
// });
// },
})
},
dispatch(componentName, eventName, params) {
var parent = this.$parent || this.$root
var name = parent.$options.componentName
while (parent && (!name || name !== componentName)) {
parent = parent.$parent
if (parent) {
name = parent.$options.componentName
}
destroyTinymce() {
const tinymce = window.tinymce.get(this.tinymceId)
if (this.fullscreen) {
tinymce.execCommand('mceFullScreen')
}
if (parent) {
parent.$emit.apply(parent, [eventName].concat(params))
if (tinymce) {
tinymce.destroy()
}
},
setContent(value) {
window.tinymce.get(this.tinymceId).setContent(value)
},
getContent() {
window.tinymce.get(this.tinymceId).getContent()
},
imageSuccessCBK(arr) {
arr.forEach(v => window.tinymce.get(this.tinymceId).insertContent(`<img class="wscnph" src="${v.url}" >`))
}
}
}
</script>
<style>
.tox .tox-tbtn--bespoke .tox-tbtn__select-label {
width: 4em !important;
<style scoped>
.tinymce-container {
position: relative;
line-height: normal;
}
.tinymce-container ::v-deep .mce-fullscreen {
z-index: 10000;
}
.tinymce-textarea {
visibility: hidden;
z-index: -1;
}
.editor-custom-btn-container {
position: absolute;
right: 4px;
top: 4px;
/*z-index: 2005;*/
}
.fullscreen ::v-deep .editor-custom-btn-container {
z-index: 10000;
position: fixed;
}
.editor-upload-btn {
display: inline-block;
}
.tinymce-container ::v-deep .tox-statusbar{
display:none;
}
</style>
import { getSignature, uploadFile } from '@/api/base'
import md5 from 'blueimp-md5'
export default function (blobInfo, succFun, failFun) {
const file = blobInfo.blob()
getSignature()
.then(response => {
const prefix = 'upload/admin/'
const fileName = file.name
const key = prefix + md5(fileName + new Date().getTime()) + fileName.substr(fileName.lastIndexOf('.'))
const { accessid, policy, signature, host } = response
const data = { key, OSSAccessKeyId: accessid, policy, signature, success_action_status: '200', file }
const fileUrl = `${host}/${key}`
uploadFile(data)
.then(() => {
succFun(fileUrl)
})
.catch(() => {
failFun('上传失败')
})
})
.catch(response => {
failFun('获取Signature失败')
})
}
// Any plugins you want to use has to be imported
// Detail plugins list see https://www.tinymce.com/docs/plugins/
// Custom builds see https://www.tinymce.com/download/custom-builds/
const plugins = ['advlist anchor autolink autosave code codesample directionality emoticons fullscreen hr image imagetools insertdatetime link lists media nonbreaking noneditable pagebreak paste preview print save searchreplace spellchecker tabfocus table template textpattern visualblocks visualchars wordcount quickbars']
export default plugins
import { fileUpload, getSignature } from '@/api/base'
import md5 from 'blueimp-md5'
export default function(blobInfo, succFun, failFun) {
const file = blobInfo.blob()
const fileName = file.name
const key = 'upload/marketing-admin/' + md5(fileName + new Date().getTime()) + fileName.substr(fileName.lastIndexOf('.'))
getSignature()
.then(response => {
const { accessid, policy, signature, host } = response
const data = { key, OSSAccessKeyId: accessid, policy, signature, success_action_status: '200', file }
const fileUrl = `${host}/${key}`
// const formData = new FormData()
// formData.append('key', data.key)
// formData.append('OSSAccessKeyId', data.OSSAccessKeyId)
// formData.append('policy', data.policy)
// formData.append('signature', data.signature)
// formData.append('success_action_status', '200')
// formData.append('file', file)
fileUpload(data).then(res => {
succFun(fileUrl)
}).catch((err) =>
failFun(err.message || '上传出错了')
)
})
.catch(err => {
console(err)
})
}
import { fileUpload, getSignature } from '@/api/base'
import { Message, Loading } from 'element-ui'
import md5 from 'blueimp-md5'
/**
* 以分隔符截取字符串,并返回最后一个截取段
*/
const splitStrLast = function(str, split) {
const fileNameArr = str.split(split)
const last = fileNameArr[fileNameArr.length - 1]
return last
}
const fetchUpload = function(file, callback) {
const loading = Loading.service({
lock: true,
text: '视频上传中',
spinner: 'el-icon-loading',
background: 'rgba(0, 0, 0, 0.7)',
customClass: 'tinymce-el-loading'
})
const fileName = file.name
const key = 'upload/marketing-admin/' + md5(fileName + new Date().getTime()) + fileName.substr(fileName.lastIndexOf('.'))
getSignature()
.then(response => {
const { accessid, policy, signature, host } = response
const data = { key, OSSAccessKeyId: accessid, policy, signature, success_action_status: '200', file }
const fileUrl = `${host}/${key}`
// const formData = new FormData()
// formData.append('key', data.key)
// formData.append('OSSAccessKeyId', data.OSSAccessKeyId)
// formData.append('policy', data.policy)
// formData.append('signature', data.signature)
// formData.append('success_action_status', '200')
// formData.append('file', file)
fileUpload(data).then(res => {
loading.close()
callback(fileUrl, { title: file.name })
}).catch(err => {
loading.close()
Message({ message: err.message || '上传上传出错了', type: 'error', customClass: 'tinymce-el-message' })
})
})
.catch(err => {
console(err)
loading.close()
})
}
/**
* 视频上传
* @param {function} callback 上传结束回调
* @param {string} value
* @param {*} meta
*/
export function mediaUpload(callback, value, meta) {
if (meta.filetype === 'media') {
const input = document.createElement('input') // 创建一个隐藏的input
input.setAttribute('type', 'file')
input.setAttribute('accept', '.mp4')
input.onchange = function() {
const file = this.files[0]
let errorMsg = ''
if (file && file.name) {
const suffix = splitStrLast(file.name, '.')
if (['mp4'].includes(suffix)) {
if (file.size > 1024 * 1024 * 1024) {
errorMsg = '视频文件大小不能超过 1GB!'
}
} else {
errorMsg = '只支持mp4格式的视频文件'
}
} else {
errorMsg = '请选取视频文件'
}
if (errorMsg) {
Message({ message: errorMsg, type: 'error', customClass: 'tinymce-el-message' })
} else {
fetchUpload(file, callback)
}
}
// 触发点击
input.click()
}
}
/**
* 添加视频回调
* @param {function} data 视频数据
* @param {string} resolve 回调函数
*/
export function mediaResolver(data, resolve) {
try {
const videoUri = encodeURI(data.url)
const embedHtml = `
<p>
<span
data-mce-selected="1"
data-mce-object="video"
data-mce-p-width="100%"
data-mce-p-height="100%"
data-mce-p-controls="controls"
data-mce-p-controlslist="nodownload"
data-mce-p-allowfullscreen="true"
data-mce-p-src=${videoUri} >
<video src=${videoUri} width="100%" height="100%" controls="controls" controlslist="nodownload">
</video>
</span>
</p>
<p style="text-align: left;"></p>
`
resolve({ html: embedHtml })
} catch (e) {
resolve({ html: '' })
}
}
// Here is a list of the toolbar
// Detail list see https://www.tinymce.com/docs/advanced/editor-control-identifiers/#toolbarcontrols
const toolbar = ['fontsizeselect fontselect lineheight bold italic underline strikethrough forecolor backcolor alignleft aligncenter alignright outdent indent blockquote removeformat subscript superscript', 'hr bullist numlist link quickimage charmap preview anchor pagebreak insertdatetime media table emoticons fullscreen code codesample searchreplace']
export default toolbar
import user from './user'
import views from './views'
import routes from './route'
export default function (options) {
......@@ -8,5 +8,5 @@ export default function (options) {
options.router.addRoute(route)
})
}
user(options)
views(options)
}
\ No newline at end of file
import httpRequest from '@/utils/axios'
/**
* 搜索统一用户
*/
export function search(params) {
return httpRequest.get('/api/marketing/admin/v1/system/search-user', { params })
}
/**
* 获取统一用户
*/
export function getUserList(params) {
return httpRequest.get('/api/marketing/admin/v1/users', { params })
}
/**
* 授权用户
*/
export function addUser(data) {
return httpRequest.post('/api/marketing/admin/v1/user', data)
}
/**
* 批量删除授权用户
*/
export function batchDeleteUser(data) {
return httpRequest.post('/api/marketing/admin/v1/user/batch-delete', data)
}
export default [
{
path: 'user',
component: () => import('./views/List.vue')
}
]
import httpRequest from '@/utils/axios'
/**
* 搜索统一用户
*/
export function search(params) {
return httpRequest.get('/api/marketing/admin/v1/system/search-user', { params })
}
/**
* 获取授权用户
*/
export function getUserList(params) {
return httpRequest.get('/api/marketing/admin/v1/users', { params })
}
/**
* 授权用户
*/
export function addUser(data) {
return httpRequest.post('/api/marketing/admin/v1/user', data)
}
/**
* 批量删除授权用户
*/
export function batchDeleteUser(data) {
return httpRequest.post('/api/marketing/admin/v1/user/batch-delete', data)
}
/**
* 获取角色列表
*/
export function getRoleList(params) {
return httpRequest.get('/api/marketing/admin/v1/system/roles', { params })
}
/**
* 创建角色
*/
export function addRole(data) {
return httpRequest.post('/api/marketing/admin/v1/system/role', data)
}
/**
* 更新角色
*/
export function updateRole(id, data) {
return httpRequest.put(`/api/marketing/admin/v1/system/role/${id}/update`, data)
}
/**
* 批量删除角色
*/
export function batchDeleteRole(data) {
return httpRequest.post('/api/marketing/admin/v1/system/role/batch-delete', data)
}
/**
* 分配角色给用户
*/
export function addRoleUsers(data) {
return httpRequest.post('/api/marketing/admin/v1/system/assign/roles-to-user', data)
}
/**
* 获取角色下用户列表
*/
export function getRoleUserList(params) {
return httpRequest.get(`/api/marketing/admin/v1/system/assign/role/${params.role_id}/users`, { params })
}
/**
* 批量删除角色下用户
*/
export function batchDeleteRoleUsers(data) {
return httpRequest.post('/api/marketing/admin/v1/system/assign/remove-user-form-role', data)
}
/**
* 保存使用手册
*/
export function keepUserManual(data) {
return httpRequest.post('/api/marketing/admin/v1/system/user-manual', data)
}
/**
* 获取使用手册
*/
export function getUserManual() {
return httpRequest.get('/api/marketing/admin/v1/system/user-manual')
}
\ No newline at end of file
......@@ -11,7 +11,7 @@
</el-select>
</template>
<script>
import { search } from '../../api'
import { search } from '../api'
const MOBILE_REG = /^1(3[0-9]|4[01456879]|5[0-35-9]|6[2567]|7[0-8]|8[0-9]|9[0-35-9])\d{4,8}$/
const EMAIL_REG = /^[A-Za-z0-9]+([_.\\-][A-Za-z0-9]+)*@[A-Za-z0-9-.]+$/
export default {
......
......@@ -7,4 +7,4 @@ export default function (options = {}) {
options.router.addRoute('system', route)
})
}
}
}
\ No newline at end of file
<template>
<app-card>
<el-tabs v-model="tabsActive" type="card">
<el-tab-pane label="使用说明" name="INSTRUCTIONS">
<Tinymce ref="editor" v-model="intro_content" :height="600"/>
</el-tab-pane>
<el-tab-pane label="常见问题" name="COMMON_PROBLEM">
<Tinymce ref="editor" v-model="ques_content" :height="600"/>
</el-tab-pane>
</el-tabs>
<el-button type="primary" @click="fetchSubmitManual" size="mini" style="margin-top:20px;" :disabled="btnDisabled">保存</el-button>
</app-card>
</template>
<script>
import AppCard from '@/components/base/AppCard.vue'
import Tinymce from '@/components/tinymce/index.vue'
import { keepUserManual, getUserManual } from '../api'
export default {
components: { AppCard, Tinymce },
data() {
return {
tabsActive: 'INSTRUCTIONS',
intro_content: '',
ques_content: '',
btnDisabled: false
}
},
created() {
this.fetchGetManual()
},
methods: {
handleKeep() {
console.log(this.content)
},
fetchSubmitManual() {
const params = {
type: this.tabsActive
}
if (this.tabsActive === 'INSTRUCTIONS') {
params.info = this.intro_content
} else {
params.info = this.ques_content
}
this.btnDisabled = true
keepUserManual(params).then(res => {
this.btnDisabled = false
if (res.code === 0 && res.data && res.data.status) {
this.$message.success('保存成功!')
} else {
this.$message.success('保存失败!')
}
}).catch(() => {
this.btnDisabled = false
})
},
fetchGetManual() {
getUserManual().then(res => {
if (res.code === 0) {
this.intro_content = res.data.instructions
this.ques_content = res.data.common_problem
}
})
}
}
}
</script>
\ No newline at end of file
<template>
<app-list v-bind="tableOptions" ref="appList" @selection-change="handleSelectionChange">
<!-- 筛选 -->
<template v-slot:filter-user="{ params }">
<user-search v-model="params.sso_id" :options="{ size: 'small' }"/>
</template>
<template #footer>
<div class="selection_bar">
已选中 {{selection.length}}
<el-button style="margin-left:15px;" size="mini" :disabled="!selection.length" @click="fetchRoleToUsers">添加</el-button>
</div>
</template>
</app-list>
</template>
<script>
import { getUserList, addRoleUsers } from '../api'
import UserSearch from '../components/UserSearch.vue'
import AppList from '@/components/base/AppList.vue'
export default {
props: {
id: {
type: String,
default: ''
}
},
components: { UserSearch, AppList },
data() {
return {
selection: []
}
},
computed: {
tableOptions() {
return {
remote: {
httpRequest: getUserList,
params: { sso_id: '' }
},
filters: [
{ prop: 'sso_id', slots: 'filter-user' }
],
columns: [
{ type: 'selection', minWidth: '50px', fixed: 'left' },
{ prop: 'sso_user.nickname', label: '用户昵称', minWidth: '160px', fixed: 'left' },
{ prop: 'sso_user.id', label: '用户ID', minWidth: '150px', fixed: 'left' },
{ prop: 'sso_user.mobile', label: '手机号', minWidth: '150px' },
{ prop: 'sso_user.email', label: '邮箱', minWidth: '150px', fixed: 'right' }
]
}
}
},
methods: {
handleSelectionChange(val) {
this.selection = val
},
fetchRoleToUsers() {
const params = {
role_id: this.id,
sso_ids: this.selection.map(item => item.sso_id)
}
addRoleUsers(params).then(res => {
if (res.code === 0 && res.data && res.data.status) {
this.$message.success('添加用户成功')
this.$emit('update')
}
})
}
}
}
</script>
<style scoped>
.table-list{
padding:20px;
}
.selection_bar{
font-size:14px;
color:#626266;
}
</style>
\ No newline at end of file
<template>
<app-card>
<app-list v-bind="tableOptions" ref="appList" @selection-change="handleSelectionChange">
<!-- 筛选 -->
<!-- <template v-slot:filter-user="{ params }">
</template> -->
<template #header-aside>
<el-button type="primary" size="small" style="margin-top:5px;" @click="handleCreate">添加角色</el-button>
</template>
<!-- 操作 -->
<template v-slot:table-operate="{ row }">
<el-button v-if="row.name !== 'administrator'" type="text" @click="handleEdit(row)" size="mini">编辑</el-button>
<el-button type="text" @click="handleManageUser(row)" size="mini">管理用户</el-button>
<el-button v-if="row.name !== 'administrator'" type="text" @click="handleRemove(row)" size="mini" style="margin-right:12px;">删除</el-button>
</template>
<template #footer>
<div class="selection_bar">
已选中 {{multipleSelection.length}}
<el-button style="margin-left:15px;" size="mini" :disabled="!multipleSelection.length" @click="handleRemove">删除</el-button>
</div>
</template>
</app-list>
<el-dialog :title="domicTitle" :visible.sync="dialogVisible" width="420px" center @close="handleDialogClose">
<el-form :model="form" :rules="rules" ref="ruleForm" label-width="80px" class="demo-ruleForm">
<el-form-item label="角色名称" prop="display_name">
<el-input v-model="form.display_name" size="small"></el-input>
</el-form-item>
<el-form-item label="角色key" prop="name">
<el-input v-model="form.name" size="small" :disabled="keyDisabled"></el-input>
</el-form-item>
<el-form-item label="角色描述" prop="description">
<el-input v-model="form.description" type="textarea" size="small" rows="3"></el-input>
</el-form-item>
</el-form>
<span slot="footer" class="dialog-footer">
<el-button type="primary" @click="handleEnter" size="mini">确 定</el-button>
<el-button @click="handleDialogClose" size="mini">取 消</el-button>
</span>
</el-dialog>
</app-card>
</template>
<script>
// 引入组件
import AppList from '@/components/base/AppList.vue'
import AppCard from '@/components/base/AppCard.vue'
// api
import { getRoleList, addRole, batchDeleteRole, updateRole } from '../api'
const defaultForm = {
name: '',
display_name: '',
description: '',
id: ''
}
export default {
components: { AppCard, AppList },
data() {
return {
multipleSelection: [],
dialogVisible: false,
dialogType: 'add',
keyDisabled: false,
form: Object.assign({}, defaultForm),
rules: {
name: { required: true, message: '请输入角色key', trigger: 'blur' },
display_name: { required: true, message: '请输入角色名称', trigger: 'blur' },
description: { required: true, message: '请输入角色描述', trigger: 'blur' }
}
}
},
computed: {
domicTitle() {
return this.dialogType === 'add' ? '新增角色' : '编辑角色'
},
tableOptions() {
return {
remote: {
httpRequest: getRoleList,
params: { name: '', display_name: '' }
},
filters: [
{
type: 'input',
prop: 'name',
placeholder: '请输入角色唯一标识',
size: 'small'
},
{
type: 'input',
prop: 'display_name',
placeholder: '请输入角色名称',
size: 'small'
}
],
data: [
{ name: 'ewrw', display_name: '测试测试', description: 'wejjjr' }
],
columns: [
{ prop: 'name', label: '角色', minWidth: '120px' },
{ prop: 'display_name', label: '角色名称', minWidth: '120px' },
{ prop: 'description', label: '角色描述', minWidth: '140px' },
{ prop: 'updated_at', label: '创建时间', minWidth: '140px' },
{ label: '操作', minWidth: '140px', slots: 'table-operate' }
]
}
}
},
methods: {
handleSelectionChange(val) {
this.multipleSelection = val
},
handleCreate() {
this.dialogVisible = true
this.dialogType = 'add'
this.keyDisabled = false
},
handleEdit(val) {
this.form = Object.assign({}, val)
this.dialogVisible = true
this.dialogType = 'edit'
this.keyDisabled = true
},
handleDialogClose() {
this.$refs.ruleForm.resetFields();
this.dialogVisible = false
this.form = Object.assign({}, defaultForm)
},
handleManageUser(val) {
this.$router.push({ path: '/system/roleToUser', query: { id: val.id } })
},
handleEnter() {
this.$refs.ruleForm.validate((valid) => {
if (valid) {
this.dialogType === 'add' ? this.fetchCreateRole() : this.fetchUpdateRole()
this.dialogVisible = false
}
})
},
handleRemove(val) {
this.$confirm('删除角色则相关的用户将无法登录,确定删除?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消'
}).then(() => {
this.fetchBatchDeleteRole(val)
}).catch(() => {})
},
fetchCreateRole() {
addRole(this.form).then(res => {
if (res.code === 0) {
this.$message.success('创建角色成功')
this.$refs.appList.refetch(true)
} else {
this.$message.error('创建角色失败')
}
})
},
fetchUpdateRole() {
updateRole(this.form.id, this.form).then((res) => {
if (res.code === 0 && res.data.status) {
this.$message.success('更新角色成功')
this.$refs.appList.refetch(true)
} else {
this.$message.error('更新角色失败')
}
})
},
fetchBatchDeleteRole(val) {
const params = {
ids: [val.id]
}
batchDeleteRole(params).then(res => {
if (res.code === 0 && res.data && res.data.status) {
this.$message.success('删除用户成功')
this.$refs.appList.refetch(true)
} else {
this.$message.error('删除用户失败')
}
})
}
}
}
</script>
<style>
.app-card{
padding: 20px 30px 10px;
}
.selection_bar{
font-size:14px;
color:#626266;
}
</style>
\ No newline at end of file
<template>
<div class="account">
<h5>
角色管理 / 管理用户
</h5>
<app-list v-bind="tableOptions" ref="appList" @selection-change="handleSelectionChange">
<template #header-aside>
<el-button type="primary" size="small" style="margin-top:5px;" @click="drawerVisible = true">添加用户</el-button>
</template>
<!-- 筛选 -->
<template v-slot:filter-user="{ params }">
<user-search v-model="params.sso_id" :options="{ size: 'small' }"/>
</template>
<template #footer>
<div class="selection_bar">
已选中 {{selection.length}}
<el-button style="margin-left:15px;" size="mini" :disabled="!selection.length" @click="handleDelete">删除</el-button>
</div>
</template>
</app-list>
<el-drawer title="添加用户" :visible.sync="drawerVisible" size="1000px">
<account-table :id="roleId" @update="handleUpdate" />
</el-drawer>
</div>
</template>
<script>
import { getRoleUserList, batchDeleteRoleUsers } from '../api'
import UserSearch from '../components/UserSearch.vue'
import AppList from '@/components/base/AppList.vue'
import AccountTable from './AccountTable.vue'
export default {
data() {
return {
ssoId: '',
listData: [],
selection: [],
drawerVisible: false
}
},
components: { UserSearch, AppList, AccountTable },
computed: {
roleId() {
return this.$route.query.id || ''
},
tableOptions() {
return {
remote: {
httpRequest: getRoleUserList,
params: { sso_id: '', role_id: this.roleId }
},
filters: [
{ prop: 'sso_id', slots: 'filter-user' }
],
columns: [
{ type: 'selection', minWidth: '50px', fixed: 'left' },
{ prop: 'user_info.nickname', label: '用户昵称', minWidth: '160px', fixed: 'left' },
{ prop: 'user_info.id', label: '用户ID', minWidth: '150px', fixed: 'left' },
{ prop: 'user_info.mobile', label: '手机号', minWidth: '150px' },
{ prop: 'user_info.email', label: '邮箱', minWidth: '150px', fixed: 'right' }
]
}
}
},
created() {
},
methods: {
handleSelectionChange(val) {
this.selection = val
},
handleDelete() {
this.$confirm('确定从角色下删除选中的用户?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消'
}).then(() => {
this.fetchRemove()
}).catch(() => {});
},
handleUpdate() {
this.drawerVisible = false
this.$refs.appList.refetch()
},
fetchRemove() {
const params = {
role_id: this.roleId,
sso_ids: this.selection.map(item => {
if (item.user_info) return item.user_info.id
})
}
batchDeleteRoleUsers(params).then(res => {
if (res.code === 0 && res.data && res.data.status) {
this.$message.success('删除用户成功')
this.$refs.appList.refetch()
}
})
}
}
}
</script>
<style scoped>
.account {
height: 100%;
background:#fff;
padding:20px 20px 10px;
border-radius:5px;
}
h5{
font-size:16px;
line-height:30px;
color:#636363;
border-bottom:1px solid #dcdfe6;
margin-bottom:15px;
padding-bottom:10px;
}
::v-deep.el-drawer header{
margin-bottom:0;
}
</style>
\ No newline at end of file
export default [
{
path: 'user',
component: () => import('./user/List.vue'),
meta: { roles: ['administrator'] }
},
{
path: 'role',
component: () => import('./role/List.vue'),
meta: { roles: ['administrator'] }
},
{
path: 'roleToUser',
component: () => import('./role/RoleToUser.vue'),
meta: { roles: ['administrator'] }
},
{
path: 'manual',
component: () => import('./manual/Index.vue'),
meta: { roles: ['administrator'] }
}
]
\ No newline at end of file
......@@ -33,7 +33,7 @@
// 引入组件
import AppList from '@/components/base/AppList.vue'
import AppCard from '@/components/base/AppCard.vue'
import UserSearch from './components/UserSearch.vue'
import UserSearch from '../components/UserSearch.vue'
// api
import { getUserList, addUser, batchDeleteUser } from '../api'
export default {
......
import Vue from 'vue'
import VueRouter from 'vue-router'
import Router from 'vue-router'
Vue.use(Router)
const originalPush = Router.prototype.push
const originalReplace = Router.prototype.replace
// push
Router.prototype.push = function push (location, onResolve, onReject) {
if (onResolve || onReject) return originalPush.call(this, location, onResolve, onReject)
return originalPush.call(this, location).catch(err => err)
}
// replace
Router.prototype.replace = function push (location, onResolve, onReject) {
if (onResolve || onReject) return originalReplace.call(this, location, onResolve, onReject)
return originalReplace.call(this, location).catch(err => err)
}
Vue.use(VueRouter)
const routes = [{ path: '*', redirect: '/tools/sign-in' }]
export default new VueRouter({
mode: 'history',
routes
const createRouter = () => new Router({
mode: 'history', // require service support
scrollBehavior: () => ({ y: 0 }),
routes: routes
})
const router = createRouter()
// Detail see: https://github.com/vuejs/vue-router/issues/1234#issuecomment-357941465
export function resetRouter() {
const newRouter = createRouter()
router.matcher = newRouter.matcher // reset router
}
export default router
import Vue from 'vue'
import Vuex from 'vuex'
import { getUser, logout } from '@/api/base'
import { getUser, logout, getUserRoles } from '@/api/base'
Vue.use(Vuex)
export default new Vuex.Store({
state: {
user: {}
user: {},
roles: []
},
mutations: {
setUser(state, user) {
state.user = user
},
setRoles(state, roles) {
state.roles = roles
}
},
actions: {
......@@ -43,6 +47,24 @@ export default new Vuex.Store({
return false
})
return isLogin
},
// 获取用户角色
async checkRoles({ commit }) {
const hasRoles = await getUserRoles().then(res => {
const roles = res.data
if (roles && Array.isArray(roles)) {
const _roles = roles.map(item => item.name)
commit('setRoles', _roles)
return true
} else {
commit('setRoles', [])
return false
}
}).catch(() => {
commit('setRoles', [])
return false
})
return hasRoles
}
}
})
import store from '@/store'
import router from '@/router'
const UA = navigator.userAgent
const isMobile = /iphone/i.test(UA) || (/android/i.test(UA) && /mobile/i.test(UA))
......@@ -27,9 +28,42 @@ export default async function (to, from, next) {
}
const isLogin = store.state.user.id || (await store.dispatch('checkLogin'))
if (!isLogin) {
window.location.href = `${import.meta.env.VITE_LOGIN_URL}?rd=${encodeURIComponent(window.location.href)}`
// window.location.href = `${import.meta.env.VITE_LOGIN_URL}?rd=${encodeURIComponent(window.location.href)}`
window.location.href = loginUrl()
return
} else {
const hasRoles = store.state.roles.length > 0 || (await store.dispatch('checkRoles'))
const roles = store.state.roles
if (hasRoles) {
if (!hasPermission(roles, to.meta.roles)) {
router.push('/401')
return
}
} else {
window.location.href = loginUrl()
return
}
}
next()
}
function loginUrl() {
const loginUrl = import.meta.env.VITE_LOGIN_URL
let rdUrl = window.location.href
if (rdUrl.includes('error-page')) {
rdUrl = window.location.protocol + '//' + window.location.host
}
return `${loginUrl}?rd=${encodeURIComponent(rdUrl)}`
}
/**
* Use meta.role to determine if the current user has permission
* @param roles
* @param routeRoles
*/
function hasPermission(roles, routeRoles) {
if (Array.isArray(routeRoles)) {
return roles.some(role => routeRoles.includes(role))
} else {
return true
}
}
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论