/** * 静态路由表(唯一定义处)。 * * 单独拆出本文件的原因:「菜单管理」的「路由地址」需要从这里派生可选页面路由, * 若路由表仍写在 index.js 里,页面组件就会反向依赖「路由实例 + 守卫」模块, * 形成 页面 → router/index → 守卫 → store 的循环依赖风险。 * 约定: * - routes.js :只声明路由表与由其派生的纯数据(无副作用、可被任意模块安全 import) * - index.js :只负责创建 router 实例、注册守卫、导出默认实例 */ import DefaultLayout from '@/layouts/DefaultLayout.vue' /** * 路由表。 * 受保护页面挂在 DefaultLayout 下(layout 的 meta.requiresAuth 由守卫统一校验); * 需要按业务模块鉴权的页面,在 meta 上配 moduleId,守卫校验无权限时跳 /dashboard。 * * 路由分组(/module、/test):父节点只写 path + redirect + children,**不写 component**。 * vue-router 的 RouterView 会跳过 matched 中没有 components 的记录,子页面因此仍渲染在 * DefaultLayout 的那个 router-view 上,keep-alive 层级不变。 * 切勿给分组节点配「内部再放一个 router-view」的容器组件:那样页面会下沉一层, * 直接脱离 DefaultLayout 的 keep-alive,页面缓存失效。 * 子路由 path 写相对路径('module-management'),完整路径由父级拼接, * 调整分组前缀时只需改父节点一处。 */ export const routes = [ { path: '/login', name: 'login', component: () => import('@/views/login/index.vue'), meta: { title: '登录' }, }, { path: '/', component: DefaultLayout, redirect: '/dashboard', meta: { requiresAuth: true }, children: [ { path: '/dashboard', name: 'dashboard', component: () => import('@/views/dashboard/index.vue'), meta: { title: '工作台', componentName: 'Dashboard' }, }, // 组件库演练场:纳入业务布局(带侧栏 / 顶栏 / Tabs) { path: '/demo', name: 'demo', component: () => import('@/views/demo/index.vue'), meta: { title: '组件库', componentName: 'Demo' }, }, // 附件组件演示 { path: '/file-demo', name: 'file-demo', component: () => import('@/views/file-demo/index.vue'), meta: { title: '附件组件演示', componentName: 'FileDemo' }, }, // 详情页:同一组件按参数复用,但每个 fullPath 独立缓存、关闭即销毁 // (per-param 的命名组件由 DefaultLayout 的 getPageWrapper 按路由动态生成) { path: '/detail/:id', name: 'detail', component: () => import('@/views/detail/DetailPage.vue'), meta: { title: '详情', cacheByKey: true }, }, // 模块中心:元数据驱动的配置页集合(系统管理员专用,meta.adminOnly: // 路由守卫拦截非 g3soft,菜单管理「路由地址」下拉整体排除本组页面) // 分组节点(见上方「路由分组」注释:不写 component,子页写相对路径) { path: '/module', redirect: '/module/module-management', meta: { adminOnly: true }, children: [ // 模块管理:元数据驱动的模块配置中心(先静态路由,权限/动态菜单后续接入) { path: 'module-management', name: 'module-module-management', component: () => import('@/views/module/module-management/index.vue'), meta: { title: '模块管理', componentName: 'ModuleManagement' }, }, // 菜单管理:占位页面,待实现 { path: 'menu-management', name: 'module-menu-management', component: () => import('@/views/module/menu-management/index.vue'), meta: { title: '菜单管理', componentName: 'MenuManagement' }, }, // 多语言类型:系统支持的语言清单维护(s_i18n_type) { path: 'i18n-type', name: 'module-i18n-type', component: () => import('@/views/module/i18n-type/index.vue'), meta: { title: '多语言类型', componentName: 'I18nTypeManagement' }, }, ], }, // 业务页面后续在此追加,配置 meta.moduleId 后自动走权限校验 ], }, { path: '/:pathMatch(.*)*', redirect: '/dashboard', }, ] /** 不作为菜单入口的路由:独立布局、无需登录即可访问的页面 */ const EXCLUDED_PATHS = new Set(['/login']) /** * 收集可作为菜单入口的页面路由(递归展开布局容器与分组节点)。 * 过滤规则: * - meta.adminOnly 的路由(系统管理员专用页,如模块中心)连同子树整体排除, * 系统页面不进业务菜单,「路由地址」下拉配不出第二入口; * - 含 children 的节点(布局容器 / 分组节点)不是最终页面,只向下递归; * - 含动态段(:id)或通配(*)的路由无法作为固定菜单入口; * - 登录页等独立布局页排除在外。 * @param {Array} list 路由节点数组,默认整张路由表 * @param {string} parentPath 父节点完整路径,用于拼接分组内子路由的相对路径 * @returns {{ value: string, label: string }[]} 选项(value 为完整 path) */ export function collectMenuRoutes(list = routes, parentPath = '') { const result = [] for (const route of list || []) { // 系统管理员专用页面(meta.adminOnly):整体排除,不作为业务菜单入口 if (route.meta?.adminOnly) continue const rawPath = route.path || '' // 绝对路径原样使用;相对路径(分组节点的 children)拼到父路径上 const path = rawPath.startsWith('/') ? rawPath : joinPath(parentPath, rawPath) if (Array.isArray(route.children) && route.children.length) { result.push(...collectMenuRoutes(route.children, path)) continue } if (!path.startsWith('/')) continue if (path.includes(':') || path.includes('*')) continue if (EXCLUDED_PATHS.has(path)) continue const title = route.meta?.title result.push({ value: path, label: title ? `${path}(${title})` : path }) } return result } /** 拼接父路径与子路径(相对路径形态):'/module' + 'i18n-type' → '/module/i18n-type' */ function joinPath(parentPath, childPath) { const parent = String(parentPath || '').replace(/\/+$/, '') const child = String(childPath || '').replace(/^\/+/, '') if (!parent) return child ? `/${child}` : '' return child ? `${parent}/${child}` : parent } /** * 菜单管理「路由地址」下拉选项:按路径排序,配合 Select 的本地包含匹配(like)筛选。 * label 同时含路径与标题,输入路径片段或标题关键词都能命中。 */ export const menuRouteOptions = collectMenuRoutes().toSorted((a, b) => a.value.localeCompare(b.value, 'zh-Hans-CN'), )