签入版本

This commit is contained in:
小蚂蚁云
2026-10-25 17:24:27 +08:00
parent 48911cc08f
commit fdbeae092a
1010 changed files with 30682 additions and 95274 deletions
+42 -204
View File
@@ -1,224 +1,62 @@
# 9.5 路由与动态菜单
:::tip 概述
前端路由采用动态生成机制,从后端 `/index/getMenus` 接口获取菜单数据,自动转换为 Vue Router 路由。
路由分两类:**静态路由**(constant,登录页/异常页等始终存在)与**动态路由**(登录后由菜单 API 生成注入)。守卫负责时序编排:token 校验 → 拉取用户信息 → 生成路由 → 放行。
:::
## 路由架构
## 目录结构
```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 文章管理
└── ...
router/
├── constant.ts 静态常量路由
├── base.ts 基础路由(错误页/关于/设置等固定页)
├── generator-routers.ts 动态路由生成器(菜单树 → 路由表)
├── router-guards.ts 全局守卫
├── router-icons.ts 图标映射
├── menus/ 菜单相关辅助
└── types.ts 类型
```
## 动态路由生成流程
## 动态路由流水线
```text
1. 用户登录 → 获取 Token
2. 路由守卫检测到 Token
3. 调用 /index/getMenus 获取菜单树
4. generator-routers.ts 将菜单树转换为路由表
5. router.addRoute() 动态注入路由
6. 渲染侧边栏菜单
登录成功存 token
↓
路由守卫 beforeEach:
无 token → 白名单放行 / 其余跳登录(带 redirect)
有 token 且未生成 → GetInfo(用户信息)→ generateRoutes(消费 GetMenus 菜单树)
↓
菜单行映射:path → 路由路径,component → views 下组件(懒加载)
↓
router.addRoute 逐条注入 → 标记 isDynamicAddedRoute
↓
侧边栏按路由表渲染(layout)
```
## 核心代码
## 菜单到组件的映射
```typescript
// src/router/generator-routers.ts
后端菜单表的 `component` 字段存 views 下相对路径(如 `system/user/index`),生成器将其转为 `() => import('@/views/system/user/index')` 懒加载——**新增页面只需在 views 放组件 + 菜单表配路径**,不改路由代码。
/**
* 动态生成菜单
* 调用后端 /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,
},
};
| 机制 | 说明 |
|---|---|
| 白名单 | 登录页免 token |
| ignoreAuth | meta 声明的免登录页 |
| URL 带 token | `?token=xxx` 自动落 storage(单点登录跳转场景) |
| redirect | 未登录跳登录后回跳原路径 |
| NProgress | 导航进度条 |
| 首路由 | findFirstRoutePath 定位登录后首个落地页 |
// 递归处理子菜单
if (item.children?.length > 0) {
currentRouter.children = routerGenerator(item.children);
}
## 排查
return currentRouter;
});
};
```
| 现象 | 检查 |
|---|---|
| 登录后白屏 | 菜单 component 路径与 views 文件不符(懒加载失败) |
| 刷新 404 | 部署服务器 history 模式回退配置(try_files) |
| 菜单少了 | 角色授权问题(看 GetMenus 返回) |
## 路由守卫
## 相关
:::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"
}
]
}
]
```
- [6.3 菜单管理](/zh/modules/menu.html)(数据源头)
- [9.8 登录流程](/zh/frontend/login.html)