Become a sponsor

概述
前端路由采用动态生成机制,从后端 /index/getMenus 接口获取菜单数据,自动转换为 Vue Router 路由。
静态路由(固定,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 文章管理
└── ...1. 用户登录 → 获取 Token
2. 路由守卫检测到 Token
3. 调用 /index/getMenus 获取菜单树
4. generator-routers.ts 将菜单树转换为路由表
5. router.addRoute() 动态注入路由
6. 渲染侧边栏菜单// 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;
});
};动态路由流程
详见上方 动态路由生成流程 章节,路由守卫中实现了该流程的第 2~6 步。
// 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 返回的菜单树:
[
{
"id": 1,
"name": "系统管理",
"path": "/system",
"icon": "SettingOutlined",
"component": "LAYOUT",
"children": [
{
"id": 11,
"name": "用户管理",
"path": "/user",
"component": "system/user/index",
"permission": "sys:user:page"
}
]
}
]