# Ether 项目前端路由最佳实践 ## 概述 本文档总结了 Ether 项目前端路由开发中的最佳实践,以及常见问题和解决方案。 ## 路由架构设计 ### 1. 路由分层结构 ``` 路由层级: ├── 公开路由 (Public Routes) │ ├── /login - 登录页 │ └── /no-project - 无项目提示页 │ ├── 布局路由 (Layout Route) │ └── / (Layout) │ ├── /space-node - 空间节点管理 │ ├── /mdm/projects - 项目管理 │ ├── /mdm/visitors - 访客管理 │ └── ... 其他业务页面 │ └── 404 路由 └── /* - 匹配所有未定义路由 ``` ### 2. 动态路由生成机制 ```typescript // 1. 定义组件映射表 const dynamicRouteComponents: Record Promise> = { "Mdm/projects": () => import("../views/mdm/project/index.vue"), "Mdm/visitors": () => import("../views/mdm/visitor/index.vue"), // ... }; // 2. 根据后端菜单数据生成路由 function generateRoutesFromMenus(menus: MenuInfo[]): RouteRecordRaw[] { return menus.map(menu => ({ path: menu.menuPath, // 如: "/mdm/visitors" name: menu.componentName, // 如: "Mdm/visitors" component: dynamicRouteComponents[menu.componentName], meta: { title: menu.permissionName, icon: menu.menuIcon, }, })); } // 3. 动态添加到路由表 menuRoutes.forEach(route => { router.addRoute("Layout", { path: route.path.substring(1), // 移除开头的 "/" name: route.name, component: route.component, meta: route.meta, }); }); ``` ## 常见问题与解决方案 ### 问题 1: 菜单点击后页面不跳转 **症状**: 点击侧边栏菜单,页面没有切换,URL 不变。 **根本原因**: - 菜单点击使用 `router.push({ name: key })`,但动态路由的 `name` 可能未正确注册 - 路由守卫中动态添加路由后,使用 `name` 导航可能找不到对应路由 **解决方案**: ```typescript // ❌ 错误方式:使用 name 导航 const handleMenuClick = ({ key }: { key: string }) => { router.push({ name: key }); // 可能找不到路由 }; // ✅ 正确方式:使用 path 导航 const handleMenuClick = ({ key }: { key: string }) => { const menu = findMenuByComponentName(menus, key); if (menu?.menuPath) { router.push(menu.menuPath); // 使用 path 更可靠 } }; ``` ### 问题 2: 动态路由添加后刷新 404 **症状**: 动态添加路由后可以访问,但刷新页面后显示 404。 **根本原因**: - 动态路由只在路由守卫中添加一次,刷新后需要重新添加 - 404 路由优先级问题 **解决方案**: ```typescript // 1. 确保每次路由守卫都检查并添加路由 router.beforeEach(async (to, from, next) => { if (!permissionStore.isInitialized) { await permissionStore.fetchUserPermissions(); // 重新生成并添加路由 const menuRoutes = generateRoutesFromMenus(permissionStore.menus); menuRoutes.forEach(route => { const existing = router.getRoutes().find(r => r.name === route.name); if (!existing) { router.addRoute("Layout", route); } }); // 重新导航 next({ ...to, replace: true }); return; } next(); }); // 2. 404 路由最后添加,确保优先级最低 router.addRoute({ path: "/:pathMatch(.*)*", name: "NotFound", component: () => import("../views/404/index.vue"), }); ``` ### 问题 3: 路由名称冲突 **症状**: 控制台警告路由名称已存在。 **根本原因**: - 多次添加相同 name 的路由 - 热更新时重复添加 **解决方案**: ```typescript // 添加前检查是否已存在 menuRoutes.forEach(route => { const existingRoute = router.getRoutes().find(r => r.name === route.name); if (!existingRoute) { router.addRoute("Layout", route); } }); ``` ### 问题 4: 路由路径格式不一致 **症状**: 菜单数据和路由配置的路径不匹配。 **根本原因**: - 后端返回的 `menuPath` 包含前导 `/`,但动态添加时需要移除 - 路由配置中 path 格式不统一 **解决方案**: ```typescript // 统一路径格式 const normalizePath = (path: string): string => { // 确保以 / 开头 if (!path.startsWith('/')) { path = '/' + path; } // 移除尾部 / return path.replace(/\/$/, '') || '/'; }; // 动态添加时移除前导 / router.addRoute("Layout", { path: route.path.substring(1), // "/mdm/visitors" -> "mdm/visitors" ... }); ``` ## 数据流设计 ### 菜单数据结构 ```typescript interface MenuInfo { id: string; permissionCode: string; // 权限编码: "mdm:visitor:menu" permissionName: string; // 显示名称: "访客管理" menuPath: string; // 路由路径: "/mdm/visitors" menuIcon?: string; // 图标: "UserAddOutlined" componentName: string; // 组件名: "Mdm/visitors" parentId?: string; // 父菜单ID sortOrder: number; // 排序 children?: MenuInfo[]; // 子菜单 } ``` ### 关键映射关系 | 后端字段 | 前端用途 | 示例 | |---------|---------|------| | `componentName` | 路由 name / 菜单 key | `Mdm/visitors` | | `menuPath` | 路由 path | `/mdm/visitors` | | `permissionName` | 页面标题 / 菜单标签 | `访客管理` | | `menuIcon` | 菜单图标 | `UserAddOutlined` | ## 开发规范 ### 1. 新增页面步骤 1. **创建页面组件**: `src/views/{module}/{page}/index.vue` 2. **注册路由映射**: 在 `src/router/index.ts` 的 `dynamicRouteComponents` 中添加 3. **配置后端权限**: 在数据库中添加菜单和权限数据 4. **验证路由**: 确保 `componentName` 和 `menuPath` 匹配 ### 2. 路由配置检查清单 - [ ] `componentName` 在 `dynamicRouteComponents` 中有对应映射 - [ ] `menuPath` 格式统一(以 `/` 开头) - [ ] 路由 `name` 唯一,不与其他路由冲突 - [ ] 动态路由添加前检查是否已存在 ### 3. 调试技巧 ```typescript // 查看所有已注册路由 console.log(router.getRoutes()); // 检查特定路由是否存在 const route = router.getRoutes().find(r => r.name === 'Mdm/visitors'); console.log(route); // 监听路由变化 router.beforeEach((to, from) => { console.log('Navigating from', from.path, 'to', to.path); }); ``` ## 相关文件 - `src/router/index.ts` - 路由配置和动态路由生成 - `src/views/layout/index.vue` - 布局组件和菜单处理 - `src/stores/permission.ts` - 权限状态和菜单数据 ## 更新记录 | 日期 | 更新内容 | 作者 | |------|---------|------| | 2026-02-10 | 初始版本,总结路由问题和解决方案 | AI Assistant |