vue3笔记(21-2)动态路由

这篇是上一篇的延伸,详细记录下动态路由的使用。

项目中使用-子系统

动态菜单格式如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
{
"status": 200,
"msg": "菜单获取成功",
"data": [{
"id": 2,
"pid": 1, // 最上层pid代表系统
"name": "一级菜单",
"title": "一级菜单",
"path": "/first",
"level": 1,
"fold": "",
"component": "BasicLayout",
"icon": "first-icon",
"hidden": false,
"status": true,
"meta": {
"title": "一级菜单",
"icon": "first-icon",
"hidden": false,
"level": 1,
"multiple": false
},
"children": [{
"id": 3,
"pid": 2,
"name": "二级菜单",
"title": "二级菜单",
"path": "/second",
"level": 1,
"fold": "home",
"component": "test",
"icon": "first-icon",
"hidden": false,
"status": true,
"children": [{
"id": 4,
"pid": 3,
"name": "删除",
"level": 3,
"hidden": false,
"status": true,
"api": "/api/partner/",
"method": "DELETE",
}]
}]
}]
}

其中 level=3 数组,用于前端按钮展示,为了用户友好,系统定义若用户无权限访问(例如只能查看,不能修改),直接将操作按钮隐藏。

store/modules/permission.ts中存储菜单信息,这里MenuType添加了类型 fold ,表示文件夹地址,因为前端在工程设计时将不同组件存放于不同文件夹下,如果直接在path中写几层地址,这里会导致导入组件失败。
routes表示静态路由,例如:首页(/),404(/404)等应用开放给所有用户的页面,dynamicRoutes 表示动态路由,是在应用初始化时从后端获取的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
import { defineStore } from 'pinia'
import { store } from '@/store/index'
import { BasicLayout } from '@/views/layout/index'
import router from '../../router'
import { getUserRoutes } from '@/api/user'

export type MenuType = {
path: string
title: string
fold?: string
meta?: any
component: string
redirect?: string
children?: Array<MenuType>
}

interface IPermissionState {
routes: any[]
dynamicRoutes: any[]
}

// 生产环境导入组件
const modules = import.meta.glob('../../views/**/*.vue')
// 本地开发环境导入组件
const _import = (path: string, fold: string) => () => import(`../../views/${fold}/${path}.vue`)

const assembleRouter = (routers: any) => {
const addRouter = routers.filter((router: any) => {
router.title &&
router.icon &&
(router.meta = {
title: router.title
// icon: router.icon, // 子菜单暂时不用icon
})
if (router.component === 'BasicLayout') {
router.component = shallowRef(BasicLayout)
} else {
if (import.meta.env.MODE === 'dev') {
router.component = _import(router.component, router.fold)
} else {
router.component = modules[`../../views/${router.fold}/${router.component}.vue`]
}
}
if (router.children && router.children.length) {
router.children = assembleRouter(router.children)
}
return true
})
return addRouter
}

export const usePermissionStore = defineStore('permission', {
state: (): IPermissionState => ({
routes: [],
dynamicRoutes: [],
}),
getters: {
getRoutes(): any[] {
return this.routes
},
getDynamicRoutes(): any[] {
return this.dynamicRoutes
}
},
actions: {
async getMenus() {
try {
const { data } = await getUserRoutes({})
this.dynamicRoutes = data
// 组件路由
const addRouter = assembleRouter(this.dynamicRoutes)
// 动态添加菜单
addRouter.forEach((ts: any) => {
router.addRoute(ts)
})
} catch (err) {
return Promise.reject(err)
}
},
clearMenus() {
this.dynamicRoutes.length = 0
}
}
})

export const usePermissionStoreWithOut = () => {
return usePermissionStore(store)
}

Sidebar/index.vue中渲染菜单,这里可以使用
(1)router.getRoutes()
(2)router.options.routes 和d ynamicRoutes 的组合来展示侧栏菜单
但是 getRoutes 中携带的数据不够,这里需要自定义菜单的部分内容,所以还是采用获取的后端原始数据的 dynamicRoutes。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<template>
<el-scrollbar wrap-class="scrollbar-wrapper">
<el-menu>
<sidebar-item
v-for="menu in menuList"
:key="menu.path"
:item="menu"
/>
</el-menu>
</el-scrollbar>
</template>

<script setup lang="ts">
const menuList = computed(() => {
// return router.getRoutes()
const routes = router.options.routes || []
const menus = permissionStore.dynamicRoutes || []
return routes.concat(menus)
})
</script>

子系统permission.ts路由守卫中中判断 token。
若有,执行获取动态菜单的操作,并且在获取成功之后进行过滤,如果当前去往的地址在白名单内,则放行;反之,跳转 404 页面。
若无 token,则跳转 UMS 首页。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
import router from './router'
import { useUserStore } from '@/store/modules/user'
import { usePermissionStore } from '@/store/modules/permission'

router.beforeEach(async (to, from, next) => {
const userStore = useUserStore()
const token = userStore.getToken

if (token) {
const permissionStore = usePermissionStore()
const { dynamicRoutes, getMenus } = permissionStore
if (dynamicRoutes.length === 0) {
try {
// 调用接口获取动态路由菜单
await getMenus()
const allowList = router.getRoutes()
const userP = allowList.filter((item) => item.path == to.path)
if (userP.length > 0) {
// 访问路径在白名单中
next({ ...to, replace: true })
} else {
next({ path: '/404' })
}
} catch (err) {
console.log(err)
}
} else { // 动态路由已获取
next()
}
} else { // 无token,跳转ums首页
window.location.href = window.location.reload()
})

同时在service.ts中也需要设置若 token 失效(请求中后端返回 401),跳转系统首页。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
service.interceptors.response.use(
(response: AxiosResponse<any>) => {
const { config, status, data } = response
if (config.responseType === 'blob') {
return response // 如果是文件流,直接过
} else if (status === result_code) {
return data
} else {
ElMessage.error(data.message)
}
},
(error: AxiosError) => {
console.log('error=', error)
if (error && String(error).indexOf('401') > -1) {
// Unauthorized
removeToken()
window.location.reload()
} else if (error && String(error).indexOf('403') > -1) {
ElMessage.error('当前没有操作权限,请联系管理员。')
} else {
ElMessage.error(error.message)
return Promise.reject(error)
}
}
)

UMS系统

这里设置白名单,无 token 状态下白名单路由放行。
casdoor 登录成功跳转页(/cb)在白名单中,用作系统登录中转(使用 casdoor SDK 登录),成功后跳转首页。
同理,若在子系统已清除 token 情况下,跳转 UMS 首页(/ums),则通过首页中转后间接跳转登录页(/login)。
ums中的路由守卫如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import router from './router'
import { useUserStore } from '@/store/modules/user'

const whiteList = ['/login', '/cb']

router.beforeEach(async (to, from, next) => {
const userStore = useUserStore()
const token = userStore.getToken
if (token) {
if (to.path === '/login') {
next({ path: '/' })
} else {
next()
}
} else {
if (whiteList.indexOf(to.path) !== -1) {
next()
} else {
next({ path: '/login' })
}
}
})