YD_Event.Web/src/router/backEnd.ts

261 lines
10 KiB
TypeScript
Raw Normal View History

2025-12-17 10:38:15 +08:00
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<string, Function> = 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,
},
},
];
2026-09-06 22:36:26 +08:00
// 工作台业务页需要携带赛事上下文,不能依赖菜单接口是否已经下发全部子菜单。
// 这些路由只作为隐藏的运行路由注册,左侧菜单仍以后端权限菜单为准。
2026-09-09 14:25:44 +08:00
// 兼容已部署数据库里的旧菜单数据:旧版“秩序册”曾错误复用 launcher 路由。
// 前端按稳定的路由名称/菜单编码修正运行地址,避免用户必须等待菜单种子重新入库。
const normalizedRouteList = normalizeScoringMenuRoutes(routeList);
const routesWithScoringFallback = ensureScoringOperationalRoutes(normalizedRouteList);
2026-09-06 22:36:26 +08:00
dynamicRoutes[0].children = await backEndComponent([...routesWithScoringFallback, ...staticRoute]);
2025-12-17 10:38:15 +08:00
// 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();
}
2026-09-09 14:25:44 +08:00
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 isScoringOrderBookTitle = title === '秩序册' && String(item?.path || '').startsWith('/scoring/');
const isOrderBook = routeName === 'scoringOrderBook' || menuCode === 'scoring:orderbook' || isScoringOrderBookTitle;
if (!isOrderBook) return children === item?.children ? item : { ...item, children };
return {
...item,
path: '/scoring/orderbook',
component: 'scoring/orderbook/index',
children,
};
});
}
2026-09-06 22:36:26 +08:00
function ensureScoringOperationalRoutes(routes: any[]) {
const paths = new Set<string>();
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/workbench/index', meta: { title: '成绩投屏', isHide: true, isKeepAlive: true } },
].filter((item) => !paths.has(item.path));
return [...routes, ...fallbacks];
}
2025-12-17 10:38:15 +08:00
/**
* 重新请求后端路由菜单接口
* @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<string, Function>, 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) {
2026-09-09 14:10:46 +08:00
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];
2025-12-17 10:38:15 +08:00
return false;
}
}
2026-09-09 14:10:46 +08:00