feature[Permission]: add role permission management page (#1605)

This commit is contained in:
Pan 2019-03-17 21:05:46 +08:00
parent f175427a61
commit b44335f012
14 changed files with 467 additions and 46 deletions

View File

@ -2,11 +2,13 @@ import login from './login'
import article from './article' import article from './article'
import search from './remoteSearch' import search from './remoteSearch'
import transaction from './transaction' import transaction from './transaction'
import role from './role'
export default { export default {
...login, ...login,
...article, ...article,
...search, ...search,
...transaction ...transaction,
...role
} }

61
mock/role/index.js Normal file
View File

@ -0,0 +1,61 @@
import Mock from 'mockjs'
// import { deepClone } from '@/utils'
// import { filterAsyncRoutes } from '@/store/modules/permission'
// import { asyncRoutes, constantRoutes } from '@/router'
// const routes = deepClone([...constantRoutes, ...asyncRoutes])
const roles = [
{
key: 'admin',
name: 'admin',
description: 'Super Administrator. Have access to view all pages.',
routes: []
},
{
key: 'editor',
name: 'editor',
description: 'Normal Editor. Can see all pages except permission page',
routes: []
},
{
key: 'visitor',
name: 'visitor',
description: 'Just a visitor. Can only see the home page and the document page',
routes: [{
path: '',
redirect: 'dashboard',
children: [
{
path: 'dashboard',
name: 'Dashboard',
meta: { title: 'dashboard', icon: 'dashboard' }
}
]
}]
}
]
export default {
'/routes': () => {
return []
},
'/roles': () => {
return roles
},
'/roles/add': () => {
return Mock.mock('@integer(300, 5000)')
},
'/roles/update/\/[A-Za-z0-9]': () => {
const res = {
data: 'success'
}
return res
},
'/roles/delete/\/[A-Za-z0-9]'() {
const res = {
data: 'success'
}
return res
}
}

38
src/api/role.js Normal file
View File

@ -0,0 +1,38 @@
import request from '@/utils/request'
export function getRoutes() {
return request({
url: '/routes',
method: 'get'
})
}
export function getRoles() {
return request({
url: '/roles',
method: 'get'
})
}
export function addRole(data) {
return request({
url: '/roles/add',
method: 'post',
data
})
}
export function updateRole(id, data) {
return request({
url: `/roles/update/${id}`,
method: 'put',
data
})
}
export function deleteRole(id) {
return request({
url: `/roles/delete/${id}`,
method: 'delete'
})
}

View File

@ -36,8 +36,8 @@ export default {
} }
}, },
computed: { computed: {
routers() { routes() {
return this.$store.getters.permission_routers return this.$store.getters.permission_routes
}, },
lang() { lang() {
return this.$store.getters.language return this.$store.getters.language
@ -45,10 +45,10 @@ export default {
}, },
watch: { watch: {
lang() { lang() {
this.searchPool = this.generateRouters(this.routers) this.searchPool = this.generateRoutes(this.routes)
}, },
routers() { routes() {
this.searchPool = this.generateRouters(this.routers) this.searchPool = this.generateRoutes(this.routes)
}, },
searchPool(list) { searchPool(list) {
this.initFuse(list) this.initFuse(list)
@ -62,7 +62,7 @@ export default {
} }
}, },
mounted() { mounted() {
this.searchPool = this.generateRouters(this.routers) this.searchPool = this.generateRoutes(this.routes)
}, },
methods: { methods: {
click() { click() {
@ -103,10 +103,10 @@ export default {
}, },
// Filter out the routes that can be displayed in the sidebar // Filter out the routes that can be displayed in the sidebar
// And generate the internationalized title // And generate the internationalized title
generateRouters(routers, basePath = '/', prefixTitle = []) { generateRoutes(routes, basePath = '/', prefixTitle = []) {
let res = [] let res = []
for (const router of routers) { for (const router of routes) {
// skip hidden router // skip hidden router
if (router.hidden) { continue } if (router.hidden) { continue }
@ -128,11 +128,11 @@ export default {
} }
} }
// recursive child routers // recursive child routes
if (router.children) { if (router.children) {
const tempRouters = this.generateRouters(router.children, data.path, data.title) const tempRoutes = this.generateRoutes(router.children, data.path, data.title)
if (tempRouters.length >= 1) { if (tempRoutes.length >= 1) {
res = [...res, ...tempRouters] res = [...res, ...tempRoutes]
} }
} }
} }

View File

@ -6,6 +6,7 @@ export default {
guide: 'Guide', guide: 'Guide',
permission: 'Permission', permission: 'Permission',
pagePermission: 'Page Permission', pagePermission: 'Page Permission',
rolePermission: 'Role Permission',
directivePermission: 'Directive Permission', directivePermission: 'Directive Permission',
icons: 'Icons', icons: 'Icons',
components: 'Components', components: 'Components',
@ -86,9 +87,14 @@ export default {
github: 'Github Repository' github: 'Github Repository'
}, },
permission: { permission: {
addRole: 'New Role',
editPermission: 'Edit Permission',
roles: 'Your roles', roles: 'Your roles',
switchRoles: 'Switch roles', switchRoles: 'Switch roles',
tips: 'In some cases it is not suitable to use v-permission, such as element Tab component or el-table-column and other asynchronous rendering dom cases which can only be achieved by manually setting the v-if.' tips: 'In some cases it is not suitable to use v-permission, such as element Tab component or el-table-column and other asynchronous rendering dom cases which can only be achieved by manually setting the v-if.',
delete: 'Delete',
confirm: 'Confirm',
cancel: 'Cancel'
}, },
guide: { guide: {
description: 'The guide page is useful for some people who entered the project for the first time. You can briefly introduce the features of the project. Demo is based on ', description: 'The guide page is useful for some people who entered the project for the first time. You can briefly introduce the features of the project. Demo is based on ',

View File

@ -5,6 +5,7 @@ export default {
documentation: 'Documentación', documentation: 'Documentación',
guide: 'Guía', guide: 'Guía',
permission: 'Permisos', permission: 'Permisos',
rolePermission: 'Permisos de rol',
pagePermission: 'Permisos de la página', pagePermission: 'Permisos de la página',
directivePermission: 'Permisos de la directiva', directivePermission: 'Permisos de la directiva',
icons: 'Iconos', icons: 'Iconos',
@ -86,9 +87,14 @@ export default {
github: 'Repositorio Github' github: 'Repositorio Github'
}, },
permission: { permission: {
addRole: 'Nuevo rol',
editPermission: 'Permiso de edición',
roles: 'Tus permisos', roles: 'Tus permisos',
switchRoles: 'Cambiar permisos', switchRoles: 'Cambiar permisos',
tips: 'In some cases it is not suitable to use v-permission, such as element Tab component or el-table-column and other asynchronous rendering dom cases which can only be achieved by manually setting the v-if.' tips: 'In some cases it is not suitable to use v-permission, such as element Tab component or el-table-column and other asynchronous rendering dom cases which can only be achieved by manually setting the v-if.',
delete: 'Borrar',
confirm: 'Confirmar',
cancel: 'Cancelar'
}, },
guide: { guide: {
description: 'The guide page is useful for some people who entered the project for the first time. You can briefly introduce the features of the project. Demo is based on ', description: 'The guide page is useful for some people who entered the project for the first time. You can briefly introduce the features of the project. Demo is based on ',

View File

@ -5,6 +5,7 @@ export default {
documentation: '文档', documentation: '文档',
guide: '引导页', guide: '引导页',
permission: '权限测试页', permission: '权限测试页',
rolePermission: '角色权限',
pagePermission: '页面权限', pagePermission: '页面权限',
directivePermission: '指令权限', directivePermission: '指令权限',
icons: '图标', icons: '图标',
@ -86,9 +87,14 @@ export default {
github: 'Github 地址' github: 'Github 地址'
}, },
permission: { permission: {
addRole: '新增角色',
editPermission: '编辑权限',
roles: '你的权限', roles: '你的权限',
switchRoles: '切换权限', switchRoles: '切换权限',
tips: '在某些情况下,不适合使用 v-permission。例如Element-UI 的 Tab 组件或 el-table-column 以及其它动态渲染 dom 的场景。你只能通过手动设置 v-if 来实现。' tips: '在某些情况下,不适合使用 v-permission。例如Element-UI 的 Tab 组件或 el-table-column 以及其它动态渲染 dom 的场景。你只能通过手动设置 v-if 来实现。',
delete: '删除',
confirm: '确定',
cancel: '取消'
}, },
guide: { guide: {
description: '引导页对于一些第一次进入项目的人很有用,你可以简单介绍下项目的功能。本 Demo 是基于', description: '引导页对于一些第一次进入项目的人很有用,你可以简单介绍下项目的功能。本 Demo 是基于',

View File

@ -9,7 +9,7 @@
:collapse-transition="false" :collapse-transition="false"
mode="vertical" mode="vertical"
> >
<sidebar-item v-for="route in permission_routers" :key="route.path" :item="route" :base-path="route.path" /> <sidebar-item v-for="route in permission_routes" :key="route.path" :item="route" :base-path="route.path" />
</el-menu> </el-menu>
</el-scrollbar> </el-scrollbar>
</template> </template>
@ -23,7 +23,7 @@ export default {
components: { SidebarItem }, components: { SidebarItem },
computed: { computed: {
...mapGetters([ ...mapGetters([
'permission_routers', 'permission_routes',
'sidebar' 'sidebar'
]), ]),
variables() { variables() {

View File

@ -54,8 +54,8 @@ export default {
visitedViews() { visitedViews() {
return this.$store.state.tagsView.visitedViews return this.$store.state.tagsView.visitedViews
}, },
routers() { routes() {
return this.$store.state.permission.routers return this.$store.state.permission.routes
} }
}, },
watch: { watch: {
@ -102,7 +102,7 @@ export default {
return tags return tags
}, },
initTags() { initTags() {
const affixTags = this.affixTags = this.filterAffixTags(this.routers) const affixTags = this.affixTags = this.filterAffixTags(this.routes)
for (const tag of affixTags) { for (const tag of affixTags) {
// Must have tag name // Must have tag name
if (tag.name) { if (tag.name) {

View File

@ -2,41 +2,49 @@ 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' // getToken from cookie import { getToken } from '@/utils/auth' // get token from cookie
NProgress.configure({ showSpinner: false })// NProgress Configuration NProgress.configure({ showSpinner: false }) // NProgress Configuration
// permission judge function // permission judge function
function hasPermission(roles, permissionRoles) { function hasPermission(roles, permissionRoles) {
if (roles.indexOf('admin') >= 0) return true // admin permission passed directly if (roles.includes('admin')) return true // admin permission passed directly
if (!permissionRoles) return true if (!permissionRoles) return true
return roles.some(role => permissionRoles.indexOf(role) >= 0) return roles.some(role => permissionRoles.indexOf(role) >= 0)
} }
const whiteList = ['/login', '/auth-redirect']// no redirect whitelist const whiteList = ['/login', '/auth-redirect'] // no redirect whitelist
router.beforeEach((to, from, next) => { router.beforeEach((to, from, next) => {
NProgress.start() // start progress bar NProgress.start() // start progress bar
if (getToken()) { // determine if there has token if (getToken()) {
// determine if there has token
/* has token*/ /* has token*/
if (to.path === '/login') { if (to.path === '/login') {
next({ path: '/' }) next({ path: '/' })
NProgress.done() // if current page is dashboard will not trigger afterEach hook, so manually handle it NProgress.done() // if current page is dashboard will not trigger afterEach hook, so manually handle it
} else { } else {
if (store.getters.roles.length === 0) { // 判断当前用户是否已拉取完user_info信息 if (store.getters.roles.length === 0) {
store.dispatch('GetUserInfo').then(res => { // 拉取user_info // 判断当前用户是否已拉取完user_info信息
const roles = res.data.roles // note: roles must be a array! such as: ['editor','develop'] store
store.dispatch('GenerateRoutes', { roles }).then(() => { // 根据roles权限生成可访问的路由表 .dispatch('GetUserInfo')
router.addRoutes(store.getters.addRouters) // 动态添加可访问路由表 .then(res => {
next({ ...to, replace: true }) // hack方法 确保addRoutes已完成 ,set the replace: true so the navigation will not leave a history record // 拉取user_info
const roles = res.data.roles // note: roles must be a object array! such as: [{id: '1', name: 'editor'}, {id: '2', name: 'developer'}]
store.dispatch('GenerateRoutes', { roles }).then(accessRoutes => {
// 根据roles权限生成可访问的路由表
router.addRoutes(accessRoutes) // 动态添加可访问路由表
next({ ...to, replace: true }) // hack方法 确保addRoutes已完成 ,set the replace: true so the navigation will not leave a history record
})
}) })
}).catch((err) => { .catch(err => {
store.dispatch('FedLogOut').then(() => { store.dispatch('FedLogOut').then(() => {
Message.error(err) Message.error(err)
next({ path: '/' }) next({ path: '/' })
})
}) })
})
} else { } else {
// 没有动态改变权限的需求可直接next() 删除下方权限判断 ↓ // 没有动态改变权限的需求可直接next() 删除下方权限判断 ↓
if (hasPermission(store.getters.roles, to.meta.roles)) { if (hasPermission(store.getters.roles, to.meta.roles)) {
@ -49,7 +57,8 @@ router.beforeEach((to, from, next) => {
} }
} else { } else {
/* has no token*/ /* has no token*/
if (whiteList.indexOf(to.path) !== -1) { // 在免登录白名单,直接进入 if (whiteList.indexOf(to.path) !== -1) {
// 在免登录白名单,直接进入
next() next()
} else { } else {
next(`/login?redirect=${to.path}`) // 否则全部重定向到登录页 next(`/login?redirect=${to.path}`) // 否则全部重定向到登录页

View File

@ -33,7 +33,7 @@ import nestedRouter from './modules/nested'
affix: true if true, the tag will affix in the tags-view affix: true if true, the tag will affix in the tags-view
} }
**/ **/
export const constantRouterMap = [ export const constantRoutes = [
{ {
path: '/redirect', path: '/redirect',
component: Layout, component: Layout,
@ -81,7 +81,6 @@ export const constantRouterMap = [
{ {
path: '/documentation', path: '/documentation',
component: Layout, component: Layout,
redirect: '/documentation/index',
children: [ children: [
{ {
path: 'index', path: 'index',
@ -109,10 +108,10 @@ export const constantRouterMap = [
export default new Router({ export default new Router({
// mode: 'history', // require service support // mode: 'history', // require service support
scrollBehavior: () => ({ y: 0 }), scrollBehavior: () => ({ y: 0 }),
routes: constantRouterMap routes: constantRoutes
}) })
export const asyncRouterMap = [ export const asyncRoutes = [
{ {
path: '/permission', path: '/permission',
component: Layout, component: Layout,
@ -141,6 +140,15 @@ export const asyncRouterMap = [
title: 'directivePermission' title: 'directivePermission'
// if do not set roles, means: this page does not require permission // if do not set roles, means: this page does not require permission
} }
},
{
path: 'role',
component: () => import('@/views/permission/role'),
name: 'RolePermission',
meta: {
title: 'rolePermission',
roles: ['admin']
}
} }
] ]
}, },

View File

@ -12,8 +12,8 @@ const getters = {
status: state => state.user.status, status: state => state.user.status,
roles: state => state.user.roles, roles: state => state.user.roles,
setting: state => state.user.setting, setting: state => state.user.setting,
permission_routers: state => state.permission.routers, permission_routes: state => state.permission.routes,
addRouters: state => state.permission.addRouters, addRoutes: state => state.permission.addRoutes,
errorLogs: state => state.errorLog.logs errorLogs: state => state.errorLog.logs
} }
export default getters export default getters

View File

@ -0,0 +1,283 @@
<template>
<div class="app-container">
<el-button type="primary" @click="handleAddRole">
{{ $t('permission.addRole') }}
</el-button>
<el-table :data="rolesList" style="width: 100%;margin-top:30px;" border>
<el-table-column align="center" label="Role Key" width="220">
<template slot-scope="scope">
{{ scope.row.key }}
</template>
</el-table-column>
<el-table-column align="center" label="Role Name" width="220">
<template slot-scope="scope">
{{ scope.row.name }}
</template>
</el-table-column>
<el-table-column align="header-center" label="Description">
<template slot-scope="scope">
{{ scope.row.description }}
</template>
</el-table-column>
<el-table-column align="center" label="Operations">
<template slot-scope="scope">
<el-button type="primary" size="small" @click="handleEdit(scope)">
{{ $t('permission.editPermission') }}
</el-button>
<el-button type="danger" size="small" @click="handleDelete(scope)">
{{ $t('permission.delete') }}
</el-button>
</template>
</el-table-column>
</el-table>
<el-dialog :visible.sync="dialogVisible" :title="dialogType==='edit'?'Edit Role':'New Role'">
<el-form :model="role" label-width="80px" label-position="left">
<el-form-item label="Name">
<el-input v-model="role.name" placeholder="Role Name" />
</el-form-item>
<el-form-item label="Desc">
<el-input
v-model="role.description"
:autosize="{ minRows: 2, maxRows: 4}"
type="textarea"
placeholder="Role Description"
/>
</el-form-item>
<el-form-item label="Menus">
<el-tree ref="tree" :check-strictly="checkStrictly" :data="routesData" :props="defaultProps" show-checkbox node-key="path" class="permission-tree" />
</el-form-item>
</el-form>
<div style="text-align:right;">
<el-button type="danger" @click="dialogVisible=false">
{{ $t('permission.cancel') }}
</el-button>
<el-button type="primary" @click="confirmRole">
{{ $t('permission.confirm') }}
</el-button>
</div>
</el-dialog>
</div>
</template>
<script>
import path from 'path'
import { deepClone } from '@/utils'
import { getRoutes, getRoles, addRole, deleteRole, updateRole } from '@/api/role'
import i18n from '@/lang'
const defaultRole = {
key: '',
name: '',
description: '',
routes: []
}
export default {
data() {
return {
role: Object.assign({}, defaultRole),
routes: [],
rolesList: [],
dialogVisible: false,
dialogType: 'new',
checkStrictly: false,
defaultProps: {
children: 'children',
label: 'title'
}
}
},
computed: {
routesData() {
return this.routes
}
},
created() {
// Mock: get all routes and roles list from server
this.getRoutes()
this.getRoles()
},
methods: {
async getRoutes() {
const res = await getRoutes()
this.serviceRoutes = res.data
const routes = this.generateRoutes(res.data)
this.routes = this.i18n(routes)
},
async getRoles() {
const res = await getRoles()
this.rolesList = res.data
},
i18n(routes) {
const app = routes.map(route => {
route.title = i18n.t(`route.${route.title}`)
if (route.children) {
route.children = this.i18n(route.children)
}
return route
})
return app
},
// Reshape the routes structure so that it looks the same as the sidebar
generateRoutes(routes, basePath = '/') {
const res = []
for (let route of routes) {
// skip some route
if (route.hidden) { continue }
const onlyOneShowingChild = this.onlyOneShowingChild(route.children, route)
if (route.children && onlyOneShowingChild && !route.alwaysShow) {
route = onlyOneShowingChild
}
const data = {
path: path.resolve(basePath, route.path),
title: route.meta && route.meta.title
}
// recursive child routes
if (route.children) {
data.children = this.generateRoutes(route.children, data.path)
}
res.push(data)
}
return res
},
generateArr(routes) {
let data = []
routes.forEach(route => {
data.push(route)
if (route.children) {
const temp = this.generateArr(route.children)
if (temp.length > 0) {
data = [...data, ...temp]
}
}
})
return data
},
handleAddRole() {
this.role = Object.assign({}, defaultRole)
if (this.$refs.tree) {
this.$refs.tree.setCheckedNodes([])
}
this.dialogType = 'new'
this.dialogVisible = true
},
handleEdit(scope) {
this.dialogType = 'edit'
this.dialogVisible = true
this.checkStrictly = true
this.role = deepClone(scope.row)
this.$nextTick(() => {
const routes = this.generateRoutes(this.role.routes)
this.$refs.tree.setCheckedNodes(this.generateArr(routes))
// set checked state of a node not affects its father and child nodes
this.checkStrictly = false
})
},
handleDelete({ $index, row }) {
this.$confirm('Confirm to remove the role?', 'Warning', {
confirmButtonText: 'Confirm',
cancelButtonText: 'Cancel',
type: 'warning'
})
.then(async() => {
await deleteRole(row.key)
this.rolesList.splice($index, 1)
this.$message({
type: 'success',
message: 'Delete succed!'
})
})
.catch(err => { console.error(err) })
},
generateTree(routes, basePath = '/', checkedKeys) {
const res = []
for (const route of routes) {
const routePath = path.resolve(basePath, route.path)
// recursive child routes
if (route.children) {
route.children = this.generateTree(route.children, routePath, checkedKeys)
}
if (checkedKeys.includes(routePath) || (route.children && route.children.length >= 1)) {
res.push(route)
}
}
return res
},
async confirmRole() {
const isEdit = this.dialogType === 'edit'
const checkedKeys = this.$refs.tree.getCheckedKeys()
this.role.routes = this.generateTree(deepClone(this.serviceRoutes), '/', checkedKeys)
if (isEdit) {
await updateRole(this.role.key, this.role)
for (let index = 0; index < this.rolesList.length; index++) {
if (this.rolesList[index].key === this.role.key) {
this.rolesList.splice(index, 1, Object.assign({}, this.role))
break
}
}
} else {
const { data } = await addRole(this.role)
this.role.key = data
this.rolesList.push(this.role)
}
const { description, key, name } = this.role
this.dialogVisible = false
this.$notify({
title: 'Success',
dangerouslyUseHTMLString: true,
message: `
<div>Role Key: ${key}</div>
<div>Role Nmae: ${name}</div>
<div>Description: ${description}</div>
`,
type: 'success'
})
},
// reference: src/view/layout/components/Sidebar/SidebarItem.vue
onlyOneShowingChild(children = [], parent) {
let onlyOneChild = null
const showingChildren = children.filter(item => !item.hidden)
// When there is only one child route, the child route is displayed by default
if (showingChildren.length === 1) {
onlyOneChild = showingChildren[0]
onlyOneChild.path = path.resolve(parent.path, onlyOneChild.path)
return onlyOneChild
}
// Show parent if there are no child route to display
if (showingChildren.length === 0) {
onlyOneChild = { ... parent, path: '', noShowingChildren: true }
return onlyOneChild
}
return false
}
}
}
</script>
<style lang="scss" scoped>
.app-container {
.roles-table {
margin-top: 30px;
}
.permission-tree {
margin-bottom: 30px;
}
}
</style>

View File

@ -1,7 +1,8 @@
'use strict' 'use strict'
require('@babel/register') require('@babel/register')
require('module-alias/register')
const path = require('path') const path = require('path')
const settings = require('./src/settings.js').default const { default: settings } = require('./src/settings.js')
const { name } = settings const { name } = settings
function resolve(dir) { function resolve(dir) {
@ -54,6 +55,7 @@ module.exports = {
// import ES2015 module from common.js module // import ES2015 module from common.js module
const { default: mocks } = require('./mock') const { default: mocks } = require('./mock')
for (const mock of mocks) { for (const mock of mocks) {
console.log(mock, mock.response)
app.all(mock.route, mock.response) app.all(mock.route, mock.response)
} }
} }