globalMenuSearch.ts
4.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
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<string>(['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);
}