import router from '@/routers' import { LOGIN_URL } from '@/config' import { type RouteRecordRaw } from 'vue-router' import { ElNotification } from 'element-plus' import { useUserStore } from '@/stores/modules/user' import { useAuthStore } from '@/stores/modules/auth' const modules = import.meta.glob('@/views/**/*.vue') const VIEWS_ALIAS_PREFIX = '@/views' const VIEWS_SRC_PREFIX = '/src/views' const ROUTER_PERF_DEBUG = import.meta.env.DEV const perfNow = () => (typeof performance !== 'undefined' ? performance.now() : Date.now()) const logRouterPerf = (label: string, start: number, extra?: Record) => { if (!ROUTER_PERF_DEBUG) return const duration = Math.round((perfNow() - start) * 100) / 100 console.info(`[router-perf] ${label}`, { duration, ...extra }) } const COMPONENT_PATH_ALIASES: Record = { // 后端菜单沿用 event-list 模块名时,前端实际页面目录为 eventList。 '/event/event-list': '/event/eventList', '/event/event-list/index': '/event/eventList/index', // 后端菜单可能使用短横线模块名,前端页面目录统一为 steadyDataView。 '/steady/steady-data-view': '/steady/steadyDataView', '/steady/steady-data-view/index': '/steady/steadyDataView/index', '/steady/steady-trend': '/steady/steadyTrend', '/steady/steady-trend/index': '/steady/steadyTrend/index', '/steady/check-square': '/steady/checksquare', '/steady/check-square/index': '/steady/checksquare/index', // 数据库监控菜单统一落到 system-ops/dbms 页面,兼容后端菜单常见 component 写法。 '/system-monitor/dbms': '/system-ops/dbms', '/system-monitor/dbms/index': '/system-ops/dbms/index', '/system-monitor/databaseMonitor': '/system-ops/dbms', '/system-monitor/databaseMonitor/index': '/system-ops/dbms/index', '/system-monitor/database-monitor': '/system-ops/dbms', '/system-monitor/database-monitor/index': '/system-ops/dbms/index', '/systemMonitor/dbms': '/system-ops/dbms', '/systemMonitor/dbms/index': '/system-ops/dbms/index', '/systemMonitor/databaseMonitor': '/system-ops/dbms', '/systemMonitor/databaseMonitor/index': '/system-ops/dbms/index', '/systemMonitor/database-monitor': '/system-ops/dbms', '/systemMonitor/database-monitor/index': '/system-ops/dbms/index', '/system-ops/database-monitor': '/system-ops/dbms', '/system-ops/database-monitor/index': '/system-ops/dbms/index', // 报告菜单后端使用 aiReport,前端目录沿用既有 aireport 命名。 '/aiReport/reportTask': '/aireport/testReport', '/aiReport/reportTask/index': '/aireport/testReport/index', '/aiReport/testReport': '/aireport/testReport', '/aiReport/testReport/index': '/aireport/testReport/index', '/aiReport/reportModel': '/aireport/reportModel', '/aiReport/reportModel/index': '/aireport/reportModel/index', '/aiReport/clientUnit': '/aireport/clientUnit', '/aiReport/clientUnit/index': '/aireport/clientUnit/index', '/aiReport/testDevice': '/aireport/testDevice', '/aiReport/testDevice/index': '/aireport/testDevice/index', '/aiReport/reportApproval': '/aireport/reportApproval', '/aiReport/reportApproval/index': '/aireport/reportApproval/index' } const STATIC_ROUTE_NAMES = new Set([ 'layout', 'login', 'home', 'tools', 'toolWaveform', 'toolMmsMapping', 'toolParsePqdif', 'deviceTypes', 'toolAddData', 'toolAddLedger', 'eventList', 'steadyDataView', 'steadyTrend', 'checksquare', 'system-monitor', 'systemMonitor', 'diskMonitor', 'systemOpsDbms', '403', '404', '500' ]) let isInitializing = false /** * 清除已有的动态路由,避免重复注入。 */ const clearDynamicRoutes = () => { const routes = router.getRoutes() routes.forEach(route => { if (route.name && !STATIC_ROUTE_NAMES.has(String(route.name))) { router.removeRoute(route.name) } }) } /** * 统一菜单 component 配置格式,只允许映射到 views 目录。 */ const normalizeComponentPath = (path: string) => { let normalizedPath = path.trim().replace(/\\/g, '/') if (normalizedPath.startsWith(VIEWS_ALIAS_PREFIX)) { normalizedPath = normalizedPath.slice(VIEWS_ALIAS_PREFIX.length) } else if (normalizedPath.startsWith(VIEWS_SRC_PREFIX)) { normalizedPath = normalizedPath.slice(VIEWS_SRC_PREFIX.length) } else if (normalizedPath.startsWith('src/views')) { normalizedPath = normalizedPath.slice('src/views'.length) } if (!normalizedPath.startsWith('/')) { normalizedPath = '/' + normalizedPath } if (normalizedPath.endsWith('.vue')) { normalizedPath = normalizedPath.slice(0, -4) } if (normalizedPath.length > 1 && normalizedPath.endsWith('/')) { normalizedPath = normalizedPath.slice(0, -1) } return COMPONENT_PATH_ALIASES[normalizedPath] ?? normalizedPath } /** * 根据菜单 component 路径查找对应页面模块。 * 兼容菜单资源里常见的多种写法,避免因为前缀或 index 差异导致误判。 */ const resolveComponentModule = (path: string) => { const normalizedPath = normalizeComponentPath(path) const viewPath = normalizedPath.endsWith('/index') ? normalizedPath.slice(0, -'/index'.length) : normalizedPath const candidatePaths = Array.from( new Set([ `${VIEWS_ALIAS_PREFIX}${normalizedPath}.vue`, `${VIEWS_ALIAS_PREFIX}${normalizedPath}/index.vue`, `${VIEWS_ALIAS_PREFIX}${viewPath}/index.vue`, `${VIEWS_SRC_PREFIX}${normalizedPath}.vue`, `${VIEWS_SRC_PREFIX}${normalizedPath}/index.vue`, `${VIEWS_SRC_PREFIX}${viewPath}/index.vue` ]) ) for (const candidatePath of candidatePaths) { if (candidatePath in modules) { return { moduleLoader: modules[candidatePath], resolvedPath: candidatePath } } } return { moduleLoader: undefined, resolvedPath: candidatePaths } } /** * 初始化动态路由。 */ export const initDynamicRouter = async () => { if (isInitializing) return Promise.reject(new Error('Dynamic router initialization in progress')) isInitializing = true const initStart = perfNow() const userStore = useUserStore() const authStore = useAuthStore() const unresolvedRoutes: Array<{ name?: string; path?: string; component?: string; candidates: string[] }> = [] try { const menuStart = perfNow() await authStore.getAuthMenuList() logRouterPerf('dynamic-router-load-menus-api', menuStart) const flattenMenuStart = perfNow() const flatMenuList = authStore.flatMenuListGet logRouterPerf('dynamic-router-flatten-menus', flattenMenuStart, { menuCount: flatMenuList.length }) const buttonStart = perfNow() await authStore.getAuthButtonList() logRouterPerf('dynamic-router-load-buttons', buttonStart) if (!authStore.authMenuListGet.length) { ElNotification({ title: '无权限访问', message: '当前账号无任何菜单权限,请联系系统管理员', type: 'warning', duration: 3000 }) userStore.setAccessToken('') userStore.setRefreshToken('') userStore.setExp(0) await router.replace(LOGIN_URL) return Promise.reject('No permission') } const clearStart = perfNow() clearDynamicRoutes() logRouterPerf('dynamic-router-clear-routes', clearStart) const addRoutesStart = perfNow() let addedRouteCount = 0 for (const item of flatMenuList) { if (item.children) delete item.children if (item.name && STATIC_ROUTE_NAMES.has(String(item.name))) { continue } // 动态菜单组件必须先映射成真实页面模块,否则 addRoute 后会直接落到 404。 if (item.component && typeof item.component === 'string') { const { moduleLoader, resolvedPath } = resolveComponentModule(item.component) if (moduleLoader) { item.component = moduleLoader } else { unresolvedRoutes.push({ name: item.name, path: item.path, component: item.component, candidates: resolvedPath }) continue } } if ( typeof item.path === 'string' && (typeof item.component === 'function' || typeof item.redirect === 'string') ) { const routeItem = item as unknown as RouteRecordRaw if (item.meta?.isFull) { router.addRoute(routeItem) } else { router.addRoute('layout', routeItem) } addedRouteCount += 1 } else { console.warn('Invalid route item:', item) } } logRouterPerf('dynamic-router-add-routes', addRoutesStart, { menuCount: flatMenuList.length, addedRouteCount, unresolvedRouteCount: unresolvedRoutes.length }) if (unresolvedRoutes.length) { console.error('[dynamic-router] unresolved route components', unresolvedRoutes) } } catch (error) { userStore.setAccessToken('') userStore.setRefreshToken('') userStore.setExp(0) await router.replace(LOGIN_URL) return Promise.reject(error) } finally { logRouterPerf('dynamic-router-total', initStart, { unresolvedRouteCount: unresolvedRoutes.length }) isInitializing = false } }