168 lines
5.3 KiB
TypeScript
168 lines
5.3 KiB
TypeScript
import NProgress from 'nprogress'
|
||
import 'nprogress/nprogress.css'
|
||
import { createRouter, createWebHistory, type RouteRecordRaw } from 'vue-router'
|
||
|
||
import airRoutes from './modules/air'
|
||
import authRoutes from './modules/auth'
|
||
import containerRoutes from './modules/container'
|
||
import crmRoutes from './modules/crm'
|
||
import financeRoutes from './modules/finance'
|
||
import oceanRoutes from './modules/ocean'
|
||
import systemRoutes from './modules/system'
|
||
|
||
import { APP_TITLE } from '@/constants/common'
|
||
import { menuItems } from '@/constants/menu'
|
||
import type { MenuItem } from '@/types/menu'
|
||
|
||
NProgress.configure({ showSpinner: false })
|
||
|
||
/** 由菜单常量派生首页重定向目标,保证「菜单唯一数据源」不被打破。 */
|
||
const firstMenuPath = menuItems[0]?.path ?? '/ocean'
|
||
|
||
const routes: RouteRecordRaw[] = [
|
||
// 认证域(登录等):顶层路由,与主框架平级、不套 DefaultLayout
|
||
...authRoutes,
|
||
{
|
||
path: '/',
|
||
component: () => import('@/layouts/DefaultLayout.vue'),
|
||
redirect: firstMenuPath,
|
||
children: [
|
||
...oceanRoutes,
|
||
...airRoutes,
|
||
...containerRoutes,
|
||
...financeRoutes,
|
||
...crmRoutes,
|
||
...systemRoutes,
|
||
// 403 / 500:作为主框架的子路由(保留侧栏/顶栏/页签,用户可直接切走),
|
||
// 与 fms-vue 一致(routes.js 里 /403 /500 挂在布局下)。
|
||
// 渲染崩溃的兜底不经过这里 —— 那由 App 层的 ErrorBoundary 全屏渲染。
|
||
//
|
||
// 页签 / 面包屑 / 文档标题统一用「403 / 500」状态码(与 URL 一一对应),
|
||
// 页面内的标题仍由 G3Result 按 locale 显示「无权访问 / 页面出错了」。
|
||
{
|
||
path: '/403',
|
||
name: 'forbidden',
|
||
component: () => import('@/views/fallback/Forbidden.vue'),
|
||
meta: { title: '403' },
|
||
},
|
||
{
|
||
path: '/500',
|
||
name: 'server-error',
|
||
component: () => import('@/views/fallback/ServerError.vue'),
|
||
meta: { title: '500' },
|
||
},
|
||
],
|
||
},
|
||
// 通配 404:独立布局(无外壳),放最后
|
||
{
|
||
path: '/:pathMatch(.*)*',
|
||
name: 'not-found',
|
||
component: () => import('@/views/fallback/NotFound.vue'),
|
||
meta: { title: '页面不存在' },
|
||
},
|
||
]
|
||
|
||
const router = createRouter({
|
||
history: createWebHistory(import.meta.env.BASE_URL),
|
||
routes,
|
||
scrollBehavior: () => ({ top: 0 }),
|
||
})
|
||
|
||
router.beforeEach((to) => {
|
||
NProgress.start()
|
||
|
||
const title = to.meta.title as string | undefined
|
||
document.title = title ? `${title} - ${APP_TITLE}` : APP_TITLE
|
||
|
||
// 登录校验待用户模块落地后接入;当前直接放行。
|
||
// vue-router 5 已弃用 next 回调,改用返回值:放行 return true,拦截 return false。
|
||
return true
|
||
})
|
||
|
||
router.afterEach(() => {
|
||
NProgress.done()
|
||
})
|
||
|
||
/**
|
||
* 在菜单树中按 path 查找菜单项(含子级)。
|
||
*/
|
||
export function findMenuItemByPath(path: string): MenuItem | undefined {
|
||
const walk = (items: MenuItem[]): MenuItem | undefined => {
|
||
for (const item of items) {
|
||
if (item.path === path) return item
|
||
|
||
if (item.children?.length) {
|
||
const found = walk(item.children)
|
||
if (found) return found
|
||
}
|
||
}
|
||
return undefined
|
||
}
|
||
|
||
return walk(menuItems)
|
||
}
|
||
|
||
/**
|
||
* 在菜单树中按 key 查找菜单项(含子级)。
|
||
* 面包屑用它把祖先 key 链还原成「分组名称」。
|
||
*/
|
||
export function findMenuItemByKey(key: string): MenuItem | undefined {
|
||
const walk = (items: MenuItem[]): MenuItem | undefined => {
|
||
for (const item of items) {
|
||
if (item.key === key) return item
|
||
|
||
if (item.children?.length) {
|
||
const found = walk(item.children)
|
||
if (found) return found
|
||
}
|
||
}
|
||
return undefined
|
||
}
|
||
|
||
return walk(menuItems)
|
||
}
|
||
|
||
/**
|
||
* 由当前路径反查它的所有祖先分组 key(从顶级到最近一级)。
|
||
* 支持多级嵌套与详情页:/ocean/booking/123 会命中「海运」「订舱业务」。
|
||
* 返回数组保证侧栏能逐层展开所有祖先分组。
|
||
*
|
||
* ⚠️ 这里的「命中」必须是【叶子】命中,不能是「分组 path 前缀命中」——
|
||
* 否则 /ocean/booking 会因 startsWith('/ocean/') 在「海运」这一层就提前返回,
|
||
* 丢掉真正的祖先链 [ocean, ocean-booking-group],导致刷新后不展开。
|
||
*/
|
||
export function findParentKeysByPath(path: string): string[] {
|
||
const walk = (items: MenuItem[], parents: string[]): string[] | null => {
|
||
for (const item of items) {
|
||
// 叶子(无 children)且精确命中 / 前缀命中(详情页):返回累计的祖先
|
||
if (!item.children?.length) {
|
||
if (item.path === path || path.startsWith(`${item.path}/`)) {
|
||
return parents
|
||
}
|
||
continue
|
||
}
|
||
|
||
// 分组:自身 path 精确命中(父级 path 与叶子相同的情况,如 /ocean/booking)
|
||
if (item.path === path) {
|
||
return parents
|
||
}
|
||
|
||
// 继续下钻,把当前分组 key 计入祖先链
|
||
const found = walk(item.children, [...parents, item.key])
|
||
if (found) return found
|
||
}
|
||
return null
|
||
}
|
||
|
||
return walk(menuItems, []) ?? []
|
||
}
|
||
|
||
/**
|
||
* 由当前路径反查所属的顶级分组 key(向后兼容,供需要单一顶级 key 的场景使用)。
|
||
*/
|
||
export function findParentKeyByPath(path: string): string | undefined {
|
||
return findParentKeysByPath(path)[0]
|
||
}
|
||
|
||
export default router
|