Files
easygoadmin-wiki/EasyGoAdmin_Beego_NaiveVue/zh/frontend/router.md
T
2026-10-20 21:19:44 +08:00

224 lines
6.7 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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"
}
]
}
]
```