返回 DeepSeek-Reasonix
SettingsNavigation.tsx
根目录 / desktop / frontend / src / components / SettingsNavigation.tsx
1 import { useMemo, useState, type ReactNode } from "react";
2 import {
3 Activity,
4 ChartNoAxesColumn,
5 Bot,
6 Box,
7 Cable,
8 Database,
9 Globe,
10 HardDrive,
11 Info,
12 Keyboard,
13 LockKeyhole,
14 Network,
15 Package,
16 Palette,
17 Plug,
18 Search,
19 Server,
20 Settings2,
21 ShieldCheck,
22 Sparkles,
23 Users,
24 Webhook,
25 X,
26 } from "lucide-react";
27 import { useT, type DictKey } from "../lib/i18n";
28 import type { SettingsTab } from "../lib/types";
29
30 export const SETTINGS_NAV_TABS: SettingsTab[] = [
31 "general", "bots", "models", "providers", "model-stats", "mcp", "remote", "skills", "subagents", "plugins", "memory",
32 "hooks", "diagnostics", "shortcuts", "permissions", "sandbox", "network", "browser", "appearance", "storage", "updates",
33 ];
34
35 const SETTINGS_TAB_GROUPS: { labelKey: DictKey; tabs: SettingsTab[] }[] = [
36 { labelKey: "settings.navGroup.preferences", tabs: ["general"] },
37 { labelKey: "settings.tab.models", tabs: ["models", "providers", "model-stats"] },
38 { labelKey: "settings.navGroup.connections", tabs: ["bots", "mcp", "remote"] },
39 { labelKey: "settings.navGroup.capabilities", tabs: ["skills", "subagents", "plugins"] },
40 { labelKey: "settings.navGroup.context", tabs: ["memory"] },
41 { labelKey: "settings.navGroup.automation", tabs: ["hooks", "diagnostics"] },
42 { labelKey: "settings.navGroup.security", tabs: ["permissions", "sandbox", "network", "browser"] },
43 { labelKey: "settings.navGroup.application", tabs: ["appearance", "shortcuts", "storage", "updates"] },
44 ];
45
46 export type SettingsNavigationItem = {
47 id: SettingsTab;
48 label: string;
49 meta: string;
50 searchTerms?: string;
51 };
52
53 export function SettingsNavigation({
54 items,
55 activeTab,
56 onSelect,
57 }: {
58 items: SettingsNavigationItem[];
59 activeTab: SettingsTab;
60 onSelect: (tab: SettingsTab) => void;
61 }) {
62 const t = useT();
63 const [query, setQuery] = useState("");
64 const itemById = useMemo(() => new Map(items.map((item) => [item.id, item])), [items]);
65 const filteredGroups = useMemo(() => {
66 const normalized = query.trim().toLocaleLowerCase();
67 if (!normalized) return SETTINGS_TAB_GROUPS;
68 return SETTINGS_TAB_GROUPS.map((group) => {
69 const groupMatches = t(group.labelKey).toLocaleLowerCase().includes(normalized);
70 const tabs = group.tabs.filter((id) => {
71 if (groupMatches) return true;
72 const item = itemById.get(id);
73 return item ? `${item.label} ${item.meta} ${item.searchTerms ?? ""}`.toLocaleLowerCase().includes(normalized) : false;
74 });
75 return { ...group, tabs };
76 }).filter((group) => group.tabs.length > 0);
77 }, [itemById, query, t]);
78
79 return (
80 <nav className="settings-center__nav" aria-label={t("settings.title")}>
81 <label className="settings-center__search">
82 <Search size={15} aria-hidden="true" />
83 <input
84 value={query}
85 onChange={(event) => setQuery(event.currentTarget.value)}
86 placeholder={t("settings.searchPlaceholder")}
87 aria-label={t("settings.searchPlaceholder")}
88 />
89 {query && (
90 <button type="button" onClick={() => setQuery("")} aria-label={t("settings.searchClear")}>
91 <X size={14} aria-hidden="true" />
92 </button>
93 )}
94 </label>
95 <div className="settings-center__navgroups">
96 {filteredGroups.map((group) => (
97 <section className="settings-center__navgroup" key={group.labelKey}>
98 <div className="settings-center__navgroup-label">{t(group.labelKey)}</div>
99 <div className="settings-center__navitems">
100 {group.tabs.map((id) => {
101 const item = itemById.get(id);
102 if (!item) return null;
103 return (
104 <button
105 key={id}
106 className={`settings-center__navitem${activeTab === id ? " settings-center__navitem--active" : ""}`}
107 aria-current={activeTab === id ? "page" : undefined}
108 title={item.meta ? `${item.label} · ${item.meta}` : item.label}
109 onClick={() => onSelect(id)}
110 >
111 <span className="settings-center__navitem-main">
112 {settingsTabIcon(id)}
113 <span>{item.label}</span>
114 </span>
115 {item.meta && query.trim() && <small>{item.meta}</small>}
116 </button>
117 );
118 })}
119 </div>
120 </section>
121 ))}
122 {filteredGroups.length === 0 && (
123 <div className="settings-center__navempty" role="status">{t("settings.searchNoResults")}</div>
124 )}
125 </div>
126 </nav>
127 );
128 }
129
130 function settingsTabIcon(id: SettingsTab): ReactNode {
131 const props = { size: 17, strokeWidth: 1.8, "aria-hidden": true as const };
132 switch (id) {
133 case "general": return <Settings2 {...props} />;
134 case "models": return <Box {...props} />;
135 case "model-stats": return <ChartNoAxesColumn {...props} />;
136 case "providers": return <Cable {...props} />;
137 case "bots": return <Bot {...props} />;
138 case "mcp": return <Plug {...props} />;
139 case "remote": return <Server {...props} />;
140 case "skills": return <Sparkles {...props} />;
141 case "subagents": return <Users {...props} />;
142 case "plugins": return <Package {...props} />;
143 case "memory": return <Database {...props} />;
144 case "hooks": return <Webhook {...props} />;
145 case "diagnostics": return <Activity {...props} />;
146 case "shortcuts": return <Keyboard {...props} />;
147 case "permissions": return <ShieldCheck {...props} />;
148 case "sandbox": return <LockKeyhole {...props} />;
149 case "network": return <Network {...props} />;
150 case "appearance": return <Palette {...props} />;
151 case "storage": return <HardDrive {...props} />;
152 case "browser": return <Globe {...props} />;
153 case "updates": return <Info {...props} />;
154 }
155 }
156
156 lines Plain Text