GitHub Demo 地址
在线预览
前言
关于动态获取路由已在这里给出方案 Vue - vue-admin-template模板项目改造:动态获取菜单路由
这里是在此基础上升级成vue3
和ts
,数据和网络请求是通过mock实现的
具体代码请看demo!!!
本地权限控制,具体是通过查询用户信息获取用户角色,在路由守卫中通过角色过滤本地配置的路由,把符合角色权限的路由生成一个路由数组
动态获取菜单路由其实思路是一样的,只不过路由数组变成从服务器获取,通过查询某个角色的菜单列表,然后在路由守卫中把获取到的菜单数组转成路由数组
动态路由实现是参考vue-element-admin的issues写的,相关issues:
vue-element-admin/issues/167
vue-element-admin/issues/293
vue-element-admin/issues/3326#issuecomment-832852647
关键点
主要在接口菜单列表中把父
component
的Layout
改为字符串 ‘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?: stringicon?: string // 设置svg图标和通过iconify使用的element-plus图标,根据 : 判断是否是iconify图标hidden?: booleanaffix?: booleankeepAlive?: booleanroles?: string[]buttons?: string[]
}export interface AppRouteType extends Omit<RouteRecordRaw, 'props'> {path: stringname?: stringcomponent?: Component | stringcomponents?: Componentchildren?: AppRouteType[]fullPath?: stringmeta?: AppRouteMetaTyperedirect?: stringalias?: string | string[]
}// 动态路由类型
export interface AppDynamicRouteType extends AppRouteType {id: stringcode: stringtitle: stringparentId: stringparentTitle: stringmenuType: stringcomponent: string | Componenticon: stringsort: numberhidden: booleanlevel: numberchildren?: AppDynamicRouteType[]buttons?: string[]
}
接口路由格式:
{id: '22',code: '/system',title: '系统设置',parentId: '',parentTitle: '',menuType: 'catalog', // catalog | menu | buttoncomponent: '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.idtmp.path = route.codetmp.name = route.codetmp.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', () => {// stateconst routes = ref<AppRouteType[]>([])// actionsfunction 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,所以查出来的菜单不用包含constantRoutesresolve(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 } = bindingif (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>