Files
front/src/router/menu-data.ts
2026-03-15 23:16:00 +08:00

259 lines
7.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { DEFAULT_LAYOUT } from './routes/base'
import type { AppRouteRecordRaw } from './routes/types'
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[]
[key: string]: any
}
// 预定义的视图模块映射(用于 Vite 动态导入)
const viewModules = import.meta.glob('@/views/**/*.vue')
console.log('viewModules', viewModules)
/**
* 动态加载视图组件
* @param componentPath 组件路径,如 'ops/pages/overview' 或 'ops/pages/overview/index'
* @returns 动态导入的组件
*/
export function loadViewComponent(componentPath: string) {
// 将路径转换为完整的视图路径
// 如果路径不以 /index 结尾且不以 .vue 结尾,自动补全
let normalizedPath = componentPath
if (!normalizedPath.endsWith('/index') && !normalizedPath.endsWith('.vue')) {
normalizedPath = `${normalizedPath}/index`
}
// 构建完整的文件路径
const filePath = `/src/views/${normalizedPath}.vue`
// 从预加载的模块中查找
const modulePath = Object.keys(viewModules).find((path) => path.endsWith(filePath) || path === filePath)
if (modulePath && viewModules[modulePath]) {
return viewModules[modulePath]
}
// 如果找不到,尝试不带 /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}`)
return () => import('@/views/redirect/index.vue')
}
/**
* 将服务器菜单数据转换为路由配置
* @param menuItems 服务器返回的菜单项(树状结构)
* @returns 路由配置数组
*/
export function transformMenuToRoutes(menuItems: ServerMenuItem[]): AppRouteRecordRaw[] {
const routes: AppRouteRecordRaw[] = []
for (const item of menuItems) {
const route: AppRouteRecordRaw = {
path: item.menu_path || '',
name: item.title || item.name || `menu_${item.id}`,
meta: {
// ...item,
locale: item.locale || item.title,
requiresAuth: item.requiresAuth !== false,
icon: item.icon || item?.menu_icon,
order: item.sort_key ?? item.order,
hideInMenu: item.hideInMenu,
hideChildrenInMenu: item.hideChildrenInMenu,
roles: item.roles,
isNewTab: item.is_new_tab,
},
component: DEFAULT_LAYOUT,
}
// 处理子菜单
if (item.children && item.children.length > 0) {
// 传递父级的 component 和 path 给子路由处理函数
route.children = transformChildRoutes(item.children, item.component, item.menu_path)
} 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,
},
},
]
}
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 父级菜单的路径(用于计算相对路径)
* @returns 子路由配置数组
*/
function transformChildRoutes(
children: ServerMenuItem[],
parentComponent?: string,
parentPath?: string
): AppRouteRecordRaw[] {
return children.map((child) => {
// 优先使用子菜单自己的 component否则继承父级的 component
const componentPath = child.component || parentComponent
// 计算子路由的相对路径
const childFullPath = child.menu_path || child.path || ''
const relativePath = extractRelativePath(childFullPath, parentPath || '')
const route: AppRouteRecordRaw = {
path: relativePath,
name: child.title || child.name || `menu_${child.id}`,
meta: {
...child,
locale: child.locale || child.title,
requiresAuth: child.requiresAuth !== false,
roles: child.roles,
},
component: componentPath
? loadViewComponent(componentPath)
: () => import('@/views/redirect/index.vue'),
}
// 递归处理子菜单的子菜单
if (child.children && child.children.length > 0) {
route.children = transformChildRoutes(
child.children,
child.component || parentComponent,
childFullPath // 传递当前子菜单的完整路径作为下一层的父路径
)
}
return route
})
}
// 本地菜单数据 - 接口未准备好时使用
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',
component: () => import('@/views/ops/pages/overview/index.vue'),
meta: {
locale: '系统概况',
requiresAuth: true,
},
},
],
},
]
export default localMenuData