import { RouteRecordRaw } from 'vue-router'; import { Message } from '@arco-design/web-vue'; import { useUserStore } from '@/store'; import { useAuthStore } from '@/store'; import router from '@/router'; import { useRbacUserStore } from '@/store/modules/rbac-user'; import { resolvePostLogoutLogin } from '@/utils/login-portal'; const HOME_URL = '/home/workplace'; // 默认首页路径 // 引入 views 文件夹下所有 vue 文件 const modules = import.meta.glob('@/views/**/*.vue'); // 存储已添加的动态路由名称,用于清除 let addedDynamicRouteNames: Set = new Set(); /** * @description 清除动态路由 */ export const clearDynamicRoutes = () => { // 移除所有已添加的动态路由 addedDynamicRouteNames.forEach(routeName => { if (router.hasRoute(routeName)) { router.removeRoute(routeName); } }); addedDynamicRouteNames.clear(); }; /** * @description 初始化动态路由 * @param options.forceRefresh 为 true 时清空菜单缓存后重建;登录进系统应传 false 以复用 getCurrentUser 已带的 menus */ export const initDynamicRouter = async (options?: { forceRefresh?: boolean; }) => { const userStore = useUserStore(); const authStore = useAuthStore(); const rbacUserStore = useRbacUserStore(); /** 路由初始化错误 */ const routerError = (isNotice: boolean = true, title: string = '无权限访问', message: string = '当前账号无任何菜单权限,请联系系统管理员!') => { if (isNotice) { Message.warning(message); } const loginType = rbacUserStore.resolveLoginTypeForRedirect(); const currentPath = router.currentRoute.value.fullPath; userStore.logout(); rbacUserStore.logout(); const target = resolvePostLogoutLogin({ loginType, path: currentPath }); if (target.path) { router.replace({ path: target.path, query: target.query }); } else { router.replace({ name: target.name, query: target.query }); } return Promise.reject('No permission'); }; try { // 先清除旧的动态路由 clearDynamicRoutes(); let chooseModule = userStore.chooseModuleGet; // 获取当前选择模块 let homePath: string = HOME_URL; // 首页路径 // 如果没有选择模块,尝试使用默认值(RBAC系统可能不需要模块,但为了兼容性使用默认值) if (!chooseModule) { chooseModule = 1; // 使用默认模块ID // 注意:这里不设置到 userStore,因为可能用户确实没有选择模块 // 如果后续需要,可以在这里设置:userStore.setModule(1); } // 1.获取菜单列表 && 按钮权限列表 // 登录后 getCurrentUser 通常已带 menus,默认不 forceRefresh,避免白等 const hasCachedMenus = (rbacUserStore.menus?.length ?? 0) > 0; const forceRefresh = options?.forceRefresh ?? !hasCachedMenus; try { await authStore.getAuthMenuList(chooseModule, forceRefresh); } catch (menuError: any) { console.error('获取菜单列表失败:', menuError); const errorMsg = menuError?.response?.data?.msg || menuError?.message || '获取菜单列表失败'; return routerError(true, '获取菜单失败', errorMsg); } await authStore.getAuthButtonList(); // 2.判断当前用户有没有菜单权限 if (!authStore.authMenuListGet.length) { console.error('菜单列表为空,用户没有菜单权限'); return routerError(true, '无权限访问', '当前账号在该模块下无任何菜单权限,请联系系统管理员!'); } // 获取首页路径 const home = authStore.authMenuListGet.filter(item => item.meta?.isHome === true); if (home.length > 0) { homePath = home[0].path || HOME_URL; } else { // 如果不存在首页设置第一个菜单为首页 const firstMenu = authStore.showMenuListGet[0]; if (firstMenu?.children && firstMenu.children.length > 0) { homePath = firstMenu.children[0].path || HOME_URL; } else if (firstMenu?.path) { homePath = firstMenu.path; } } // 3.添加动态路由 // 参考 SimpleAdmin:使用扁平化的菜单列表,删除 children,然后添加路由 authStore.flatMenuListGet.forEach(item => { // 删除 children,因为扁平化后不需要 const { children, ...routeItem } = item; // 转换组件路径(参考 use-menu-tree.ts 中的逻辑) if (routeItem.component && typeof routeItem.component === 'string') { // 后端返回的 component 格式可能是: // - 'views/api/upstreamInterface/index' // - 'api/upstreamInterface/index' // - '@/views/api/upstreamInterface/index' // - '/src/views/api/upstreamInterface/index' // 这里统一处理,避免出现重复前缀 let componentPath = routeItem.component; // 去掉开头的 @/、/src/、views/(兼容 /views/xxx) componentPath = componentPath .replace(/^@\/views\//, '') .replace(/^\/src\/views\//, '') .replace(/^\/?views\//, ''); // 去掉 .vue 后缀(如果有) componentPath = componentPath.replace(/\.vue$/, ''); // 尝试多种路径格式来匹配组件 // import.meta.glob('@/views/**/*.vue') 生成的键格式可能是: // - /src/views/... (Vite 默认) // - /Users/.../src/views/... (绝对路径) // - @/views/... (在某些配置下) // - ./src/views/... (相对路径) // 首先尝试精确匹配 let foundComponent = null; // 构建可能的路径格式 const possiblePaths = [ `/src/views/${componentPath}.vue`, `@/views/${componentPath}.vue`, `./src/views/${componentPath}.vue`, `src/views/${componentPath}.vue`, ]; // 尝试精确匹配 for (const path of possiblePaths) { if (modules[path]) { foundComponent = modules[path]; break; } } // 如果精确匹配失败,尝试模糊匹配(不区分大小写,支持路径部分匹配) if (!foundComponent) { // 规范化组件路径用于匹配 const normalizedPath = componentPath.toLowerCase().replace(/[\/\\]/g, '/'); const pathSegments = normalizedPath.split('/').filter(Boolean); // 尝试多种匹配策略 const matchingKey = Object.keys(modules).find(key => { const normalizedKey = key.toLowerCase().replace(/[\/\\]/g, '/'); // 策略1: 完全匹配(去掉 .vue 后缀) if (normalizedKey.endsWith(`${normalizedPath}.vue`)) return true; // 策略2: 匹配最后3段(例如:channel/servicechannels/index) if (pathSegments.length >= 3) { const last3 = pathSegments.slice(-3).join('/'); if (normalizedKey.endsWith(`${last3}.vue`)) return true; } // 策略3: 匹配最后2段 if (pathSegments.length >= 2) { const last2 = pathSegments.slice(-2).join('/'); if (normalizedKey.endsWith(`${last2}.vue`)) return true; } // 策略4: 非 index 文件名才做宽松匹配,避免 order/myOrders/index 与 order/waybillManage/index 串页 const keyParts = normalizedKey.split('/').filter(Boolean); if (pathSegments.length > 0) { const lastSegment = pathSegments[pathSegments.length - 1]; if (lastSegment === 'index') { return false; } const keyLastPart = keyParts[keyParts.length - 1]; if (keyLastPart && keyLastPart.replace('.vue', '') === lastSegment) { if ( pathSegments.length === 1 || keyParts.slice(0, -1).join('/').includes(pathSegments.slice(0, -1).join('/')) ) { return true; } } } // 策略5: 包含整个路径(作为后备) return normalizedKey.includes(`/${normalizedPath}.vue`) || normalizedKey.includes(normalizedPath); }); if (matchingKey) { foundComponent = modules[matchingKey]; } } if (foundComponent) { routeItem.component = foundComponent; } else { // 列出所有可用的模块键(仅用于兜底匹配) const allKeys = Object.keys(modules); if (allKeys.length > 0) { // 保留 allKeys,供下面兜底匹配使用 } // 如果找不到组件,尝试在所有模块键中查找最接近的匹配(不区分大小写) // 注意:import.meta.glob 返回的值本身就是懒加载函数,可以直接使用 const normalizedTarget = componentPath.toLowerCase().replace(/[\/\\]/g, '/'); const bestMatch = Object.keys(modules).find(key => { const normalizedKey = key.toLowerCase().replace(/[\/\\]/g, '/'); // 检查是否包含目标路径的关键部分 const targetParts = normalizedTarget.split('/').filter(Boolean); const keyParts = normalizedKey.split('/').filter(Boolean); // 检查最后几段是否匹配(更精确的匹配) if (targetParts.length >= 2 && keyParts.length >= 2) { const targetLast2 = targetParts.slice(-2).join('/'); const keyLast2 = keyParts.slice(-2).join('/'); if (keyLast2 === targetLast2) return true; } // 检查是否以目标路径结尾 return normalizedKey.endsWith(`${normalizedTarget}.vue`) || normalizedKey.includes(`/${normalizedTarget}.vue`); }); if (bestMatch) { // 使用找到的匹配(modules[key] 已经是懒加载函数) routeItem.component = modules[bestMatch]; } else { // 如果还是找不到,输出错误信息但不设置 component // 让路由使用默认布局或父路由的布局,避免完全失败 console.error(`无法找到组件: 原始路径=${routeItem.component}, 处理后路径=${componentPath}`); console.error('请检查:'); console.error('1. 文件是否存在'); console.error('2. 路径是否正确(大小写、拼写)'); console.error('3. 后端返回的 component 字段是否正确'); // 不设置 component,让路由使用默认布局 // 这样路由仍然可以工作,只是没有对应的组件 delete routeItem.component; } } } // 直接使用路由配置中的真实路由名称,不使用映射 const route = routeItem as unknown as RouteRecordRaw; if (route.name) { // 记录已添加的路由名称 addedDynamicRouteNames.add(route.name as string); } if (route.meta?.isFull) { // 全屏页面,直接添加到根路由 router.addRoute(route); } else { // 普通页面:挂到路径最长的已有父路由下(兼容 /user/marketing/invite 等多段路径) const fullPath = String(route.path || ''); const parentRoute = router .getRoutes() .filter( (r) => Boolean(r.name) && r.path !== '/' && fullPath !== r.path && fullPath.startsWith(`${r.path}/`), ) .sort((a, b) => b.path.length - a.path.length)[0]; if (parentRoute?.name) { const relativePath = fullPath .slice(parentRoute.path.length) .replace(/^\//, ''); const childRoute: RouteRecordRaw = { ...route, path: relativePath || fullPath, }; const routeExists = (parentRoute.children || []).some( (child: any) => child.path === childRoute.path || child.path === fullPath || child.name === childRoute.name, ); if (!routeExists) { router.addRoute(parentRoute.name, childRoute); if (childRoute.name) { addedDynamicRouteNames.add(childRoute.name as string); } } } else { // 如果父路由不存在,直接添加到根路由 router.addRoute(route); } } }); return Promise.resolve(homePath); } catch (error) { // 当按钮 || 菜单请求出错时,重定向到登陆页 console.error('初始化动态路由失败:', error); const loginType = rbacUserStore.resolveLoginTypeForRedirect(); const currentPath = router.currentRoute.value.fullPath; userStore.logout(); rbacUserStore.logout(); const target = resolvePostLogoutLogin({ loginType, path: currentPath }); if (target.path) { router.replace({ path: target.path, query: target.query }); } else { router.replace({ name: target.name, query: target.query }); } return Promise.reject(error); } };