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:
105
apps/web/components/settings/CredentialRow.tsx
Normal file
105
apps/web/components/settings/CredentialRow.tsx
Normal file
@@ -0,0 +1,105 @@
|
||||
"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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user