Files
writer-work-flow/apps/web/components/settings/CredentialRow.tsx
Yaojia Wang 67e30a6863 feat(ui): P3 页面级应用(作品库卡片/空态/加载/设置/次级页)
- 作品库卡片重设计:书脊封面字块 + 衬线书名 + 一句话故事(缺失给占位) +
  元信息行(题材徽章/StatusDot 待审/mono 时间),Card tone=card 浮起消塌陷
- EmptyState 中性化 + inline/card/bare 变体 + eyebrow(向后兼容)
- loading 骨架化(Skeleton 拼作品库结构,motion-safe)
- 设置页拆分 ProvidersSettings 489→198 + 5 个子组件,凭据点用 StatusDot
- 次级页(向导/模板/codex/大纲)排版字阶与圆角 token 收敛
2026-07-11 07:36:32 +02:00

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>
);
}