08-HR-权限管理模块(给员工分配角色,权限点管理页面开发,给角色分配权限,前端权限-页面访问权(路由),前端权限-按钮操作权)

421 阅读13分钟

权限管理模块

RBAC的权限设计思想

采用方案: RBAC的权限模型,RBAC(Role-Based Access control) ,也就是基于角色的权限分配解决方案

权限控制目标:不同的用户登录系统后可以操作不同的菜单

其权限模式如下:

image.png 三个关键点: 员工用户, 角色, 权限

  1. 给员工分配角色
  2. 给角色分配权限

以你自己为例, 你进入一家公司, 入职, 人事将你录入系统 => 分配你的角色 (前端研发工程师)

同角色有着相同的权限, 操作角色权限的同时, 所有该角色的用户对应权限, 就会同步更新

给员工分配角色

目标在员工管理页面,分配角色

image.png

总结:给员工分配角色(1、只能分配一个角色;2、可以分配多个角色(1:n))

一旦把角色分配给用户,那么该用户登录系统后就可以访问对应的权限了。

新建分配角色弹框

image.png

首先,新建分配角色窗体 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>

弹层的显示和关闭

  • 点击角色按钮显示弹层

image.png

  • 注册组件
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>

总结:控制弹窗的显示和隐藏

  1. el-dialog组件的基本用法(sync修饰符用法)
  2. 父子之间传值的简化写法 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>

总结:

  1. 调用接口获取所有的角色列表数据
  2. 动态渲染CheckBox角色列表

获取用户的当前角色

  • 获取用户的当前角色, 进行回显
import { getBaseInfo } from '@/api/user'

// 获取所有的角色列表
async loadAllRoles () {
  try {
    // 获取该用户默认拥有的角色
    const info = await getBaseInfo(this.userId)
    this.roleIds = info.data.roleIds
    // 获取所有的角色列表数据
    const ret = await reqGetRoleList({ page: 1, pagesize: 10000 })
    if (ret.code === 10000) {
      this.allRoles = ret.data.rows
    }
  } catch {
    this.$message.error('获取角色列表失败')
  }
},
  • 基于open事件触发接口调用
<el-dialog class="assign-role" title="分配角色" :visible="showRoleDialog" @open="loadAllRoles" @close="closeDialog">

总结:

  1. 获取用于本来拥有的角色,初始化默认选中的状态。
  • 基于Promise.all优化接口调用
// 获取所有的角色列表数据
loadSettingList () {
  // 获取用户已有角色
  const info = getBaseInfo(this.currentUserId)
  // 获取所有角色
  const ret = reqGetRoleList({
    page: 1,
    pagesize: 10000
  })
  // Promise.all 并发触发多个异步任务,所有任务完成后触发thne
  // Promise.race() 仅仅接收最先返回的结果
  Promise.all([info, ret]).then(result => {
    // 获取已有角色
    this.roleIds = result[0].data.roleIds
    // 获取所有角色
    this.list = result[1].data.rows
  }).catch(() => {
    this.$message.error('获取角色失败')
  })
},

给员工分配角色

  • 分配角色接口 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.code === 10000) {
    // 分配角色成功
    this.$message.success(ret.message)
    // 关闭弹窗
    this.handleClose()
  }
},

总结:给用户分配角色(一个用户可以分配多个角色)

添加loading效果

需求:显示角色列表的过程提供一个

<el-checkbox-group v-model="roleIds" v-loading="loading">
loadAllRoles () {
  this.loading = true
  // 获取该用户默认拥有的角色
  const info = reqGetUserDetailById(this.userId)
  // 获取所有的角色列表数据
  const ret = reqGetRoleList({ page: 1, pagesize: 10000 })
  // 并发触发多个异步任务
  Promise.all([info, ret]).then(result => {
    this.roleIds = result[0].data.roleIds
    this.allRoles = result[1].data.rows
  }).catch(() => {
    this.$message.error('获取角色列表失败')
  }).finally(() => {
    this.loading = false
  })
},

总结: 共同点(都是并发触发多个任务);不同点:all保证所有任务都完成后获取异步结果;race只要有一个任务返回就得到该任务的结果,其他任务的结果不做处理。

  • Promise.all
  • Promise.race

注意:Promise.all注意的应用场景:优化性能(并发触发多个任务)

权限点管理页面开发

image.png

目标: 完成权限点页面的开发和管理 => 为了后面做准备

  1. 便于分配权限, 只有有权限了才能分配
  2. 只有分配好了权限, 有对应的权限规则, 才能控制路由(模块访问权), 才能控制按钮的显示(操作权)

新建权限点管理页面

  • 完成权限页面结构 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}`
  })
}

总结:

  1. 准备表格布局
  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">
          <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/permission'

export default {
  name: 'Permission',
  data() {
    return {
      list: []
    }
  },
  created() {
    this.loadPermissionList()
  },
  methods: {
    // 加载权限列表数据
    async loadPermissionList () {
      try {
        const ret = await reqGetPermissionList()
        if (ret.success) {
          this.list = ret.data
        } else {
          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>

总结:

  1. 展示树形表格结构:1)el-table组件添加row-key属性,他的值是表示唯一值的key名称;2)每一行有子节点的数据需要children属性(之前封装的递归函数实现的)
  2. 区分一级和二级权限(通过每一行数据的type值区分:1表示一级权限(左侧菜单);2表示二级权限(页面中的功能按钮))

准备新增的弹层

  • 弹层结构
<!-- 新增编辑的弹层 -->
<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>

总结:

  1. 表单验证流程:
    1. el-form(ref/model/rules)
    2. el-form-item(prop)
    3. 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" 表示禁用的值

新增功能

新增有两个新增:

  1. 点击上面的添加权限, 添加是一级, 是一级访问权
<el-button type="primary" size="small" @click="handleAdd(1, '0')">添加权限</el-button>
  1. 点击下面的添加权限点, 添加的是二级, 是二级操作权(这里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.addRef.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)
        }
      })
    },

总结:

  1. 区分一级和二级菜单的type和pid
  2. 调用接口实现添加功能

删除功能

需求:1、绑定事件;2、提示删除;3、调用接口删除;4、刷新列表

  1. 注册点击事件
<el-button size="small" type="text" @click="handleDelete(row.id)">删除</el-button>
  1. 点击时发送删除请求
// 删除权限点
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. 删除需要添加确认
  2. 调用接口删除流程

查看修改功能

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

总结:

  1. 重用提交表单的方法(根据id的存在与否区分添加的编辑操作)

给角色分配权限

  • 员工
    • 把角色分配给员工
  • 角色
    • 把权限点分配给角色
  • 权限点

新建分配权限弹出层

  • 准备弹层
<!-- 分配权限的弹层 -->
<el-dialog title="分配权限" :visible.sync="showAuthDialog" >
  <div>权限列表</div>
  <template #footer>
    <div style="text-align: right;">
      <el-button @click="showAuthDialog=false">取消</el-button>
      <el-button type="primary">确定</el-button>
    </div>
  </template>
</el-dialog>
  • 注册事件
<el-button size="small" type="success" @click="handleAuth(row.id)">分配权限</el-button>
  • 提供数据方法
showAuthDialog: false, // 控制弹层的显示隐藏
roleId: '' // 记录正在操作的角色

// 给指定角色进行授权
hanldeAuth (id) {
  this.roleId = id
  this.showAuthDialog = true
},

总结:

  1. 点击【分配权限】按钮记录当前要分配角色的id
  2. 控制弹窗的显示和隐藏

获取权限数据

  • 这里要进行权限分配, 先要请求拿到权限数据
permissionData: [] // 存储权限数据
  • 弹层显示, 发送请求, 给弹层注册open事件
<el-tree
  :data="permissionData"
  show-checkbox
  node-key="id"
  :default-expand-all="true"
  :default-checked-keys="[5]"
  :props="defaultProps">
</el-tree>
  • 拿到数据处理成功树形结构:触发条件(弹窗组件的open事件)
// \获取权限列表数据
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
import { translateListToTreeData } from '@/utils/index.js'
this.permissionData = translateListToTreeData(ret.data, '0')

总结:

  1. 打开弹窗时,加载权限列表数据并转换为树形结构
  2. 树形组件的基本用法

结合树形控件显示

  • 基本展示
<el-tree
  :data="permissionData"
  :props="defaultProps"
  :default-expand-all="true"
  node-key="id"
  :default-checked-keys="[5, 10]"
  :show-checkbox="true"
  :check-strictly="true"
/>
  • data 表示树的所有数据
  • props 设置节点显示的数据名称
  • show-checkbox 显示选择框
  • node-key 设置数据节点的唯一表示的属性
  • :default-checked-keys="[5, 10]"表示id是5的节点被选中
  • show-checkbox 表示是否显示复选框
  • default-expand-all 默认展开
  • check-strictly 设置true, 可以关闭父子关联(父级节点可以单独选择)

回显默认的权限

树形结构认知: 回显数据, 需要有一些树形结构的认知

  1. node-key=‘id’ 唯一标识
  2. this.$refs.tree.setCheckedKeys([ ]) => 传入选中的node-key数组
<el-tree
  ref="tree"
  v-loading="treeLoading"
  :data="permissionData"
  :props="defaultProps"
  :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.roleId)
  // this.defaultAuthList = auths.data.permIds
  Promise.all([ret, auths]).then(results => {
    this.permissionData = translateListToTreeData(results[0].data, '0')
    this.$refs.authTree.setCheckedKeys(results[1].data.permIds)
  }).catch(() => {
    this.$message.error('获取权限列表失败')
  })
},

总结:控制树形节点的选中有两种方法

  1. 可以基于属性default-checked-keys设置树节点的选中
  2. 也可以基于setCheckedKeys实例方法设置树节点的选中

给角色分配权限

  • 封装分配权限的api src/api/setting.js
// 给角色分配权限
export function reqAssignPerm(data) {
  return request({
    url: '/sys/role/assignPrem',
    method: 'put',
    data
  })
}
  • 分配权限
// 授权提交操作
async handleAuth () {
  try {
    const ret = await reqAssignPerm({
      id: this.roleId,
      permIds: this.$refs.tree.getCheckedKeys()
    })
    if (ret.code === 10000) {
      // 授权成功,关闭弹窗,清空默认的权限数据
      this.showAuthDialog = false
      this.$refs.tree.setCheckedKeys([])
      this.permissionData = []
    }
  } catch {
    this.$message.error('授权失败')
  }
},

总结:重新选择权限,然后分配给角色

  1. 获取树的选中的节点方式:this.$refs.tree.getCheckedKeys()

  • 用户
    • 给用户分配角色
  • 角色
    • 给角色分配权限点
  • 权限点

不同的用户登录系统后可以操作不同的功能

前端权限-页面访问权(路由)

权限受控的思路

到了最关键的环节,我们设置的权限如何应用?

在上面的几个小节中,我们已经把给用户分配了角色, 给角色分配了权限,

那么在用户登录获取资料的时候,会自动查出该用户拥有哪些权限,这个权限需要和我们的菜单含有路由有效结合起来

image-20210419084113013

  • menus表示左侧路由菜单的权限(一级权限)
  • points指的是路由组件中按钮的操作权限(二级权限)

而动态路由表其实就是根据用户的实际权限来访问的,接下来我们操作一下

image-20210218171005124

在权限管理页面中,我们设置了一个标识, 这个标识可以和我们的路由模块进行关联,

如果用户拥有这个标识,那么用户就可以 拥有这个路由模块,如果没有这个标识,就不能访问路由模块

如果用户拥有权限点标识,那么用户就可以操作对应的按钮,否则不可以。

注意:权限控制

  1. 建立用户和权限的关联关系(通过角色间接建立)
  2. 如何利用用户的权限关系控制访问操作:路由(一级权限);功能按钮(二级权限)

addRoutes 的基本使用

image.png

  • 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) {
        next()
      } else {
        // 用户信息不存在,获取用户信息
        await store.dispatch('user/getInfo')
        // await之后是可以获取用户信息的(menu)
        const menus = store.state.user.userInfo.roles.menus
        // 下一步通过menus控制当前用户的路由权限
        // 根据menus信息从所有的动态路由asyncRoutes中过滤出该用户所拥有的动态路由
        const myRoutes = asyncRoutes.filter(item => {
          return menus.includes(item.children[0].name)
        })
        // 动态配置路由
        router.addRoutes(myRoutes)
        // 继续跳转当前路由
        next({
          ...to,
          // 仅仅保留一个跳转历史(如果动态添加路由了,那么路由需要重新访问一次,这样的话,同样的路径访问的两次,路由历史重复了,不友好,所以可以把这两个合并为一个)
          replace: true
        })
      }
  	...
  ...
})

总结:

  1. 判断用户信息的必要性(控制该用户的路由访问权限)
  2. next选项replace作用:防止出现重复的路由访问历史
  3. 左侧菜单为何不存在(其实动态添加的路由映射已经生效)

注意:动态路由仅仅可以添加一次(用户信息仅仅可以获取一次)

为了能正确的显示菜单, 为了能够将来正确的获取到, 目前用户的路由, 我们需要用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

总结:

  1. 把静态路由存储在Store的permission模块中
  2. 通过mutation把动态路由添加进去并且与静态路由进行合并

思考:我们为何要这样做呢?因为左侧菜单是通过this.$options.routes方式获取路由映射信息的,但是这种方式无法获取动态添加的路由映射信息,所以需要全局共享路由映射。

Vuex-action筛选权限路由

将来登录成功时, 个人信息中会有 roles 的 menus 信息, 要基于menus来过滤出我们需要给用户 add 的路由,

但是**menus**中的标识又该怎么和路由对应呢?

可以将路由模块**name**属性命名和权限标识一致,这样只要标识能对上,就说明用户拥有了该权限

这一步,在我们命名路由的时候已经操作过了

接下来, vuex的 permission 中提供一个action,进行关联

import { asyncRoutes, constantRoutes } from '@/router'

const actions = {
  // 根据用户的权限筛选其拥有的动态路由
  filterAuth (context, menus) {
    // 参数menus表示当前用户拥有的路由权限
    const myRoutes = asyncRoutes.filter(item => {
      return menus.includes(item.children[0].name)
    })
    // 触发mutation变更路由
    context.commit('updateRoutes', myRoutes)
    // 返回动态路由
    return myRoutes
  }
}
  • 导航守卫控制调用该action
// 用户信息尚未存在,需要首次获取
// 获取用户的资料信息:用户的权限信息:menus;points(触发action完成)
await store.dispatch('user/getInfo')
// filterDynamicRoutes 负责根据用户的权限过滤动态路由,与静态路由合并放到vuex的状态里面
// 然后返回动态路由,用于添加动态路由
const myAuth = await store.dispatch('permission/filterDynamicRoutes')
// 基于vue-router的API动态添加路由映射
router.addRoutes(myAuth)
next({
  ...to,
  // 如果添加了动态路由,那么路由会重新请求一次,这样会导致同一个路由链接被请求两次
  // 那么浏览器回退会多出一个历史记录,体验不好,所以可以使用replace属性覆盖第一次请求。
  replace: true
})

总结:根据用户的路由权限,筛选出对应的路由映射,并且存储在state中,方便左侧菜单使用。

调用Action管理路由渲染菜单

  • 在 permission 拦截的位置,调用关联action, 获取新增routes,并且addRoutes
...
    // 用户信息尚未存在,需要首次获取
    // 获取用户的资料信息:用户的权限信息:menus;points(触发action完成)
    await store.dispatch('user/getInfo')
    // filterDynamicRoutes 负责根据用户的权限过滤动态路由,与静态路由合并放到vuex的状态里面
    // 然后返回动态路由,用于添加动态路由
    const myAuth = await store.dispatch('permission/filterDynamicRoutes')
    // 基于vue-router的API动态添加路由映射
    router.addRoutes(myAuth)
    next({
      ...to,
      // 如果添加了动态路由,那么路由会重新请求一次,这样会导致同一个路由链接被请求两次
      // 那么浏览器回退会多出一个历史记录,体验不好,所以可以使用replace属性覆盖第一次请求。
      replace: true
    })
...
  • 在**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'
  ])
}

总结:

  1. 导航守卫触发action过滤用户动态路由权限,添加的store中
  2. 左侧菜单组件中取出store中的所有路由映射进行渲染。

注意:导航守卫中,必须显示查询用户信息,否则条件判断userId 会有问题(导致一直递归触发路由的导航守卫)

必须保证 if (!store.getters.userId) 条件仅仅成立一次( await store.dispatch('user/getUserInfo'))

处理刷新 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的路由映射必须放到最底部(路由匹配从上向下)

退出时重置路由

退出时, 需要将路由权限重置 (恢复默认), 将来登录后, 再次追加

我们的**router/index.js**文件,发现一个重置路由方法

// 重置路由
export function resetRouter() {
  const newRouter = createRouter()
  router.matcher = newRouter.matcher // 重新设置路由的可匹配路径
}

这个方法就是将路由重新实例化,相当于换了一个新的路由,之前**加的路由**就不存在了,需要在登出的时候, 调用一下即可

store/modules/user.js

import { resetRouter } from '@/router'

// 1、清空token
this.$store.commit('user/removeToken')
// 2、清空用户信息
this.$store.commit('user/updateUserInfo', null)
// 3、重置路由为静态路由
resetRouter()
// 4、清除vuex中动态路由
this.$store.commit('permission/updateRoutes', [])
// 5、跳转到登录页面
this.$router.push('/login')

注意: 这里后台的权限标识, 和 name 要一一对应, 例如: 叫 permissions, 就要把 name 改一下

export default {
  path: '/permission',
  component: Layout,
  children: [
    // 默认权限管理的首页
    {
      path: '',
      name: 'permissions', // name 加上, 后面有用
      component: () => import('@/views/permission/index'),
      meta: { title: '权限管理', icon: 'lock' }
    }
  ]
}

总结:

  1. 在store模块内部默认访问的mutation,在当前模块查找,如果希望在全局模块查找,需要设置root属性为true commit('permission/setRoutes', [], { root: true })
  2. 如何在模块内部访问全局状态?通过action的context.rootState获取,getters中也可以获取全局状态。

前端权限-按钮操作权

按钮操作权的受控思路

当我们拥有了一个模块的访问权限之后,页面中的某些功能,用户可能有,也可能没有

这就是上小节,查询出来的数据中的**points**

当然现在是空, 所以首先需要在员工管理的权限点下, 新增一个删除权限点,并启用

我们要做的就是看看用户,是否拥有point-user-delete这个point,有就可以让删除能用,没有就隐藏或者禁用

演示功能-将方法挂载到原型

  • 配置getters
const getters = {
  ...,
  roles: state => state.user.userInfo.roles,
}
export default getters
  • 所有的页面中, 都要用到这个校验方法
import { Message } from 'element-ui'
// 扩展一个实例方法 $isOk, 用于判断当前用户对某个按钮是否拥有操作权限
Vue.prototype.$isOk = (authPoint) => {
    // 获取当前用户拥有的所有权限点
    const points = store.getters.points
    // 判断特定按钮是否有权限
    const flag = points.includes(authPoint)
    if (flag) {
        return true
    } else {
        Message.error('没有该操作权限!')
        return false
    }
}
  • 按钮控制
<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>

总结:

  1. 根据登录成功后获取到的用户信息中的points数据判断相关的按钮是否具有操作权限
  2. 权限标识必须和创建权限点时,添加的标识要一致