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