import type { MenuOption } from 'naive-ui' import type { RouteRecordRaw } from 'vue-router' import { h } from 'vue' import { usePermission } from '@/hooks' import Layout from '@/layouts/index.vue' import { arrayToTree, renderIcon } from '@/utils' import { safeAsyncComponent } from '@/utils/component-guard' import { clone, min, omit, pick } from 'radash' import { RouterLink } from 'vue-router' const metaFields: AppRoute.MetaKeys[] = ['title', 'icon', 'requiresAuth', 'roles', 'auth', 'keepAlive', 'hide', 'order', 'href', 'activeMenu', 'withoutTab', 'pinTab', 'menuType'] // 将后端菜单数据转换为前端路由数据 function transformBackendToRoute(backendRoute: AppRoute.BackendRoute): AppRoute.RowRoute { return { id: backendRoute.menuId, pid: backendRoute.parentId === 0 ? null : backendRoute.parentId, name: backendRoute.name, path: backendRoute.path, componentPath: backendRoute.component || null, redirect: backendRoute.redirect || undefined, title: backendRoute.menuName, icon: backendRoute.icon, auth: backendRoute.auth, // 权限标识 requiresAuth: true, // 动态路由都需要认证 hide: backendRoute.isHide === '0', // 0-隐藏 1-显示 keepAlive: backendRoute.isKeepAlive === '0', // 0-是 1-否 pinTab: backendRoute.isAffix === '0', // 0-是 1-否 activeMenu: backendRoute.activeMenu || undefined, menuType: backendRoute.menuType as AppRoute.MenuType, href: backendRoute.isLink === '0' ? backendRoute.path : undefined, // 如果是外链 } } function standardizedRoutes(route: AppRoute.RowRoute[]) { return clone(route).map((i) => { const route = omit(i, metaFields) Reflect.set(route, 'meta', pick(i, metaFields)) return route }) as AppRoute.Route[] } // 处理路由数据的主函数 - 支持动态和静态路由以及混合模式 export function createRoutes(routeData: (AppRoute.BackendRoute | AppRoute.RowRoute)[]) { const { hasPermission } = usePermission() // 处理混合数据:分别处理后端数据和前端数据 const backendRoutes = routeData.filter(item => 'menuId' in item) as AppRoute.BackendRoute[] const frontendRoutes = routeData.filter(item => !('menuId' in item)) as AppRoute.RowRoute[] // 转换后端路由数据 const transformedBackendRoutes = backendRoutes.map(transformBackendToRoute) // 合并所有路由 const routes = [...frontendRoutes, ...transformedBackendRoutes] // Structure the meta field let resultRouter = standardizedRoutes(routes) // Route permission filtering resultRouter = resultRouter.filter(i => hasPermission(i.meta.roles)) // Generate routes, no need to import files for those with redirect const modules = import.meta.glob('@/views/**/*.vue') resultRouter = resultRouter.map((item: AppRoute.Route) => { if (item.componentPath && !item.redirect) { // 对于动态路由,只有菜单类型才需要组件;对于静态路由,都需要组件 const needComponent = item.meta.menuType === '2' || !item.meta.menuType if (needComponent) { // 处理组件路径,确保正确的路径格式 let componentPath = item.componentPath // 确保路径以 / 开头 if (!componentPath.startsWith('/')) { componentPath = `/${componentPath}` } // 确保路径以 .vue 结尾 if (!componentPath.endsWith('.vue')) { componentPath = `${componentPath}.vue` } const fullPath = `/src/views${componentPath}` const originalComponent = modules[fullPath] // 如果组件未找到,输出调试信息并提供默认组件 if (!originalComponent) { // console.warn(`组件未找到: ${fullPath}`) // console.warn('可用组件路径:', Object.keys(modules).slice(0, 10)) // 只显示前10个避免日志过长 // 为找不到组件的页面提供一个默认的空页面组件 item.component = safeAsyncComponent( () => Promise.resolve({ template: `

页面开发中

组件路径: ${fullPath}

请联系开发人员创建对应的页面组件

`, }), { delay: 0, timeout: 5000, }, ) } else { // 使用安全的异步组件加载器包装原有组件 item.component = safeAsyncComponent( originalComponent as any, { delay: 100, timeout: 10000, onError: (error, retry, fail, attempts) => { console.error(`组件加载失败: ${fullPath}`, error) if (attempts <= 2) { retry() } else { fail() } }, }, ) } } else if (item.meta.menuType === '1') { // 目录类型不需要组件,但需要确保有children item.component = undefined } } return item }) // Generate route tree resultRouter = arrayToTree(resultRouter) as AppRoute.Route[] const appRootRoute: RouteRecordRaw = { path: '/appRoot', name: 'appRoot', redirect: import.meta.env.VITE_HOME_PATH || '/dashboard/monitor', component: Layout, meta: { title: '', icon: 'icon-park-outline:home', }, children: [], } // Set the correct redirect path for the route setRedirect(resultRouter) // Insert the processed route into the root route appRootRoute.children = resultRouter as unknown as RouteRecordRaw[] return appRootRoute } // Generate an array of route names that need to be kept alive export function generateCacheRoutes(routes: AppRoute.RowRoute[]) { return routes .filter(i => i.keepAlive) .map(i => i.name) } function setRedirect(routes: AppRoute.Route[]) { routes.forEach((route) => { if (route.children) { if (!route.redirect) { // Filter out a collection of child elements that are not hidden const visibleChilds = route.children.filter(child => !child.meta.hide) // Redirect page to the path of the first child element by default let target = visibleChilds[0] // Filter out pages with the order attribute const orderChilds = visibleChilds.filter(child => child.meta.order) if (orderChilds.length > 0) target = min(orderChilds, i => i.meta.order!) as AppRoute.Route if (target) route.redirect = target.path } setRedirect(route.children) } }) } /* 生成侧边菜单的数据 */ export function createMenus(routeData: (AppRoute.BackendRoute | AppRoute.RowRoute)[]) { // 处理混合数据:分别处理后端数据和前端数据 const backendRoutes = routeData.filter(item => 'menuId' in item) as AppRoute.BackendRoute[] const frontendRoutes = routeData.filter(item => !('menuId' in item)) as AppRoute.RowRoute[] // 转换后端路由数据 const transformedBackendRoutes = backendRoutes.map(transformBackendToRoute) // 合并所有路由 const userRoutes = [...frontendRoutes, ...transformedBackendRoutes] const resultMenus = standardizedRoutes(userRoutes) // filter menus that do not need to be displayed const visibleMenus = resultMenus.filter(route => !route.meta.hide && route.meta.menuType !== '3') // 过滤按钮类型 // 处理权限过滤和父子关系 const menusWithPermission = processMenuPermissions(visibleMenus) // generate side menu return arrayToTree(transformAuthRoutesToMenus(menusWithPermission)) } // 处理菜单权限,确保有子菜单权限时父菜单也可见 function processMenuPermissions(routes: AppRoute.Route[]): AppRoute.Route[] { const { hasPermission } = usePermission() // 创建路由映射表 const routeMap = new Map() routes.forEach(route => routeMap.set(route.id, route)) // 找出有权限的路由 const authorizedRoutes = new Set() routes.forEach((route) => { if (hasPermission(route.meta.roles)) { authorizedRoutes.add(route.id) // 如果是页面类型(menuType='2')或没有menuType的路由,确保其父菜单也被包含 if (!route.meta.menuType || route.meta.menuType === '2') { let parentId = route.pid while (parentId !== null && parentId !== undefined) { const parentRoute = routeMap.get(parentId) if (parentRoute) { authorizedRoutes.add(parentId) parentId = parentRoute.pid } else { break } } } } }) // 返回有权限的路由 return routes.filter(route => authorizedRoutes.has(route.id)) } // render the returned routing table as a sidebar function transformAuthRoutesToMenus(userRoutes: AppRoute.Route[]) { return userRoutes // Sort the menu according to the order size .sort((a, b) => { if (a.meta && a.meta.order && b.meta && b.meta.order) return a.meta.order - b.meta.order else if (a.meta && a.meta.order) return -1 else if (b.meta && b.meta.order) return 1 else return 0 }) // Convert to side menu data structure .map((item) => { const target: MenuOption = { id: item.id, pid: item.pid, label: (!item.meta.menuType || item.meta.menuType === '2') ? () => h( RouterLink, { to: { path: item.path, }, }, { default: () => item.meta.title }, ) : () => item.meta.title, key: item.path, icon: item.meta.icon ? renderIcon(item.meta.icon) : undefined, } return target }) }