美文网首页
Vue3 - 实现动态获取菜单路由和按钮权限控制指令

Vue3 - 实现动态获取菜单路由和按钮权限控制指令

作者: 西半球_ | 来源:发表于2023-10-11 08:35 被阅读0次

    GitHub Demo 地址

    在线预览

    效果图

    前言

    关于动态获取路由已在这里给出方案 Vue - vue-admin-template模板项目改造:动态获取菜单路由
    这里是在此基础上升级成vue3ts,数据和网络请求是通过mock实现的
    具体代码请看demo!!!

    本地权限控制,具体是通过查询用户信息获取用户角色,在路由守卫中通过角色过滤本地配置的路由,把符合角色权限的路由生成一个路由数组

    动态获取菜单路由其实思路是一样的,只不过路由数组变成从服务器获取,通过查询某个角色的菜单列表,然后在路由守卫中把获取到的菜单数组转成路由数组

    动态路由实现是参考vue-element-admin的issues写的,相关issues:
    vue-element-admin/issues/167
    vue-element-admin/issues/293
    vue-element-admin/issues/3326#issuecomment-832852647

    关键点

    主要在接口菜单列表中把父componentLayout 改为字符串 'Layout',
    children的component: () => import('@/views/system/user/index.vue'), 改成 字符串'system/user/index',然后在获取到数据后再转回来
    !!!!!!!!!!!! 接口格式可以根据项目需要自定义,不一定非得按照这里的来

    vue3 中component使用和vue略有差异,需要加上完整路径,并且从字符串换成组件的方式也有不同
    !!!!!!!!!注意文件路径

    import { defineAsyncComponent } from 'vue'
    const modules = import.meta.glob('../../views/**/**.vue')
    
    // 加载路由
    const loadView = (view: string) => {
      // 路由懒加载
      // return defineAsyncComponent(() => import(`/src/views/${view}.vue`))
      return modules[`../../views/${view}.vue`]
    }
    

    调用

    loadView(route.component)
    

    本地路由格式:

    import { AppRouteType } from '@/router/types'
    
    const Layout = () => import('@/layout/index.vue')
    
    const systemRouter: AppRouteType = {
      path: '/system',
      name: 'system',
      component: Layout,
      meta: { title: 'SystemSetting', icon: 'ep:setting', roles: ['admin'] },
      children: [
        {
          path: 'user',
          name: 'user',
          component: () => import('@/views/system/user/index.vue'),
          meta: {
            title: 'SystemUser',
            icon: 'user',
            buttons: ['user-add', 'user-edit', 'user-look', 'user-export', 'user-delete', 'user-assign', 'user-resetPwd']
          }
        },
        {
          path: 'role',
          name: 'role',
          component: () => import('@/views/system/role/index.vue'),
          meta: {
            title: 'SystemRole',
            icon: 'role',
            buttons: ['role-add', 'role-edit', 'role-look', 'role-delete', 'role-setting']
          }
        },
        {
          path: 'menu',
          name: 'menu',
          component: () => import('@/views/system/menu/index.vue'),
          meta: {
            title: 'SystemMenu',
            icon: 'menu',
            buttons: ['menu-add', 'menu-edit', 'menu-look', 'menu-delete']
          }
        },
        {
          path: 'dict',
          name: 'dict',
          component: () => import('@/views/system/dict/index.vue'),
          meta: {
            title: 'SystemDict',
            icon: 'dict',
            buttons: ['dict-type-add', 'dict-type-edit', 'dict-type-delete', 'dict-item-add', 'dict-item-edit', 'dict-item-delete']
          }
        }
      ]
    }
    export default systemRouter
    

    ts路由类型定义

    import type { RouteRecordRaw, RouteMeta, RouteRecordRedirectOption } from 'vue-router'
    
    export type Component<T = any> = ReturnType<typeof defineComponent> | (() => Promise<typeof import('*.vue')>) | (() => Promise<T>)
    
    // element-plus图标
    // https://icon-sets.iconify.design/ep/
    // 其他的
    // https://icon-sets.iconify.design/
    // 动态图标
    // https://icon-sets.iconify.design/line-md/
    // https://icon-sets.iconify.design/svg-spinners/
    
    export interface AppRouteMetaType extends RouteMeta {
      title?: string
      icon?: string // 设置svg图标和通过iconify使用的element-plus图标,根据 : 判断是否是iconify图标
      hidden?: boolean
      affix?: boolean
      keepAlive?: boolean
      roles?: string[]
      buttons?: string[]
    }
    
    export interface AppRouteType extends Omit<RouteRecordRaw, 'props'> {
      path: string
      name?: string
      component?: Component | string
      components?: Component
      children?: AppRouteType[]
      fullPath?: string
      meta?: AppRouteMetaType
      redirect?: string
      alias?: string | string[]
    }
    
    // 动态路由类型
    export interface AppDynamicRouteType extends AppRouteType {
      id: string
      code: string
      title: string
      parentId: string
      parentTitle: string
      menuType: string
      component: string | Component
      icon: string
      sort: number
      hidden: boolean
      level: number
      children?: AppDynamicRouteType[]
      buttons?: string[]
    }
    

    接口路由格式:

    {
        id: '22',
        code: '/system',
        title: '系统设置',
        parentId: '',
        parentTitle: '',
        menuType: 'catalog', // catalog | menu | button
        component: 'Layout', // "Layout" | "system/menu" (文件路径: src/views/) | ""
        // component: Layout,
        icon: 'ep:setting',
        sort: 1,
        hidden: false,
        level: 1,
        children: [
          {
            id: '22-1',
            code: 'user',
            title: '用户管理',
            parentId: '22',
            parentTitle: '系统设置',
            menuType: 'menu',
            component: 'system/user/index',
            // component: () => import('@/views/system/user'),
            icon: 'user',
            sort: 2,
            hidden: false,
            level: 2,
            children: [],
            buttons: ['user-add', 'user-edit', 'user-look', 'user-export', 'user-delete', 'user-assign', 'user-resetPwd']
          },
          {
            id: '22-2',
            code: 'role',
            title: '角色管理',
            parentId: '22',
            parentTitle: '系统设置',
            menuType: 'menu',
            component: 'system/role/index',
            icon: 'role',
            sort: 3,
            hidden: false,
            level: 2,
            children: [],
            buttons: ['role-add', 'role-edit', 'role-look', 'role-delete', 'role-setting']
          },
          {
            id: '22-3',
            code: 'menu',
            title: '菜单管理',
            parentId: '22',
            parentTitle: '系统设置',
            menuType: 'menu',
            component: 'system/menu/index',
            icon: 'menu',
            sort: 4,
            hidden: false,
            level: 2,
            children: [],
            buttons: ['menu-add', 'menu-edit', 'menu-look', 'menu-delete']
          },
          {
            id: '22-4',
            code: 'dict',
            title: '字典管理',
            parentId: '22',
            parentTitle: '系统设置',
            menuType: 'menu',
            component: 'system/dict/index',
            icon: 'dict',
            sort: 5,
            hidden: false,
            level: 2,
            children: [],
            buttons: ['dict-type-add', 'dict-type-edit', 'dict-type-delete', 'dict-item-add', 'dict-item-edit', 'dict-item-delete']
          }
        ]
      }
    

    我这里在mock中加了个角色editor2,当editor2登录使用的从服务器获取动态路由,其他角色从本地获取路由

    permission.ts 实现,其中filterAsyncRoutes2方法就是格式化菜单路由的方法

    import { defineAsyncComponent } from 'vue'
    import { cloneDeep } from 'lodash-es'
    import { defineStore } from 'pinia'
    import { store } from '@/store'
    import { asyncRoutes, constantRoutes } from '@/router'
    
    import { AppRouteType, AppDynamicRouteType } from '@/router/types'
    
    const modules = import.meta.glob('../../views/**/**.vue')
    const Layout = () => import('@/layout/index.vue')
    
    /**
     * Use meta.role to determine if the current user has permission
     * @param roles
     * @param route
     */
    const hasPermission = (roles: string[], route: AppRouteType) => {
      if (route.meta && route.meta.roles) {
        return roles.some((role) => {
          if (route.meta?.roles !== undefined) {
            return (route.meta.roles as string[]).includes(role)
          }
        })
      }
      return true
    }
    
    /**
     * Filter asynchronous routing tables by recursion
     * @param routes asyncRoutes
     * @param roles
     */
    const filterAsyncRoutes = (routes: AppRouteType[], roles: string[]) => {
      const res: AppRouteType[] = []
    
      routes.forEach((route) => {
        const tmp = cloneDeep(route)
        // const tmp = { ...route }
        if (hasPermission(roles, tmp)) {
          if (tmp.children) {
            tmp.children = filterAsyncRoutes(tmp.children, roles)
          }
          res.push(tmp)
        }
      })
    
      return res
    }
    
    // 加载路由
    const loadView = (view: string) => {
      // 路由懒加载
      // return defineAsyncComponent(() => import(`/src/views/${view}.vue`))
      return modules[`../../views/${view}.vue`]
    }
    
    /**
     * 通过递归格式化菜单路由 (配置项规则:https://panjiachen.github.io/vue-element-admin-site/zh/guide/essentials/router-and-nav.html#配置项)
     * @param routes
     */
    export function filterAsyncRoutes2(routes: AppDynamicRouteType[]) {
      const res: AppDynamicRouteType[] = []
      routes.forEach((route) => {
        const tmp = cloneDeep(route)
        // const tmp = { ...route }
        tmp.id = route.id
        tmp.path = route.code
        tmp.name = route.code
        tmp.meta = { title: route.title, icon: route.icon, buttons: route.buttons }
        if (route.component === 'Layout') {
          tmp.component = Layout
        } else if (route.component) {
          tmp.component = loadView(route.component)
        }
        if (route.children && route.children.length > 0) {
          tmp.children = filterAsyncRoutes2(route.children)
        }
        res.push(tmp)
      })
      return res
    }
    
    // setup
    export const usePermissionStore = defineStore('permission', () => {
      // state
      const routes = ref<AppRouteType[]>([])
    
      // actions
      function setRoutes(newRoutes: AppRouteType[]) {
        routes.value = constantRoutes.concat(newRoutes)
      }
    
      function generateRoutes(roles: string[]) {
        return new Promise<AppRouteType[]>((resolve, reject) => {
          let accessedRoutes: AppRouteType[] = []
          if (roles.includes('admin')) {
            accessedRoutes = asyncRoutes || []
          } else {
            accessedRoutes = filterAsyncRoutes(asyncRoutes, roles)
          }
          setRoutes(accessedRoutes)
          resolve(accessedRoutes)
        })
      }
    
      function generateDynamicRoutes(menus: AppDynamicRouteType[]) {
        return new Promise<AppRouteType[]>((resolve, reject) => {
          const accessedRoutes = filterAsyncRoutes2(menus)
          setRoutes(accessedRoutes) // Todo: 内部拼接constantRoutes,所以查出来的菜单不用包含constantRoutes
          resolve(accessedRoutes)
        })
      }
    
      return { routes, setRoutes, generateRoutes, generateDynamicRoutes }
    })
    
    // 非setup
    export function usePermissionStoreHook() {
      return usePermissionStore(store)
    }
    
    

    按钮权限控制

    directive文件夹,创建permission.ts指令设置路由内的按钮权限

    import { useUserStoreHook } from '@/store/modules/user'
    import { Directive, DirectiveBinding } from 'vue'
    import router from '@/router/index'
    
    /**
     * 按钮权限 eg: v-hasPerm="['user-add','user-edit']"
     */
    export const hasPerm: Directive = {
      mounted(el: HTMLElement, binding: DirectiveBinding) {
        // 「超级管理员」拥有所有的按钮权限
        const { roles, perms } = useUserStoreHook()
        if (roles.includes('admin')) {
          return true
        }
    
        // 「其他角色」按钮权限校验
        const buttons = router.currentRoute.value.meta.buttons as string[]
        const { value } = binding
        if (value) {
          const requiredPerms = value // DOM绑定需要的按钮权限标识
          const hasPerm = buttons?.some((perm) => {
            return requiredPerms.includes(perm)
          })
    
          if (!hasPerm) {
            el.parentNode && el.parentNode.removeChild(el)
          }
        } else {
          throw new Error("need perms! Like v-has-perm=\"['user-add','user-edit']\"")
        }
      }
    }
    

    创建index.ts文件,全局注册 directive

    import type { App } from 'vue'
    
    import { hasPerm } from './permission'
    
    // 全局注册 directive
    export function setupDirective(app: App<Element>) {
      // 使 v-hasPerm 在所有组件中都可用
      app.directive('hasPerm', hasPerm)
    }
    
    

    在main.ts注册自定义指令

    import { setupDirective } from '@/directive'
    
    const app = createApp(App)
    // 全局注册 自定义指令(directive)
    setupDirective(app)
    

    使用

    <el-button v-hasPerm="['user-item-add']"> 新增 </el-button>
    

    相关文章

      网友评论

          本文标题:Vue3 - 实现动态获取菜单路由和按钮权限控制指令

          本文链接:https://www.haomeiwen.com/subject/esxbbdtx.html