front/src/router/dynamicRouter.ts
zzh b9702cd0bc
All checks were successful
Build and Deploy Front / deploy (push) Successful in 5m47s
fix: 订单详情编辑体验、接口参数与价格表批量填价
优化运单详情抽屉操作栏与申报表编辑观感,限制我的订单入库后不可编辑;完善接口参数全量替换/脏检查与全局 Message;修复价格表批量填价选中被清与弹窗被遮挡。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-10 16:27:22 +08:00

330 lines
13 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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<string> = 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);
}
};