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