Files
workspace/code/fms/fms-vue/src/router/routes.js
T
2026-09-13 23:16:08 +08:00

158 lines
6.9 KiB
JavaScript
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.
/**
* 静态路由表(唯一定义处)。
*
* 单独拆出本文件的原因:「菜单管理」的「路由地址」需要从这里派生可选页面路由,
* 若路由表仍写在 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'),
)