import { RouteRecordRaw } from 'vue-router'; import { useUserInfo } from '/@/stores/userInfo'; // import { useRequestOldRoutes } from '/@/stores/requestOldRoutes'; import pinia from '/@/stores/index'; import { Session } from '/@/utils/storage'; import { NextLoading } from '/@/utils/loading'; import { dynamicRoutes, notFoundAndNoPower } from '/@/router/route'; import { formatTwoStageRoutes, formatFlatteningRoutes, router } from '/@/router/index'; import { useRoutesList } from '/@/stores/routesList'; import { useTagsViewRoutes } from '/@/stores/tagsViewRoutes'; import SysMenuApi from '/@/api/SysMenuApi'; import { accessTokenKey } from '/@/utils/request'; // 后端控制路由 /** * 获取目录下的 .vue、.tsx 全部文件 * @method import.meta.glob * @link 参考:https://cn.vitejs.dev/guide/features.html#json */ const layouModules: any = import.meta.glob('../layout/routerView/*.{vue,tsx}'); const viewsModules: any = import.meta.glob('../views/**/*.{vue,tsx}'); const dynamicViewsModules: Record = Object.assign({}, { ...layouModules }, { ...viewsModules }); /** * 后端控制路由:初始化方法,防止刷新时路由丢失 * @method NextLoading 界面 loading 动画开始执行 * @method useUserInfo().setUserInfos() 触发初始化用户信息 pinia * @method useRequestOldRoutes().setRequestOldRoutes() 存储接口原始路由(未处理component),根据需求选择使用 * @method setAddRoute 添加动态路由 * @method setFilterMenuAndCacheTagsViewRoutes 设置路由到 pinia routesList 中(已处理成多级嵌套路由)及缓存多级嵌套数组处理后的一维数组 */ export async function initBackEndControlRoutes() { // 界面 loading 动画开始执行 if (window.nextLoading === undefined) NextLoading.start(); // 无 token 停止执行下一步 if (!Session.get(accessTokenKey)) return false; // 触发初始化用户信息 pinia // https://gitee.com/lyt-top/vue-next-admin/issues/I5F1HP await useUserInfo().getUserInfo(); // 获取路由菜单数据 const { data } = await getBackEndControlRoutes(); // 无登录权限时,添加判断 // https://gitee.com/lyt-top/vue-next-admin/issues/I64HVO const routeList = data ?? []; if (routeList.length === 0) return Promise.resolve(true); // 存储接口原始路由(未处理component),根据需求选择使用 // useRequestOldRoutes().setRequestOldRoutes(JSON.parse(JSON.stringify(data ?? []))); // 处理路由(component),替换 dynamicRoutes(/@/router/route)第一个顶级 children 的路由 //静态路由 let staticRoute = [ { path: '/website-competition', name: 'websiteCompetition', component: 'website/competition/index', meta: { title: '官网赛事信息', isHide: true, isKeepAlive: false, icon: 'iconfont icon-shouye', }, }, { path: '/personal', name: 'personal', component: 'personal/index', meta: { title: '个人中心', isHide: true, isKeepAlive: true, }, }, { path: '/system/config/design', name: 'configDesign', component: 'system/config/design', meta: { title: '配置设计', isKeepAlive: false, isHide: true, }, }, { path: '/system/config/render', name: 'configRender', component: 'system/config/render', meta: { title: '编辑配置项', isKeepAlive: false, isHide: true, }, }, { path: '/system/config/items', name: 'configItems', component: 'system/config/itemList', meta: { title: '配置项列表', isKeepAlive: false, isHide: true, }, }, { path: '/blog/article/operate', name: 'articleOperate', component: 'blog/article/operate', meta: { title: '新增文章', isKeepAlive: false, isHide: true, }, }, ]; // 工作台业务页需要携带赛事上下文,不能依赖菜单接口是否已经下发全部子菜单。 // 这些路由只作为隐藏的运行路由注册,左侧菜单仍以后端权限菜单为准。 // 兼容已部署数据库里的旧菜单数据:旧版“秩序册”曾错误复用 launcher 路由。 // 前端按稳定的路由名称/菜单编码修正运行地址,避免用户必须等待菜单种子重新入库。 const normalizedRouteList = normalizeScoringMenuRoutes(routeList); const routesWithScoringFallback = ensureScoringOperationalRoutes(normalizedRouteList); dynamicRoutes[0].children = await backEndComponent([...routesWithScoringFallback, ...staticRoute]); // dynamicRoutes[0].children = await backEndComponent(routeList); // 添加动态路由 await setAddRoute(); // 设置路由到 pinia routesList 中(已处理成多级嵌套路由)及缓存多级嵌套数组处理后的一维数组 setFilterMenuAndCacheTagsViewRoutes(); } /** * 设置路由到 pinia routesList 中(已处理成多级嵌套路由)及缓存多级嵌套数组处理后的一维数组 * @description 用于左侧菜单、横向菜单的显示 * @description 用于 tagsView、菜单搜索中:未过滤隐藏的(isHide) */ export async function setFilterMenuAndCacheTagsViewRoutes() { const storesRoutesList = useRoutesList(pinia); storesRoutesList.setRoutesList(dynamicRoutes[0].children as any); setCacheTagsViewRoutes(); } /** * 缓存多级嵌套数组处理后的一维数组 * @description 用于 tagsView、菜单搜索中:未过滤隐藏的(isHide) */ export function setCacheTagsViewRoutes() { const storesTagsView = useTagsViewRoutes(pinia); storesTagsView.setTagsViewRoutes(formatTwoStageRoutes(formatFlatteningRoutes(dynamicRoutes))[0].children); } /** * 处理路由格式及添加捕获所有路由或 404 Not found 路由 * @description 替换 dynamicRoutes(/@/router/route)第一个顶级 children 的路由 * @returns 返回替换后的路由数组 */ export function setFilterRouteEnd() { let filterRouteEnd: any = formatTwoStageRoutes(formatFlatteningRoutes(dynamicRoutes)); // notFoundAndNoPower 防止 404、401 不在 layout 布局中,不设置的话,404、401 界面将全屏显示 // 关联问题 No match found for location with path 'xxx' filterRouteEnd[0].children = [...filterRouteEnd[0].children, ...notFoundAndNoPower]; return filterRouteEnd; } /** * 添加动态路由 * @method router.addRoute * @description 此处循环为 dynamicRoutes(/@/router/route)第一个顶级 children 的路由一维数组,非多级嵌套 * @link 参考:https://next.router.vuejs.org/zh/api/#addroute */ export async function setAddRoute() { await setFilterRouteEnd().forEach((route: RouteRecordRaw) => { router.addRoute(route); }); } /** * 请求后端路由菜单接口 * @description isRequestRoutes 为 true,则开启后端控制路由 * @returns 返回后端路由菜单数据 */ export function getBackEndControlRoutes() { return SysMenuApi.getMenus(); } function normalizeScoringMenuRoutes(routes: any[]) { return routes.map((item) => { const children = item?.children?.length ? normalizeScoringMenuRoutes(item.children) : item?.children; const routeName = String(item?.name || item?.routeName || ''); const menuCode = String(item?.code || item?.meta?.code || ''); const title = String(item?.meta?.title || item?.title || ''); const path = String(item?.path || ''); // 识别 launcher: // 旧版种子中曾把 scoring:launcher 或 /scoring/launcher 误命名为“秩序册”; // 无论其 title 叫“秩序册”、“工作台”还是“打分系统工作台”,只要编码/路由为 launcher,就规范为 launcher const isLauncher = routeName === 'scoringLauncher' || menuCode === 'scoring:launcher' || path === '/scoring/launcher' || ((title === '工作台' || title === '计分工作台') && path.startsWith('/scoring')); const isOrderBook = !isLauncher && (routeName === 'scoringOrderBook' || menuCode === 'scoring:orderbook' || path === '/scoring/orderbook' || (title === '秩序册' && path.startsWith('/scoring'))); const isWorkbench = routeName === 'scoringWorkbench' || menuCode === 'scoring:workbench' || path === '/scoring/workbench' || (title === '成绩管理' && path.startsWith('/scoring')); const isScoringCast = routeName === 'scoringCast' || menuCode === 'scoring:cast' || path === '/scoring/cast' || ((title === '成绩投屏' || title === '赛事大屏') && path.startsWith('/scoring')); if (isScoringCast) { return { ...item, path: '/scoring/cast', component: 'scoring/cast/index', children, }; } if (isWorkbench) { return { ...item, path: '/scoring/workbench', component: 'scoring/workbench/index', children, }; } if (isLauncher) { return { ...item, path: '/scoring/launcher', component: 'scoring/launcher/index', children, }; } if (isOrderBook) { return { ...item, path: '/scoring/orderbook', component: 'scoring/orderbook/index', children, }; } return children === item?.children ? item : { ...item, children }; }); } function isScoringRelatedRoute(item: any): boolean { if (!item) return false; const path = String(item?.path || ''); const code = String(item?.code || item?.meta?.code || ''); const name = String(item?.name || item?.routeName || ''); const title = String(item?.meta?.title || item?.title || ''); return ( path === '/scoring' || path.startsWith('/scoring/') || code === 'scoring' || code.startsWith('scoring:') || name === 'scoring' || name.startsWith('scoring') || title.includes('打分系统工作台') || title.includes('计分工作台') || title === '工作台' ); } function ensureScoringOperationalRoutes(routes: any[]) { const paths = new Set(); const collectPaths = (items: any[]) => items.forEach((item) => { if (item?.path) paths.add(item.path); if (item?.children?.length) collectPaths(item.children); }); collectPaths(routes); const scoringEnabled = paths.has('/scoring') || paths.has('/scoring/launcher') || paths.has('/scoring/workbench') || paths.has('/scoring/orderbook') || paths.has('/scoring/cast') || routes.some((item) => isScoringRelatedRoute(item)); if (!scoringEnabled) return routes; // 提取原有菜单中的图标(若有),优先使用 let scoringIcon = 'ele-Monitor'; const findScoringIcon = (items: any[]) => { for (const item of items) { const icon = item?.meta?.icon || item?.icon; if (isScoringRelatedRoute(item) && icon) { scoringIcon = icon; return; } if (item?.children?.length) findScoringIcon(item.children); } }; findScoringIcon(routes); // 从原有树中清理所有打分系统相关节点,并记录首个打分节点的插入位置,彻底避免出现重复菜单或多余的秩序册 let firstScoringIndex = -1; const cleanRoutes = (items: any[]): any[] => { const result: any[] = []; for (let i = 0; i < items.length; i++) { const item = items[i]; if (isScoringRelatedRoute(item)) { if (firstScoringIndex === -1) { firstScoringIndex = result.length; } continue; } if (item?.children?.length) { result.push({ ...item, children: cleanRoutes(item.children), }); } else { result.push(item); } } return result; }; const routesWithoutScoring = cleanRoutes(routes); if (firstScoringIndex === -1) firstScoringIndex = routesWithoutScoring.length; // 唯一的打分系统工作台父级目录及其 4 个标准业务子菜单: // 1. 打分系统工作台:选择计分模式和赛事 // 2. 秩序册:秩序册导入 // 3. 成绩管理:出场名单、打分单与更正 // 4. 成绩投屏:大屏投屏操作台 const scoringParent = { path: '/scoring', name: 'scoring', component: 'layout/routerView/parent', redirect: '/scoring/launcher', meta: { title: '打分系统工作台', isHide: false, isKeepAlive: true, icon: scoringIcon || 'ele-Trophy', }, children: [ { path: '/scoring/launcher', name: 'scoringLauncher', component: 'scoring/launcher/index', meta: { title: '打分系统工作台', isHide: false, isKeepAlive: true, icon: 'ele-Monitor', }, }, { path: '/scoring/orderbook', name: 'scoringOrderBook', component: 'scoring/orderbook/index', meta: { title: '秩序册', isHide: false, isKeepAlive: true, icon: 'ele-Notebook', }, }, { path: '/scoring/workbench', name: 'scoringWorkbench', component: 'scoring/workbench/index', meta: { title: '成绩管理', isHide: false, isKeepAlive: true, icon: 'ele-Management', }, }, { path: '/scoring/cast', name: 'scoringCast', component: 'scoring/cast/index', meta: { title: '成绩投屏', isHide: false, isKeepAlive: true, icon: 'ele-DataLine', }, }, ], }; routesWithoutScoring.splice(firstScoringIndex, 0, scoringParent); return routesWithoutScoring; } /** * 重新请求后端路由菜单接口 * @description 用于菜单管理界面刷新菜单(未进行测试) * @description 路径:/src/views/system/menu/component/addMenu.vue */ export async function setBackEndControlRefreshRoutes() { await getBackEndControlRoutes(); } /** * 后端路由 component 转换 * @param routes 后端返回的路由表数组 * @returns 返回处理成函数后的 component */ export function backEndComponent(routes: any) { if (!routes) return; return routes.map((item: any) => { if (item.component === '') { item.component = 'layout/routerView/parent'; } if (item.component) item.component = dynamicImport(dynamicViewsModules, item.component as string); item.children && item.children.length > 0 && backEndComponent(item.children); return item; }); } /** * 后端路由 component 转换函数 * @param dynamicViewsModules 获取目录下的 .vue、.tsx 全部文件 * @param component 当前要处理项 component * @returns 返回处理成函数后的 component */ export function dynamicImport(dynamicViewsModules: Record, component: string) { const keys = Object.keys(dynamicViewsModules); const matchKeys = keys.filter((key) => { const k = key.replace(/..\/views|../, ''); return k.startsWith(`${component}`) || k.startsWith(`/${component}`); }); if (matchKeys?.length === 1) { const matchKey = matchKeys[0]; return dynamicViewsModules[matchKey]; } if (matchKeys?.length > 1) { const exactKey = matchKeys.find((key) => { const k = key.replace(/..\/views|../, ''); return k === `${component}.vue` || k === `/${component}.vue` || k === `${component}.tsx` || k === `/${component}.tsx`; }); if (exactKey) return dynamicViewsModules[exactKey]; return false; } }