Bootstrap

Vue如何实现动态路由以及动态路由详解

RBAC权限设计思想

目标

了解RBAC的权限模型

背景

为了达成不同的帐号登陆系统后能看到不同的页面,能执行不同的功能的目标,我们有很多种解决方案,RBAC(Role-Based Access control)权限模型 ,也就是基于角色的权限分配解决方案。

其权限模式如下:

在这里插入图片描述

三个关键点:

用户: 就是使用系统的人

权限点:这个系统中有多少个功能(例始:有3个页面,每个页面上的有不同的操作)

角色:不同的权限点的集合

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CxmEMg8V-1673401451130)(asset/image-20210427155035187.png)]

  1. 给用户分配角色
  2. 给角色分配权限点

实际业务里面:

  1. 先给员工分配一个具体的角色

  2. 然后给角色分配具体的权限点 (工资页面 工资页面下的操作按钮)

    员工就拥有了权限点

员工分配角色-弹层组件

背景

目前系统中已经有一些角色,我们下面要将这些角色分配给不同的员工,让他们进入系统后,做不同的事情。

用户和角色是**1对多**的关系:一个用户可以拥有多个角色,这样他就会具体这多个角色的权限了。比如公司的董事长可以拥有财务主管和保安队长的角色: 董事长可以看财务报表,也可以查看监控。

目标

在员工管理页面中,点击分配角色时,以弹层的方式打开/关闭组件

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kH47ePb8-1673401451131)(asset/permissionUse/18.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-aQ1JfSXk-1673401451131)(asset/permissionUse/03.png)]

思路

  1. 把具体的功能给拆分出去(角色的功能比较复杂,拆分组件会减轻工作量)

  2. 通过弹层控制显示

新建角色管理组件

建立文件**employees/assignRole.vue** ,模板内容如下

<template>
  <!-- // 分配角色 -->
  <div>
    这里将来会放置多选列表

    <div style="margin-top: 20px; text-align: right">
      <el-button type="primary">确定</el-button>
      <el-button @click="closeDialog">取消</el-button>
    </div>
  </div>
</template>
<script>
export default {
  data() {
    return {
      roleIds: []
    }
  },
  methods: {
    closeDialog() {

    }
  }
}
</script>

注册并使用组件

在员工管理的主页employee.vue中,引入上面添加的组件

import AssignRole from './assignRole'

components: {
  // 省略其他....
  AssignRole // 注册组件
},
  
  // 使用
<el-dialog :visiable.sync="showDialogRole">
  <assign-role/>
</el-dialog>  

补充数据项控制弹层的显示隐藏

data () {
  return {
    // 省略其它
    showDialogRole: false
  }
}

员工分配角色-基本交互

目标

完成显示关闭弹层的效果

交互效果-显示弹层

点击分配角色按钮,记录id,显示弹层.

模板

<el-button type="text" size="small" @click="hAssignRole(scope.row)">分配角色</el-button>

代码

hEditRole({id}) {
  console.log('当前要分配角色id是', id)
  this.showRoleDialog = true
}

交互效果-关闭弹层

有如下操作会导致弹层关闭:

  1. 用户点击了取消按钮
  2. 用户点击了确定按钮,且操作成功了
  3. 用户点击了弹层的右上角的X
<el-dialog
      title="分配角色"
      :close-on-click-modal="false"
      :close-on-press-escape="false"
      :visible.sync="showDialogRole"
    >
       <assign-role @close="showDialogRole=false" />
    </el-dialog>

子组件

<template>
  <div>
    <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>

    <div style="margin-top: 20px; text-align: right">
      <el-button type="primary">确定</el-button>
+      <el-button @click="closeDialog">取消</el-button>
    </div>
  </div>
</template>
<script>
export default {
  data() {
    return {
      roleIds: []
    }
  },
  methods: {
    closeDialog() {
+      this.$emit('close')
    }
  }
}
</script>

员工分配角色-获取角色列表并用el-checkbox显示

组件:employees/assignRole.vue

目标

发请求获取本系统中所有的角色列表并显示在el-checkbox-group中

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-SHM57fGx-1673401451132)(asset/07-1619407883065.png)]

思路

  1. 准备静态模板,学习el-checkbox-group
  2. 准备api接口
  3. 发请求获取后端数据,再渲染

学习el-checkbox-group多选框

模板

<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>

对于用来表示多选的el-checkbox-group来说:

  • v-model的值是数组(表示多选)
  • 它的子元素el-checkbox的label属性决定了选中这一项之后值

数据

data () {
  return {
    roleIds: [] // 保存当前选中的权限列表
  }
}

准备api获取角色列表

目标是: 要获取所有的角色。但是后端并没有提供现成的接口可以直接获取所有的角色。

注意:我们没有专门用来做当前功能的角色列表,我们可以暂时使用pageSize为100(相当于取第一页,一页100条)获取数据。在文件src\api\setting.js中,


/**
 * 获取所有角色信息
 * @param {*} params  {page, pagesize}
 * @returns
 */
export function getRoles(params) {
  return request({
    url: '/sys/role',
    method: 'GET',
    params: params
  })
}

在业务组件中调用

src\views\employees\assignRole.vue

<script>
import { getRoles } from '@/api/setting'
export default {
  data() {
    return {
      roleIds: [],
+     list: []
    }
  },
  created() {
    this.loadRoles()
  },
  methods: {
    async loadRoles() {
      const { data } = await getRoles({ page: 1, pagesize: 100 })
+     this.list = data.rows
    },
    closeDialog() {
      this.$emit('close')
    }
  }
}
</script>

在模板中渲染数据

<el-checkbox-group v-model="roleIds">
  <!-- 注意:label决定当前选中的值 -->
  <el-checkbox v-for="item in list" :key="item.id" :label="item.id">
    {{ item.name }}
  </el-checkbox>
</el-checkbox-group>

注意:label决定当前选中的值

小结

员工分配角色-获取数据并回填

目标

​ 如果当前用户已经配置过一些角色数据,应该先把已经配置过的角色数据回显出来: 有些checkbox是选中的!

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YLTZ10fZ-1673401451132)(asset/08-1619407893456.png)]

思路

父组件中传入用户id

在打开弹层后,根据用户id去获取当前的角色信息,再回显

父传子-父

定义数据项

data() {
    return {
      // 省略其他 ...
      curEmployeId: '', // 当前的员工编号
    }
}

在点击分配角色时,给它赋值

// 用户点击分配角色
hAssignRole(id) {
  this.showDialogRole = true

  this.curEmployeId = id
}

模板

给子组件传递props

<el-dialog :visible.sync="showDialogRole" title="权限">
  <assign-role
+        :employee-id="curEmployeId"
        @close="showDialogRole=false"
      />
</el-dialog>

父传子-子接收

<script>
 import { getUserDetailById } from '@/api/user'
 export default {
    props: {
       // 用户的id 用来查询当前用户的角色信息
        employeeId: {
      		type: String,
      		required: true
    		}
    },
  	created() {
    	this.loadRoles()
  	},

  methods: {
    async loadRoles() {
      const res = await getRoles({ page: 1, pagesize: 100 })
      // 保存所有的角色
      this.list = res.data.rows
      // console.log('loadRoles...........', res)
      const info = await getUserDetailById(this.employeeId)
      console.log('getUserDetailById...........', info)
      // 保存这个员工当前的已经有的角色
      this.roleIds = info.data.roleIds
    },
 }
</script>

员工分配角色-回填问题:created只执行一次

原因

由于子组件在dialog嵌套,所以,它只会创建一次:created只执行一次,后续的显示隐藏操作,都不会导致组件重建,所以:后面打开的内容与第一次是一样的。

解决

方案一: 让弹层隐藏时,把子组件销毁。

<el-dialog
      title="分配角色"
      :close-on-click-modal="false"
      :close-on-press-escape="false"
      :visible.sync="showDialogRole"
    >
      <assign-role
+       v-if="showDialogRole"
        :employee-id="curEmployeId"
        @close="showDialogRole=false"
      />
    </el-dialog>

优点:简单;缺点:销毁组件,有一定性能问题

方案二:

思路:在父组件中点击分配角色时,直接调用子组件中方法获取数据

给子组件添加引用

<assign-role
        ref="assignRole"
        :employee-id="curEmployeId"
        @close="showDialogRole=false"
      />
// 用户点击分配角色
hAssignRole(id) {
  this.showDialogRole = true

  this.curEmployeId = id
  console.log('父组件', this.curEmployeId)
  // this.$nextTick
  // 直接找到子组件,调用方法去获取最新的数据
  this.$nextTick(() => {
    this.$refs.assignRole.loadRoles()
    // console.log('子组件中的props', this.$refs.assignRole.employeeId)
  })
}

把子组价中的created删除

// created() {
  //   // 组件创建时执行一次
  //   this.loadRoles()
  // },

员工分配角色-保存

目标

用户修改后的分配角色的具体功能保存

思路

封装接口 -> 调用接口

分配角色接口

在**api/employees.js**文件中,补充一个名为assignRoles的方法

/**
 * @description: 为用户分配角色
 * @param {*} data { id:当前用户id, roleIds:选中的角色id组成的数组 }
 * @return {*}
 */
export function assignRoles(data) {
  return request({
    url: '/sys/user/assignRoles',
    data,
    method: 'put'
  })
}

在业务代码中确定保存

导入上面定义的api

import { assignRoles } from '@/api/employees'

给按钮添加点击事件

<template>
  <el-button type="primary" size="small" @click="hSave">确定</el-button>
</template>

补充保存的回调

// 保存当前角色信息
async hSubmit() {
  const res = await assignRoles({ id: this.employeeId, roleIds: this.roleIds })
  console.log('保存角色', res)
  this.$emit('update-close')
}

在父组件中,监听事件

<assign-role
        ref="assignRole"
        :employee-id="curEmployeId"
        @update-close="hUpdateClose"
        @close="showDialogRole=false"
      />

hUpdateClose:

// 用户分配角色成功
    hUpdateClose() {
      this.showDialogRole = false
      this.loadEmployeeList()
    }

角色分配权限-整体说明

为什么要给角色分配权限

用户是什么角色,他就具备某些功能

前面的代码中已经给用户加了角色了,那员工到底能做什么事,还是由角色中携带的具体的功能来定的。

image-20210428094452560

权限管理功能比较多,需要封装组件。

角色分配权限-弹层空组件及基本交互

目标

在角色管理模块(views/setings/setings.vue)中,实现子组件

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0zTwqJw2-1673401451132)(asset/分配权限.gif)]

思路

准备弹框 -> 注册事件 -> 提供数据方法

完成给角色分配权限点的业务

封装子组件

在settings下先封装一个assignPermission.vue组件,备用。

|--settings
|---------settings.vue # 角色管理主页
|---------assignPermission.vue  #给角色分配权限

它将会在settings.vue中引用并使用。

在父组件添加弹层并引入子组件

在settings.vue中引入子组件

import assignPermission from './assignPermission'

注册

components: {
    assignPermission
  },

在模板中添加el-dialog组件并引入使用

<!-- 分配权限的弹层 -->
<el-dialog 
    title="分配权限(一级为路由页面查看权限-二级为按钮操作权限)" :visible.sync="showDialogAssign">
  <assign-permission />
</el-dialog>

补充数据

return {
  //... 省略其它
  showDialogAssign: false, // 分配权限对话框
}

交互-显示弹层

显示弹层。在按钮在添加点击事件

<el-button size="small" type="success" @click="hAssign">
    分配权限
</el-button>

在回调中设置showDialogAssign为true

methods:{
  hAssign() {
    this.showDialogAssign = true
  } 
}

交互-隐藏弹层

自定义事件:子传父

<el-dialog
      title="分配权限(一级为路由页面查看权限-二级为按钮操作权限)"
      :visible.sync="showDialogAssign"
    >
      <assign-permission
+        @close="showDialogAssign=false"
      />
    </el-dialog>

在子组件中

methods: {
    hCancel() {
    // 通过父组件去关闭弹层
      this.$emit('close')
    }
  }

角色分配权限-获取权限点数据并显示

目标

在组件assignPermission.vue中,获取当前系统中所有的权限点数据,并以树状结构显示出来,目标效果如下:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-oYbV8FQO-1673401451133)(asset/image-20210428100556359.png)]

思路

  1. 准备权限点接口

  2. 弹框展示之后:

    1. 调用api发请求获取数据;
    2. 对数据进行格式转换(数组转树)
    3. 模板绑定(把数据显示到el-tree上)

准备api

在src\api\permission.js中准备api(这个api在权限点页面已经用过了)

import request from '@/utils/request'

// 获取权限点列表
export function getPermissionList(params) {
  return request({
    url: '/sys/permission',
    params
  })
}

准备数据项

permissionData: [] // 存储权限数据

发请求获取数据

引入方法

import { getPermissionList } from '@/api/permission'
import { tranListToTreeData } from '@/utils/index'

在created中调用

created() {
  this.loadPermissionList()
},
  async loadPermissionList() {
    // 发送请求, 获取权限列表
    const { data } = await getPermissionList()
    console.log('权限列表的数据是', data)
    this.permissionData = tranListToTreeData(data)
  }

在el-tree中显示数据

<!-- 权限点数据展示 -->
<el-tree
    :data="permissionData"
    :props="{ label: 'name' }"
/>

注意:props

角色分配权限-设置el-tree的属性

目标

对el-tree进一步设置:

  1. 显示选择框
  2. 默认全部展开
  3. 关闭父子关联
image-20210524231614693

属性配置

https://element.eleme.io/#/zh-CN/component/tree

  1. show-checkbox 显示选择框
  2. default-expand-all 默认展开
  3. check-strictly 设置true,可以关闭父子关联
<!-- 权限点数据展示 -->
      <el-tree
        :data="permissionData"
        :props="{ label: 'name' }"
        default-expand-all
        :show-checkbox="true"
        :check-strictly="true"
      />

default-expand-all写法等价于:default-expand-all="true"

效果

角色分配权限-数据回填

目标

当前用户可能有一些已有的权限,需要我们回显出来

思路

  1. 准备api
  2. 组装 当前 参数 ,调用 api获取数据;
  3. 把数据回填显示到tree中

准备api

文件: src\api\settings.js 中,补充一个getRoleDetail方法

/**
 * @description: 获取角色详情
 * @param {*} id 角色id
 * @return {*}
 */
export function getRoleDetail(id) {
  return request({
    url: `/sys/role/${id}`
  })
}

将id从父传子

在父组件setting.vue中,定义数据项:

data () {
  return {
  	// 省略其他...
  	roleId: ''
	}
}

在点击分配权限时,保存roleId

<el-button size="mini" type="success" @click="hAssign(scope.row.id)">分配权限</el-button>

对应的回调是:

hAssign(id) {
   // 记下来id
   this.roleId = id
   this.showDialogAssign = true
},

在子级件中接收roleId

在assignPerimission.vue中,补充定义props接收roleId值

props: {
  roleId: {
    type: String,
    required: true
  }
}

调用api获取数据

引入前面封装的api

import { 
  assignPerm, 
+ getRoleDetail 
} from '@/api/setting'
created() {
    // 调用接口,获取所有的权限点数据
    this.loadPermissionList()

    // 调用接口,获取当前这个角色已经具备的权限
+   this.loadPermissionByRoleId()
  },
    
async loadPermissionByRoleId() {
      // 根据roleId获取当前这个角色已经具备的权限
      const res = await getRoleDetail(this.roleId)
+      console.log('获取当前角色的已有的权限点数据', res.data.permIds)
      // 回填到树上
      this.$refs.tree.setCheckedKeys(res.data.permIds)
    },
    async loadPermissionList() {
      const res = await getPermissionList()
      console.log('获取所有的权限点数据', res)
      // 转成树状结构
      this.permissionData = tranListToTreeData(res.data)
    },

将数据回填到el-tree中

已经获取到了数据了,如何把它填充到el-tree中,让某些个复选框处于选中状态?

答: setCheckedKeys + node-key

官网: https://element.eleme.io/#/zh-CN/component/tree#fang-fa

  1. 给tree补充属性node-key
<!-- 权限点数据展示 -->
    <el-tree
      ref="refTree"
      :data="permissionData"
      :props="{ label: 'name' }"
      :default-expand-all="true"
      :show-checkbox="true"
      :check-strictly="true"
      node-key="id"
    />
  1. 调用setCheckedKeys
// 获取角色现有的权限
    async loadRoleDetail() {
      const res = await getRoleDetail(this.roleId)
      console.log('获取角色现有的权限', res.data.permIds)

      // 回填
      this.$refs.refTree.setCheckedKeys(res.data.permIds)
    },

小结

  1. 在el-tree组件中通过setCheckedKeys方法将数据回显到el-tree组件中

角色分配权限-数据回填问题:created只执行一次

原因

由于子组件在dialog嵌套,所以,它只会创建一次:created只执行一次,后续的显示隐藏操作,都不会导致组件重建,所以:后面打开的内容与第一次是一样的。

解决

方案一: 让弹层隐藏时,把子组件销毁。

<el-dialog
      title="分配角色"
      :visible.sync="showDialogRole"
      :close-on-click-modal="false"
      :close-on-press-escape="false"
    >
      <子组件
+       v-if="showDialogAssign"
      />
    </el-dialog>

优点:简单;取到的是最新的数据;

缺点:销毁组件,有一定性能问题,

方案二:通过refs来引用子组件,直接调用它的方法来发请求

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JrmAm932-1673401451133)(asset/image-20210428105921353.png)]

// 用户点击了权限分配
    hAssign(id) {
      // alert(id)
      // 1. 保存角色编号
      //    它会影响子组件中的props,但是,这个传递的过程是异步的
      this.roleId = id
      // 2. 弹层
      this.showDialogAssign = true
      // 3. 手动调用子组件的loadPermissionByRoleId, 去根据最新的roleId获取权限信息
      this.$nextTick(() => {
        this.$refs.permission.loadPermissionByRoleId()
      })
      }
    }

角色分配权限-保存设置

目标

完成权限分配的功能

思路

准备api, 在点击保存时调用

准备api

文件src\api\settings.js中,补充一个api用来分配权限

/**
 * 给角色分配权限
 * @param {*} data {id:角色id, permIds:[] 所有选中的节点的id组成的数组}
 * @returns
 */
export function assignPerm(data) {
  return request({
    url: '/sys/role/assignPrem',
    method: 'put',
    data
  })
}

调用api分配权限-分析

只需要调用上面定义的api,并传入相关参数即可。

这里的参数有两个:

  1. 当前的角色id是什么? 在点击分配权限时,可以从表格中获取, 父传子
  2. 对应的权限列表id的是什么?通过el-tree组件的getCheckedKeys来获取用户选中的id列表

调用api分配权限-功能实现

async hSave() {
      const permIds = this.$refs.tree.getCheckedKeys()
      // console.log('当前选中的节点数组是', permIds)
      const res = await assignPerm({
        id: this.roleId,
        permIds
      })
      console.log('保存角色的权限点的结果是', res)

      // 提示
      this.$message.success('保存角色的权限成功')
      // 关闭弹层
      this.hCancel()
    },
    hCancel() {
      // 通过父组件去关闭弹层
      this.$emit('close-dialog')

      // 清空当前的选择
      this.$refs.tree.setCheckedKeys([])
    }

最后,在弹层关闭时,去清空el-tree中用户选中的数据

小结

  • el-tree 获取当前选中的节点的keys: getCheckedKeys
  • 对于el-tree组件,清空当前的选择: this.$refs.tree.setCheckedKeys([])

认识用户的权限数据

到目前为止,我们实现了RBAC权限设计思想的各个环节,我们给员工分配了角色,给角色又分配了权限点,员工现在已经有了相对应的权限点,接下来我们就可以利用这些权限点做实际的权限控制,在人资项目里,权限的控制有两个地方:

  1. 左侧菜单权限控制(不同的用户进来系统之后,看到的菜单是不同的)
  2. 操作按钮权限控制 (页面上的按钮,不同的人也有不同权限)

权限数据在哪里

在员工管理中新建一个全新的员工数据,然后使用全新的员工账号登录(密码为123456),查看个人信息接口(/api/sys/profile)的返回数据,下图看到的是没有配置任何权限的返回状态,可以看到,roles下的menus和points都为空,此时员工没有任何权限

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-WN9TIr43-1673401451134)(asset/permissionUse/13.png)]

如何修改权限数据

使用管理员账号登录,然后给刚才创建的新员工分配俩个菜单权限和一个操作按钮权限,然后我们再次登录员工账号查看个人信息返回数据

操作步骤:

  1. 权限点管理 > 给员工管理下增加导入,导出 按钮操作权限点

  2. 角色管理 > 新建角色人事总监 > 给角色分配权限 (员工管理,导入,导出)

  3. 员工管理 > 给员工分配人事总监角色

  4. 重新登录新员工账号,查看权限数据,观察data.roles.menus, points项目

权限应用-动态生成左侧菜单-整体分析

分析

登录成功,进入导航守卫:

  • 获取个人权限信息
  • 生成可以访问的动态路由

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wMfe8si3-1673401451134)(asset/image-20210428122059657.png)]

示例

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-L1oHNZ59-1673401451134)(asset/permissionUse/17.png)]

权限应用-动态生成左侧菜单-addRoutes方法

目标

​ 学习vue-router对象中的addRoutes,用它来动态添加路由配置

思路

用户能访问到的页面(路由配置)必须是动态的, 所以要先掌握一个可以动态添加路由地址的API

addRoutes基本使用

格式

router.addRoutes([路由配置对象])
或者:
this.$router.addRoutes([路由配置对象])

作用:动态添加路由配置

示例

// 按钮
<button @click="hAddRoute">addRoute</button>

// 回调
hAddRoute() {
	this.$router.addRoutes([{
		path: '/abc',
		component: () => import('@/views/abc'),
		}])
},

效果

点击了按钮之后,就可以在地址中访问/abc了。

改造代码

  1. 在router/index.js中的路由配置中删除动态路由的部分

    const createRouter = () => new Router({
      // mode: 'history', // require service support
      scrollBehavior: () => ({ y: 0 }),
      // routes: constantRoutes
      // 合并动态和静态的路由  , ...asyncRoutes
    - routes: [...constantRoutes, ...asyncRoutes]
    + routes: [...constantRoutes]
    })
    
  2. 在permission.js中引入,并使用addRoutes动态添加

把之前在router中直接静态写死的动态路由表改造成通过addRoutes 方法调用添加的形式

// 引入所有的动态路由表(未经过筛选)
+ import router, { asyncRoutes } from '@/router'

const whiteList = ['/login', '/404']
router.beforeEach(async(to, from, next) => {
  // 开启进度条
  NProgress.start()
  // 获取本地token 全局getter
  const token = store.getters.token
  if (token) {
    // 有token
    if (to.path === '/login') {
      next('/')
    } else {
      if (!store.getters.userId) {
        await store.dispatch('user/getUserInfo')
        // 改写成动态添加的方式
+       router.addRoutes(asyncRoutes)
      }
      next()
    }
  } else {
    // 没有token
    if (whiteList.includes(to.path)) {
      next()
    } else {
      next('/login')
    }
  }
  // 结束进度条
  NProgress.done()
})

验收效果

  1. 左侧的菜单只剩下静态的首页了(后续来解决)

  2. 浏览器手动输入某一个动态路由地址,依旧是可用的,这证明我们其实已经把动态路由添加到我们的路由系统了。

权限应用-动态生成左侧菜单-改写菜单保存位置

问题分析

当前的菜单渲染(src\layout\components\Sidebar\index.vue)使用的数据:this.$router.options.routes 这个数据是固定,我们通过addRoutes添加的路由表只存在内存中,并不会改变this.$router.options.routes

如果我们希望在调用addRoutes方法之后,要路由数据立刻反映到菜单中,我们需要想一个额外的方法,思考一下,vue开发中,哪个技术可以保证响应式特性还可以动态修改? vuex!

目标

在vuex中保存菜单数据

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4Q4i5Ywp-1673401451134)(asset/image-20210428122540218.png)]

定义vuex管理菜单数据

  1. 补充模块。在src/store/modules下补充menu.js模块:
    • 定义数据menuList
    • 修改数据的方法setMenuList
// 导入静态路由
import { constantRoutes } from '@/router'
export default {
  namespaced: true,
  state: {
    // 先以静态路由作为菜单数据的初始值
    menuList: [...constantRoutes]
  },
  mutations: {
    setMenuList(state, asyncRoutes) {
      // 将动态路由和静态路由组合起来
      state.menuList = [...constantRoutes, ...asyncRoutes]
    }
  }
}

当然,要在src/store/index.js中注册这个模块

+ import menu from './modules/menu'

Vue.use(Vuex)

const store = new Vuex.Store({
  modules: {
    app,
    settings,
    user,
+   menu
  },
  getters
})

2. 提交setMenuList生成完整的菜单数据

修改src/permission.js中的代码

if (!store.getters.userId) {
        await store.dispatch('user/getUserInfo')

        // 动态添加可以访问的路由设置
        router.addRoutes(asyncRoutes)

        // 根据用户实际能访问几个页面来决定从整体8个路由设置
        // 中,过滤中出来几个,然后保存到vuex中
        store.commit('menu/setMenuList', asyncRoutes)
      }

3. 菜单生成部分改写使用vuex中的数据

在src\layout\components\Sidebar\index.vue文件中,修改

routes() {
  // 拿到的是一个完整的包含了静态路由和动态路由的数据结构
  // return this.$router.options.routes
  return this.$store.state.menu.menuList
}

小结

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FQFaCeh7-1673401451135)(asset/image-20210525124226388.png)]

权限应用-使用权限数据做过滤处理

目标

上一步我们实现了:

  • 把动态路由通过addRoutes动态添加到了路由系统里
  • 把动态路由保存到vuex的menu中

但是我们没有和权限数据做搭配,接下来我们通过接口返回的权限数据对动态菜单做过滤处理,以确定完成菜单与用户权限相关。

过滤的思路

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JQ1M2Yrk-1673401451135)(asset/image-20210525000107003.png)]

过滤使用name作为标识,对照下标检查路由name是否一致

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-c8GBacDW-1673401451135)(asset/image-20210428153438963.png)]

后端的接口约定如下:

  • 页面名字: 员工 标识: employees
  • 页面名字: 权限 标识: permissions
  • 页面名字: 组织架构 标识: departments
  • 页面名字: 设置 标识: settings
  • 页面名字: 工资 标识: salarys
  • 页面名字: 审核 标识: approvals
  • 页面名字: 考勤 标识: attendances
  • 页面名字: 社保 标识: social_securitys

从actions中返回菜单项

用户能访问哪些页面是通过actions获取到的,只需要从action中返回即可。

修改 store/modules/user.js ,补充return语句。

// 用来获取用户信息的action
    async getUserInfo(context) {
      // 1. ajax获取基本信息,包含用户id
      const rs = await getUserInfoApi()
      console.log('用来获取用户信息的,', rs)
      // 2. 根据用户id(rs.data.userId)再发请求,获取详情(包含头像)
      const info = await getUserDetailById(rs.data.userId)
      console.log('获取详情', info.data)
      // 把上边获取的两份合并在一起,保存到vuex中
      context.commit('setUserInfo', { ...info.data, ...rs.data })
+     return rs.data.roles.menus
    },

在permission.js中获取action的返回值并过滤

src/permission.js

if (!store.getters.userId) {
        // 有token,要去的不是login,就直接放行
        // 进一步获取用户信息
        // 发ajax---派发action来做
        const menus = await store.dispatch('user/getUserInfo')
        console.log('当前用户能访问的页面', menus)
        console.log('当前系统功能中提供的所有的动态路由页面是', asyncRoutes)
        // 根据本用户实际的权限menus去 asyncRoutes 中做过滤,选出本用户能访问的页面

        const filterRoutes = asyncRoutes.filter(route => {
          const routeName = route.children[0].name
          return menus.includes(routeName)
        })

        // 一定要在进入主页之前去获取用户信息

        // addRoutes用来动态添加路由配置
        // 只有在这里设置了补充了路由配置,才可能去访问页面
        // 它们不会出现左侧
        router.addRoutes(filterRoutes)

        // 把它们保存在vuex中,在src\layout\components\Sidebar\index.vue
        // 生成左侧菜单时,也应该去vuex中拿
        store.commit('menu/setMenuList', filterRoutes)
      }

效果

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-EgzHy9V4-1673401451135)(asset/permissionUse/15.png)]

小结

  • 从actions中获取返回值
  • asyncRoutes.filter
image-20210525002110439

刷新页面时的bug修复

问题

如果我们刷新浏览器,会发现跳到了404页面

对于addRoute添加的路由,在刷新时会白屏

原因

现在我们的路由设置中的404页处在中间位置而不是所有路由的末尾了。

解决

把404页改到路由配置的最末尾就可以了

代码

  1. 从route/index.js中的静态路由中删除path:'*'这一项

  2. 在permission.js中补充在最后

// if(没有userInfo) {
if (!store.getters.userId) {
  // 有token,要去的不是login,就直接放行
  // 进一步获取用户信息
  // 发ajax---派发action来做
  const menus = await store.dispatch('user/getUserInfo')
  console.log('当前用户能访问的页面', menus)
  console.log('当前系统功能中提供的所有的动态路由页面是', asyncRoutes)
  // 根据本用户实际的权限menus去 asyncRoutes 中做过滤,选出本用户能访问的页面

  const filterRoutes = asyncRoutes.filter(route => {
    const routeName = route.children[0].name
    return menus.includes(routeName)
  })

  // 一定要在进入主页之前去获取用户信息

  // 把404加到最后一条
  filterRoutes.push( // 404 page must be placed at the end !!!
    { path: '*', redirect: '/404', hidden: true })

  // addRoutes用来动态添加路由配置
  // 只有在这里设置了补充了路由配置,才可能去访问页面
  // 它们不会出现左侧
  router.addRoutes(filterRoutes)

  // 把它们保存在vuex中,在src\layout\components\Sidebar\index.vue
  // 生成左侧菜单时,也应该去vuex中拿
  store.commit('menu/setMenuList', filterRoutes)

  // 解决刷新出现的白屏bug
  next({
    ...to, // next({ ...to })的目的,是保证路由添加完了再进入页面 (可以理解为重进一次)
    replace: true // 重进一次, 不保留重复历史
  })
} else {
  next()
}

退出登录时重置路由

问题

退出后,再次登陆,发现菜单异常 (控制台有输出说路由重复);

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xfeMibRM-1673401451136)(asset/image-20210525161618731.png)]

原因

路由设置是通过router.addRoutes(filterRoutes)来添加的,退出时,并没有清空,再次登陆,又加了一次,所以有重复。

需要将路由权限重置 (恢复默认) 将来登录后再次追加才可以,不然的话,就会重复添加

解决

我们的**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 中的路由信息 只保留每个用户都一样的静态路由数据
  //    在moudules中的一个module中去调用另一个modules中的mutation要加{root:true}
  // context.commit('setMenuList', [], { root: true })
}

权限应用-按钮级控制-分析

目标

员工A和员工B都可以访问同一个页面(以员工管理为例),但是员工A可以导出excel,员工B就不可以导出excel

思路

用户登陆成功后,用户可以访问的按钮级别权限保存在points数组中。而这个数据我们是保存在vuex中的,所以,就可以在项目的任意地方来中访问。

  • 如果某个按钮上的标识在points出现,则可以显示出来

权限应用-按钮级控制-自定义指令

指令: v-for, v-if…

自定义指令:自己定义的指令,因为本身指令不够用,所以我们需要自已去定义。

用它来做按钮级别权限控制

复习一下自定义指令

注册格式

// 注册一个全局自定义指令 `v-focus`
Vue.directive('focus', {
  // 当被绑定的元素插入到 DOM 中时inserted会自动执行
  inserted: function(el, binding) {
    // v-focus="'abc'"  ===> binding.value = 'abc'
    console.log('focus.... binding', binding.value)
    // 聚焦元素
    el.focus()
  }
})

使用格式

<input v-foucs="'xxxx'" />

解决按钮级别的权限验证

在main.js中,定义全局指令

// 注册一个全局自定义指令 `v-allow`
Vue.directive('allow', {
  inserted: function(el, binding) {
    // 从vuex中取出points,
    const points = store.state.user.userInfo.roles.points
    // 如果points有binding.value则显示
    if (points.includes(binding.value)) {
      // console.log('判断这个元素是否会显示', el, binding.value)
    } else {
      el.parentNode.removeChild(el)
      // el.style.display = 'none'
    }
  }
})

使用

<el-button
+           v-allow="'import_employee'"
            type="warning"
            size="small"
            @click="$router.push('/import')"
          >导入excel</el-button>

这里的:'import_employee'是从标识符来的

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-OHL0NZor-1673401451136)(asset/image-20210428165654008.png)]

权限控制流程重点梳理总结

业务场景

公司里有不同的职能部门,都在用同一套系统 ,不一样部门的人员进入系统里面需要操作的事情是不一样的

必定需要根据不同的员工角色配置不同的权限

RBAC权限设计思想

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Zjgpc0W3-1673401451136)(asset/image-20210427155035187.png)]

一种基于角色的设计思想

  1. 给员工配置角色 (一个员工可以拥有多个角色)
  2. 给角色配置权限点 (一个角色可以有多个权限点)

员工只要有了角色之后,就自动拥有了角色绑定的所有权限点

3. 根据权限设计思想对应业务模块

  1. 员工管理
  2. 角色管理
  3. 权限点管理

员工得到权限数据

​ 员工信息接口中有当前员工的所有权限数据

userInfo:{
  roles: {
    menus: [],  // 菜单权限数据
    points: [] // 按钮权限数据
  }
}

使用权限数据做具体的权限处理

  1. 菜单权限控制

    登录 > 菜单权限数据 > 和本地的所有的动态路由数据做匹配出具 > 得到根据权限筛选之后的动态路由数据

    1. 添加到路由系统中 (可以根据路径标识渲染组件 addRoutes
    2. 添加到左侧菜单渲染 (vuex管理 + v-for遍历)
  2. 按钮权限控制

    登录 > 按钮权限数据 > 使用按钮单独的权限标识 去权限数据里面查找

    自定义指令

;