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: '/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 || ''); const path = String(item?.path || ''); const isScoringOrderBookTitle = title === '秩序册' && String(item?.path || '').startsWith('/scoring/'); const isOrderBook = routeName === 'scoringOrderBook' || menuCode === 'scoring:orderbook' || isScoringOrderBookTitle; const isScoringCastTitle = title === '成绩投屏' && path.startsWith('/scoring/'); const isScoringCast = routeName === 'scoringCast' || menuCode === 'scoring:cast' || path === '/scoring/cast' || isScoringCastTitle; if (isScoringCast) { return { ...item, path: '/scoring/cast', component: 'scoring/screen/index', children, }; } if (!isOrderBook) return children === item?.children ? item : { ...item, children }; return { ...item, path: '/scoring/orderbook', component: 'scoring/orderbook/index', children, }; }); } 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'); if (!scoringEnabled) return routes; const fallbacks = [ { path: '/scoring/launcher', name: 'scoringLauncherFallback', component: 'scoring/launcher/index', meta: { title: '工作台', isKeepAlive: true } }, { path: '/scoring/orderbook', name: 'scoringOrderBookFallback', component: 'scoring/orderbook/index', meta: { title: '秩序册', isHide: true, isKeepAlive: true } }, { path: '/scoring/workbench', name: 'scoringWorkbenchFallback', component: 'scoring/workbench/index', meta: { title: '成绩管理', isHide: true, isKeepAlive: true } }, { path: '/scoring/cast', name: 'scoringCastFallback', component: 'scoring/screen/index', meta: { title: '成绩投屏', isHide: true, isKeepAlive: true } }, ].filter((item) => !paths.has(item.path)); return [...routes, ...fallbacks]; } /** * 重新请求后端路由菜单接口 * @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; } }