import { computed, ref } from 'vue'; import type { LocationQueryRaw, RouteLocationNormalized, RouteLocationRaw } from 'vue-router'; import { defineStore } from 'pinia'; import type { ElegantConstRoute } from '@elegant-router/types'; import { OBJECT_CONTEXT_QUERY_KEY, getObjectContextDomainConfigByPath, isObjectContextEntryPath } from '@/constants/object-context'; import { fetchGetObjectContext } from '@/service/api/object-context'; import { $t } from '@/locales'; import { SetupStoreId } from '@/enum'; import { useRouteStore } from '../route'; function createEmptyState(): App.ObjectContext.State { return { domainKey: '', objectType: '', objectId: '', objectName: '', objectSummary: null, contextScopedMenus: [], buttonCodes: [], defaultRouteKey: '', defaultRoutePath: '', isReady: false }; } function normalizePath(path: string) { if (!path) { return '/'; } return path.endsWith('/') && path !== '/' ? path.slice(0, -1) : path; } function isRouteMatchedByPrefix(path: string, prefix: string) { const normalizedPath = normalizePath(path); const normalizedPrefix = normalizePath(prefix); return normalizedPath === normalizedPrefix || normalizedPath.startsWith(`${normalizedPrefix}/`); } function findDomainRootRoute( routes: ElegantConstRoute[], config: App.ObjectContext.DomainConfig ): ElegantConstRoute | null { for (const route of routes) { if (config.routePathPrefixes.some(prefix => isRouteMatchedByPrefix(route.path, prefix))) { return route; } if (route.children?.length) { const matchedChild = findDomainRootRoute(route.children, config); if (matchedChild) { return matchedChild; } } } return null; } function isEntryRoute(route: ElegantConstRoute, config: App.ObjectContext.DomainConfig) { return route.name === config.entryRouteKey || normalizePath(route.path) === normalizePath(config.entryRoutePath); } function getContextMenuLabel(route: ElegantConstRoute) { const routeName = String(route.name || route.path); return route.meta?.i18nKey ? $t(route.meta.i18nKey) : String(route.meta?.title || routeName); } type ContextRouteLookupItem = { key: string; label: string; routeKey: string | null; routePath: string | null; }; function createContextRouteLookup( routes: ElegantConstRoute[], lookup = new Map() ): Map { routes.forEach(route => { const routeName = route.name ? String(route.name) : ''; const routePath = route.path ? String(route.path) : ''; const item: ContextRouteLookupItem = { key: routeName || routePath, label: getContextMenuLabel(route), routeKey: routeName || null, routePath: routePath || null }; if (routeName) { lookup.set(routeName, item); } if (routePath) { lookup.set(routePath, item); } route.children?.forEach(child => { createContextRouteLookup([child], lookup); }); }); return lookup; } function enrichContextMenu( menu: App.ObjectContext.Menu, routeLookup: Map ): App.ObjectContext.Menu { const matchedRoute = routeLookup.get(String(menu.routeKey || '')) || routeLookup.get(String(menu.routePath || '')) || routeLookup.get(menu.key); return { key: matchedRoute?.key || menu.key, label: menu.label || matchedRoute?.label || menu.key, routeKey: menu.routeKey || matchedRoute?.routeKey || null, routePath: menu.routePath || matchedRoute?.routePath || null, children: menu.children?.map(child => enrichContextMenu(child, routeLookup)) || [] }; } function getLeafRoutes(routes: ElegantConstRoute[]): ElegantConstRoute[] { return routes.flatMap(route => { if (route.children?.length) { return getLeafRoutes(route.children); } return [route]; }); } export const useObjectContextStore = defineStore(SetupStoreId.ObjectContext, () => { const routeStore = useRouteStore(); const domainKey = ref(''); const objectType = ref(''); const objectId = ref(''); const objectName = ref(''); const objectSummary = ref(null); const contextScopedMenus = ref([]); const buttonCodes = ref([]); const defaultRouteKey = ref(''); const defaultRoutePath = ref(''); const isReady = ref(false); const hasContext = computed(() => isReady.value && Boolean(domainKey.value) && Boolean(objectId.value)); function patchState(state: App.ObjectContext.State) { domainKey.value = state.domainKey; objectType.value = state.objectType; objectId.value = state.objectId; objectName.value = state.objectName; objectSummary.value = state.objectSummary; contextScopedMenus.value = state.contextScopedMenus; buttonCodes.value = state.buttonCodes; defaultRouteKey.value = state.defaultRouteKey; defaultRoutePath.value = state.defaultRoutePath; isReady.value = state.isReady; } function clearContext() { patchState(createEmptyState()); } function resolveDefaultRoute( config: App.ObjectContext.DomainConfig, domainRoutes: ElegantConstRoute[], context: Api.ObjectContext.ContextInfo ) { const leafRoutes = getLeafRoutes(domainRoutes); const defaultRouteByKey = (routeKey?: string | null) => leafRoutes.find(route => route.name === routeKey); const defaultRouteByPath = (routePath?: string | null) => leafRoutes.find(route => normalizePath(route.path) === normalizePath(routePath || '')); const matchedContextByKey = defaultRouteByKey(context.defaultRouteKey); if (matchedContextByKey?.name && matchedContextByKey.path) { return { defaultRouteKey: String(matchedContextByKey.name), defaultRoutePath: matchedContextByKey.path }; } const matchedContextByPath = defaultRouteByPath(context.defaultRoutePath); if (matchedContextByPath?.name && matchedContextByPath.path) { return { defaultRouteKey: String(matchedContextByPath.name), defaultRoutePath: matchedContextByPath.path }; } const matchedFallbackByKey = defaultRouteByKey(config.fallbackDefaultRouteKey); if (matchedFallbackByKey?.name && matchedFallbackByKey.path) { return { defaultRouteKey: String(matchedFallbackByKey.name), defaultRoutePath: matchedFallbackByKey.path }; } const matchedFallbackByPath = defaultRouteByPath(config.fallbackDefaultRoutePath); if (matchedFallbackByPath?.name && matchedFallbackByPath.path) { return { defaultRouteKey: String(matchedFallbackByPath.name), defaultRoutePath: matchedFallbackByPath.path }; } const [firstLeafRoute] = leafRoutes; if (firstLeafRoute?.name && firstLeafRoute.path) { return { defaultRouteKey: String(firstLeafRoute.name), defaultRoutePath: firstLeafRoute.path }; } return { defaultRouteKey: context.defaultRouteKey || config.fallbackDefaultRouteKey, defaultRoutePath: context.defaultRoutePath || config.fallbackDefaultRoutePath }; } function applyContext(config: App.ObjectContext.DomainConfig, context: Api.ObjectContext.ContextInfo) { const domainRootRoute = findDomainRootRoute(routeStore.authRoutes, config); const domainRoutes: ElegantConstRoute[] = domainRootRoute?.children?.filter((route: ElegantConstRoute) => !isEntryRoute(route, config)) || []; const routeLookup = createContextRouteLookup(domainRoutes); // 对象上下文菜单以接口返回为准,前端只补全跳转所需的本地路由信息。 const contextMenus = context.contextScopedMenus.map(menu => enrichContextMenu(menu, routeLookup)); const resolvedDefaultRoute = resolveDefaultRoute(config, domainRoutes, context); patchState({ ...context, contextScopedMenus: contextMenus, defaultRouteKey: resolvedDefaultRoute.defaultRouteKey, defaultRoutePath: resolvedDefaultRoute.defaultRoutePath, isReady: true }); } function getObjectIdFromRoute(route: Pick) { const routeObjectId = route.query?.[OBJECT_CONTEXT_QUERY_KEY]; if (Array.isArray(routeObjectId)) { return String(routeObjectId[0] || ''); } if (routeObjectId === null || routeObjectId === undefined) { return ''; } return String(routeObjectId); } function getContextQuery(targetObjectId = objectId.value): LocationQueryRaw { if (!targetObjectId) { return {}; } return { [OBJECT_CONTEXT_QUERY_KEY]: targetObjectId }; } function createEntryLocation(config: App.ObjectContext.DomainConfig): RouteLocationRaw { return { path: config.entryRoutePath }; } function createDefaultLocation(config: App.ObjectContext.DomainConfig, targetObjectId: string): RouteLocationRaw { const query = getContextQuery(targetObjectId); if (defaultRouteKey.value) { return { name: defaultRouteKey.value, query }; } if (defaultRoutePath.value) { return { path: defaultRoutePath.value, query }; } return { path: config.fallbackDefaultRoutePath, query }; } async function enterContext(config: App.ObjectContext.DomainConfig, targetObjectId: string) { const result = await fetchGetObjectContext(config, targetObjectId); if (!result.error && result.data) { applyContext(config, result.data); } return result; } async function switchContext(config: App.ObjectContext.DomainConfig, targetObjectId: string) { return enterContext(config, targetObjectId); } async function ensureContextByRoute(to: RouteLocationNormalized): Promise { const domainConfig = getObjectContextDomainConfigByPath(to.path); if (!domainConfig) { if (hasContext.value) { clearContext(); } return null; } const routeObjectId = getObjectIdFromRoute(to); if (!routeObjectId) { clearContext(); if (isObjectContextEntryPath(to.path, domainConfig)) { return null; } return createEntryLocation(domainConfig); } const isSameContext = hasContext.value && domainKey.value === domainConfig.domainKey && objectId.value === routeObjectId; if (!isSameContext) { const { error } = await enterContext(domainConfig, routeObjectId); if (error) { clearContext(); return createEntryLocation(domainConfig); } } if (isObjectContextEntryPath(to.path, domainConfig)) { return createDefaultLocation(domainConfig, routeObjectId); } return null; } function getMenuRouteLocation( menu: App.ObjectContext.Menu, targetObjectId = objectId.value ): RouteLocationRaw | null { const query = getContextQuery(targetObjectId); if (menu.routeKey) { return { name: menu.routeKey, query }; } if (menu.routePath) { return { path: menu.routePath, query }; } return null; } return { domainKey, objectType, objectId, objectName, objectSummary, contextScopedMenus, buttonCodes, defaultRouteKey, defaultRoutePath, isReady, hasContext, clearContext, enterContext, switchContext, ensureContextByRoute, getContextQuery, getMenuRouteLocation }; });