feat(ui): P3 页面级应用(作品库卡片/空态/加载/设置/次级页)
- 作品库卡片重设计:书脊封面字块 + 衬线书名 + 一句话故事(缺失给占位) + 元信息行(题材徽章/StatusDot 待审/mono 时间),Card tone=card 浮起消塌陷 - EmptyState 中性化 + inline/card/bare 变体 + eyebrow(向后兼容) - loading 骨架化(Skeleton 拼作品库结构,motion-safe) - 设置页拆分 ProvidersSettings 489→198 + 5 个子组件,凭据点用 StatusDot - 次级页(向导/模板/codex/大纲)排版字阶与圆角 token 收敛
This commit is contained in:
85
apps/web/components/settings/CredentialsPanel.tsx
Normal file
85
apps/web/components/settings/CredentialsPanel.tsx
Normal file
@@ -0,0 +1,85 @@
|
||||
"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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user