在后端篇中已对权限资源进行了分类:API接口、路由菜单、页面按钮。本文重点讲一下如何对这些权限资源进行分配并对不一样的登陆用户,根据权限的不一样,呈现不同的功能菜单、按钮进行统一处理。css
多对多关系:一个用户能够拥有多个角色,一个角色包含多个用户前端
多对多关系:一个角色有多个菜单,一个菜单能够分配给多个角色vue
多对多关系:一个角色有多个权限资源,一个权限资源能够分配给多个角色node
vue能够经过router.addRoutes动态地添加路由信息git
// 动态添加可访问路由表
router.addRoutes(accessRoutes)
复制代码
这里只是简单的介绍,想详细了解的,可自行查看资料。vuex
vue能够自定义指令,拿当前dom元素,而后进行移除操做,下面的el便是原生的dom对象。后端
const directives = {
has: {
inserted: function(el, binding, vnode) {
if(binding.value) {
el.parentNode.removeChild(el)
}
}
}
}
export default directives
复制代码
import directive from './directives'
const importDirective = Vue => {
/** * 权限指令 * options ===>权限字符串数组 ['admin','/sys/role/add'] */
Vue.directive('hasPerm', directive.has)
}
export default importDirective
复制代码
hasPerm即为组件注册时定义的hasPerm,经过v-hasPerm的方式使用,['admin','sys:role:save']即为binding.valueapi
<el-button v-hasPerm="['admin','sys:role:save']">添加</el-button>
复制代码
接口名称 | 接口地址 |
---|---|
保存用户角色关系 | /sys/rbac/saveUserRole |
保存角色菜单关系 | /sys/rbac/saveRoleMenu |
保存角色权限资源关系 | /sys/rbac/saveRoleAccess |
从角色中移除用户 | /sys/rbac/deleteUserRole |
角色成员列表 | /sys/rbac/listUserByRoleId |
查询未加入指定角色的用户列表 | /sys/rbac/listUserNoInRole |
获取权限资源树 | /sys/rbac/listAccessTree |
经过角色id获取菜单 | /sys/rbac/listMenuByRoleId |
获取当前用户信息 | /sys/user/info |
├── src
├── api/sys
└── sys.rbac.service.js
├── directive
├── directives.js
└── index.js
├── layout
├── components/Sidebar
└── index.js
├── router
└── index.js
├── store
├── modules
└── permission.js
└── getters.js
├── views/modules/sys
└── role
├── drawer.vue
└── selectUser.vue
├── main.js
└── permission.js
复制代码
src/api/sys/sys.rbac.service.js
权限管理相关的接口定义数组
src/directive/directives.js
按钮权限指令定义逻辑处理bash
const directives = {
has: {
inserted: function(el, binding, vnode) {
var arr = binding.value
// 判断要查询的数组(arr)是否至少有一个元素包含在目标数组(vnode.context.$store.state.user.access)中
if (!vnode.context.$store.state.user.accessList.some(_ => arr.indexOf(_) > -1)) {
vnode.context.$nextTick(() => {
if (el.parentNode) {
// 节点存在,就删除
el.parentNode.removeChild(el)
}
})
}
}
}
}
export default directives
复制代码
src/directive/index.js
指定注册
import directive from './directives'
const importDirective = Vue => {
/** * 权限指令 * options ===>权限字符串数组 ['admin','/sys/role/add'] */
Vue.directive('hasPerm', directive.has)
}
export default importDirective
复制代码
src/layout/components/Sidebar/index.js
使用addRouters添加的路由,this.$router.options.routes没法获取到,因此须要修改为vuex的
<sidebar-item v-for="route in permission_routes" :key="route.path" :item="route" :base-path="route.path" />
<!-- ===>修改为-->
<sidebar-item v-for="route in routes" :key="route.path" :item="route" :base-path="route.path" />
复制代码
新增permission_routes
computed: {
...mapGetters([
'permission_routes',
'sidebar'
]),
复制代码
src/router/index.js
将动态路由从静态路由中移除
const createRouter = () => new Router({
mode: 'history', // require service support
scrollBehavior: () => ({ y: 0 }),
routes: [
... constantRoutes // ,
//... asyncRoutes
]
})
复制代码
src/store/modules/permission.js
这里权限状态管理--vuex
主要是使用用户信息接口中的menuList去加工asyncRoutes中的路由菜单,排序、修更名称、修改图标、控制显示。
import { asyncRoutes, constantRoutes } from '@/router'
/** * Use meta.access to determine if the current user has permission * @param menus * @param route */
function hasPermission(menus, route) {
if (route.name) {
var currMenu = getMenu(route.name, menus)
if (currMenu !== null) {
// 设置菜单的标题、图标
if (currMenu.name) {
route.meta.title = currMenu.name
}
if (currMenu.icon) {
route.meta.icon = currMenu.icon
}
if (currMenu.sort) {
route.sort = currMenu.sort
}
} else {
route.sort = 10
}
}
if (route.meta && route.meta.access) {
return menus.some(menu => route.meta.access.includes(menu.routeName))
} else {
return true
}
}
// 根据路由名称获取菜单
function getMenu(access, menus) {
for (let i = 0; i < menus.length; i++) {
var menu = menus[i]
if (access === menu.routeName) {
return menu
}
}
return null
}
// 对菜单进行排序
function sortRouters(accessedRouters) {
for (let i = 0; i < accessedRouters.length; i++) {
var router = accessedRouters[i]
if (router.children && router.children.length > 0) {
router.children.sort(compare('sort'))
}
}
accessedRouters.sort(compare('sort'))
}
// 降序比较函数
function compare(p) {
return function(m, n) {
var a = m[p]
var b = n[p]
return b - a
}
}
/** * Filter asynchronous routing tables by recursion * @param routes asyncRoutes * @param menus */
export function filterAsyncRoutes(routes, menus) {
const res = []
routes.forEach(route => {
const tmp = { ...route }
if (hasPermission(menus, tmp)) {
if (tmp.children) {
tmp.children = filterAsyncRoutes(tmp.children, menus)
}
res.push(tmp)
}
})
return res
}
const state = {
routes: [],
addRoutes: []
}
const mutations = {
SET_ROUTES: (state, routes) => {
state.addRoutes = routes
state.routes = constantRoutes.concat(routes)
}
}
const actions = {
generateRoutes({ commit }, menus) {
return new Promise(resolve => {
var accessedRoutes = filterAsyncRoutes(asyncRoutes, menus)
// 对菜单进行排序
sortRouters(accessedRoutes)
commit('SET_ROUTES', accessedRoutes)
resolve(accessedRoutes)
})
}
}
export default {
namespaced: true,
state,
mutations,
actions
}
复制代码
src/store/getters.js
const getters = {
sidebar: state => state.app.sidebar,
device: state => state.app.device,
token: state => state.user.token,
avatar: state => state.user.avatar,
name: state => state.user.name,
// 这里追加dictMap的get方法,可使用mapGetters,详见src/components/m/Dict/index.vue
dictMap: state => state.dict.dictMap,
// 使用addRouter动态添加路由后,须要用vuex维护路由信息
permission_routes: state => state.permission.routes
}
export default getters
复制代码
src/views/modules/sys/role/drawer.vue
角色成员管理抽屉,index.vue已由代码生成器生成,drawer.vue暂时须要手工添加,添加再自定义修改。
<template>
<div class="app-container">
<el-button
style="margin-bottom:10px;margin-left:10px;"
type="primary"
icon="el-icon-plus"
size="small"
@click="handleOpenSelectUser"
v-hasPerm="['admin','sys:rbac:saveUserRole']"
>新增</el-button>
<el-table :header-cell-style="{background:'#eef1f6',color:'#606266'}" v-loading="loading" :data="tableData">
<el-table-column prop="userName" label="用户名">
<template slot-scope="scope">
{{ scope.row.userName }}
</template>
</el-table-column>
<el-table-column prop="realName" label="姓名">
<template slot-scope="scope">
{{ scope.row.realName }}
</template>
</el-table-column>
<el-table-column prop="mobilePhone" label="手机号">
<template slot-scope="scope">
{{ scope.row.mobilePhone }}
</template>
</el-table-column>
<el-table-column
label="操做"
align="center">
<template slot-scope="scope">
<el-button type="text" size="small" icon="el-icon-delete" v-hasPerm="['admin','sys:rbac:deleteUserRole']" @click.native.stop="handleDeleteUserRole(scope.row.id)">移除</el-button>
</template>
</el-table-column>
</el-table>
<pagination
v-show="recordCount>0"
:total="recordCount"
:page.sync="pageNum"
:limit.sync="pageSize"
@pagination="requestData"
/>
</div>
</template>
<script>
import { listUserByRoleId, deleteUserRole } from '@/api/sys/sys.rbac.service.js'
export default {
props: {
id: {
type: [String, Number],
default: undefined
}
},
data() {
return {
// 总记录数
recordCount: 0,
// 表格数据加载中
loading: false,
// 列表数据
tableData: [],
// 当前页
pageNum: 1,
// 每页大小
pageSize: Number(process.env.VUE_APP_PAGE_SIZE)
}
},
watch: {
id(n) {
this.requestData()
}
},
mounted() {
this.requestData()
},
methods: {
// 请求数据
requestData(page) {
if (!this.id) {
return
}
if (!page) {
page = {
page: this.pageNum,
limit: this.pageSize
}
}
this.loading = true
listUserByRoleId({
pageNum: page.page,
pageSize: page.limit,
roleId: this.id,
...this.searchForm
}).then(res => {
this.loading = false
if (res.code === 0) {
this.tableData = res.data.rows
this.recordCount = res.data.recordCount
}
}).catch(() => {
this.loading = false
})
},
// 打开弹窗--使用index.vue中的dialog,因此须要$parent.$parent selectUser -> drawer->index.vue
handleOpenSelectUser() {
this.$parent.$parent.openDialog(this.id, `选择用户`, 'selectUser', true)
},
handleDeleteUserRole(id) {
this.$confirm('此操做将永久删除该记录, 是否继续?', '提示', {
confirmButtonText: '肯定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
var ids = []
if (id) {
ids.push(id)
} else {
return
}
deleteUserRole({
ids: ids,
id: this.id
}).then(res => {
if (res.code === 0) {
this.$message({
message: '删除成功',
type: 'success'
})
this.requestData()
} else {
this.$message({
message: res.msg || '删除失败',
type: 'error'
})
}
})
}).catch((e) => {
this.$message({
type: 'info',
message: '已取消删除' + e
})
})
}
}
}
</script>
<style lang="scss" scoped>
</style>
复制代码
src/views/modules/sys/role/selectUser.vue
选择成员添加,index.vue已由代码生成器生成,selectUser.vue暂时须要手工添加,添加再自定义修改。
<template>
<div class="app-container">
<el-form ref="searchForm" :model="searchForm" :inline="true">
<el-form-item label="关键字" prop="keywords">
<el-input v-model="searchForm.keywords" placeholder="请输入用户名、手机号" size="small" style="width: 240px"></el-input>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="el-icon-search" size="small" @click="handleSearch">查询</el-button>
<el-button icon="el-icon-refresh" size="small" @click="resetform">重置</el-button>
</el-form-item>
</el-form>
<el-table :header-cell-style="{background:'#eef1f6',color:'#606266'}" v-loading="loading" :data="tableData" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="55" align="center" />
<el-table-column prop="userName" label="用户名">
<template slot-scope="scope">
{{ scope.row.userName }}
</template>
</el-table-column>
<el-table-column prop="realName" label="姓名">
<template slot-scope="scope">
{{ scope.row.realName }}
</template>
</el-table-column>
<el-table-column prop="mobilePhone" label="手机号">
<template slot-scope="scope">
{{ scope.row.mobilePhone }}
</template>
</el-table-column>
</el-table>
<pagination
v-show="recordCount>0"
:total="recordCount"
:page.sync="pageNum"
:limit.sync="pageSize"
@pagination="requestData"
/>
</div>
</template>
<script>
import { listUserNoInRole, saveUserRole } from '@/api/sys/sys.rbac.service.js'
export default {
props: {
id: {
type: [String, Number],
default: undefined
}
},
data() {
return {
// 非单个禁用
single: true,
// 非多个禁用
multiple: true,
// 总记录数
recordCount: 0,
// 表格数据加载中
loading: false,
// 弹出层
open: false,
// 弹出层内容
dialogContent: 'add',
// 是否显示Ok
showOk: true,
// 当前弹出层标题
title: '',
// 列表数据
tableData: [],
// 提交按钮状态
submitLoading: false,
// 当前页
pageNum: 1,
// 每页大小
pageSize: Number(process.env.VUE_APP_PAGE_SIZE),
// 当前勾选行id
ids: [],
// 当前勾选行集合
selection: [],
searchForm: {
keywords: undefined
}
}
},
watch: {
id(n) {
this.requestData()
}
},
mounted() {
this.requestData()
},
methods: {
// 多选框选中数据
handleSelectionChange(selection) {
this.selection = selection
this.ids = selection.map(item => item.id)
this.single = selection.length !== 1
this.multiple = !selection.length
},
// 请求数据
requestData(page) {
if (!this.id) {
return
}
if (!page) {
page = {
page: this.pageNum,
limit: this.pageSize
}
}
this.loading = true
listUserNoInRole({
pageNum: page.page,
pageSize: page.limit,
roleId: this.id,
...this.searchForm
}).then(res => {
this.loading = false
if (res.code === 0) {
this.tableData = res.data.rows
this.recordCount = res.data.recordCount
}
}).catch(() => {
this.loading = false
})
},
// 查询
handleSearch() {
this.requestData()
},
// 重置
resetform(e) {
this.$refs['searchForm'].resetFields()
},
resetFields() {
this.$refs['searchForm'].resetFields()
this.requestData()
},
// 提交
submit() {
return new Promise((resolve, reject) => {
if (!this.ids.length) {
reject(new Error('id不能为空'))
return
}
saveUserRole({
ids: this.ids,
id: this.id
}).then(res => {
this.$parent.$parent.$refs.drawer.requestData()
resolve(res)
}).catch(e => {
reject(e)
})
})
}
}
}
</script>
<style lang="scss" scoped>
</style>
复制代码
src/main.js
这里主要是注册自定义指令
import importDirective from '@/directive'
/** * 注册指令 */
importDirective(Vue)
复制代码
src/permission.js
这里处理一下添加动态路由的逻辑
// 进入页面前拦截
router.beforeEach(async(to, from, next) => {
// 进度条开始
NProgress.start()
// 重设页面标题
document.title = getPageTitle(to.meta.title)
// 获取token,判断是否已经登陆
const hasToken = getToken()
if (hasToken) {
if (to.path === '/login') {
// 若是已经登陆且是登陆页,则重定向到首页
next({ path: '/' })
NProgress.done()
} else {
const hasGetUserInfo = store.getters.name
// 判断用户信息是否存在,若是已经存在,则能够进入页面
if (hasGetUserInfo) {
next()
} else {
try {
// 拉取用户信息
const { menuList } = await store.dispatch('user/getInfo')
// 生成可访问的路由表
const accessRoutes = await store.dispatch('permission/generateRoutes', menuList)
// 动态添加可访问路由表
router.addRoutes(accessRoutes)
router.app.$nextTick(() => {
next({ ...to, replace: true })
})
} catch (error) {
// 获取用户信息失败,则删除会话信息并跳转到登陆页
await store.dispatch('user/resetToken')
Message.error(error || 'Has Error')
next(`/login?redirect=${to.path}`)
NProgress.done()
}
}
}
} else {
/* 没有token*/
if (whiteList.indexOf(to.path) !== -1) {
// 是白名单的页面,则能够进入页面
next()
} else {
// 非白名单,则跳转到登陆页
next(`/login?redirect=${to.path}`)
NProgress.done()
}
}
})
复制代码
写到这里,前端篇也告一段落了,bug确定是有的,不事后续发现再慢慢优化吧。我要开启新的篇章了-手把手带你玩转k8s!
打造一款适合本身的快速开发框架-前端篇之框架分层及CURD样例