- 作品库卡片重设计:书脊封面字块 + 衬线书名 + 一句话故事(缺失给占位) + 元信息行(题材徽章/StatusDot 待审/mono 时间),Card tone=card 浮起消塌陷 - EmptyState 中性化 + inline/card/bare 变体 + eyebrow(向后兼容) - loading 骨架化(Skeleton 拼作品库结构,motion-safe) - 设置页拆分 ProvidersSettings 489→198 + 5 个子组件,凭据点用 StatusDot - 次级页(向导/模板/codex/大纲)排版字阶与圆角 token 收敛
86 lines
2.7 KiB
TypeScript
86 lines
2.7 KiB
TypeScript
"use client";
|
||
|
||
import { PlugZap } from "lucide-react";
|
||
|
||
import {
|
||
CredentialRow,
|
||
type TestResult,
|
||
} from "@/components/settings/CredentialRow";
|
||
import { EmptyState } from "@/components/ui/EmptyState";
|
||
import { SectionHeader } from "@/components/ui/SectionHeader";
|
||
import { API_KEY_PROVIDERS } from "@/lib/settings/providers";
|
||
import type { ProviderView } from "@/lib/api/types";
|
||
|
||
interface CredentialsPanelProps {
|
||
providers: ProviderView[];
|
||
drafts: Record<string, string>;
|
||
savingId: string | null;
|
||
testingId: string | null;
|
||
results: Record<string, TestResult>;
|
||
onDraftChange: (providerId: string, value: string) => void;
|
||
onSave: (providerId: string) => void;
|
||
onTest: (providerId: string) => void;
|
||
}
|
||
|
||
// 提供商凭据面板:API-key 提供商列表,逐行保存/测试(UX §6.10)。
|
||
export function CredentialsPanel({
|
||
providers,
|
||
drafts,
|
||
savingId,
|
||
testingId,
|
||
results,
|
||
onDraftChange,
|
||
onSave,
|
||
onTest,
|
||
}: CredentialsPanelProps) {
|
||
const maskedFor = (id: string): string | null =>
|
||
providers.find((p) => p.provider === id)?.masked_key ?? null;
|
||
|
||
return (
|
||
<section className="min-w-0">
|
||
<SectionHeader
|
||
eyebrow="凭据"
|
||
title="提供商凭据"
|
||
description="API Key 只用于后端探活和调用,列表中只显示脱敏后的已保存凭据。"
|
||
/>
|
||
<div className="mt-3 grid gap-2 text-xs text-ink-soft sm:grid-cols-3">
|
||
<StatItem label="已保存" value={providers.length} />
|
||
<StatItem label="可配置" value={API_KEY_PROVIDERS.length} />
|
||
<StatItem label="测试结果" value={Object.keys(results).length} />
|
||
</div>
|
||
{providers.length === 0 ? (
|
||
<EmptyState
|
||
icon={PlugZap}
|
||
title="还没有可用提供商"
|
||
description="至少连接一个提供商即可开始写作。求质量可选 Anthropic,求性价比可选 DeepSeek。"
|
||
className="mt-4"
|
||
/>
|
||
) : null}
|
||
<ul className="mt-4 divide-y divide-line rounded border border-line bg-panel">
|
||
{API_KEY_PROVIDERS.map((prov) => (
|
||
<CredentialRow
|
||
key={prov.id}
|
||
provider={prov}
|
||
masked={maskedFor(prov.id)}
|
||
draft={drafts[prov.id] ?? ""}
|
||
saving={savingId === prov.id}
|
||
testing={testingId === prov.id}
|
||
result={results[prov.id]}
|
||
onDraftChange={(value) => onDraftChange(prov.id, value)}
|
||
onSave={() => onSave(prov.id)}
|
||
onTest={() => onTest(prov.id)}
|
||
/>
|
||
))}
|
||
</ul>
|
||
</section>
|
||
);
|
||
}
|
||
|
||
function StatItem({ label, value }: { label: string; value: number }) {
|
||
return (
|
||
<div className="rounded border border-line bg-panel px-3 py-2">
|
||
{label} <span className="font-mono text-ink">{value}</span>
|
||
</div>
|
||
);
|
||
}
|