动态路由原创
# 具体实现
- 创建vue实例的时候将vue-router挂载,但这个时候vue-router挂载一些登录或者不用权限的公用的页面。
- 当用户登录后,获取用role,将role和路由表每个页面的需要的权限作比较,生成最终用户可访问的路由表。
- 调用router.addRoute(route)添加用户可访问的路由。
- 使用pinia管理路由表,根据vuex中可访问的路由渲染侧边栏组件。
# 1. 路由文件分静态路由和动态路由
export const constantRoutes: RouteRecordRaw[] = [
{
path: "/redirect",
component: Layout,
meta: { hidden: true },
children: [
{
path: "/redirect/:path(.*)",
component: () => import("@/views/redirect/index.vue"),
},
],
},
{
path: "/login",
component: () => import("@/views/login/index.vue"),
meta: { hidden: true },
},
{
path: "/",
component: Layout,
redirect: "/dashboard",
children: [
{
path: "dashboard",
component: () => import("@/views/dashboard/index.vue"),
name: "Dashboard",
meta: { title: "dashboard", icon: "homepage", affix: true },
},
{
path: "401",
component: () => import("@/views/error-page/401.vue"),
meta: { hidden: true },
},
{
path: "404",
component: () => import("@/views/error-page/404.vue"),
meta: { hidden: true },
},
],
}
];
// 动态路由
export const asyncRoute: RouteRecordRaw[] = [
// 多级嵌套路由
{
path: '/nested',
component: Layout,
redirect: '/nested/level1/level2',
name: 'Nested',
meta: {title: '多级菜单', icon: 'nested', userRole: ['user']},
children: [
{
path: 'level1',
component: () => import('@/views/nested/level1/index.vue'),
name: 'Level1',
meta: {title: '菜单一级',userRole: ['user']},
redirect: '/nested/level1/level2',
children: [
{
path: 'level2',
component: () => import('@/views/nested/level1/level2/index.vue'),
name: 'Level2',
meta: {title: '菜单二级',userRole: ['user']},
redirect: '/nested/level1/level2/level3',
children: [
{
path: 'level3-1',
component: () => import('@/views/nested/level1/level2/level3/index1.vue'),
name: 'Level3-1',
meta: {title: '菜单三级-1', userRole: ["admin"]}
},
{
path: 'level3-2',
component: () => import('@/views/nested/level1/level2/level3/index2.vue'),
name: 'Level3-2',
meta: {title: '菜单三级-2', userRole: ["admin"]}
}
]
}
]
},
]
},
// 外部链接
{
path: '/external-link',
component: Layout,
name: "blogs",
meta: {title: '外部链接', icon: 'nested', userRole: ['user']},
redirect: 'https://www.cnblogs.com/haoxianrui/',
children: [
{
path: 'https://www.cnblogs.com/haoxianrui/',
name: 'link',
meta: { title: '有来技术官方博客', icon: 'link', userRole: ['user'] },
redirect: 'https://www.cnblogs.com/haoxianrui/',
}
]
}
]
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
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
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
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
# 2. 初始挂载静态路由
router.ts
const router = createRouter({
history: createWebHashHistory(),
routes: constantRoutes as RouteRecordRaw[],
// 刷新时,滚动条位置还原
scrollBehavior: () => ({ left: 0, top: 0 }),
});
1
2
3
4
5
6
2
3
4
5
6
store/permission.ts
export const usePermissionStore = defineStore("permission", () => {
// state
const routes = ref<RouteRecordRaw[]>([]);
// actions
function setRoutes(newRoutes: RouteRecordRaw[]) {
routes.value = constantRoutes.concat(newRoutes);
}
}
1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
# 3. 递归过滤有权限的异步(动态)路由
const hasPermission = (userRole: string[], route: RouteRecordRaw) => {
if (route.meta && route.meta.userRole) {
// 角色【超级管理员】拥有所有权限,忽略校验
if (userRole.includes("admin")) {
return true;
}
return userRole.some((userRole) => {
if (route.meta?.userRole !== undefined) {
return (route.meta.userRole as string[]).includes(userRole);
}
});
}
return false;
};
/**
* 递归过滤有权限的异步(动态)路由
*
* @param routes 接口返回的异步(动态)路由
* @param userRole
* @returns 返回用户有权限的异步(动态)路由
*/
const filterAsyncRoutes = (routes: RouteRecordRaw[], userRole: string[]) => {
const asyncRoutes: RouteRecordRaw[] = [];
routes.forEach((route) => {
const tmpRoute = { ...route }; // ES6扩展运算符复制新对象
// 判断用户(角色)是否有该路由的访问权限
if (hasPermission(userRole, tmpRoute)) {
if (tmpRoute.component?.toString() == "Layout") {
tmpRoute.component = Layout;
} else {
const component = modules[`../../views/${tmpRoute.component}.vue`];
if (component) {
tmpRoute.component = component;
}
}
if (tmpRoute.children) {
tmpRoute.children = filterAsyncRoutes(tmpRoute.children, userRole);
}
asyncRoutes.push(tmpRoute);
}
});
return asyncRoutes;
};
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
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
# 4. 生成动态路由
export const usePermissionStore = defineStore("permission", () => {
// state
const routes = ref<RouteRecordRaw[]>([]);
// actions
function setRoutes(newRoutes: RouteRecordRaw[]) {
routes.value = constantRoutes.concat(newRoutes);
}
/**
* 生成动态路由
*
* @returns
* @param userRole
*/
function generateRoutes(userRole: string[]) {
return new Promise<RouteRecordRaw[]>((resolve) => {
// 接口获取所有路由
const accessedRoutes = filterAsyncRoutes(asyncRoute, userRole)
setRoutes(accessedRoutes)
// todo 必不可少
resolve(accessedRoutes)
});
}
return { routes, setRoutes, generateRoutes };
});
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
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
# 5. 调用router.addRoute(route)添加用户可访问的路由
src/permission
try {
const { userRole } = await userStore.getInfo(); //获取当前登录用户
const accessRoutes = await permissionStore.generateRoutes(userRole); 获取可访问的路由
//添加路由到页面
accessRoutes.forEach((route) => {
router.addRoute(route);
});
// todo 必不可少
next({ ...to, replace: true });
} catch (error) {
// 移除 token 并跳转登录页
await userStore.resetToken();
next(`/login?redirect=${to.path}`);
NProgress.done();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 6. 完整代码
store/permission.ts
import { RouteRecordRaw } from "vue-router";
import { defineStore } from "pinia";
import { asyncRoute, constantRoutes } from "@/router";
import { store } from "@/store";
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 userRole
* @param route 路由
* @returns
*/
const hasPermission = (userRole: string[], route: RouteRecordRaw) => {
if (route.meta && route.meta.userRole) {
// 角色【超级管理员】拥有所有权限,忽略校验
if (userRole.includes("admin")) {
return true;
}
return userRole.some((userRole) => {
if (route.meta?.userRole !== undefined) {
return (route.meta.userRole as string[]).includes(userRole);
}
});
}
return false;
};
/**
* 递归过滤有权限的异步(动态)路由
*
* @param routes 接口返回的异步(动态)路由
* @param userRole
* @returns 返回用户有权限的异步(动态)路由
*/
const filterAsyncRoutes = (routes: RouteRecordRaw[], userRole: string[]) => {
const asyncRoutes: RouteRecordRaw[] = [];
routes.forEach((route) => {
const tmpRoute = { ...route }; // ES6扩展运算符复制新对象
// 判断用户(角色)是否有该路由的访问权限
if (hasPermission(userRole, tmpRoute)) {
if (tmpRoute.component?.toString() == "Layout") {
tmpRoute.component = Layout;
} else {
const component = modules[`../../views/${tmpRoute.component}.vue`];
if (component) {
tmpRoute.component = component;
}
}
if (tmpRoute.children) {
tmpRoute.children = filterAsyncRoutes(tmpRoute.children, userRole);
}
asyncRoutes.push(tmpRoute);
}
});
return asyncRoutes;
};
// setup
export const usePermissionStore = defineStore("permission", () => {
// state
const routes = ref<RouteRecordRaw[]>([]);
// actions
function setRoutes(newRoutes: RouteRecordRaw[]) {
routes.value = constantRoutes.concat(newRoutes);
}
/**
* 生成动态路由
*
* @returns
* @param userRole
*/
function generateRoutes(userRole: string[]) {
return new Promise<RouteRecordRaw[]>((resolve) => {
// let accessedRoutes: RouteRecordRaw[]
// 接口获取所有路由
// if (userRole.includes('admin')){
// accessedRoutes = asyncRoute
// } else{
// accessedRoutes = filterAsyncRoutes(asyncRoute, userRole)
// }
const accessedRoutes = filterAsyncRoutes(asyncRoute, userRole)
setRoutes(accessedRoutes)
// todo 必不可少
resolve(accessedRoutes)
});
}
return { routes, setRoutes, generateRoutes };
});
// 非setup
export function usePermissionStoreHook() {
return usePermissionStore(store);
}
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
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
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
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
src/permission
import router from "@/router";
import { useUserStoreHook } from "@/store/modules/user";
import { usePermissionStoreHook } from "@/store/modules/permission";
import NProgress from "nprogress";
import "nprogress/nprogress.css";
NProgress.configure({ showSpinner: false }); // 进度条
const permissionStore = usePermissionStoreHook();
// 白名单路由
const whiteList = ["/login"];
router.beforeEach(async (to, from, next) => {
NProgress.start();
const hasToken = localStorage.getItem("accessToken");
const expirationDate = localStorage.getItem('expirationDate')
if (hasToken && expirationDate) {
if (to.path === "/login") {
// 如果已登录,跳转首页
next({ path: "/" });
NProgress.done();
} else {
const userStore = useUserStoreHook();
const hasRoles = userStore.userRole && userStore.userRole.length > 0;
if (hasRoles) {
// 未匹配到任何路由,跳转404
if (to.matched.length === 0) {
from.name ? next({ name: from.name }) : next("/404");
} else {
next();
}
} else {
try {
const { userRole } = await userStore.getInfo();
const accessRoutes = await permissionStore.generateRoutes(userRole);
accessRoutes.forEach((route) => {
router.addRoute(route);
});
// todo 必不可少
next({ ...to, replace: true });
} catch (error) {
// 移除 token 并跳转登录页
await userStore.resetToken();
next(`/login?redirect=${to.path}`);
NProgress.done();
}
}
}
} else {
// 未登录可以访问白名单页面
if (whiteList.indexOf(to.path) !== -1) {
next();
} else {
next(`/login?redirect=${to.path}`);
NProgress.done();
}
}
});
router.afterEach(() => {
NProgress.done();
});
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
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
router.js
import { createRouter, createWebHistory, RouteRecordRaw } from "vue-router";
export const Layout = () => import("@/layout/index.vue");
// 静态路由
export const constantRoutes: RouteRecordRaw[] = [
{
path: "/login",
component: () => import("@/views/login/index.vue"),
meta: { hidden: true },
},
{
path: "/",
component: Layout,
redirect: "/dashboard",
children: [
{
path: "dashboard",
component: () => import("@/views/dashboard/index.vue"),
name: "Dashboard",
meta: { title: "dashboard", icon: "homepage", affix: true },
},
{
path: "401",
component: () => import("@/views/error-page/401.vue"),
meta: { hidden: true },
},
{
path: "404",
component: () => import("@/views/error-page/404.vue"),
meta: { hidden: true },
},
],
},
{
path: "/redirect",
component: Layout,
meta: { hidden: true },
children: [
{
path: "/redirect/:path(.*)",
component: () => import("@/views/redirect/index.vue"),
},
],
},
];
export const asyncRoute: RouteRecordRaw[] = [
// 多级嵌套路由
{
path: '/nested',
component: Layout,
redirect: '/nested/level1/level2',
name: 'Nested',
meta: {title: '多级菜单', icon: 'nested', userRole: ['admin']},
children: [
{
path: 'level1',
component: () => import('@/views/demo/multi-level/level1.vue'),
name: 'Level1',
meta: {title: '菜单一级',userRole: ['user']},
redirect: '/nested/level1/level2',
children: [
{
path: 'level2',
component: () => import('@/views/demo/multi-level/children/level2.vue'),
name: 'Level2',
meta: {title: '菜单二级',userRole: ['user']},
redirect: '/nested/level1/level2/level3',
children: [
{
path: 'level3-1',
component: () => import('@/views/demo/multi-level/children/children/level3-1.vue'),
name: 'Level3-1',
meta: {title: '菜单三级-1', userRole: ["admin"]}
},
{
path: 'level3-2',
component: () => import('@/views/demo/multi-level/children/children/level3-2.vue'),
name: 'Level3-2',
meta: {title: '菜单三级-2', userRole: ["admin"]}
}
]
}
]
},
]
},
{
path: "/captcha",
component: Layout,
redirect: "/captcha/test",
meta: {title: '路由测试', icon: 'nested', userRole: ['user']},
children: [
{
path: "test",
component: () => import("@/views/demo/api-doc.vue"),
name: "Test",
meta: { title: "验证码测试",userRole: ['user'] },
},
{
path: "doc",
component: () => import('@/views/test/index.vue'),
name: "Doc",
meta: { title: "接口文档",userRole: ['user'] },
},
{
path: "editor",
component: () => import("@/views/test/index.vue"),
name: "Editor",
meta: { title: "文档编辑器",userRole: ['user'] },
},
]
},
// 外部链接
{
path: '/external-link',
component: Layout,
name: "blogs",
meta: {title: '外部链接', icon: 'nested', userRole: ['user']},
redirect: 'https://www.cnblogs.com/haoxianrui/',
children: [
{
path: 'https://www.cnblogs.com/haoxianrui/',
name: 'link',
meta: { title: '有来技术官方博客', icon: 'link', userRole: ['user'] },
redirect: 'https://www.cnblogs.com/haoxianrui/',
}
]
},
{
path: "*",
redirect: '/404',
meta: { hidden: true }
},
]
/**
* 创建路由
*/
const router = createRouter({
history: createWebHistory(),
routes: constantRoutes as RouteRecordRaw[],
// 刷新时,滚动条位置还原
scrollBehavior: () => ({ left: 0, top: 0 }),
});
/**
* 重置路由
*/
export function resetRouter() {
router.replace({ path: "/login" });
}
export default router;
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
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
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
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
上次更新: 2024/01/14 16:27:31
- 01
- element-plus多文件手动上传 原创11-03
- 02
- TrueLicense 创建及安装证书 原创10-25
- 03
- 手动修改迅捷配置 原创09-03
- 04
- 安装 acme.sh 原创08-29
- 05
- zabbix部署 原创08-20