- 作品库卡片重设计:书脊封面字块 + 衬线书名 + 一句话故事(缺失给占位) + 元信息行(题材徽章/StatusDot 待审/mono 时间),Card tone=card 浮起消塌陷 - EmptyState 中性化 + inline/card/bare 变体 + eyebrow(向后兼容) - loading 骨架化(Skeleton 拼作品库结构,motion-safe) - 设置页拆分 ProvidersSettings 489→198 + 5 个子组件,凭据点用 StatusDot - 次级页(向导/模板/codex/大纲)排版字阶与圆角 token 收敛
106 lines
3.3 KiB
TypeScript
106 lines
3.3 KiB
TypeScript
"use client";
|
|
|
|
import { CheckCircle2, PlugZap, Save, XCircle } from "lucide-react";
|
|
|
|
import { CapabilityBadges } from "@/components/settings/CapabilityBadges";
|
|
import { Badge } from "@/components/ui/Badge";
|
|
import { Button } from "@/components/ui/Button";
|
|
import { StatusDot } from "@/components/ui/StatusDot";
|
|
import { TextInput } from "@/components/ui/TextInput";
|
|
import type { CapabilitiesView } from "@/lib/api/types";
|
|
import type { KnownProvider } from "@/lib/settings/providers";
|
|
|
|
export interface TestResult {
|
|
ok: boolean;
|
|
capabilities: CapabilitiesView;
|
|
}
|
|
|
|
interface CredentialRowProps {
|
|
provider: KnownProvider;
|
|
masked: string | null;
|
|
draft: string;
|
|
saving: boolean;
|
|
testing: boolean;
|
|
result: TestResult | undefined;
|
|
onDraftChange: (value: string) => void;
|
|
onSave: () => void;
|
|
onTest: () => void;
|
|
}
|
|
|
|
// 单个 API-key 提供商行:脱敏态 + Key 输入 + 保存/测试 + 探活结果。
|
|
export function CredentialRow({
|
|
provider,
|
|
masked,
|
|
draft,
|
|
saving,
|
|
testing,
|
|
result,
|
|
onDraftChange,
|
|
onSave,
|
|
onTest,
|
|
}: CredentialRowProps) {
|
|
return (
|
|
<li className="px-4 py-4">
|
|
<div className="grid gap-3 lg:grid-cols-[10rem_8rem_minmax(12rem,1fr)_auto_auto] lg:items-center">
|
|
<span className="flex items-center gap-2 text-sm text-ink">
|
|
<StatusDot
|
|
tone={masked ? "success" : "neutral"}
|
|
label={masked ? "已配置" : "未配置"}
|
|
/>
|
|
{provider.label}
|
|
</span>
|
|
<span className="font-mono text-xs text-ink-soft">
|
|
{masked ?? "未配置"}
|
|
</span>
|
|
<label className="sr-only" htmlFor={`key-${provider.id}`}>
|
|
{provider.label} API Key
|
|
</label>
|
|
<TextInput
|
|
id={`key-${provider.id}`}
|
|
type="password"
|
|
autoComplete="off"
|
|
value={draft}
|
|
onChange={(e) => onDraftChange(e.target.value)}
|
|
placeholder={masked ? "输入新 Key 以更新" : "输入 API Key"}
|
|
/>
|
|
<Button
|
|
onClick={onSave}
|
|
disabled={saving || draft.trim().length === 0}
|
|
variant="primary"
|
|
size="sm"
|
|
>
|
|
<Save className="h-4 w-4" aria-hidden="true" />
|
|
{saving ? "保存中…" : masked ? "更新" : "添加"}
|
|
</Button>
|
|
<Button
|
|
onClick={onTest}
|
|
disabled={testing}
|
|
variant="secondary"
|
|
size="sm"
|
|
>
|
|
<PlugZap className="h-4 w-4" aria-hidden="true" />
|
|
{testing ? "测试中…" : "测试"}
|
|
</Button>
|
|
</div>
|
|
<p className="mt-2 text-xs leading-5 text-ink-soft lg:pl-[10.5rem]">
|
|
{masked
|
|
? "已保存脱敏凭据;输入新 Key 可覆盖更新。"
|
|
: "保存后再测试连接,成功后即可在档位路由中使用。"}
|
|
</p>
|
|
{result ? (
|
|
<div className="mt-2 flex flex-wrap items-center gap-2 lg:pl-[10.5rem]">
|
|
<Badge variant={result.ok ? "success" : "danger"}>
|
|
{result.ok ? (
|
|
<CheckCircle2 className="h-3 w-3" aria-hidden="true" />
|
|
) : (
|
|
<XCircle className="h-3 w-3" aria-hidden="true" />
|
|
)}
|
|
{result.ok ? "已连接" : "未连接"}
|
|
</Badge>
|
|
<CapabilityBadges caps={result.capabilities} />
|
|
</div>
|
|
) : null}
|
|
</li>
|
|
);
|
|
}
|