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

2023-09-21 10:47:33

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>
更多推荐

【Hierarchical Coverage Path Planning in Complex 3D Environments】

HierarchicalCoveragePathPlanninginComplex3DEnvironments复杂三维环境下的分层覆盖路径规划视点采样全局TSP算法分两层,一层高级一层低级:高层算法将环境分离多个子空间,如果给定体积中有大量的结构,则空间会进一步细分。全局TSP问题;低层算法采用简单的采样路径规划,解决

Go语言简介:历史背景、发展现状及语言特性

一、简述Go语言背景和发展1.软件开发的新挑战多核硬件架构超大规模分布式计算集群Web模式导致的前所未有的开发规模和更新速度2.Go的三位创始人RobPikeUnix的早期开发者UTF-8创始人KenThompsonUnix的创始人C语言创始人1983年获图灵奖RobertGriesemerGoogleV8JSEngi

【C++】动态内存管理 ⑤ ( 基础数据类型数组 内存分析 | 类对象 内存分析 | malloc 分配内存 delete 释放 | new 分配内存 free 释放内存 )

文章目录一、基础数据类型数组内存分析1、malloc分配内存delete释放内存2、new分配内存free释放内存二、类对象内存分析1、malloc分配内存delete释放内存2、new分配内存free释放内存博客总结:C语言中使用malloc分配的内存,使用free进行释放;C++语言中推荐使用new分配的内存,使用

【测试开发】基础篇 · 专业术语 · 软件测试生命周期 · bug的描述 · bug的级别 · bug的生命周期 · 处理争执

【测试开发】基础篇文章目录【测试开发】基础篇1.软件测试生命周期1.1软件生命周期1.2软件测试生命周期2.描述bug3.如何定义bug的级别3.1为什么要对bug进行级别划分3.2bug的一些常见级别4.bug的生命周期5.产生争执这么怎么办(处理人际关系)6.如何开始第一次测试7.测试的执行和bug管理8.如何发现

使用JAXB将xml转成Java对象

文章目录使用JAXB将xml转成Java对象1.xml内容2.Java对象类3.封装的工具类4.测试使用JAXB将xml转成Java对象工作中遇到个问题,需要将xml转对象,之前复杂的xml都是自己用dom4j来解析组装成Java对象,但是对于简单的,看到了JAXB处理的这种方式,就整理下,写成个工具类。1.xml内容

Linux系统编程——线程的学习

学习参考博文:Linux多线程编程初探Linux系统编程学习相关博文Linux系统编程——文件编程的学习Linux系统编程——进程的学习Linux系统编程——进程间通信的学习Linux系统编程——网络编程的学习Linux系统编程——线程的学习一、概述1.进程与线程的区别2.使用线程的理由3.互斥量4.条件变量二、线程A

武汉凯迪正大—继保校验仪的产品特点

一、武汉凯迪正大继保仪的产品特点有哪些?1、经典的WindowsXP操作界面,人机界面友好,操作简便快捷,为了方便用户使用,定义了大量键盘快捷键,使得操作“一键到位”2、高性能的嵌入式工业控制计算机和8.4〞大屏幕高分辨力彩色TFT液晶显示屏,可以提供丰富直观的信息,包括设备当前的工作状态、下一步工作提示及各种帮助信息

【AI视野·今日NLP 自然语言处理论文速览 第三十五期】Mon, 18 Sep 2023

AI视野·今日CS.NLP自然语言处理论文速览Mon,18Sep2023Totally51papers👉上期速览✈更多精彩请移步主页DailyComputationandLanguagePapers"MergeConflicts!"ExploringtheImpactsofExternalDistractorstoP

小白带你学习ceph分布式存储

目录一、概述1、分布式存储系统2、特点2、1统一存储2、2高扩展性2、3可靠性强2、4高性能二、组件1.Monitor2.OSD3.MDS4.Object5.PG6.RADOS7.Libradio8.CRUSH9.RBD10.RGW11.CephFS三、架构图1、文件上传2、文件存储前四、部署1、环境拓扑2、准备工作3

【Robotframework+python】实现http接口自动化测试

前言下周即将展开一个http接口测试的需求,刚刚完成的java类接口测试工作中,由于之前犯懒,没有提前搭建好自动化回归测试框架,以至于后期rd每修改一个bug,经常导致之前没有问题的case又产生了bug,所以需要一遍遍回归case,过程一直手工去执行,苦不堪言。所以,对于即将开始的http接口测试需求,立马花了两天时

Openresty(二十二)ngx.balance和balance_by_lua终结篇

一灰度发布铺垫①init_by_lua*init_by_luainit_by_lua_block特点:在openresty'start'、'reload'、'restart'时执行,属于'masterinit'阶段机制:nginx'master'主进程'加载配置文件'时,运行全局LuaVM级别上的参数指定的'Lua代码

热文推荐