打造一款适合自己的快速开发框架-前端篇之权限管理

5,244 阅读7分钟

前言

在后端篇中已对权限资源进行了分类:API接口、路由菜单、页面按钮。本文重点讲一下如何对这些权限资源进行分配并对不同的登录用户,根据权限的不同,呈现不一样的功能菜单、按钮进行统一处理。

关系梳理

用户与角色

多对多关系:一个用户可以拥有多个角色,一个角色包含多个用户

角色与菜单

多对多关系:一个角色有多个菜单,一个菜单可以分配给多个角色

角色与权限资源

多对多关系:一个角色有多个权限资源,一个权限资源可以分配给多个角色

vue相关

动态路由

vue可以通过router.addRoutes动态地添加路由信息

// 动态添加可访问路由表
router.addRoutes(accessRoutes)

自定义指令

这里只是简单的介绍,想详细了解的,可自行查看资料。

指令定义

vue可以自定义指令,拿当前dom元素,然后进行移除操作,下面的el即是原生的dom对象。

const directives = {
  has: {
    inserted: function(el, binding, vnode) {
        if(binding.value) {
      		el.parentNode.removeChild(el)
        }
    }
  }
}
export default directives

组件注册

import directive from './directives'
const importDirective = Vue => {
  /**
   * 权限指令
   * options ===>权限字符串数组 ['admin','/sys/role/add']
   */
  Vue.directive('hasPerm', directive.has)
}
export default importDirective

指令使用

hasPerm即为组件注册时定义的hasPerm,通过v-hasPerm的方式使用,['admin','sys:role:save']即为binding.value

<el-button v-hasPerm="['admin','sys:role:save']">添加</el-button>

接口清单

接口名称 接口地址
保存用户角色关系 /sys/rbac/saveUserRole
保存角色菜单关系 /sys/rbac/saveRoleMenu
保存角色权限资源关系 /sys/rbac/saveRoleAccess
从角色中移除用户 /sys/rbac/deleteUserRole
角色成员列表 /sys/rbac/listUserByRoleId
查询未加入指定角色的用户列表 /sys/rbac/listUserNoInRole
获取权限资源树 /sys/rbac/listAccessTree
通过角色id获取菜单 /sys/rbac/listMenuByRoleId
获取当前用户信息 /sys/user/info

开始编码

目录结构

├── src
	├──	api/sys
		└── sys.rbac.service.js
	├──	directive
		├── directives.js
		└── index.js
	├── layout
		├── components/Sidebar
			└── index.js
	├── router
		└── index.js
	├── store
		├──	modules
			└── permission.js
		└──	getters.js
	├── views/modules/sys
		└──	role
			├── drawer.vue
			└── selectUser.vue
	├── main.js
	└── permission.js

文件详解

  • src/api/sys/sys.rbac.service.js

权限管理相关的接口定义

  • src/directive/directives.js

按钮权限指令定义逻辑处理

const directives = {
  has: {
    inserted: function(el, binding, vnode) {
      var arr = binding.value
      //  判断要查询的数组(arr)是否至少有一个元素包含在目标数组(vnode.context.$store.state.user.access)中
      if (!vnode.context.$store.state.user.accessList.some(_ => arr.indexOf(_) > -1)) {
        vnode.context.$nextTick(() => {
          if (el.parentNode) {
            // 节点存在,就删除
            el.parentNode.removeChild(el)
          }
        })
      }
    }
  }
}
export default directives
  • src/directive/index.js

指定注册

import directive from './directives'

const importDirective = Vue => {
  /**
   * 权限指令
   * options ===>权限字符串数组 ['admin','/sys/role/add']
   */
  Vue.directive('hasPerm', directive.has)
}
export default importDirective
  • src/layout/components/Sidebar/index.js

使用addRouters添加的路由,this.$router.options.routes无法获取到,所以需要修改成vuex的

 <sidebar-item v-for="route in permission_routes" :key="route.path" :item="route" :base-path="route.path" />
<!-- ===>修改成-->
<sidebar-item v-for="route in routes" :key="route.path" :item="route" :base-path="route.path" />

新增permission_routes

computed: {
    ...mapGetters([
      'permission_routes',
      'sidebar'
    ]),
  • src/router/index.js

将动态路由从静态路由中移除

const createRouter = () => new Router({
  mode: 'history', // require service support
  scrollBehavior: () => ({ y: 0 }),
  routes: [
    ... constantRoutes // ,
    //... asyncRoutes
  ]
})
  • src/store/modules/permission.js

这里权限状态管理--vuex

主要是使用用户信息接口中的menuList去加工asyncRoutes中的路由菜单,排序、修改名称、修改图标、控制显示。

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

/**
 * Use meta.access to determine if the current user has permission
 * @param menus
 * @param route
 */
function hasPermission(menus, route) {
  if (route.name) {
    var currMenu = getMenu(route.name, menus)
    if (currMenu !== null) {
      // 设置菜单的标题、图标
      if (currMenu.name) {
        route.meta.title = currMenu.name
      }
      if (currMenu.icon) {
        route.meta.icon = currMenu.icon
      }
      if (currMenu.sort) {
        route.sort = currMenu.sort
      }
    } else {
      route.sort = 10
    }
  }
  if (route.meta && route.meta.access) {
    return menus.some(menu => route.meta.access.includes(menu.routeName))
  } else {
    return true
  }
}
// 根据路由名称获取菜单
function getMenu(access, menus) {
  for (let i = 0; i < menus.length; i++) {
    var menu = menus[i]
    if (access === menu.routeName) {
      return menu
    }
  }
  return null
}
// 对菜单进行排序
function sortRouters(accessedRouters) {
  for (let i = 0; i < accessedRouters.length; i++) {
    var router = accessedRouters[i]
    if (router.children && router.children.length > 0) {
      router.children.sort(compare('sort'))
    }
  }
  accessedRouters.sort(compare('sort'))
}

// 降序比较函数
function compare(p) {
  return function(m, n) {
    var a = m[p]
    var b = n[p]
    return b - a
  }
}
/**
 * Filter asynchronous routing tables by recursion
 * @param routes asyncRoutes
 * @param menus
 */
export function filterAsyncRoutes(routes, menus) {
  const res = []

  routes.forEach(route => {
    const tmp = { ...route }
    if (hasPermission(menus, tmp)) {
      if (tmp.children) {
        tmp.children = filterAsyncRoutes(tmp.children, menus)
      }
      res.push(tmp)
    }
  })

  return res
}

const state = {
  routes: [],
  addRoutes: []
}

const mutations = {
  SET_ROUTES: (state, routes) => {
    state.addRoutes = routes
    state.routes = constantRoutes.concat(routes)
  }
}

const actions = {
  generateRoutes({ commit }, menus) {
    return new Promise(resolve => {
      var accessedRoutes = filterAsyncRoutes(asyncRoutes, menus)
      // 对菜单进行排序
      sortRouters(accessedRoutes)
      commit('SET_ROUTES', accessedRoutes)
      resolve(accessedRoutes)
    })
  }
}

export default {
  namespaced: true,
  state,
  mutations,
  actions
}

  • src/store/getters.js
const getters = {
  sidebar: state => state.app.sidebar,
  device: state => state.app.device,
  token: state => state.user.token,
  avatar: state => state.user.avatar,
  name: state => state.user.name,
  // 这里追加dictMap的get方法,可以使用mapGetters,详见src/components/m/Dict/index.vue
  dictMap: state => state.dict.dictMap,
  // 使用addRouter动态添加路由后,需要用vuex维护路由信息
  permission_routes: state => state.permission.routes
}
export default getters

  • src/views/modules/sys/role/drawer.vue

角色成员管理抽屉,index.vue已由代码生成器生成,drawer.vue暂时需要手工添加,添加再自定义修改。

<template>
  <div class="app-container">
    <el-button
      style="margin-bottom:10px;margin-left:10px;"
      type="primary"
      icon="el-icon-plus"
      size="small"
      @click="handleOpenSelectUser"
      v-hasPerm="['admin','sys:rbac:saveUserRole']"
    >新增</el-button>
    <el-table :header-cell-style="{background:'#eef1f6',color:'#606266'}" v-loading="loading" :data="tableData">
      <el-table-column prop="userName" label="用户名">
        <template slot-scope="scope">
          {{ scope.row.userName }}
        </template>
      </el-table-column>
      <el-table-column prop="realName" label="姓名">
        <template slot-scope="scope">
          {{ scope.row.realName }}
        </template>
      </el-table-column>
      <el-table-column prop="mobilePhone" label="手机号">
        <template slot-scope="scope">
          {{ scope.row.mobilePhone }}
        </template>
      </el-table-column>
      <el-table-column
        label="操作"
        align="center">
        <template slot-scope="scope">
          <el-button type="text" size="small" icon="el-icon-delete" v-hasPerm="['admin','sys:rbac:deleteUserRole']" @click.native.stop="handleDeleteUserRole(scope.row.id)">移除</el-button>
        </template>
      </el-table-column>
    </el-table>
    <pagination
      v-show="recordCount>0"
      :total="recordCount"
      :page.sync="pageNum"
      :limit.sync="pageSize"
      @pagination="requestData"
    />
  </div>
</template>
<script>
import { listUserByRoleId, deleteUserRole } from '@/api/sys/sys.rbac.service.js'
export default {
  props: {
    id: {
      type: [String, Number],
      default: undefined
    }
  },
  data() {
    return {
      // 总记录数
      recordCount: 0,
      // 表格数据加载中
      loading: false,
      // 列表数据
      tableData: [],
      // 当前页
      pageNum: 1,
      // 每页大小
      pageSize: Number(process.env.VUE_APP_PAGE_SIZE)
    }
  },
  watch: {
    id(n) {
      this.requestData()
    }
  },
  mounted() {
    this.requestData()
  },
  methods: {
    // 请求数据
    requestData(page) {
      if (!this.id) {
        return
      }
      if (!page) {
        page = {
          page: this.pageNum,
          limit: this.pageSize
        }
      }
      this.loading = true
      listUserByRoleId({
        pageNum: page.page,
        pageSize: page.limit,
        roleId: this.id,
        ...this.searchForm
      }).then(res => {
        this.loading = false
        if (res.code === 0) {
          this.tableData = res.data.rows
          this.recordCount = res.data.recordCount
        }
      }).catch(() => {
        this.loading = false
      })
    },
    // 打开弹窗--使用index.vue中的dialog,所以需要$parent.$parent selectUser -> drawer->index.vue
    handleOpenSelectUser() {
      this.$parent.$parent.openDialog(this.id, `选择用户`, 'selectUser', true)
    },
    handleDeleteUserRole(id) {
      this.$confirm('此操作将永久删除该记录, 是否继续?', '提示', {
        confirmButtonText: '确定',
        cancelButtonText: '取消',
        type: 'warning'
      }).then(() => {
        var ids = []
        if (id) {
          ids.push(id)
        } else {
          return
        }
        deleteUserRole({
          ids: ids,
          id: this.id
        }).then(res => {
          if (res.code === 0) {
            this.$message({
              message: '删除成功',
              type: 'success'
            })
            this.requestData()
          } else {
            this.$message({
              message: res.msg || '删除失败',
              type: 'error'
            })
          }
        })
      }).catch((e) => {
        this.$message({
          type: 'info',
          message: '已取消删除' + e
        })
      })
    }
  }
}
</script>
<style lang="scss" scoped>
</style>

  • src/views/modules/sys/role/selectUser.vue

选择成员添加,index.vue已由代码生成器生成,selectUser.vue暂时需要手工添加,添加再自定义修改。

<template>
  <div class="app-container">
    <el-form ref="searchForm" :model="searchForm" :inline="true">
      <el-form-item label="关键字" prop="keywords">
        <el-input v-model="searchForm.keywords" placeholder="请输入用户名、手机号" size="small" style="width: 240px"></el-input>
      </el-form-item>
      <el-form-item>
        <el-button type="primary" icon="el-icon-search" size="small" @click="handleSearch">查询</el-button>
        <el-button icon="el-icon-refresh" size="small" @click="resetform">重置</el-button>
      </el-form-item>
    </el-form>
    <el-table :header-cell-style="{background:'#eef1f6',color:'#606266'}" v-loading="loading" :data="tableData" @selection-change="handleSelectionChange">
      <el-table-column type="selection" width="55" align="center" />
      <el-table-column prop="userName" label="用户名">
        <template slot-scope="scope">
          {{ scope.row.userName }}
        </template>
      </el-table-column>
      <el-table-column prop="realName" label="姓名">
        <template slot-scope="scope">
          {{ scope.row.realName }}
        </template>
      </el-table-column>
      <el-table-column prop="mobilePhone" label="手机号">
        <template slot-scope="scope">
          {{ scope.row.mobilePhone }}
        </template>
      </el-table-column>
    </el-table>
    <pagination
      v-show="recordCount>0"
      :total="recordCount"
      :page.sync="pageNum"
      :limit.sync="pageSize"
      @pagination="requestData"
    />
  </div>
</template>
<script>
import { listUserNoInRole, saveUserRole } from '@/api/sys/sys.rbac.service.js'
export default {
  props: {
    id: {
      type: [String, Number],
      default: undefined
    }
  },
  data() {
    return {
      // 非单个禁用
      single: true,
      // 非多个禁用
      multiple: true,
      // 总记录数
      recordCount: 0,
      // 表格数据加载中
      loading: false,
      // 弹出层
      open: false,
      // 弹出层内容
      dialogContent: 'add',
      // 是否显示Ok
      showOk: true,
      // 当前弹出层标题
      title: '',
      // 列表数据
      tableData: [],
      // 提交按钮状态
      submitLoading: false,
      // 当前页
      pageNum: 1,
      // 每页大小
      pageSize: Number(process.env.VUE_APP_PAGE_SIZE),
      // 当前勾选行id
      ids: [],
      // 当前勾选行集合
      selection: [],
      searchForm: {
        keywords: undefined
      }
    }
  },
  watch: {
    id(n) {
      this.requestData()
    }
  },
  mounted() {
    this.requestData()
  },
  methods: {
    // 多选框选中数据
    handleSelectionChange(selection) {
      this.selection = selection
      this.ids = selection.map(item => item.id)
      this.single = selection.length !== 1
      this.multiple = !selection.length
    },
    // 请求数据
    requestData(page) {
      if (!this.id) {
        return
      }
      if (!page) {
        page = {
          page: this.pageNum,
          limit: this.pageSize
        }
      }
      this.loading = true
      listUserNoInRole({
        pageNum: page.page,
        pageSize: page.limit,
        roleId: this.id,
        ...this.searchForm
      }).then(res => {
        this.loading = false
        if (res.code === 0) {
          this.tableData = res.data.rows
          this.recordCount = res.data.recordCount
        }
      }).catch(() => {
        this.loading = false
      })
    },
    // 查询
    handleSearch() {
      this.requestData()
    },
    // 重置
    resetform(e) {
      this.$refs['searchForm'].resetFields()
    },
    resetFields() {
      this.$refs['searchForm'].resetFields()
      this.requestData()
    },
    // 提交
    submit() {
      return new Promise((resolve, reject) => {
        if (!this.ids.length) {
          reject(new Error('id不能为空'))
          return
        }
        saveUserRole({
          ids: this.ids,
          id: this.id
        }).then(res => {
          this.$parent.$parent.$refs.drawer.requestData()
          resolve(res)
        }).catch(e => {
          reject(e)
        })
      })
    }
  }
}
</script>
<style lang="scss" scoped>
</style>

  • src/main.js

这里主要是注册自定义指令

import importDirective from '@/directive'
/**
 * 注册指令
 */
importDirective(Vue)
  • src/permission.js

这里处理一下添加动态路由的逻辑

// 进入页面前拦截
router.beforeEach(async(to, from, next) => {
  // 进度条开始
  NProgress.start()

  // 重设页面标题
  document.title = getPageTitle(to.meta.title)

  // 获取token,判断是否已经登录
  const hasToken = getToken()

  if (hasToken) {
    if (to.path === '/login') {
      // 如果已经登录且是登录页,则重定向到首页
      next({ path: '/' })
      NProgress.done()
    } else {
      const hasGetUserInfo = store.getters.name
      // 判断用户信息是否存在,如果已经存在,则可以进入页面
      if (hasGetUserInfo) {
        next()
      } else {
        try {
          // 拉取用户信息
          const { menuList } = await store.dispatch('user/getInfo')
          // 生成可访问的路由表
          const accessRoutes = await store.dispatch('permission/generateRoutes', menuList)
          // 动态添加可访问路由表
          router.addRoutes(accessRoutes)
          router.app.$nextTick(() => {
            next({ ...to, replace: true })
          })
        } catch (error) {
          // 获取用户信息失败,则删除会话信息并跳转到登录页
          await store.dispatch('user/resetToken')
          Message.error(error || 'Has Error')
          next(`/login?redirect=${to.path}`)
          NProgress.done()
        }
      }
    }
  } else {
    /* 没有token*/
    if (whiteList.indexOf(to.path) !== -1) {
      // 是白名单的页面,则可以进入页面
      next()
    } else {
      // 非白名单,则跳转到登录页
      next(`/login?redirect=${to.path}`)
      NProgress.done()
    }
  }
})

效果图

小结

写到这里,前端篇也告一段落了,bug肯定是有的,不过后续发现再慢慢优化吧。我要开启新的篇章了-手把手带你玩转k8s!

项目源码地址

  • 后端

gitee.com/mldong/mldo…

  • 前端

gitee.com/mldong/mldo…

相关文章

打造一款适合自己的快速开发框架-先导篇

打造一款适合自己的快速开发框架-前端脚手架搭建

打造一款适合自己的快速开发框架-前端篇之登录与路由模块化

打造一款适合自己的快速开发框架-前端篇之框架分层及CURD样例

打造一款适合自己的快速开发框架-前端篇之字典组件设计与实现

打造一款适合自己的快速开发框架-前端篇之下拉组件设计与实现

打造一款适合自己的快速开发框架-前端篇之选择树组件设计与实现

打造一款适合自己的快速开发框架-前端篇之代码生成器