20260904232445

This commit is contained in:
oneao committed 2026-09-04 23:24:45 +08:00
1 parent 4e167df0f0
commit 1b01a31252
19 files changed
+1099 -1013

No files matched your search

+21 -110
View File
@@ -1,122 +1,22 @@
import { createRouter, createWebHistory } from 'vue-router'
import NProgress from 'nprogress'
import 'nprogress/nprogress.css'
import DefaultLayout from '@/layouts/DefaultLayout.vue'
import { routes } from './routes'
import { useAuthStore } from '@/stores/auth'
import { usePermissionStore } from '@/stores/permissions'
import { useMenuStore } from '@/stores/menus'
import { useI18nStore } from '@/stores/i18n'
NProgress.configure({ showSpinner: false })
/**
* 路由实例。
* 路由表本身在 ./routes.js(菜单管理的「路由地址」下拉也要读它),
* 本文件只保留实例创建与全局守卫,避免页面组件反向依赖守卫逻辑。
*/
const router = createRouter({
history: createWebHistory(),
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: '/test/list',
name: 'test-list',
component: () => import('@/views/test/TestPage.vue'),
meta: { title: '测试-列表', componentName: 'TestPage' },
},
{
path: '/test/form',
name: 'test-form',
component: () => import('@/views/test/TestPage.vue'),
meta: { title: '测试-表单', componentName: 'TestPage' },
},
{
path: '/test/detail',
name: 'test-detail',
component: () => import('@/views/test/TestPage.vue'),
meta: { title: '测试-详情', componentName: 'TestPage' },
},
{
path: '/test/chart',
name: 'test-chart',
component: () => import('@/views/test/TestPage.vue'),
meta: { title: '测试-图表', componentName: 'TestPage' },
},
{
path: '/test/table',
name: 'test-table',
component: () => import('@/views/test/TestPage.vue'),
meta: { title: '测试-表格', componentName: 'TestPage' },
},
{
path: '/test/settings',
name: 'test-settings',
component: () => import('@/views/test/TestPage.vue'),
meta: { title: '测试-设置', componentName: 'TestPage' },
},
// 组件库演练场:纳入业务布局(带侧栏 / 顶栏 / 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 },
},
// 模块管理:元数据驱动的模块配置中心(先静态路由,权限/动态菜单后续接入)
{
path: '/module/module-management',
name: 'module-module-management',
component: () => import('@/views/module/module-management/index.vue'),
meta: { title: '模块管理', componentName: 'ModuleManagement' },
},
// 菜单管理:占位页面,待实现
{
path: '/module/menu-management',
name: 'module-menu-management',
component: () => import('@/views/module/menu-management/index.vue'),
meta: { title: '菜单管理', componentName: 'MenuManagement' },
},
// 多语言类型:系统支持的语言清单维护(s_i18n_type)
{
path: '/module/i18n-type',
name: 'module-i18n-type',
component: () => import('@/views/module/i18n-type/index.vue'),
meta: { title: '多语言类型', componentName: 'I18nTypeManagement' },
},
// 业务页面后续在此追加,配置 meta.moduleId 后自动走权限校验
],
},
{
path: '/:pathMatch(.*)*',
redirect: '/dashboard',
},
],
routes,
})
// 登录守卫:未登录访问受保护页跳 /login?redirect=xxx;已登录访问 /login 跳 /dashboard
@@ -135,10 +35,14 @@ router.beforeEach(async (to) => {
return { path: '/login', query: { redirect: to.fullPath } }
}
// 统一在路由守卫加载权限:首屏(/dashboard)与业务页都在挂载前 await 完成,
// 统一在路由守卫加载权限与动态菜单:首屏(/dashboard)与业务页都在挂载前 await 完成,
// 菜单一次性到位、不会“跳出”;load 内部按主体去重,重复进入同一主体不重复请求
const permissionStore = usePermissionStore()
await permissionStore.load().catch(() => {})
const menuStore = useMenuStore()
await Promise.all([
permissionStore.load().catch(() => {}),
menuStore.load().catch(() => {}),
])
// 业务数据多语言配置(s_i18n_type 语言清单):随权限一并加载,
// 供菜单/模块等多语言编辑读取;内部去重、失败不阻断页面
@@ -149,6 +53,13 @@ router.beforeEach(async (to) => {
if (moduleId && !permissionStore.canAccess(moduleId)) {
return '/dashboard'
}
// 模块中心(/module/*)是系统管理员的专用页面:非 g3soft 一律拦回工作台。
// 这是路由层的特殊判断,与菜单树无关(菜单隐藏与路由放行由同一管理员判定决定),
// 避免「菜单上看不见、手输地址却能进」的越权。
if (to.path.startsWith('/module/') && !permissionStore.isG3soft) {
return '/dashboard'
}
}
})
+151
View File
@@ -0,0 +1,151 @@
/**
* 静态路由表(唯一定义处)。
*
* 单独拆出本文件的原因:「菜单管理」的「路由地址」需要从这里派生可选页面路由,
* 若路由表仍写在 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 },
},
// 模块中心:元数据驱动的配置页集合
// 分组节点(见上方「路由分组」注释:不写 component,子页写相对路径)
{
path: '/module',
redirect: '/module/module-management',
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'])
/**
* 收集可作为菜单入口的页面路由(递归展开布局容器与分组节点)。
* 过滤规则:
* - 含 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 || []) {
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 同时含路径与标题,输入 "/module" 或 "模块" 都能命中。
*/
export const menuRouteOptions = collectMenuRoutes().toSorted((a, b) =>
a.value.localeCompare(b.value, 'zh-Hans-CN'),
)