import type { RouteMeta } from 'vue-router'; import type { ElegantConstRoute, LastLevelRouteKey } from '@elegant-router/types'; import { objectContextDomainConfigs } from '@/constants/object-context'; import { SYSTEM_SERVICE_PREFIX } from '@/constants/service'; import { createStaticRoutes } from '@/router/routes'; import { request } from '../request'; import { type ServiceRequestResult, safeJsonRequestConfig } from './shared'; type BackendMenuRoute = Omit & { id: string | number; children?: BackendMenuRoute[]; }; interface BackendUserRouteDTO { routes?: BackendMenuRoute[] | null; home?: string | null; } let userRoutePromise: Promise> | null = null; const staticObjectContextRouteMap = new Map( createStaticRoutes() .authRoutes.filter(route => objectContextDomainConfigs.some(config => config.domainKey === route.name)) .map(route => [route.name as App.ObjectContext.DomainKey, route]) ); export function clearUserRouteCache() { userRoutePromise = null; } function normalizeMenuRoute(route: BackendMenuRoute): Api.Route.MenuRoute { return { ...route, id: String(route.id), children: route.children?.map(child => normalizeMenuRoute(child)) }; } function normalizePath(path?: string | null) { if (!path) { return '/'; } return path.endsWith('/') && path !== '/' ? path.slice(0, -1) : path; } function isPathMatchedByPrefix(path: string, prefix: string) { const normalizedPath = normalizePath(path); const normalizedPrefix = normalizePath(prefix); return normalizedPath === normalizedPrefix || normalizedPath.startsWith(`${normalizedPrefix}/`); } function isTopLevelObjectContextEntryRoute(route: Api.Route.MenuRoute, config: App.ObjectContext.DomainConfig) { const routePath = normalizePath(route.path); return ( route.component?.startsWith('view.') && !route.children?.length && (route.name === config.entryRouteKey || routePath === normalizePath(config.entryRoutePath)) ); } function cloneStaticRouteAsMenuRoute(route: ElegantConstRoute, idPrefix: string): Api.Route.MenuRoute { return { ...route, id: `${idPrefix}:${String(route.name || route.path)}`, children: route.children?.map(child => cloneStaticRouteAsMenuRoute(child, idPrefix)) }; } function replaceWithStaticObjectContextDomainRoute(routes: Api.Route.MenuRoute[]) { let normalizedRoutes = [...routes]; objectContextDomainConfigs.forEach(config => { const hasDomainRootRoute = normalizedRoutes.some(route => route.name === config.domainKey); if (hasDomainRootRoute) { return; } const domainTopLevelRoutes = normalizedRoutes.filter(route => config.routePathPrefixes.some(prefix => isPathMatchedByPrefix(route.path, prefix)) ); const entryRoute = domainTopLevelRoutes.find(route => isTopLevelObjectContextEntryRoute(route, config)); if (!entryRoute) { return; } const staticDomainRoute = staticObjectContextRouteMap.get(config.domainKey); if (!staticDomainRoute) { return; } // Create a map of backend routes by name for quick lookup const backendRouteMap = new Map(); domainTopLevelRoutes.forEach(route => { if (route.name) { backendRouteMap.set(String(route.name), route); } }); // Clone static route but preserve backend route's meta for children // 待重构:拆 helper 以降低复杂度,暂以 disable 注释临时放行 // eslint-disable-next-line complexity function cloneStaticRoutePreservingBackendMeta(route: ElegantConstRoute, idPrefix: string): Api.Route.MenuRoute { const backendRoute = route.name ? backendRouteMap.get(String(route.name)) : undefined; const { children: _children, ...routeWithoutChildren } = route; const baseRoute: Api.Route.MenuRoute = { ...routeWithoutChildren, id: `${idPrefix}:${String(route.name || route.path)}` }; // If there's a backend route, preserve its meta if (backendRoute?.meta) { baseRoute.meta = { ...baseRoute.meta, title: backendRoute.meta.title || baseRoute.meta?.title || String(route.name || route.path), icon: backendRoute.meta.icon || baseRoute.meta?.icon, localIcon: backendRoute.meta.localIcon || baseRoute.meta?.localIcon, order: backendRoute.meta.order !== undefined && backendRoute.meta.order !== null ? backendRoute.meta.order : baseRoute.meta?.order, keepAlive: backendRoute.meta.keepAlive !== undefined && backendRoute.meta.keepAlive !== null ? backendRoute.meta.keepAlive : baseRoute.meta?.keepAlive, i18nKey: backendRoute.meta.i18nKey || baseRoute.meta?.i18nKey }; } // Recursively process children if (route.children?.length) { baseRoute.children = route.children.map(child => cloneStaticRoutePreservingBackendMeta(child, idPrefix)); } return baseRoute; } const wrappedDomainRoute = cloneStaticRoutePreservingBackendMeta( staticDomainRoute, `object-context:${config.domainKey}` ); // Merge entry route's meta to domain route if (entryRoute.meta) { wrappedDomainRoute.meta = { ...wrappedDomainRoute.meta, title: entryRoute.meta.title || wrappedDomainRoute.meta?.title || config.domainKey, icon: entryRoute.meta.icon || wrappedDomainRoute.meta?.icon, localIcon: entryRoute.meta.localIcon || wrappedDomainRoute.meta?.localIcon, order: entryRoute.meta.order !== undefined && entryRoute.meta.order !== null ? entryRoute.meta.order : wrappedDomainRoute.meta?.order, keepAlive: entryRoute.meta.keepAlive !== undefined && entryRoute.meta.keepAlive !== null ? entryRoute.meta.keepAlive : wrappedDomainRoute.meta?.keepAlive }; } const entryRouteIndex = normalizedRoutes.findIndex(route => route.id === entryRoute.id); const domainRouteIds = new Set(domainTopLevelRoutes.map(route => route.id)); normalizedRoutes = normalizedRoutes.filter(route => !domainRouteIds.has(route.id)); normalizedRoutes.splice(entryRouteIndex < 0 ? normalizedRoutes.length : entryRouteIndex, 0, wrappedDomainRoute); }); return normalizedRoutes; } function normalizeUserRoute(data: BackendUserRouteDTO): Api.Route.UserRoute { return { routes: replaceWithStaticObjectContextDomainRoute((data.routes ?? []).map(route => normalizeMenuRoute(route))), home: (data.home || 'system_user') as LastLevelRouteKey }; } /** 获取常量路由 */ export function fetchGetConstantRoutes() { return request({ ...safeJsonRequestConfig, url: '/route/getConstantRoutes' }); } /** 获取用户路由 */ export async function fetchGetUserRoutes(force = false): Promise> { if (!userRoutePromise || force) { userRoutePromise = request({ ...safeJsonRequestConfig, url: `${SYSTEM_SERVICE_PREFIX}/auth/get-user-routes` }).then(result => result as ServiceRequestResult); } const result = await userRoutePromise; if (result.error || !result.data) { userRoutePromise = null; return result as ServiceRequestResult; } return { ...result, data: normalizeUserRoute(result.data) }; } /** * 判断路由是否存在 * * @param routeName 路由名称 */ export async function fetchIsRouteExist(routeName: string): Promise> { const result = await fetchGetUserRoutes(); if (result.error || !result.data) { return { ...result, data: false } as unknown as ServiceRequestResult; } const isExist = result.data.routes.some(route => recursiveIsRouteExist(route, routeName)); return { ...result, data: isExist }; } function recursiveIsRouteExist(route: Api.Route.MenuRoute, routeName: string): boolean { if (route.name === routeName) { return true; } return route.children?.some(child => recursiveIsRouteExist(child, routeName)) || false; }