2026-03-21 15:52:38 +08:00
|
|
|
|
import { DEFAULT_LAYOUT, STANDALONE_LAYOUT } from './routes/base'
|
2026-03-07 20:11:25 +08:00
|
|
|
|
import type { AppRouteRecordRaw } from './routes/types'
|
2026-03-08 22:41:42 +08:00
|
|
|
|
import type { TreeNodeBase } from '@/utils/tree'
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 服务器返回的菜单数据结构
|
|
|
|
|
|
*/
|
|
|
|
|
|
export interface ServerMenuItem extends TreeNodeBase {
|
|
|
|
|
|
id: number | string
|
|
|
|
|
|
parent_id: number | string | null
|
|
|
|
|
|
name?: string
|
|
|
|
|
|
title?: string // 菜单标题
|
|
|
|
|
|
title_en?: string // 英文标题
|
|
|
|
|
|
code?: string // 菜单编码
|
|
|
|
|
|
menu_path?: string // 菜单路径,如 '/overview'
|
|
|
|
|
|
component?: string // 组件路径,如 'ops/pages/overview'
|
|
|
|
|
|
icon?: string
|
|
|
|
|
|
locale?: string
|
|
|
|
|
|
sort_key?: number // 排序字段
|
|
|
|
|
|
order?: number
|
|
|
|
|
|
hideInMenu?: boolean
|
|
|
|
|
|
hideChildrenInMenu?: boolean
|
|
|
|
|
|
requiresAuth?: boolean
|
|
|
|
|
|
roles?: string[]
|
|
|
|
|
|
children?: ServerMenuItem[]
|
2026-03-21 15:52:38 +08:00
|
|
|
|
is_full?: boolean // 是否为独立页面(不包含菜单栏)
|
|
|
|
|
|
hide_menu?: boolean // 是否隐藏菜单栏
|
2026-03-08 22:41:42 +08:00
|
|
|
|
[key: string]: any
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 预定义的视图模块映射(用于 Vite 动态导入)
|
|
|
|
|
|
const viewModules = import.meta.glob('@/views/**/*.vue')
|
|
|
|
|
|
|
2026-03-14 18:55:23 +08:00
|
|
|
|
console.log('viewModules', viewModules)
|
2026-03-08 22:41:42 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 动态加载视图组件
|
|
|
|
|
|
* @param componentPath 组件路径,如 'ops/pages/overview' 或 'ops/pages/overview/index'
|
|
|
|
|
|
* @returns 动态导入的组件
|
|
|
|
|
|
*/
|
|
|
|
|
|
export function loadViewComponent(componentPath: string) {
|
|
|
|
|
|
// 将路径转换为完整的视图路径
|
2026-03-14 18:55:23 +08:00
|
|
|
|
// 如果路径不以 /index 结尾且不以 .vue 结尾,自动补全
|
|
|
|
|
|
let normalizedPath = componentPath
|
|
|
|
|
|
if (!normalizedPath.endsWith('/index') && !normalizedPath.endsWith('.vue')) {
|
|
|
|
|
|
normalizedPath = `${normalizedPath}/index`
|
2026-03-08 22:41:42 +08:00
|
|
|
|
}
|
|
|
|
|
|
// 构建完整的文件路径
|
2026-03-14 18:55:23 +08:00
|
|
|
|
const filePath = `/src/views/${normalizedPath}.vue`
|
2026-03-08 22:41:42 +08:00
|
|
|
|
|
|
|
|
|
|
// 从预加载的模块中查找
|
|
|
|
|
|
const modulePath = Object.keys(viewModules).find((path) => path.endsWith(filePath) || path === filePath)
|
2026-03-15 23:16:00 +08:00
|
|
|
|
|
2026-03-08 22:41:42 +08:00
|
|
|
|
if (modulePath && viewModules[modulePath]) {
|
|
|
|
|
|
return viewModules[modulePath]
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-14 18:55:23 +08:00
|
|
|
|
// 如果找不到,尝试不带 /index 的路径
|
|
|
|
|
|
const directFilePath = `/src/views/${componentPath}.vue`
|
|
|
|
|
|
const directModulePath = Object.keys(viewModules).find((path) => path.endsWith(directFilePath) || path === directFilePath)
|
|
|
|
|
|
|
|
|
|
|
|
if (directModulePath && viewModules[directModulePath]) {
|
|
|
|
|
|
return viewModules[directModulePath]
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 如果都找不到,返回一个默认组件或抛出错误
|
|
|
|
|
|
console.warn(`View component not found: ${filePath} or ${directFilePath}`)
|
2026-03-08 22:41:42 +08:00
|
|
|
|
return () => import('@/views/redirect/index.vue')
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 将服务器菜单数据转换为路由配置
|
|
|
|
|
|
* @param menuItems 服务器返回的菜单项(树状结构)
|
|
|
|
|
|
* @returns 路由配置数组
|
|
|
|
|
|
*/
|
|
|
|
|
|
export function transformMenuToRoutes(menuItems: ServerMenuItem[]): AppRouteRecordRaw[] {
|
|
|
|
|
|
const routes: AppRouteRecordRaw[] = []
|
|
|
|
|
|
|
|
|
|
|
|
for (const item of menuItems) {
|
2026-03-21 15:52:38 +08:00
|
|
|
|
// 根据 is_full 决定如何设置 component
|
|
|
|
|
|
let routeComponent: AppRouteRecordRaw['component']
|
|
|
|
|
|
|
|
|
|
|
|
if (item.is_full) {
|
|
|
|
|
|
// 独立页面:直接加载视图组件,不使用布局
|
|
|
|
|
|
if (item.component) {
|
|
|
|
|
|
routeComponent = loadViewComponent(item.component)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// 如果没有 component,使用默认重定向页面
|
|
|
|
|
|
routeComponent = () => import('@/views/redirect/index.vue')
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// 非独立页面:使用默认布局
|
|
|
|
|
|
routeComponent = DEFAULT_LAYOUT
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-08 22:41:42 +08:00
|
|
|
|
const route: AppRouteRecordRaw = {
|
|
|
|
|
|
path: item.menu_path || '',
|
|
|
|
|
|
name: item.title || item.name || `menu_${item.id}`,
|
2026-03-21 15:52:38 +08:00
|
|
|
|
component: routeComponent,
|
2026-03-08 22:41:42 +08:00
|
|
|
|
meta: {
|
2026-03-14 18:55:23 +08:00
|
|
|
|
// ...item,
|
2026-03-08 22:41:42 +08:00
|
|
|
|
locale: item.locale || item.title,
|
|
|
|
|
|
requiresAuth: item.requiresAuth !== false,
|
2026-03-12 22:38:13 +08:00
|
|
|
|
icon: item.icon || item?.menu_icon,
|
2026-03-08 22:41:42 +08:00
|
|
|
|
order: item.sort_key ?? item.order,
|
2026-03-21 15:52:38 +08:00
|
|
|
|
hideInMenu: item.hideInMenu || item.hide_menu,
|
2026-03-08 22:41:42 +08:00
|
|
|
|
hideChildrenInMenu: item.hideChildrenInMenu,
|
|
|
|
|
|
roles: item.roles,
|
2026-03-12 22:38:13 +08:00
|
|
|
|
isNewTab: item.is_new_tab,
|
2026-03-08 22:41:42 +08:00
|
|
|
|
},
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-21 15:52:38 +08:00
|
|
|
|
// 非独立页面需要处理子菜单
|
|
|
|
|
|
if (!item.is_full) {
|
|
|
|
|
|
if (item.children && item.children.length > 0) {
|
|
|
|
|
|
// 传递父级的 component 和 path 给子路由处理函数
|
|
|
|
|
|
route.children = transformChildRoutes(item.children, item.component, item.menu_path, false)
|
|
|
|
|
|
} else if (item.component) {
|
|
|
|
|
|
// 一级菜单没有 children 但有 component,创建一个空路径的子路由
|
|
|
|
|
|
const routeName = route.name
|
|
|
|
|
|
route.children = [
|
|
|
|
|
|
{
|
|
|
|
|
|
path: item.menu_path || '',
|
|
|
|
|
|
name: typeof routeName === 'string' ? `${routeName}Index` : `menu_${item.id}_index`,
|
|
|
|
|
|
component: loadViewComponent(item.component),
|
|
|
|
|
|
meta: {
|
|
|
|
|
|
locale: item.locale || item.title,
|
|
|
|
|
|
requiresAuth: item.requiresAuth !== false,
|
|
|
|
|
|
isNewTab: item.is_new_tab,
|
|
|
|
|
|
},
|
2026-03-08 22:41:42 +08:00
|
|
|
|
},
|
2026-03-21 15:52:38 +08:00
|
|
|
|
]
|
|
|
|
|
|
}
|
2026-03-08 22:41:42 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
routes.push(route)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return routes
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 将路径转换为相对路径(去掉开头的 /)
|
|
|
|
|
|
* @param path 路径
|
|
|
|
|
|
* @returns 相对路径
|
|
|
|
|
|
*/
|
|
|
|
|
|
function toRelativePath(path: string): string {
|
|
|
|
|
|
if (!path) return ''
|
|
|
|
|
|
// 去掉开头的 /
|
|
|
|
|
|
return path.startsWith('/') ? path.slice(1) : path
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 从完整路径中提取子路由的相对路径
|
|
|
|
|
|
* 例如:父路径 '/dashboard',子路径 '/dashboard/workplace' -> 'workplace'
|
|
|
|
|
|
* @param childPath 子菜单的完整路径
|
|
|
|
|
|
* @param parentPath 父菜单的路径
|
|
|
|
|
|
* @returns 相对路径
|
|
|
|
|
|
*/
|
|
|
|
|
|
function extractRelativePath(childPath: string, parentPath: string): string {
|
|
|
|
|
|
if (!childPath) return ''
|
|
|
|
|
|
|
|
|
|
|
|
// 如果子路径以父路径开头,提取相对部分
|
|
|
|
|
|
if (parentPath && childPath.startsWith(parentPath)) {
|
|
|
|
|
|
let relativePath = childPath.slice(parentPath.length)
|
|
|
|
|
|
// 去掉开头的 /
|
|
|
|
|
|
if (relativePath.startsWith('/')) {
|
|
|
|
|
|
relativePath = relativePath.slice(1)
|
|
|
|
|
|
}
|
|
|
|
|
|
return relativePath
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 否则转换为相对路径
|
|
|
|
|
|
return toRelativePath(childPath)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 转换子路由配置
|
|
|
|
|
|
* @param children 子菜单项
|
|
|
|
|
|
* @param parentComponent 父级菜单的 component 字段(用于子菜单没有 component 时继承)
|
|
|
|
|
|
* @param parentPath 父级菜单的路径(用于计算相对路径)
|
2026-03-21 15:52:38 +08:00
|
|
|
|
* @param parentIsFull 父级菜单的 is_full 字段
|
2026-03-08 22:41:42 +08:00
|
|
|
|
* @returns 子路由配置数组
|
|
|
|
|
|
*/
|
2026-03-21 17:13:47 +08:00
|
|
|
|
/** 许可授权中心菜单路径(严格匹配末段,避免 menu_path 中含 query 等导致误伤其它菜单如用户管理) */
|
|
|
|
|
|
const LICENSE_CENTER_VIEW = 'ops/pages/system-settings/license-center'
|
|
|
|
|
|
|
|
|
|
|
|
function isLicenseCenterMenuPath(fullPath: string): boolean {
|
|
|
|
|
|
const path = String(fullPath ?? '')
|
|
|
|
|
|
.trim()
|
|
|
|
|
|
.split('?')[0]
|
|
|
|
|
|
.replace(/\/+$/, '')
|
|
|
|
|
|
return path.endsWith('/license-center') || path === 'license-center'
|
|
|
|
|
|
}
|
2026-03-21 17:06:54 +08:00
|
|
|
|
|
2026-03-08 22:41:42 +08:00
|
|
|
|
function transformChildRoutes(
|
|
|
|
|
|
children: ServerMenuItem[],
|
|
|
|
|
|
parentComponent?: string,
|
2026-03-21 15:52:38 +08:00
|
|
|
|
parentPath?: string,
|
|
|
|
|
|
parentIsFull?: boolean
|
2026-03-08 22:41:42 +08:00
|
|
|
|
): AppRouteRecordRaw[] {
|
|
|
|
|
|
return children.map((child) => {
|
2026-03-21 17:13:47 +08:00
|
|
|
|
const childFullPath = String(child.menu_path ?? child.path ?? '').trim()
|
2026-03-21 17:06:54 +08:00
|
|
|
|
|
2026-03-21 17:13:47 +08:00
|
|
|
|
// 已配置 component 的菜单绝不覆盖;仅对许可页做路径/code 兜底,避免 includes 误匹配
|
2026-03-21 17:06:54 +08:00
|
|
|
|
let componentPath = child.component || parentComponent
|
2026-03-21 17:13:47 +08:00
|
|
|
|
if (
|
|
|
|
|
|
!child.component &&
|
|
|
|
|
|
(isLicenseCenterMenuPath(childFullPath) || child.code === 'LicenseCenter')
|
|
|
|
|
|
) {
|
|
|
|
|
|
componentPath = LICENSE_CENTER_VIEW
|
2026-03-21 17:06:54 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-08 22:41:42 +08:00
|
|
|
|
const relativePath = extractRelativePath(childFullPath, parentPath || '')
|
|
|
|
|
|
|
|
|
|
|
|
const route: AppRouteRecordRaw = {
|
|
|
|
|
|
path: relativePath,
|
|
|
|
|
|
name: child.title || child.name || `menu_${child.id}`,
|
|
|
|
|
|
meta: {
|
2026-03-14 18:55:23 +08:00
|
|
|
|
...child,
|
2026-03-08 22:41:42 +08:00
|
|
|
|
locale: child.locale || child.title,
|
|
|
|
|
|
requiresAuth: child.requiresAuth !== false,
|
|
|
|
|
|
roles: child.roles,
|
2026-03-21 15:52:38 +08:00
|
|
|
|
hideInMenu: child.hideInMenu || child.hide_menu,
|
2026-03-08 22:41:42 +08:00
|
|
|
|
},
|
|
|
|
|
|
component: componentPath
|
|
|
|
|
|
? loadViewComponent(componentPath)
|
|
|
|
|
|
: () => import('@/views/redirect/index.vue'),
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 递归处理子菜单的子菜单
|
|
|
|
|
|
if (child.children && child.children.length > 0) {
|
|
|
|
|
|
route.children = transformChildRoutes(
|
|
|
|
|
|
child.children,
|
|
|
|
|
|
child.component || parentComponent,
|
2026-03-21 15:52:38 +08:00
|
|
|
|
childFullPath, // 传递当前子菜单的完整路径作为下一层的父路径
|
|
|
|
|
|
child.is_full || parentIsFull // 传递 is_full 标志
|
2026-03-08 22:41:42 +08:00
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return route
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
2026-03-07 20:11:25 +08:00
|
|
|
|
|
|
|
|
|
|
// 本地菜单数据 - 接口未准备好时使用
|
|
|
|
|
|
export const localMenuData: AppRouteRecordRaw[] = [{
|
|
|
|
|
|
path: '/dashboard',
|
|
|
|
|
|
name: 'dashboard',
|
|
|
|
|
|
component: DEFAULT_LAYOUT,
|
|
|
|
|
|
meta: {
|
|
|
|
|
|
locale: '仪表盘',
|
|
|
|
|
|
requiresAuth: true,
|
|
|
|
|
|
icon: 'icon-dashboard',
|
|
|
|
|
|
order: 0,
|
|
|
|
|
|
},
|
|
|
|
|
|
children: [
|
|
|
|
|
|
{
|
|
|
|
|
|
path: 'workplace',
|
|
|
|
|
|
name: 'Workplace',
|
|
|
|
|
|
component: () => import('@/views/dashboard/workplace/index.vue'),
|
|
|
|
|
|
meta: {
|
|
|
|
|
|
locale: 'menu.dashboard.workplace',
|
|
|
|
|
|
requiresAuth: true,
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
path: 'monitor',
|
|
|
|
|
|
name: 'Monitor',
|
|
|
|
|
|
component: () => import('@/views/dashboard/monitor/index.vue'),
|
|
|
|
|
|
meta: {
|
|
|
|
|
|
locale: 'menu.dashboard.monitor',
|
|
|
|
|
|
requiresAuth: true,
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
],
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
path: '/overview',
|
|
|
|
|
|
name: 'Overview',
|
|
|
|
|
|
component: DEFAULT_LAYOUT,
|
|
|
|
|
|
meta: {
|
|
|
|
|
|
locale: '系统概况',
|
|
|
|
|
|
requiresAuth: true,
|
|
|
|
|
|
icon: 'icon-home',
|
|
|
|
|
|
order: 1,
|
|
|
|
|
|
},
|
|
|
|
|
|
children: [
|
|
|
|
|
|
{
|
|
|
|
|
|
path: '',
|
|
|
|
|
|
name: 'OverviewIndex',
|
2026-03-08 22:41:42 +08:00
|
|
|
|
component: () => import('@/views/ops/pages/overview/index.vue'),
|
2026-03-07 20:11:25 +08:00
|
|
|
|
meta: {
|
|
|
|
|
|
locale: '系统概况',
|
|
|
|
|
|
requiresAuth: true,
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
],
|
|
|
|
|
|
},
|
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
export default localMenuData
|