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?: 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>
