【菜单以及页面按钮权限控制方案】进来看看吧

element-ui表格操作栏fixed且宽度自适应

由于近期给公司的项目开发页面的按钮权限控制,目前这功能已经完事了,所以在此做个总结,希望能给看到此篇文章的小伙伴一些帮助和思路

权限控制这个一般后台管理系统肯定是都有的,只不过有的是只细化到导航栏菜单的权限控制,有的需要精确到页面上每一个按钮。

只要是后台管理系统,少不了用户管理,角色管理,菜单管理等功能
而要想实现前端菜单页面以及按钮的权限控制,则这三项功能就得搭配使用

正文开始

首先需要5张数据库表,sys_user(用户表),sys_role(角色表),sys_menu(菜单表),sys_user_role(用户角色关联表),sys_role_menu(角色菜单关联表)

正因为有了需要每个用户可能只能看到不同菜单或按钮等需求,所以才需要权限控制,而最好的方式就是让角色绑定菜单,用户绑定角色,而不是用户直接绑定菜单,这样角色方便给每个用户账号复用其拥有的菜单权限

sys_menu菜单表的结构可以根据自己的需求以及实现方式来定,我这里提供我两种前端权限控制的实现方式

  1. 前端写死路由结构,每个路由里面带一个值唯一的permissionKey属性,然后后端接口返回permissionKey集合,通过这个集合来过滤掉的路由完成权限控制,缺点是难以精确到按钮,不能动态改菜单的图标和名称,这种方案就可以把菜单表设计的简单一点,只要至少有一个菜单名称和permissionKey字段对应就ok
  2. 后端返回所有的路由数据,这种方案的菜单表需要以下字段菜单名称,菜单标识,类型,访问路径,组件路径,图标,排序,是否隐藏,是否启用等字段,类型是用来判断某条数据是按钮还是菜单页面还是目录,是否隐藏用来判断是否显示在导航菜单里面,比如一些详情页面就不需要显示在导航菜单里,但是需要被后端返回。

因为需要细化到页面按钮的控制,所以我选的是第二种方案,这种方案挺方便的,可以动态修改菜单名称图标,访问路径,组件地址,顺序,是否启用等等。

在前端请求到所有的路由数据后(我推荐后端返回平铺的数据,而不是有层级关系的),在前端写递归去构建路由树,并且我们也需要把组件地址的字符串路径真正的导入组件,构建成如下面所示的路由数据结构

const route = {
				name:'systemManager',
				path:'/systemManager'
				component:一个vue组件实例,
				meta:{
					title:'系统管理',
					isHide:false,//是否隐藏
					buttons:[],//用来存放当前路由页面上所拥有的按钮的标识
					icon:'el-icon-menu',//菜单图标
					keepAlive:false,//是否使用keepAlive缓存页面,这个公司项目没有用多标签切换已经打开的多个页面,所以我没用
				},
				children:[]//子路由
			  }

值得注意的是如果一个路由里面有子路由,那么这个路由的组件里面一定有些<router-view :key='route.path'/>标签
这样子路由才能显示出来,不然白屏

以下是我写的代码

ForRouter.vue组件 用来显示子路由的组件

<template>
  <router-view/>
</template>

<script>
export default {
  name: "ForRouter"
}
</script>

_import_development.js 用来根据组件路径动态导入组件的

module.exports = file => require('@/' + file + '.vue').default

_import_production.js 用来根据组件路径动态导入组件的

module.exports = file => () => import('@/' + file + '.vue')

然后是生成路由树的工具类,这里我分了两个generateRoutesgenerateMenuTree ,前者用来生成路由树,后者用来生成导航菜单树

import ForRouter from "@/layout/ForRouter";
const _import = require('@/utils/_import_' + process.env.NODE_ENV)
const _import_common = require('@/utils/_import_common_' + process.env.NODE_ENV)
import {error404, Main} from '@/common'

export const generateRoutes = (routes) => {
  let allParentNode = []
  const allChildNode = []
  const allButtons = []

  routes.forEach(route => {
    if (route.parentId === 0) {
      allParentNode.push(route)
    } else if (route.menuType === 1) {
      allButtons.push(route)
    } else {
      allChildNode.push(route)
    }
  })
  //筛选出所有非顶级node
  //筛选出所有按钮
  allParentNode = allParentNode.map(node => {
    const newNode = {
      path: node.path, name: node.name
    }
    if (node.redirect && node.redirect !== '') {
      newNode.redirect = node.redirect
    }
    newNode.meta = {
      icon: node.icon, title: node.title, hide: node.hide, menuType: node.menuType, iconType: node.iconType
    }
    if (node.component && node.component !== '') {
      try {
        newNode.component = _import(node.component)
        // eslint-disable-next-line no-empty
      } catch (e) {
        newNode.component = Main
      }
    }
    newNode.children = getChildren(node.id, allChildNode, allButtons)
    return newNode
  })
  return allParentNode
}

const getChildren = (parentId, allChildNode, allButtons) => {
  let childNode = allChildNode.filter(node => node.parentId === parentId)
  const newChildNode = []
  childNode.forEach(node => {
    const newNode = {
      path: node.path, name: node.name, sortValue: node.sortValue
    }
    const meta = {
      icon: node.icon, title: node.title, hide: node.hide, menuType: node.menuType, iconType: node.iconType
    }
    //0:菜单,1:按钮,2:目录
    if (node.menuType === 2) {
      newNode.children = getChildren(node.id, allChildNode, allButtons)
      if (node.component && node.component !== '') {
        try {
          newNode.component = _import_common(node.component)
          // eslint-disable-next-line no-empty
        } catch (e) {
          newNode.component = ForRouter
        }
      }
    } else if (node.menuType === 0) {
      if (node.redirect && node.redirect !== '') {
        newNode.redirect = node.redirect
      }
      try {
        newNode.component = _import(node.component)
      } catch (e) {
        console.log(e)
        newNode.component = error404
      }
      meta.buttons = allButtons.filter(button => button.parentId === node.id).map(item => item.name)
    }
    newNode.meta = meta
    newChildNode.push(newNode)
  })
  newChildNode.sort((a, b) => a.sortValue - b.sortValue)
  return newChildNode
}

export const generateMenuTree = (menus, skipBtn) => {
  let allParentMenu = []
  const allChildMenu = []

  menus.forEach(route => {
    if (skipBtn) {
      if (route.parentId === 0) {
        allParentMenu.push(route)
      } else if (route.menuType !== 1) {
        allChildMenu.push(route)
      }
    } else {
      if (route.parentId === 0) {
        allParentMenu.push(route)
      } else {
        allChildMenu.push(route)
      }
    }
  })
  allParentMenu.forEach(menu => {
    const child = getChildrenMenu(menu.id, allChildMenu)
    if (child && child.length > 0) {
      child.sort((a, b) => a.sortValue - b.sortValue)
      menu.children = child
    }
  })
  return allParentMenu
}

const getChildrenMenu = (parentId, allChildMenu) => {
  let childNode = allChildMenu.filter(node => node.parentId === parentId)
  childNode.forEach(menu => {
    const child = getChildrenMenu(menu.id, allChildMenu)
    if (child && child.length > 0) {
      child.sort((a, b) => a.sortValue - b.sortValue)
      menu.children = child
    }
  })
  return childNode
}

然后是vuex的代码,routePermission.js 用来请求后端路由数据,然后调用上面生成路由树的工具类生成路由树,然后保存到vuex里面,然其他地方也能访问到路由数据

import {system} from "@/api/system";
import {generateRoutes} from "@/utils";

const routePermission = {
  state: {
    myRoutes: [],
  },
   mutations: {
    setRoutes(state, routes) {
      state.myRoutes = routes
    },
     removeRoutes(state, routes) {
      state.myRoutes = []
    },
  },
   actions: {
    fetchRoutes({commit}, data) {
      let allRoutes
      return new Promise((resolve) => {
        system.listMenuRefRoles().then(res => {
          let hasRoute = false
          if (res.status === 200) {
            hasRoute = res.data.length > 0
            if (!hasRoute) {
              allRoutes = [{
                path: '/', redirect: '/401', hidden: true, children: [], meta: {title: '无权限访问', hide: true}
              }]
            } else {
              allRoutes = generateRoutes(res.data)
            }
            // 动态添加可访问路由表
            commit('setRoutes', allRoutes)
          }
          resolve({status: res.status, hasRoute: hasRoute})
        })
      })
    }, removeRoutes({commit}, data) {
      commit('removeRoutes')
    }
  }
}
export default routePermission

然后是拦截路由,permission.js 在每次进入路由时,判断如果vuex里面没有路由信息则重新拉一次,有的话直接进入路由,当前中间包括了一些鉴权,比如没有登录直接跳到登录页面,访问不存在的路由跳到404页面等等

import router from './router'
import store from './store'
import NProgress from 'nprogress' // Progress 进度条
import 'nprogress/nprogress.css' // Progress 进度条样式

const whiteList = ['/login', '/404', '/401'] // 不重定向白名单
router.beforeEach((to, from, next) => {
  NProgress.start()
  const token = sessionStorage.getItem("access_token");
  if (token) {
    if (to.path === '/login') {
      document.title = to.meta.title
      next({path: '/home'})
      NProgress.done() // if current page is dashboard will not trigger	afterEach hook, so manually handle it
    } else {
      if (to.meta.title) {
        document.title = to.meta.title
      } else {
        document.title = '加载中...'
      }
      if (store.getters.myRoutes.length === 0) {
        store.dispatch('fetchRoutes').then(({status, hasRoute}) => { // 生成可访问的路由表
          if (status === 400) {
            next({
              path: '/login', replace: true
            })
          } else if (status === 200 && hasRoute) {
            router.addRoutes(store.getters.myRoutes)
            if (router.getMatchedComponents({path: to.path}).length === 0) {
              next({path: '/404'})
            } else {
              next({...to, replace: true})
            }
          } else {
            next({
              path: '/401', replace: true
            })
          }
        })
      } else {
        if (router.getMatchedComponents({path: to.path}).length === 0) {
          next({path: '/404'})
        } else {
          next()
        }
      }
    }
  } else {
    if (whiteList.indexOf(to.path) !== -1) {
      if (to.path === '/login') {
        document.title = to.meta.title
      } else if (to.path) {
        document.title = to.meta.title
      }
      next()
    } else {
      document.title = '登陆'
      next('/login')
      NProgress.done()
    }
  }
})

router.afterEach(() => {
  NProgress.done() // 结束Progress
})

最后是判断是否有按钮权限的工具类,可以随便放在一个工具类的js里面

export const hasPerm = (route, ...btnCode) => {
  const buttons = route.meta.buttons
  let i = 0
  btnCode.forEach(item => {
    if (buttons.includes(item)) {
      i++
    }
  })
  return i > 0
}

在main.js引入并将其挂载到vue的原型链上

import {hasPerm} from "@/permission";
Vue.prototype.$hasPerm = hasPerm

然后新建一个ts声明文件,让我们在页面上使用的使用能自动提示出来

import Vue from "vue";
import type {Route} from "vue-router/types/router";

export {}; // 这句不能删,删除了报错
declare module 'vue/types/vue' {
  interface Vue {
    $hasPerm: (route: Route, ...btnCode: string) => boolean
  }
}

页面上使用
在这里插入图片描述
需要将当前路由传进去是因为我们的页面上的按钮集合是放在当前路由的meta里面的在这里插入图片描述这样每个路由都有它自己拥有的按钮集合btnCode就是菜单标识字段,如果懒得每个按钮都加个英文标识,那就直接写中文标识也是一样的在这里插入图片描述
这样我们页面上也改一下
在这里插入图片描述
这样一个菜单权限控制+页面按钮权限控制就完成了。

我主页还有element-ui表格fixed操作栏且宽度自适应解决方案,确定不来看看吗

  • 2
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Vue菜单权限设计和实现主要包括以下几个方面: 1. 菜单数据结构设计:首先,我们需要设计一个合适的菜单数据结构,一般可以使用树形结构来表示菜单的层级关系。每个菜单项包括菜单名称、路由路径、图标、是否显示等属性。 2. 权限控制设计:在用户登录成功后,后端会返回该用户所拥有的菜单权限列表。前端可以通过保存用户的菜单权限,对用户进行菜单权限控制。一种常用的做法是在路由守卫中判断用户是否有访问该路由的权限,并根据权限来动态渲染菜单。 3. 菜单渲染和动态展示:根据用户的菜单权限列表,前端通过递归遍历菜单数据结构,来渲染用户可见的菜单项。通过在菜单组件中动态绑定数据,可以实现菜单的展开和收起、高亮当前路由等功能。 4. 菜单操作和权限判断:除了菜单的展示,还需要考虑菜单的操作权限判断。例如,某个菜单项只有管理员才有权限访问,可以根据用户的角色或权限列表来判断该菜单项是否应该显示或可操作。 5. 菜单数据的动态更新:在某些场景下,菜单的数据可能会有动态更新。比如,管理员新增了一个菜单项,对应的路由也发生了变化。这时候,前端需要通过调用后端接口来更新用户的菜单权限列表,并且重新渲染菜单。 综上所述,Vue菜单权限设计和实现需要从菜单数据结构设计权限控制菜单渲染和动态展示、菜单操作和权限判断、菜单数据的动态更新等方面进行考虑和实现。通过合理的设计和实现,可以确保系统具备良好的菜单权限管理功能,提高用户体验和系统安全性。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值