import { DEFAULT_LAYOUT, STANDALONE_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[] is_full?: boolean // 是否为独立页面(不包含菜单栏) hide_menu?: boolean // 是否隐藏菜单栏 [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) { // 根据 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 } const route: AppRouteRecordRaw = { path: item.menu_path || '', name: item.title || item.name || `menu_${item.id}`, component: routeComponent, 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 || item.hide_menu, hideChildrenInMenu: item.hideChildrenInMenu, roles: item.roles, isNewTab: item.is_new_tab, }, } // 非独立页面需要处理子菜单 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, }, }, ] } } 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 父级菜单的路径(用于计算相对路径) * @param parentIsFull 父级菜单的 is_full 字段 * @returns 子路由配置数组 */ /** 许可授权中心菜单路径(严格匹配末段,避免 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' } function transformChildRoutes( children: ServerMenuItem[], parentComponent?: string, parentPath?: string, parentIsFull?: boolean ): AppRouteRecordRaw[] { return children.map((child) => { const childFullPath = String(child.menu_path ?? child.path ?? '').trim() // 已配置 component 的菜单绝不覆盖;仅对许可页做路径/code 兜底,避免 includes 误匹配 let componentPath = child.component || parentComponent if ( !child.component && (isLicenseCenterMenuPath(childFullPath) || child.code === 'LicenseCenter') ) { componentPath = LICENSE_CENTER_VIEW } 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, hideInMenu: child.hideInMenu || child.hide_menu, }, 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, // 传递当前子菜单的完整路径作为下一层的父路径 child.is_full || parentIsFull // 传递 is_full 标志 ) } 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