224 lines
6.7 KiB
Markdown
224 lines
6.7 KiB
Markdown
# 9.5 路由与动态菜单
|
||
|
||
:::tip 概述
|
||
前端路由采用动态生成机制,从后端 `/index/getMenus` 接口获取菜单数据,自动转换为 Vue Router 路由。
|
||
:::
|
||
|
||
## 路由架构
|
||
|
||
```text
|
||
静态路由(固定,src/router/index.ts + src/router/base.ts)
|
||
├── /login 登录页(LoginRoute)
|
||
├── / 根路径,重定向到首页(RootRoute)
|
||
├── /redirect/:path 重定向路由(RedirectRoute)
|
||
├── /:path(.*)* 404 错误页(ErrorPageRoute,动态路由加载后注入)
|
||
├── /monitorJob/log 关于项目(AboutPageRoute,动态路由加载后注入)
|
||
└── /setting/profile 个人设置(SettingPageRoute,动态路由加载后注入)
|
||
|
||
动态路由(从后端菜单生成)
|
||
├── /dashboard 仪表盘
|
||
├── /system/user 用户管理
|
||
├── /system/role 角色管理
|
||
├── /content/article 文章管理
|
||
└── ...
|
||
```
|
||
|
||
## 动态路由生成流程
|
||
|
||
```text
|
||
1. 用户登录 → 获取 Token
|
||
2. 路由守卫检测到 Token
|
||
3. 调用 /index/getMenus 获取菜单树
|
||
4. generator-routers.ts 将菜单树转换为路由表
|
||
5. router.addRoute() 动态注入路由
|
||
6. 渲染侧边栏菜单
|
||
```
|
||
|
||
## 核心代码
|
||
|
||
```typescript
|
||
// src/router/generator-routers.ts
|
||
|
||
/**
|
||
* 动态生成菜单
|
||
* 调用后端 /index/getMenus 接口获取菜单数据
|
||
*/
|
||
export const generatorDynamicRouter = (): Promise<RouteRecordRaw[]> => {
|
||
return new Promise((resolve, reject) => {
|
||
adminMenus()
|
||
.then((result) => {
|
||
const routeList = routerGenerator(result);
|
||
asyncImportRoute(routeList);
|
||
resolve(routeList);
|
||
})
|
||
.catch((err) => reject(err));
|
||
});
|
||
};
|
||
|
||
/**
|
||
* 格式化后端菜单为路由结构
|
||
*/
|
||
export const routerGenerator = (routerMap): any[] => {
|
||
return routerMap.map((item) => {
|
||
const currentRouter = {
|
||
path: item.path,
|
||
name: item.path.replaceAll('/', ''),
|
||
component: item.component, // 如 'system/user/index'
|
||
meta: {
|
||
title: item.name,
|
||
icon: item.icon,
|
||
permissions: item.permission,
|
||
},
|
||
};
|
||
|
||
// 递归处理子菜单
|
||
if (item.children?.length > 0) {
|
||
currentRouter.children = routerGenerator(item.children);
|
||
}
|
||
|
||
return currentRouter;
|
||
});
|
||
};
|
||
```
|
||
|
||
## 路由守卫
|
||
|
||
:::tip 动态路由流程
|
||
详见上方 [动态路由生成流程](#动态路由生成流程) 章节,路由守卫中实现了该流程的第 2~6 步。
|
||
:::
|
||
|
||
```typescript
|
||
// src/router/router-guards.ts
|
||
|
||
const LOGIN_PATH = PageEnum.BASE_LOGIN;
|
||
const whitePathList = [LOGIN_PATH]; // no redirect whitelist
|
||
|
||
export function createRouterGuards(router: Router) {
|
||
const userStore = useUserStoreWidthOut();
|
||
const asyncRouteStore = useAsyncRouteStoreWidthOut();
|
||
|
||
router.beforeEach(async (to, from, next) => {
|
||
// 1. Token 可能来自 URL 查询参数 ?token=xxx(SSO 等场景)
|
||
if (to.query && 'token' in to.query) {
|
||
const tokenVal = String(to.query.token);
|
||
storage.set(ACCESS_TOKEN, tokenVal);
|
||
userStore.setToken(tokenVal);
|
||
}
|
||
|
||
NProgress.start();
|
||
|
||
// 2. 白名单且未登录 → 直接放行
|
||
const token = storage.get(ACCESS_TOKEN);
|
||
if (whitePathList.includes(to.path as PageEnum) && !token) {
|
||
next();
|
||
return;
|
||
}
|
||
|
||
// 3. 未登录处理
|
||
if (!token) {
|
||
// 路由 meta.ignoreAuth = true 时无需登录也可访问
|
||
if (to.meta.ignoreAuth) {
|
||
next();
|
||
return;
|
||
}
|
||
// 重定向到登录页,携带当前路径作为 redirect 参数
|
||
const redirectData = { path: LOGIN_PATH, replace: true };
|
||
if (to.path) {
|
||
redirectData.query = { redirect: to.path };
|
||
}
|
||
next(redirectData);
|
||
return;
|
||
}
|
||
|
||
// 4. 已登录 + 动态路由已加载 → 直接放行
|
||
if (asyncRouteStore.getIsDynamicAddedRoute) {
|
||
next();
|
||
return;
|
||
}
|
||
|
||
// 5. 首次加载:生成动态路由
|
||
asyncRouteStore.setDynamicAddedRoute(true);
|
||
const userInfo = await userStore.GetInfo();
|
||
const routes = await asyncRouteStore.generateRoutes(userInfo);
|
||
await useOssConfigStore().setOssConfig();
|
||
|
||
// 动态添加可访问路由表
|
||
routes.forEach((item) => {
|
||
router.addRoute(item as unknown as RouteRecordRaw);
|
||
});
|
||
|
||
// 添加 404 路由(避免重复注册)
|
||
const isErrorPage = router.getRoutes().findIndex((item) => item.name === ErrorPageRoute.name);
|
||
if (isErrorPage === -1) {
|
||
router.addRoute(ErrorPageRoute as unknown as RouteRecordRaw);
|
||
}
|
||
router.addRoute(AboutPageRoute as unknown as RouteRecordRaw);
|
||
router.addRoute(SettingPageRoute as unknown as RouteRecordRaw);
|
||
|
||
// 保存第一个路由路径,用于默认跳转
|
||
const firstRoutePath = findFirstRoutePath(routes);
|
||
storage.set(FIRST_ROUTE, firstRoutePath);
|
||
|
||
// 确定重定向目标
|
||
const formPath =
|
||
findTreeByPath(routes, from.query.redirect).length > 0 ? from.query.redirect : to.path;
|
||
const redirect = decodeURIComponent(formPath as string);
|
||
const nextData = to.path === redirect ? { ...to, replace: true } : { path: redirect };
|
||
|
||
NProgress.done();
|
||
if (nextData.path === PageEnum.BASE_HOME || (token && to.path === '/login')) {
|
||
next(firstRoutePath);
|
||
return;
|
||
}
|
||
next(nextData);
|
||
});
|
||
|
||
// 6. afterEach:管理 keep-alive 缓存 + 页面标题
|
||
router.afterEach((to, _, failure) => {
|
||
document.title =
|
||
(to?.meta?.documentTitle ? to?.meta?.documentTitle : (to?.meta?.title as string)) ||
|
||
document.title;
|
||
if (isNavigationFailure(failure)) {
|
||
// navigation failed
|
||
}
|
||
const asyncRouteStore = useAsyncRouteStoreWidthOut();
|
||
const keepAliveComponents = asyncRouteStore.keepAliveComponents;
|
||
const currentComName: any = to.matched.find((item) => item.name == to.name)?.name;
|
||
if (currentComName && !keepAliveComponents.includes(currentComName) && to.meta?.keepAlive) {
|
||
keepAliveComponents.push(currentComName);
|
||
} else if (!to.meta?.keepAlive || to.name == 'Redirect') {
|
||
const index = asyncRouteStore.keepAliveComponents.findIndex((name) => name == currentComName);
|
||
if (index != -1) {
|
||
keepAliveComponents.splice(index, 1);
|
||
}
|
||
}
|
||
asyncRouteStore.setKeepAliveComponents(keepAliveComponents);
|
||
NProgress.done();
|
||
});
|
||
}
|
||
```
|
||
|
||
## 后端菜单数据结构
|
||
|
||
后端 `/index/getMenus` 返回的菜单树:
|
||
|
||
```json
|
||
[
|
||
{
|
||
"id": 1,
|
||
"name": "系统管理",
|
||
"path": "/system",
|
||
"icon": "SettingOutlined",
|
||
"component": "LAYOUT",
|
||
"children": [
|
||
{
|
||
"id": 11,
|
||
"name": "用户管理",
|
||
"path": "/user",
|
||
"component": "system/user/index",
|
||
"permission": "sys:user:page"
|
||
}
|
||
]
|
||
}
|
||
]
|
||
``` |