HR-09-权限管理模块

168 阅读7分钟

权限管理模块

RBAC的权限设计思想

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


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

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


以你自己为例, 你进入一家公司, 入职, 人事将你录入系统 => 分配你的角色 (学生)

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

给员工分配角色

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



![image-20210217194808393.png](https://cdn.nlark.com/yuque/0/2021/png/22000767/1625654199475-482c3a74-0e58-4729-a464-11571f404513.png#align=left&display=inline&height=236&margin=%5Bobject%20Object%5D&name=image-20210217194808393.png&originHeight=236&originWidth=511&size=5823&status=done&style=none&width=511) > 总结:给员工分配角色(1、只能分配一个角色;2、可以分配多个角色(1:n)) > 一旦把角色分配给用户,那么该用户登录系统后就可以访问对应的权限了。

新建分配角色弹框

image-20210131125420085.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-20210131122251352.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组件的基本用法
  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 { 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

权限点管理页面开发

image-20210418084456453.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}`
  })
}

动态渲染权限列表

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

总结:

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

总结:

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

总结:

  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>
  • 拿到数据处理成功树形结构
// \获取权限列表数据
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'

总结:

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

结合树形控件显示

  • 基本展示
<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, 可以关闭父子关联

回显默认的权限

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

  1. node-key 唯一标识
  2. 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('获取权限列表失败')
  })
},

总结:

  1. 可以基于属性default-checked-keys设置树节点的选中
  2. 也可以基于setCheckedKeys实例方法设置树节点的选中
  3. 如何获取选中的节点?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?用组件的实例方法

不同的用户进入系统后,可以操作不同的功能(权限)

  • 用户
    • 给用户分配角色(支持多个角色的分配)
  • 角色
    • 给角色分配权限(一个角色可以拥有多个权限)
  • 权限

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

权限受控的思路

到了最关键的环节,我们设置的权限如何应用? 在上面的几个小节中,我们已经把给用户分配了角色, 给角色分配了权限, 那么在用户登录获取资料的时候,会自动查出该用户拥有哪些权限,这个权限需要和我们的菜单含有路由有效结合起来 image-20210419084113013.png

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

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


image-20210218171005124.png

在权限管理页面中,我们设置了一个标识, 这个标识可以和我们的路由模块进行关联, 如果用户拥有这个标识,那么用户就可以 拥有这个路由模块,如果没有这个标识,就不能访问路由模块

addRoutes 的基本使用


image-20210218171039521.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) {
        // 调用获取信息的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

总结:

  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 = {
  // 筛选路由权限
  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'
  ])
}

总结:

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

注意:导航守卫中,必须显示查询用户信息,否则条件判断userId 会有问题(导致一直递归触发路由的导航守卫) 必须保证 if (!store.getters.userId)  条件仅仅成立一次( await store.dispatch('user/getUserInfo'))


  1. 添加员工
  2. 添加角色
  3. 添加权限点
  4. 给角色分配权限点
  5. 给员工分配角色
  6. 使用新员工账号登录系统
  7. 那么主页中显示的左侧路由菜单就是授权的权限

  1. 用户登录主页
  2. 登录时首先进入导航守卫
  3. 导航守卫中判断Store中的userId是否存在
  4. 如果第一次判断不存在,就需要手动查询用户信息(包含权限),然后根据权限过滤动态路由,把动态路由添加到Store中,那么下一次userId就存在了
  5. 进入主页之后,左侧菜单从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' }
    }
  ]
}

总结:

  1. 在store模块内部默认访问的mutation,在当前模块查找,如果希望在全局模块查找,需要设置root属性为true
  2. 如何在模块内部访问全局状态?通过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>

总结:

  1. 根据登录成功后获取到的用户信息中的points数据判断相关的按钮是否具有操作权限