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:
Yaojia Wang
2026-07-11 07:36:32 +02:00
parent d3e9ae972f
commit 67e30a6863
14 changed files with 826 additions and 370 deletions

View File

@@ -0,0 +1,126 @@
"use client";
import { Save } from "lucide-react";
import { Button } from "@/components/ui/Button";
import { SectionHeader } from "@/components/ui/SectionHeader";
import { Select } from "@/components/ui/Select";
import { StatusNote } from "@/components/ui/StatusNote";
import { TextInput } from "@/components/ui/TextInput";
import {
KNOWN_PROVIDERS,
TIER_LABELS,
type RoutingDraft,
} from "@/lib/settings/providers";
// 档位路由「模型」输入框的候选 model iddatalist 建议,仍可自由输入)。按 provider 分组。
const MODEL_SUGGESTIONS: Record<string, readonly string[]> = {
anthropic: ["claude-opus-4", "claude-sonnet-4", "claude-3-5-haiku"],
deepseek: ["deepseek-chat", "deepseek-reasoner"],
kimi: ["moonshot-v1-128k", "moonshot-v1-32k", "kimi-k2"],
openai: ["gpt-4o", "gpt-4o-mini", "o3-mini"],
qwen: ["qwen-max", "qwen-plus", "qwen-turbo"],
glm: ["glm-4-plus", "glm-4-air", "glm-4-flash"],
gemini: ["gemini-2.0-flash", "gemini-1.5-pro"],
"kimi-code-key": ["kimi-for-coding"],
"kimi-code": ["kimi-for-coding"],
};
interface RoutingPanelProps {
routing: RoutingDraft[];
saving: boolean;
onProviderChange: (tier: string, providerId: string) => void;
onModelChange: (tier: string, model: string) => void;
onSave: () => void;
}
// 能力档位路由面板:写手/分析/轻量三档各指向一个 provider+modelUX §6.10)。
export function RoutingPanel({
routing,
saving,
onProviderChange,
onModelChange,
onSave,
}: RoutingPanelProps) {
return (
<section className="min-w-0">
<SectionHeader
eyebrow="路由"
title="能力档位路由"
description="写手、分析、轻量三类能力可分别指向不同模型。保存时只提交完整填写的行。"
/>
<StatusNote className="mt-3" variant="info">
稿
</StatusNote>
<ul className="mt-4 divide-y divide-line rounded border border-line bg-panel">
{routing.map((row) => (
<RoutingRow
key={row.tier}
row={row}
onProviderChange={onProviderChange}
onModelChange={onModelChange}
/>
))}
</ul>
<div className="mt-3 flex justify-end">
<Button
onClick={onSave}
disabled={saving}
variant="primary"
size="sm"
>
<Save className="h-4 w-4" aria-hidden="true" />
{saving ? "保存中…" : "保存档位路由"}
</Button>
</div>
</section>
);
}
interface RoutingRowProps {
row: RoutingDraft;
onProviderChange: (tier: string, providerId: string) => void;
onModelChange: (tier: string, model: string) => void;
}
function RoutingRow({ row, onProviderChange, onModelChange }: RoutingRowProps) {
const tierLabel = TIER_LABELS[row.tier] ?? row.tier;
return (
<li className="grid gap-3 px-4 py-3 text-sm md:grid-cols-[6rem_minmax(10rem,14rem)_1fr]">
<span className="self-center text-ink">{tierLabel}</span>
<label className="sr-only" htmlFor={`route-provider-${row.tier}`}>
{tierLabel}
</label>
<Select
id={`route-provider-${row.tier}`}
value={row.provider}
onChange={(e) => onProviderChange(row.tier, e.target.value)}
>
<option value=""></option>
{KNOWN_PROVIDERS.map((p) => (
<option key={p.id} value={p.id}>
{p.label}
</option>
))}
</Select>
<label className="sr-only" htmlFor={`route-model-${row.tier}`}>
{tierLabel}
</label>
<div className="min-w-0">
<TextInput
id={`route-model-${row.tier}`}
value={row.model}
onChange={(e) => onModelChange(row.tier, e.target.value)}
placeholder="model"
className="w-full font-mono"
list={`route-models-${row.tier}`}
/>
<datalist id={`route-models-${row.tier}`}>
{(MODEL_SUGGESTIONS[row.provider] ?? []).map((m) => (
<option key={m} value={m} />
))}
</datalist>
</div>
</li>
);
}