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

GitHub Demo 地址

在线预览

效果图

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

前言

关于动态获取路由已在这里给出方案 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登录使用的从服务器获取动态路由,其他角色从本地获取路由

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

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>

© 版权声明

相关文章

暂无评论

您必须登录才能参与评论!
立即登录
none
暂无评论...