编程随笔 编程随笔
  • 前端
  • 后端
  • 星球项目
  • 开源项目
  • 海康AGV
  • 四向车
  • 工具类
  • 项目仓库

    • 部署仓库 (opens new window)
    • 代码仓库 (opens new window)
  • vuepress插件

    • 自动生成导航栏与侧边栏 (opens new window)
    • 评论系统 (opens new window)
    • 全文搜索 (opens new window)
    • 选项卡 (opens new window)
    • 自动生成sitemap (opens new window)
  • 自主开发插件

    • 批量操作frontmatter (opens new window)
    • 链接美化 (opens new window)
    • 折叠代码块 (opens new window)
    • 复制代码块 (opens new window)

liyao52033

走运时,要想到倒霉,不要得意得过了头;倒霉时,要想到走运,不必垂头丧气。心态始终保持平衡,情绪始终保持稳定,此亦长寿之道
  • 前端
  • 后端
  • 星球项目
  • 开源项目
  • 海康AGV
  • 四向车
  • 工具类
  • 项目仓库

    • 部署仓库 (opens new window)
    • 代码仓库 (opens new window)
  • vuepress插件

    • 自动生成导航栏与侧边栏 (opens new window)
    • 评论系统 (opens new window)
    • 全文搜索 (opens new window)
    • 选项卡 (opens new window)
    • 自动生成sitemap (opens new window)
  • 自主开发插件

    • 批量操作frontmatter (opens new window)
    • 链接美化 (opens new window)
    • 折叠代码块 (opens new window)
    • 复制代码块 (opens new window)
  • 知识点

  • 代码调试

  • vue2

  • vue3

    • 注意事项
    • swagger自动生成接口
    • 动态路由
      • 1. 路由文件分静态路由和动态路由
      • 2. 初始挂载静态路由
      • 3. 递归过滤有权限的异步(动态)路由
      • 4. 生成动态路由
      • 5. 调用router.addRoute(route)添加用户可访问的路由
      • 6. 完整代码
    • 整合 Element Plus
    • wangeditor使用
    • monacoEditor使用
    • 自定义404页面
    • 验证码封装
    • 联表查询SQL
    • element-plus多文件手动上传
  • react

  • typescript

  • 前端
  • vue3
华总
2023-08-24
0
0
目录

动态路由原创

# 具体实现

  1. 创建vue实例的时候将vue-router挂载,但这个时候vue-router挂载一些登录或者不用权限的公用的页面。
  2. 当用户登录后,获取用role,将role和路由表每个页面的需要的权限作比较,生成最终用户可访问的路由表。
  3. 调用router.addRoute(route)添加用户可访问的路由。
  4. 使用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. 初始挂载静态路由

router.ts

const router = createRouter({
  history: createWebHashHistory(),
  routes: constantRoutes as RouteRecordRaw[],
  // 刷新时,滚动条位置还原
  scrollBehavior: () => ({ left: 0, top: 0 }),
});
1
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

# 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

# 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

# 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

# 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

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

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
#前端
上次更新: 2024/01/14 16:27:31
swagger自动生成接口
整合 Element Plus

← swagger自动生成接口 整合 Element Plus→

最近更新
01
element-plus多文件手动上传 原创
11-03
02
TrueLicense 创建及安装证书 原创
10-25
03
手动修改迅捷配置 原创
09-03
04
安装 acme.sh 原创
08-29
05
zabbix部署 原创
08-20
更多文章>
Copyright © 2023-2024 liyao52033
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式