All checks were successful
Build and Deploy Front / deploy (push) Successful in 5m47s
优化运单详情抽屉操作栏与申报表编辑观感,限制我的订单入库后不可编辑;完善接口参数全量替换/脏检查与全局 Message;修复价格表批量填价选中被清与弹窗被遮挡。 Co-authored-by: Cursor <cursoragent@cursor.com>
330 lines
13 KiB
TypeScript
330 lines
13 KiB
TypeScript
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);
|
||
}
|
||
};
|
||
|