feat(web): 命令面板 combobox+可见入口 + sticky 导航壳 + 焦点环统一 + 抽屉锁滚动/关闭按钮 + 主题首帧修复 + 设置徽标/datalist

This commit is contained in:
Yaojia Wang
2026-06-30 08:56:23 +02:00
parent 164e98887e
commit 6854dac98f
11 changed files with 251 additions and 77 deletions

View File

@@ -7,6 +7,7 @@ import {
Route,
Save,
XCircle,
type LucideIcon,
} from "lucide-react";
import { useState, type ReactNode } from "react";
@@ -49,12 +50,35 @@ interface TestResult {
type SettingsSection = "routing" | "oauth" | "keys";
const SECTION_OPTIONS: Array<{ value: SettingsSection; label: string }> = [
{ value: "routing", label: "路由" },
{ value: "oauth", label: "OAuth" },
{ value: "keys", label: "API Key" },
// 分组的单一标签源:桌面 aside 与移动 SegmentedControl 共用,消除「档位路由 / 路由」漂移。
const SETTINGS_SECTIONS: Array<{
value: SettingsSection;
label: string;
icon: LucideIcon;
}> = [
{ value: "routing", label: "档位路由", icon: Route },
{ value: "oauth", label: "OAuth", icon: PlugZap },
{ value: "keys", label: "API Key", icon: KeyRound },
];
const SECTION_OPTIONS = SETTINGS_SECTIONS.map(({ value, label }) => ({
value,
label,
}));
// 档位路由「模型」输入框的候选 model iddatalist 建议,仍可自由输入)。按 provider 分组。
const MODEL_SUGGESTIONS: Record<string, readonly string[]> = {
anthropic: ["claude-opus-4", "claude-sonnet-4", "claude-3-5-haiku"],
deepseek: ["deepseek-chat", "deepseek-reasoner"],
kimi: ["moonshot-v1-128k", "moonshot-v1-32k", "kimi-k2"],
openai: ["gpt-4o", "gpt-4o-mini", "o3-mini"],
qwen: ["qwen-max", "qwen-plus", "qwen-turbo"],
glm: ["glm-4-plus", "glm-4-air", "glm-4-flash"],
gemini: ["gemini-2.0-flash", "gemini-1.5-pro"],
"kimi-code-key": ["kimi-for-coding"],
"kimi-code": ["kimi-for-coding"],
};
// 设置页主体UX §6.10):档位路由(可编辑)+ API-key 凭据行 + Kimi Code OAuth 连接区。
export function ProvidersSettings({
initial,
@@ -76,6 +100,16 @@ export function ProvidersSettings({
const maskedFor = (id: string): string | null =>
providers.find((p) => p.provider === id)?.masked_key ?? null;
const sectionDetail = (value: SettingsSection): string => {
if (value === "routing") {
return `${routing.filter((r) => r.provider && r.model).length}/3 已配置`;
}
if (value === "oauth") {
return kimiOauth.connected ? "Kimi 已连接" : "未连接";
}
return `${providers.length} 个凭据`;
};
const updateRouting = (tier: string, providerId: string): void => {
setRouting((prev) =>
prev.map((d) =>
@@ -163,28 +197,17 @@ export function ProvidersSettings({
return (
<div className="grid gap-6 lg:grid-cols-[12rem_1fr]">
<aside className="hidden lg:block">
<nav aria-label="设置分组" className={cardClass("sticky top-4 p-2")}>
<SettingsNavButton
active={activeSection === "routing"}
icon={<Route className="h-4 w-4" aria-hidden="true" />}
label="档位路由"
detail={`${routing.filter((r) => r.provider && r.model).length}/3 已配置`}
onClick={() => setActiveSection("routing")}
/>
<SettingsNavButton
active={activeSection === "oauth"}
icon={<PlugZap className="h-4 w-4" aria-hidden="true" />}
label="OAuth"
detail={kimiOauth.connected ? "Kimi 已连接" : "未连接"}
onClick={() => setActiveSection("oauth")}
/>
<SettingsNavButton
active={activeSection === "keys"}
icon={<KeyRound className="h-4 w-4" aria-hidden="true" />}
label="API Key"
detail={`${providers.length} 个凭据`}
onClick={() => setActiveSection("keys")}
/>
<nav aria-label="设置分组" className={cardClass("sticky top-20 p-2")}>
{SETTINGS_SECTIONS.map((section) => (
<SettingsNavButton
key={section.value}
active={activeSection === section.value}
icon={<section.icon className="h-4 w-4" aria-hidden="true" />}
label={section.label}
detail={sectionDetail(section.value)}
onClick={() => setActiveSection(section.value)}
/>
))}
</nav>
</aside>
@@ -236,15 +259,23 @@ export function ProvidersSettings({
<label className="sr-only" htmlFor={`route-model-${row.tier}`}>
{TIER_LABELS[row.tier] ?? row.tier}
</label>
<TextInput
id={`route-model-${row.tier}`}
value={row.model}
onChange={(e) =>
updateRoutingModel(row.tier, e.target.value)
}
placeholder="model"
className="font-mono"
/>
<div className="min-w-0">
<TextInput
id={`route-model-${row.tier}`}
value={row.model}
onChange={(e) =>
updateRoutingModel(row.tier, e.target.value)
}
placeholder="model"
className="w-full font-mono"
list={`route-models-${row.tier}`}
/>
<datalist id={`route-models-${row.tier}`}>
{(MODEL_SUGGESTIONS[row.provider] ?? []).map((m) => (
<option key={m} value={m} />
))}
</datalist>
</div>
</li>
))}
</ul>
@@ -445,16 +476,13 @@ function CapabilityBadges({ caps }: { caps: CapabilitiesView }) {
return (
<div className="flex gap-1.5">
{badges.map((b) => (
<span
<Badge
key={b.label}
className={`rounded border px-2 py-0.5 text-[11px] ${
b.on
? "border-cinnabar/20 bg-[var(--color-cinnabar-wash)] text-cinnabar"
: "border-line bg-bg text-ink-soft/50"
}`}
variant={b.on ? "accent" : "neutral"}
className="text-2xs"
>
{b.label}
</span>
</Badge>
))}
</div>
);