权限管理模块
RBAC的权限设计
思想
采用方案: RBAC的权限模型,RBAC(Role-Based Access control) ,也就是基于角色的权限分配解决方案 其权限模式如下:
三个关键点: 员工用户, 角色, 权限
- 给员工分配角色
- 给角色分配权限
以你自己为例, 你进入一家公司, 入职, 人事将你录入系统 => 分配你的角色 (学生)
同角色有着相同的权限, 操作角色权限的同时, 所有该角色的用户对应权限, 就会同步更新
给员工分配角色
**
目标
**在员工管理页面,分配角色
 > 总结:给员工分配角色(1、只能分配一个角色;2、可以分配多个角色(1:n)) > 一旦把角色分配给用户,那么该用户登录系统后就可以访问对应的权限了。
新建分配角色弹框
首先,新建分配角色窗体 employees/components/assign-role.vue
<template>
<el-dialog title="分配角色" :visible="showRoleDialog" @close="handleClose">
<!-- 角色列表 -->
<div>角色列表</div>
<template #footer>
<el-button type="primary" size="small">确定</el-button>
<el-button size="small" @click='handleClose'>取消</el-button>
</template>
</el-dialog>
</template>
<script>
export default {
name: 'AssignRole',
props: {
showRoleDialog: {
type: Boolean,
required: true
}
},
methods: {
handleClose () {
this.$emit('update:showRoleDialog', false)
}
}
}
</script>
弹层的显示和关闭
- 点击角色按钮显示弹层
- 注册组件
import AssignRole from './components/assign-role'
components: {
AddEmployee,
AssignRole
},
<assign-role :show-role-dialog.sync="showRoleDialog" :user-id="userId" />
- 点击角色按钮, 记录id, 显示弹层
<el-button type="text" size="small" @click="showRoleBox(row.id)">角色</el-button>
showRoleBox(id) {
this.userId = id
this.showRoleDialog = true
}
- 弹层的关闭
<template>
<el-dialog class="assign-role" title="分配角色" :visible="showRoleDialog" @close="handleClose">
<!-- el-checkbox-group选中的是 当前用户所拥有的角色 需要绑定 当前用户拥有的角色-->
<el-checkbox-group>
<!-- 选项 -->
</el-checkbox-group>
<template #footer>
<div style="text-align: right">
<el-button type="primary">确定</el-button>
<el-button @click="handleClose">取消</el-button>
</div>
</template>
</el-dialog>
</template>
<script>
export default {
props: {
showRoleDialog: {
type: Boolean,
default: false
},
// 用户的id 用来查询当前用户的角色信息
userId: {
type: String,
default: null
}
},
methods: {
handleClose () {
this.$emit('update:showRoleDialog', false)
}
}
}
</script>
总结:控制弹窗的显示和隐藏
- el-dialog组件的基本用法
- 父子之间传值的简化写法 sync 修饰符
获取角色列表
- 基本布局
<el-dialog title="分配角色" :visible="showRoleDialog" @open="loadRoleList" @close="handleClose">
<el-checkbox-group v-model="roleIds">
<el-checkbox label="110">管理员</el-checkbox>
<el-checkbox label="113">开发者</el-checkbox>
<el-checkbox label="115">人事</el-checkbox>
</el-checkbox-group>
- 发送请求获取角色列表
import { reqGetRoleList } from '@/api/setting.js'
export default {
name: 'AssignRole',
props: {
showRoleDialog: {
type: Boolean,
required: true
},
userId: {
type: String,
required: true
}
},
data () {
return {
// 选中的角色ids
roleIds: [],
// 角色列表
list: []
}
},
created () {
// 获取橘色列表数据
this.loadRoleList()
},
methods: {
// 加载角色类别数据
async loadRoleList () {
const ret = await reqGetRoleList({
page: 1,
pagesize: 1000
})
if (ret.code === 10000) {
this.list = ret.data.rows
} else {
this.$message.error(ret.message)
}
},
// 关闭弹窗
handleClose () {
this.$emit('update:showRoleDialog', false)
}
}
- 渲染数据
<el-checkbox-group v-model="roleIds">
<el-checkbox v-for="item in list" :key="item.id" :label="item.id">{{ item.name }}</el-checkbox>
</el-checkbox-group>
- 微调样式
<style lang="scss" scoped>
.assign-role {
::v-deep {
.el-checkbox {
font-size: 30px;
}
}
}
</style>
总结:
- 调用接口获取所有的角色列表数据
- 动态渲染CheckBox角色列表
获取用户的当前角色
- 获取用户的当前角色, 进行回显
import { reqGetUserDetailById } from '@/api/user'
// 获取所有的角色列表数据
async loadRoleList () {
// 获取所有角色数据
const ret = await reqGetRoleList({
page: 1,
pagesize: 1000
})
this.allList = ret.data.rows
// 获取当前用户数据
const info = await reqGetUserDetailById(this.userId)
this.roleList = info.data.roleIds
},
- 基于open事件触发接口调用
<el-dialog class="assign-role" title="分配角色" :visible="showRoleDialog" @open="dialogOpen" @close="closeDialog">
methods: {
loadRoleList() {
this.loadRoleList()
},
}
给员工分配角色
- 分配角色接口
api/employees.js
export function reqAssignRoles(data) {
return request({
url: '/sys/user/assignRoles',
data,
method: 'put'
})
}
- 确定保存
assign-role
<el-button type="primary" @click="handleSubmit">确定</el-button>
async handleSubmit () {
const ret = await reqAssignRoles({
// 用户id
id: this.userId,
// 选中的角色列表
roleIds: this.roleList
})
if (ret.success) {
// 分配角色成功
this.$message.success(ret.message)
// 关闭弹窗
this.handleClose()
}
},
总结:给用户分配角色(一个用户可以分配多个角色)
添加loading效果
需求:显示角色列表的过程提供一个
<el-checkbox-group v-model="roleIds" v-loading="loading">
// 获取所有的角色列表数据
async loadRoleList () {
// 开启加载状态
this.loading = true
// 获取所有角色数据
const ret = reqGetRoleList({
page: 1,
pagesize: 1000
})
// this.allList = ret.data.rows
// 获取当前用户数据
const info = reqGetUserDetailById(this.userId)
// this.roleList = info.data.roleIds
// Promise.all方法的作用:并行触发多个异步任务,并且所有任务的结果都返回后触发then
Promise.all([ret, info]).then(result => {
// result是两个任务的结果
this.allList = result[0].data.rows
this.roleList = result[1].data.roleIds
// 隐藏加载状态
this.loading = false
}).catch((err) => {
console.log(err)
this.$message.error('获取角色数据失败')
})
},
总结: 共同点(都是并发触发多个任务);不同点:all保证所有任务都完成后获取异步结果;race只要有一个任务返回就得到该任务的结果,其他任务的结果不做处理。
- Promise.all
- Promise.race
权限点管理页面开发
目标
: 完成权限点页面的开发和管理 => 为了后面做准备
- 便于分配权限, 只有有权限了才能分配
- 只有分配好了权限, 有对应的权限规则, 才能控制路由(模块访问权), 才能控制按钮的显示(操作权)
新建权限点管理页面
- 完成权限页面结构
src/views/permission/index.vue
<template>
<div class="permission-container">
<div class="app-container">
<!-- 表格 -->
<el-card>
<div style="text-align: right; margin-bottom: 20px">
<el-button type="primary" size="small">添加权限</el-button>
</div>
<el-table border>
<el-table-column label="名称" />
<el-table-column label="标识" />
<el-table-column label="描述" />
<el-table-column label="操作">
<template>
<el-button size="small" type="text">添加权限点</el-button>
<el-button size="small" type="text">查看</el-button>
<el-button size="small" type="text">删除</el-button>
</template>
</el-table-column>
</el-table>
</el-card>
</div>
</div>
</template>
<script>
export default {
name: 'Permission'
}
</script>
- 封装权限管理的增删改查请求
src/api/permisson.js
import request from '@/utils/request'
// 获取权限
export function reqGetPermissionList() {
return request({
method: 'get',
url: '/sys/permission'
})
}
// 新增权限
export function reqAddPermission(data) {
return request({
method: 'post',
url: '/sys/permission',
data
})
}
// 更新权限
export function reqUpdatePermission(data) {
return request({
method: 'put',
url: `/sys/permission/${data.id}`,
data
})
}
// 删除权限
export function reqDelPermission(id) {
return request({
method: 'delete',
url: `/sys/permission/${id}`
})
}
// 获取权限详情
export function reqGetPermissionDetail(id) {
return request({
method: 'get',
url: `/sys/permission/${id}`
})
}
动态渲染权限列表
<template>
<div class="permission-container">
<div class="app-container">
<!-- 表格 -->
<el-card>
<div style="text-align: right; margin-bottom: 20px">
<el-button type="primary" size="small">添加权限</el-button>
</div>
<el-table border :data="list">
<el-table-column label="名称" prop="name" />
<el-table-column label="标识" prop="code" />
<el-table-column label="描述" prop="description" />
<el-table-column label="操作">
<template>
<el-button size="small" type="text">添加权限点</el-button>
<el-button size="small" type="text">查看</el-button>
<el-button size="small" type="text">删除</el-button>
</template>
</el-table-column>
</el-table>
</el-card>
</div>
</div>
</template>
<script>
import { reqGetPermissionList } from '@/api/permisson.js'
export default {
name: 'Permission',
data() {
return {
list: []
}
},
created() {
this.loadPermissionList()
},
methods: {
// 加载权限列表数据
async loadPermissionList () {
try {
const ret = await reqGetPermissionList()
if (ret.code === 10000) {
this.list = ret.data
} else {
this.$message.error(ret.message)
}
} catch {
this.$message.error('获取权限列表失败')
}
}
}
</script>
注意:但是这里的数据, 拿到的是列表式的数据, 但是希望渲染的是树形结构的, 所以需要处理 调用接口;获取数据;填充表格
获取权限数据并转化树形
这里,我们通过树形操作方法,将列表转化成层级数据
import { reqGetPermissionList } from '@/api/permission'
import { translateListToTreeData } from '@/utils'
export default {
name: 'Permission',
data() {
return {
list: []
}
},
created () {
this.loadPermissionList()
},
methods: {
// 加载权限列表数据
async loadPermissionList () {
try {
const ret = await reqGetPermissionList()
if (ret.success) {
// 把列表数据转换为树形数据
this.list = translateListToTreeData(ret.data, '0')
console.log(this.list)
} else {
this.$message.error('获取权限列表失败')
}
} catch (e) {
console.log(e)
this.$message.error('获取权限列表失败!')
}
}
}
}
- 给 table 表格添加 row-key 属性(不要添加冒号),table的列表数据必须包含children属性
<el-table border :data="list" row-key="id">
<el-table-column label="名称" prop="name" />
<el-table-column label="标识" prop="code" />
<el-table-column label="描述" prop="description" />
<el-table-column label="操作">
<template>
<el-button size="small" type="text">添加权限点</el-button>
<el-button size="small" type="text">查看</el-button>
<el-button size="small" type="text">删除</el-button>
</template>
</el-table-column>
</el-table>
- 需要注意的是,当 type为1 时为一级权限, type为2 时为二级权限, 没有三级权限
<template>
<div class="permission-container">
<div class="app-container">
<!-- 表格 -->
<el-card>
<div style="text-align: right; margin-bottom: 20px">
<el-button type="primary" size="small">添加权限</el-button>
</div>
<el-table border :data="list" row-key="id">
<el-table-column label="名称" prop="name" />
<el-table-column label="标识" prop="code" />
<el-table-column label="描述" prop="description" />
<el-table-column label="操作">
<template #default="{ row }">
<el-button v-if="row.type === 1" size="small" type="text">添加权限点</el-button>
<el-button size="small" type="text">查看</el-button>
<el-button size="small" type="text">删除</el-button>
</template>
</el-table-column>
</el-table>
</el-card>
</div>
</div>
</template>
总结:
- 展示树形表格结构
- 区分一级和二级权限
准备新增的弹层
- 弹层结构
<!-- 新增编辑的弹层 -->
<el-dialog :visible="showDialog" title="弹层标题" @close="showDialog = false">
<!-- 表单内容 -->
<el-form label-width="100px">
<el-form-item label="权限名称">
<el-input />
</el-form-item>
<el-form-item label="权限标识">
<el-input />
</el-form-item>
<el-form-item label="权限描述">
<el-input />
</el-form-item>
<el-form-item label="权限启用">
switch
</el-form-item>
</el-form>
<template #footer>
<div style="text-align: right;">
<el-button @click="showDialog = false">取消</el-button>
<el-button type="primary">确定</el-button>
</div>
</template>
</el-dialog>
绑定数据
- 基于文档准备数据
data() {
return {
list: [],
showDialog: false,
formData: {
enVisible: '0', // 开启
name: '', // 名称
code: '', // 权限标识
description: '', // 描述
type: '', // 类型
pid: '' // 添加到哪个节点下
},
// 表单验证
rules: {
name: [
{ required: true, message: '权限点名称不能为空', trigger: ['blur', 'change'] }
],
code: [
{ required: true, message: '权限点编码不能为空', trigger: ['blur', 'change'] }
]
}
}
},
- 表单绑定
<!-- 新增编辑的弹层 -->
<el-dialog :visible="showDialog" title="弹层标题" @close="showDialog = false">
<!-- 表单内容 -->
<el-form ref="authForm" :model="formData" :rules="rules" label-width="100px">
<el-form-item label="权限名称" prop='name'>
<el-input v-model="formData.name"/>
</el-form-item>
<el-form-item label="权限标识" prop='code'>
<el-input v-model="formData.code"/>
</el-form-item>
<el-form-item label="权限描述" prop='description'>
<el-input v-model="formData.description"/>
</el-form-item>
<el-form-item label="是否展示" prop='enVisible'>
switch
</el-form-item>
</el-form>
<template #footer>
<div style="text-align: right;">
<el-button @click="showDialog = false">取消</el-button>
<el-button type="primary">确定</el-button>
</div>
</template>
</el-dialog>
总结:
- 表单验证流程:
- el-form(ref/model/rules)
- el-form-item(prop)
- el-input(v-model)
Switch组件用法
<el-switch
v-model="formData.enVisible"
active-value="0"
inactive-value="1"
active-text="启用"
inactive-text="禁用"
active-color="#13ce66"
inactive-color="#eee">
</el-switch>
总结:定制选项值的格式 active-value="0" 表示选中的值
inactive-value="1" 表示禁用的值
新增功能
新增有两个新增:
- 点击上面的添加权限, 添加是一级, 是一级访问权
<el-button type="primary" size="small" @click="handleAdd(1, '0')">添加权限</el-button>
- 点击下面的添加权限点, 添加的是二级, 是二级操作权(这里row.id 作为将来的pid)
<el-button v-if="row.type === 1" size="small" type="text" @click="handleAdd(2, row.id)">添加权限点</el-button>
提供事件处理函数
// 添加弹窗动作
handleAdd (type, pid) {
// 设置权限的级别
this.formData.type = type
// 设置权限的父节点
this.formData.pid = pid
// 显示弹窗
this.showDialog = true
},
// 提交表单
handleSubmit () {
this.$refs.authForm.validate(async valid => {
if (!valid) return
const ret = await reqAddPermission(this.formData)
if (ret.success) {
// 关闭弹窗
this.showDialog = false
// 刷新列表
this.loadPermissionList()
// 清空表单
this.$refs.authForm.resetFields()
this.formData = this.$options.data().formData
} else {
this.$message.error(ret.message)
}
})
},
总结:
- 区分一级和二级菜单的type和pid
- 调用接口实现添加功能
删除功能
需求:1、绑定事件;2、提示删除;3、调用接口删除;4、刷新列表
- 注册点击事件
<el-button size="small" type="text" @click="handleDelete(row.id)">删除</el-button>
- 点击时发送删除请求
// 删除权限点
handleDelete (id) {
this.$confirm('确认要删除吗?', '温馨提示').then(async () => {
const ret = await reqDelPermission(id)
if (ret.success) {
// 删除成功
this.loadPermissionList()
this.showDialog = false
} else {
this.$message.error(ret.message)
}
}).catch((e) => {
if (e !== 'cancel') {
// 出错了
this.$message.error('删除失败')
}
})
},
总结:
- 删除需要添加确认
- 调用接口删除流程
查看修改功能
1 注册点击事件
<el-button size="small" type="text" @click="toEdit(row.id)">查看</el-button>
2 查看时回显
// 控制关闭弹窗
handleClose () {
this.showDialog = false
this.$refs.authForm.resetFields()
this.formData = this.$options.data().formData
},
// 编辑第一步(回填表单)
async toEdit (id) {
const ret = await reqGetPermissionDetail(id)
this.formData = ret.data
this.showDialog = true
},
3 三元表达式定制标题
<el-dialog :visible="showDialog" :title="formData.id?'编辑权限':'添加权限'" @close="handleClose">
4 提交修改, 通过判断 formData 中有没有 id (新增是没有 id 的)
// 提交表单
handleSubmit () {
this.$refs.authForm.validate(async valid => {
if (!valid) return
if (this.formData.id) {
// 编辑权限
const ret = await reqUpdatePermission(this.formData)
if (ret.success) {
this.loadPermissionList()
this.handleClose()
} else {
this.$message.error(ret.message)
}
} else {
// 添加权限
const ret = await reqAddPermission(this.formData)
if (ret.success) {
// 关闭弹窗
// this.showDialog = false
// 刷新列表
this.loadPermissionList()
// 清空表单
// this.$refs.authForm.resetFields()
// this.formData = this.$options.data().formData
this.handleClose()
} else {
this.$message.error(ret.message)
}
}
})
},
5 关闭时重置数据
// 控制关闭弹窗
handleClose () {
this.showDialog = false
this.$refs.authForm.resetFields()
this.formData = this.$options.data().formData
},
总结:
- 重用提交表单的方法(根据id的存在与否区分添加的编辑操作)
用户 -> 角色 -> 权限
给角色分配权限
新建分配权限弹出层
- 准备弹层
<!-- 分配权限的弹层 -->
<el-dialog title="分配权限" :visible="showAuthDialog" @close="handleClose">
<div>权限列表</div>
<template #footer>
<div style="text-align: right;">
<el-button @click="handleClose">取消</el-button>
<el-button type="primary">确定</el-button>
</div>
</template>
</el-dialog>
- 注册事件
<el-button size="small" type="success" @click="assignAuth(row.id)">分配权限</el-button>
- 提供数据方法
showAuthDialog: false, // 控制弹层的显示隐藏
ruleId: '' // 记录正在操作的角色
// 关闭授权的弹窗
handleClose () {
this.showAuthDialog = false
},
// 给指定角色进行授权
assignAuth (id) {
this.ruleId = id
this.showAuthDialog = true
},
总结:
- 点击【分配权限】按钮记录当前要分配角色的id
- 控制弹窗的显示和隐藏
获取权限数据
- 这里要进行权限分配, 先要请求拿到权限数据
permissionData: [] // 存储权限数据
- 弹层显示, 发送请求, 给弹层注册open事件
<el-tree
:data="permissionData"
show-checkbox
node-key="id"
:default-expand-all="true"
:default-checked-keys="[5]"
:props="defaultProps">
</el-tree>
- 拿到数据处理成功树形结构
// \获取权限列表数据
async loadAuthList () {
// 加载角色的权限数据
try {
const ret = await reqGetPermissionList()
if (ret.code === 10000) {
this.permissionData = ret.data
} else {
this.$message.error(ret.message)
}
} catch {
this.$message.error('获取权限列表失败')
}
},
- 把列表数据转换为树形结构 src/utils/index.js
// 查找pid是指定参数的子元素
export function translateData (list, pid) {
const treeData = []
// 自己设计算法做转换
list.forEach(item => {
if (item.pid === pid) {
// 查找该元素的子元素,如果找到子元素后,以children属性方式添加到对象里面
const childList = translateData(list, item.id)
if (childList && childList.length > 0) {
// 找到了子元素
item.children = childList
}
treeData.push(item)
}
})
return treeData
}
import { translateData } from '@/utils/index.js'
总结:
- 打开弹窗时,加载权限列表数据并转换为树形结构
- 树形组件的基本用法
结合树形控件显示
- 基本展示
<el-tree
:data="permissionData"
:props="{ label: 'name' }"
:default-expand-all="true"
:show-checkbox="true"
:check-strictly="true"
/>
- show-checkbox 显示选择框
- default-expand-all 默认展开
- check-strictly 设置true, 可以关闭父子关联
回显默认的权限
树形结构认知: 回显数据, 需要有一些树形结构的认知
- node-key 唯一标识
- this.$refs.tree.setCheckedKeys([ ]) => 传入选中的node-key数组
<el-tree
ref="tree"
v-loading="treeLoading"
:data="permissionData"
:props="{ label: 'name' }"
:default-expand-all="true"
:default-checked-keys="defaultAuthList"
:show-checkbox="true"
:check-strictly="true"
node-key="id"
/>
defaultAuthList: [] // 存储已选中的权限id列表
- 发送请求, 获取已选中的权限 id 列表, 进行回显
// 获取权限列表数据
loadAuthList () {
// 加载所有的权限
const ret = reqGetPermissionList()
// this.permissionData = translateListToTreeData(ret.data, '0')
// 加载角色本来拥有的权限列表
const auths = reqGetRoleDetail(this.ruleId)
// this.defaultAuthList = auths.data.permIds
// this.$nextTick(() => {
// this.$refs.authTree.setCheckedKeys(auths.data.permIds)
// })
Promise.all([ret, auths]).then(results => {
this.permissionData = translateListToTreeData(results[0].data, '0')
this.$nextTick(() => {
this.$refs.authTree.setCheckedKeys(results[1].data.permIds)
})
}).catch(() => {
this.$message.error('获取权限列表失败')
})
},
总结:
- 可以基于属性default-checked-keys设置树节点的选中
- 也可以基于setCheckedKeys实例方法设置树节点的选中
- 如何获取选中的节点?getCheckedKeys()
给角色分配权限
- 封装分配权限的api
src/api/setting.js
// 给角色分配权限
export function reqAssignPerm(data) {
return request({
url: '/sys/role/assignPrem',
method: 'put',
data
})
}
- 分配权限
// 给角色分配权限的提交动作
async handleAuthSubmit () {
const ret = await reqAssignPerm({
// 当前的角色id
id: this.ruleId,
// 选中权限点的节点(id)
permIds: this.$refs.authTree.getCheckedKeys()
})
if (ret.success) {
// 分配角色的权限成功
this.loadRoleList()
this.showAuthDialog = false
} else {
this.$message.error(ret.message)
}
},
总结:重新选择权限,然后分配给角色 获取树的选中的节点方式:
this.$refs.authTree.getCheckedKeys()
- 总结
- 权限点的管理
- 权限列表的动态渲染
- 添加权限
- 熟悉Switch组件的用法
- 删除权限点
- 编辑权限点
- 给角色分配权限(给用户分配角色类似)
- 展示所有的权限列表(树形方式展示)
- 展示并选中角色原有的权限
- 重新选择权限并提交表单
- 如何选中树形的组件节点?(用属性设置值;或者用户方法设置值)
- 如何获取树形的组件的节点id?用组件的实例方法
- 权限点的管理
不同的用户进入系统后,可以操作不同的功能(权限)
- 用户
- 给用户分配角色(支持多个角色的分配)
- 角色
- 给角色分配权限(一个角色可以拥有多个权限)
- 权限
前端权限-页面访问权(路由)
权限受控的思路
到了最关键的环节,我们设置的权限如何应用? 在上面的几个小节中,我们已经把给用户分配了角色, 给角色分配了权限, 那么在用户登录获取资料的时候,会自动查出该用户拥有哪些权限,这个权限需要和我们的菜单含有路由有效结合起来
- menus表示左侧路由菜单的权限(一级权限)
- points指的是路由组件中按钮的操作权限(二级权限)
而动态路由表其实就是根据用户的实际权限来访问的,接下来我们操作一下
在权限管理页面中,我们设置了一个标识, 这个标识可以和我们的路由模块进行关联, 如果用户拥有这个标识,那么用户就可以 拥有这个路由模块,如果没有这个标识,就不能访问路由模块
addRoutes 的基本使用
- router/index.js` 去掉 asyncRoutes 的默认合并,
const createRouter = () => new Router({
// mode: 'history', // require service support
scrollBehavior: () => ({ y: 0 }),
routes: [
...constantRoutes // 静态路由, 首页
// ...asyncRoutes // 所有的动态路由
]
})
permission.js
我们通过 addRoutes 动态添加试一下
import { asyncRoutes } from '@/router/index'
router.beforeEach(async(to, from, next) => {
...
...
if (!store.getters.userId) {
// 调用获取信息的action
const res = await store.dispatch('user/getUserInfo')
console.log('进行权限处理', res)
// 拿到权限信息之后, 应该根据权限信息, 从动态路由模块中筛选出, 需要追加的路由,
// 追加到routes规则中, addRoutes
router.addRoutes(asyncRoutes)
next({
...to, // next({ ...to })的目的,是保证路由添加完了再进入页面 (可以理解为重进一次)
replace: true // 重进一次, 不保留重复历史
})
return
}
...
...
})
可以新增出来, 但是菜单却没有动态渲染出来 => router.options.routes (拿的是默认配置的项, 拿不到动态新增的) 不是响应式的! 为了能正确的显示菜单, 为了能够将来正确的获取到, 目前用户的路由, 我们需要用vuex管理routes路由数组
新建Vuex权限模块
可以在vuex中新增一个permission模块, 专门维护管理, 所有的路由 routes 数组 (响应式的) 在任何的组件中, 都可以非常方便的将来拿到 vuex 中存的 routes, 而不是去找 router.options.routes (拿的是默认配置)
src/store/modules/permission.js
import { constantRoutes } from '@/router'
const state = {
// 路由表, 标记当前用户所拥有的所有路由
routes: constantRoutes // 默认静态路由表
}
const mutations = {
// otherRoutes登录成功后, 需要添加的新路由
setRoutes(state, otherRoutes) {
// 静态路由基础上, 累加其他权限路由
state.routes = [...constantRoutes, ...otherRoutes]
}
}
const actions = {}
export default {
namespaced: true,
state,
mutations,
actions
}
- 在Vuex管理模块中引入permisson模块
import Vue from 'vue'
import Vuex from 'vuex'
import getters from './getters'
import app from './modules/app'
import settings from './modules/settings'
import user from './modules/user'
import permission from './modules/permission'
Vue.use(Vuex)
const store = new Vuex.Store({
modules: {
app,
settings,
user,
permission
},
getters
})
export default store
总结:
- 把静态路由存储在Store的permission模块中
- 通过mutation把动态路由添加进去并且与静态路由进行合并
思考:我们为何要这样做呢?因为左侧菜单是通过this.$options.routes方式获取路由映射信息的,但是这种方式无法获取动态添加的路由映射信息,所以需要全局共享路由映射。
Vuex-action筛选权限路由
将来登录成功时, 个人信息中会有 roles 的
menus
信息, 要基于menus来过滤出我们需要给用户 add 的路由, 但是**menus
中的标识又该怎么和路由对应呢? 可以将路由模块name
**属性命名和权限标识一致,这样只要标识能对上,就说明用户拥有了该权限 这一步,在我们命名路由的时候已经操作过了
接下来, vuex的 permission 中提供一个action,进行关联
import { asyncRoutes, constantRoutes } from '@/router'
const actions = {
// 筛选路由权限
filterRoutes(context, menus) {
const otherRoutes = asyncRoutes.filter(item => {
// 如果路由模块的首页name, 在menus数组中包含, 就是这个模块开放
if (menus.includes(item.children[0].name)) {
return true
} else {
return false
}
})
context.commit('setRoutes', otherRoutes)
return otherRoutes
}
}
调用Action管理路由渲染菜单
- 在 permission 拦截的位置,调用关联action, 获取新增routes,并且addRoutes
...
if (!store.state.user.userInfo.userId) {
// 调用获取信息的action
const { roles } = await store.dispatch('user/getUserInfo')
// 调用action同步到vuex中
const otherRoutes = await store.dispatch('permission/filterRoutes', roles.menus)
// 动态新增路由
router.addRoutes(otherRoutes)
next({
...to, // next({ ...to })的目的,是保证路由添加完了再进入页面 (可以理解为重进一次)
replace: true // 重进一次, 不保留重复历史
})
return
}
...
- 在**
src/store/getters.js
**配置导出routes
const getters = {
...
routes: state => state.permission.routes // 导出当前的路由
}
export default getters
- 在左侧菜单
layout/components/Sidebar/index.vue
组件中, 引入routes, 使用vuex的routes动态渲染
computed: {
...mapGetters([
'sidebar',
'routes'
])
}
总结:
- 导航守卫触发action过滤用户动态路由权限,添加的store中
- 左侧菜单组件中取出store中的所有路由映射进行渲染。
注意:导航守卫中,必须显示查询用户信息,否则条件判断userId 会有问题(导致一直递归触发路由的导航守卫) 必须保证 if (!store.getters.userId) 条件仅仅成立一次( await store.dispatch('user/getUserInfo'))
- 添加员工
- 添加角色
- 添加权限点
- 给角色分配权限点
- 给员工分配角色
- 使用新员工账号登录系统
- 那么主页中显示的左侧路由菜单就是授权的权限
- 用户登录主页
- 登录时首先进入导航守卫
- 导航守卫中判断Store中的userId是否存在
- 如果第一次判断不存在,就需要手动查询用户信息(包含权限),然后根据权限过滤动态路由,把动态路由添加到Store中,那么下一次userId就存在了
- 进入主页之后,左侧菜单从Store中获取静态和动态路由合并的数据进行展示
处理刷新 404 的问题
页面刷新的时候,本来应该拥有权限的页面出现了404,这是因为404的匹配权限放在了静态路由中 (静态路由的404要删除) 我们需要将404放置到动态路由的最后
if (!store.state.user.userInfo.userId) {
// 调用获取信息的action
const { roles } = await store.dispatch('user/getUserInfo')
// 调用action同步到vuex中
const otherRoutes = await store.dispatch('permission/filterRoutes', roles.menus)
// 动态新增路由
router.addRoutes([...otherRoutes, { path: '*', redirect: '/404', hidden: true }])
next({
...to, // next({ ...to })的目的,是保证路由添加完了再进入页面 (可以理解为重进一次)
replace: true // 重进一次, 不保留重复历史
})
return
}
注意:这个位置添加的404路由映射,Store中看不到(没有添加到Store中),本来也不需要再Store中添加404路由映射。
退出时重置路由
退出时, 需要将路由权限重置 (恢复默认), 将来登录后, 再次追加 我们的**
router/index.js
**文件,发现一个重置路由方法
// 重置路由
export function resetRouter() {
const newRouter = createRouter()
router.matcher = newRouter.matcher // 重新设置路由的可匹配路径
}
这个方法就是将路由重新实例化,相当于换了一个新的路由,之前**加的路由
**就不存在了,需要在登出的时候, 调用一下即可
store/modules/user.js
import { resetRouter } from '@/router'
// 退出的action操作
logout(context) {
// 1. 移除vuex个人信息
context.commit('removeUserInfo')
// 2. 移除token信息
context.commit('removeToken')
// 3. 重置路由
resetRouter()
// 4. 重置 vuex 中的路由信息
context.commit('permission/setRoutes', [], { root: true })
},
注意: 这里后台的权限标识, 和 name 要一一对应, 例如: 叫 permissions, 就要把 name 改一下
export default {
path: '/permission',
component: Layout,
children: [
// 默认权限管理的首页
{
path: '',
name: 'permissions', // name 加上, 后面有用
component: () => import('@/views/permission/index'),
meta: { title: '权限管理', icon: 'lock' }
}
]
}
总结:
- 在store模块内部默认访问的mutation,在当前模块查找,如果希望在全局模块查找,需要设置root属性为true
- 如何在模块内部访问全局状态?通过action的context.rootState获取,getters中也可以获取全局状态。
前端权限-按钮操作权
按钮操作权的受控思路
当我们拥有了一个模块的访问权限之后,页面中的某些功能,用户可能有,也可能没有
这就是上小节,查询出来的数据中的**points
当然现在是空, 所以首先需要在员工管理的权限点下, 新增一个删除权限点,并启用
我们要做的就是看看用户,是否拥有point-user-delete**这个point,有就可以让删除能用,没有就隐藏或者禁用
演示功能-将方法挂载到原型
- 配置getters
const getters = {
...,
roles: state => state.user.userInfo.roles,
}
export default getters
- 所有的页面中, 都要用到这个校验方法
// 扩展一个方法:验证按钮的权限
// code = POINT-EMPLOYEE-ADD
Vue.prototype.$isok = function (code) {
// 获取所有的当前用户的权限点列表
const points = store.getters.roles.points
// 判断code是否包含在points中
return points.includes(code)
}
- 按钮控制
<template #right>
<el-button v-if="$isok('POINT-EMPLOYEE-IMPORT')" type="warning" size="small" @click="$router.push('/import')">Excel导入</el-button>
<el-button v-if="$isok('POINT-EMPLOYEE-EXPORT')" type="danger" size="small" @click="handleExport">Excel导出</el-button>
<el-button v-if="$isok('POINT-EMPLOYEE-ADD')" type="primary" size="small" @click="handleAdd">新增员工</el-button>
</template>
总结:
- 根据登录成功后获取到的用户信息中的points数据判断相关的按钮是否具有操作权限