打造一款適合本身的快速開發框架-前端篇之權限管理

前言

在後端篇中已對權限資源進行了分類:API接口、路由菜單、頁面按鈕。本文重點講一下如何對這些權限資源進行分配並對不一樣的登陸用戶,根據權限的不一樣,呈現不同的功能菜單、按鈕進行統一處理。css

關係梳理

用戶與角色

多對多關係:一個用戶能夠擁有多個角色,一個角色包含多個用戶前端

角色與菜單

多對多關係:一個角色有多個菜單,一個菜單能夠分配給多個角色vue

角色與權限資源

多對多關係:一個角色有多個權限資源,一個權限資源能夠分配給多個角色node

vue相關

動態路由

vue能夠經過router.addRoutes動態地添加路由信息git

// 動態添加可訪問路由表
router.addRoutes(accessRoutes)
複製代碼

自定義指令

這裏只是簡單的介紹,想詳細瞭解的,可自行查看資料。vuex

指令定義

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

<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

按鈕權限指令定義邏輯處理bash

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

打造一款適合本身的快速開發框架-前端篇之字典組件設計與實現

打造一款適合本身的快速開發框架-前端篇之下拉組件設計與實現

打造一款適合本身的快速開發框架-前端篇之選擇樹組件設計與實現

打造一款適合本身的快速開發框架-前端篇之代碼生成器

相關文章
相關標籤/搜索