import { DEFAULT_APP_NAV_GROUP, type FrontendAppDefinition, type FrontendPermissionContext, type FrontendPermissionRule, type FrontendAppSectionDefinition, type FrontendSidebarNavigationDefinition, } from './app-contract'; export interface FrontendSidebarMenuItem { key: string; appId: string; label: string; path: string; group?: string; iconKey?: FrontendSidebarNavigationDefinition['iconKey']; description?: string; matchPaths?: string[]; order: number; } export interface FrontendSidebarMenuGroup { groupLabel: string; items: FrontendSidebarMenuItem[]; } export interface FrontendAppSectionNavItem { key: string; label: string; path: string; /** 所属菜单组 */ group?: string; iconKey?: FrontendSidebarNavigationDefinition['iconKey']; description?: string; end?: boolean; matchPaths?: string[]; order: number; } function matchesRule(values: string[] | undefined, rule: FrontendPermissionRule | undefined): boolean { if (!rule) { return true; } const currentValues = values ?? []; if (rule.allOf?.some((value) => !currentValues.includes(value))) { return false; } if (rule.anyOf && rule.anyOf.length > 0) { return rule.anyOf.some((value) => currentValues.includes(value)); } return true; } export function filterFrontendAppDefinitions( appDefinitions: FrontendAppDefinition[], permissionContext: FrontendPermissionContext, ): FrontendAppDefinition[] { return appDefinitions.filter((appDefinition) => { const visibility = appDefinition.visibility; if (!visibility) { return true; } if (visibility.enabled === false) { return false; } if (visibility.requiresAuthentication && !permissionContext.isAuthenticated) { return false; } if (!matchesRule(permissionContext.permissions, visibility.permissions)) { return false; } if (!matchesRule(permissionContext.roles, visibility.roles)) { return false; } if (!matchesRule(permissionContext.featureFlags, visibility.featureFlags)) { return false; } if ( visibility.allowedAppIds?.length && (!permissionContext.appId || !visibility.allowedAppIds.includes(permissionContext.appId)) ) { return false; } if ( visibility.allowedTenantIds?.length && (!permissionContext.tenantId || !visibility.allowedTenantIds.includes(permissionContext.tenantId)) ) { return false; } return true; }); } export function buildFrontendSidebarMenuGroups( appDefinitions: FrontendAppDefinition[], ): FrontendSidebarMenuGroup[] { const groupedItems = new Map(); appDefinitions.forEach((appDefinition, index) => { const sidebar = appDefinition.navigation?.sidebar; if (!sidebar || sidebar.visible === false) { return; } const groupLabel = sidebar.group || DEFAULT_APP_NAV_GROUP; const items = groupedItems.get(groupLabel) || []; items.push({ key: appDefinition.appId, appId: appDefinition.appId, label: sidebar.label, path: appDefinition.routeBase, group: groupLabel, iconKey: sidebar.iconKey, matchPaths: sidebar.matchPaths, order: sidebar.order ?? index, }); groupedItems.set(groupLabel, items); }); return Array.from(groupedItems.entries()) .map(([groupLabel, items]) => ({ groupLabel, items: items.sort((left, right) => { if (left.order !== right.order) { return left.order - right.order; } return left.label.localeCompare(right.label, 'zh-CN'); }), })) .sort((left, right) => left.groupLabel.localeCompare(right.groupLabel, 'zh-CN')); } export function buildFrontendAppSectionNavItems( sections: FrontendAppSectionDefinition[] | undefined, ): FrontendAppSectionNavItem[] { if (!sections || sections.length === 0) { return []; } return sections .map((section, index) => ({ key: section.key, label: section.label, path: section.path, group: section.group, iconKey: section.iconKey, description: section.description, end: section.end, matchPaths: section.matchPaths, order: section.order ?? index, })) .sort((left, right) => { if (left.order !== right.order) { return left.order - right.order; } return left.label.localeCompare(right.label, 'zh-CN'); }); } /** * 从所有 app 的 sections 构建侧边栏菜单组。 * * 规则: * - 每个 section 可以指定 group,没有指定则使用 app 的 displayName 作为默认组 * - 默认组(displayName)优先级更高,排在菜单最前面 * - 同一组内按 order 排序 */ export function buildFrontendSidebarMenuGroupsFromSections( appDefinitions: FrontendAppDefinition[], ): FrontendSidebarMenuGroup[] { // 收集所有 sections 及其所属 app 信息 const groupedItems = new Map(); appDefinitions.forEach((appDefinition) => { const sections = appDefinition.runtime.sections; if (!sections || sections.length === 0) { return; } const defaultGroupName = appDefinition.displayName; sections.forEach((section, index) => { const groupName = section.group || defaultGroupName; const isDefault = !section.group; // 没有指定 group 的是默认组 const existing = groupedItems.get(groupName) || { items: [], isDefault: false }; // 如果任一 section 使用该组作为默认组,则标记为默认组 if (isDefault) { existing.isDefault = true; } existing.items.push({ key: `${appDefinition.appId}:${section.key}`, appId: appDefinition.appId, label: section.label, path: section.path, group: groupName, iconKey: section.iconKey, description: section.description, matchPaths: section.matchPaths, order: section.order ?? index, }); groupedItems.set(groupName, existing); }); }); // 转换为数组并排序 return Array.from(groupedItems.entries()) .map(([groupLabel, { items, isDefault }]) => ({ groupLabel, isDefault, items: items.sort((left, right) => { if (left.order !== right.order) { return left.order - right.order; } return left.label.localeCompare(right.label, 'zh-CN'); }), })) .sort((left, right) => { // 默认组优先级更高,排在前面 if (left.isDefault !== right.isDefault) { return left.isDefault ? -1 : 1; } return left.groupLabel.localeCompare(right.groupLabel, 'zh-CN'); }) .map(({ groupLabel, items }) => ({ groupLabel, items })); // 移除 isDefault 字段 }