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略有差异,需要加上完整路径,并且从字符串换成组件的方式也有不同
!!!!!!!!!注意文件路径
ts
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`]
}
调用
ts
loadView(route.component)
本地路由格式:
ts
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路由类型定义
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[]
}
接口路由格式:
ts
{
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
方法就是格式化菜单路由的方法
ts
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指令设置路由内的按钮权限
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
ts
import type { App } from 'vue'
import { hasPerm } from './permission'
// 全局注册 directive
export function setupDirective(app: App<Element>) {
// 使 v-hasPerm 在所有组件中都可用
app.directive('hasPerm', hasPerm)
}
在main.ts注册自定义指令
ts
import { setupDirective } from '@/directive'
const app = createApp(App)
// 全局注册 自定义指令(directive)
setupDirective(app)
使用
ts
<el-button v-hasPerm="['user-item-add']"> 新增 </el-button>