import type { CurrentUserMenuNodeDto } from '../types/authSession'; export type GlobalMenuSearchItem = { /** 传给 navigateToView / setCurrentView 的视图键 */ viewKey: string; /** 展示名 */ label: string; /** 父级路径,如 Labeling › Labels */ breadcrumb?: string; }; /** 与 App.tsx / Sidebar 一致的菜单名解析 */ export function resolveMenuViewKey(name: string): string { const s = (name ?? '').trim(); if (!s) return 'Dashboard'; if (s === 'System Management') return 'System Menu'; if (s === 'Location Management') return 'Location Manager'; if (s === 'Account Manager') return 'Account Management'; if (s === 'Multiple Options') return 'Multiple Option Sets'; return s; } function normalizeLabel(name: string): string { return name.trim() || 'N/A'; } function displayMenuLabel(name: string): string { const s = normalizeLabel(name); if (s === 'Multiple Options') return 'Multiple Option Sets'; return s; } function isDirectory(node: CurrentUserMenuNodeDto): boolean { const children = node.children ?? []; return (node.menuType ?? 0) === 0 || children.length > 0; } function walkRemoteMenus( nodes: CurrentUserMenuNodeDto[], parentTrail: string[], push: (item: GlobalMenuSearchItem) => void, ): void { for (const node of nodes) { const raw = normalizeLabel(String(node.menuName ?? node.routerName ?? node.routeUrl ?? node.id ?? '')); const label = displayMenuLabel(raw); const children = node.children ?? []; if (isDirectory(node)) { if (label !== 'Management' && label !== 'N/A') { walkRemoteMenus(children, [...parentTrail, label], push); } else { walkRemoteMenus(children, parentTrail, push); } continue; } const viewKey = resolveMenuViewKey(label); if (viewKey === 'Log Out') continue; push({ viewKey, label, breadcrumb: parentTrail.length ? parentTrail.join(' › ') : undefined, }); } } const FALLBACK_NAVIGABLE: GlobalMenuSearchItem[] = [ { viewKey: 'Dashboard', label: 'Dashboard' }, { viewKey: 'Labels', label: 'Labels', breadcrumb: 'Labeling' }, { viewKey: 'Label Categories', label: 'Label Categories', breadcrumb: 'Labeling' }, { viewKey: 'Label Types', label: 'Label Types', breadcrumb: 'Labeling' }, { viewKey: 'Label Templates', label: 'Label Templates', breadcrumb: 'Labeling' }, { viewKey: 'Multiple Option Sets', label: 'Multiple Option Sets', breadcrumb: 'Labeling' }, { viewKey: 'Account Management', label: 'Account Management', breadcrumb: 'Management' }, { viewKey: 'Location Manager', label: 'Location Manager', breadcrumb: 'Management' }, { viewKey: 'Menu Management', label: 'Menu Management', breadcrumb: 'Management' }, { viewKey: 'System Menu', label: 'System Menu', breadcrumb: 'Management' }, { viewKey: 'Reports', label: 'Reports', breadcrumb: 'Management' }, { viewKey: 'Support', label: 'Support', breadcrumb: 'Management' }, ]; /** 从当前用户菜单树生成可跳转项(含 Dashboard) */ export function buildGlobalMenuSearchItems(menus?: CurrentUserMenuNodeDto[]): GlobalMenuSearchItem[] { const items: GlobalMenuSearchItem[] = [{ viewKey: 'Dashboard', label: 'Dashboard' }]; const seen = new Set(['Dashboard']); const push = (item: GlobalMenuSearchItem) => { if (seen.has(item.viewKey)) return; seen.add(item.viewKey); items.push(item); }; if ((menus?.length ?? 0) > 0) { walkRemoteMenus(menus ?? [], [], push); return items; } for (const item of FALLBACK_NAVIGABLE) { push(item); } return items; } /** 按关键词过滤(匹配 label / breadcrumb / viewKey) */ export function filterGlobalMenuSearchItems( items: GlobalMenuSearchItem[], query: string, limit = 12, ): GlobalMenuSearchItem[] { const q = query.trim().toLowerCase(); if (!q) return []; const scored = items .map((item) => { const label = item.label.toLowerCase(); const crumb = (item.breadcrumb ?? '').toLowerCase(); const key = item.viewKey.toLowerCase(); const hay = `${label} ${crumb} ${key}`; if (!hay.includes(q)) return null; let score = 0; if (label.startsWith(q)) score += 3; else if (label.includes(q)) score += 2; if (key.startsWith(q)) score += 2; if (crumb.includes(q)) score += 1; return { item, score }; }) .filter((x): x is { item: GlobalMenuSearchItem; score: number } => x != null); scored.sort((a, b) => b.score - a.score || a.item.label.localeCompare(b.item.label)); return scored.slice(0, limit).map((x) => x.item); }