Files
front/src/router/menu-data.ts

301 lines
9.3 KiB
TypeScript
Raw Normal View History

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