404 lines
11 KiB
TypeScript
404 lines
11 KiB
TypeScript
|
|
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<string, ContextRouteLookupItem>()
|
||
|
|
): Map<string, ContextRouteLookupItem> {
|
||
|
|
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<string, ContextRouteLookupItem>
|
||
|
|
): 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<App.ObjectContext.DomainKey>('');
|
||
|
|
const objectType = ref<App.ObjectContext.ObjectType>('');
|
||
|
|
const objectId = ref('');
|
||
|
|
const objectName = ref('');
|
||
|
|
const objectSummary = ref<App.ObjectContext.Summary | null>(null);
|
||
|
|
const contextScopedMenus = ref<App.ObjectContext.Menu[]>([]);
|
||
|
|
const buttonCodes = ref<string[]>([]);
|
||
|
|
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<RouteLocationNormalized, 'query'>) {
|
||
|
|
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<RouteLocationRaw | null> {
|
||
|
|
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
|
||
|
|
};
|
||
|
|
});
|