refactor(web): 采用 Button/Card 原语替换多处手搓样式

This commit is contained in:
Yaojia Wang
2026-06-29 16:52:43 +02:00
parent 47eb42f274
commit d7fcb8a329
12 changed files with 71 additions and 44 deletions

View File

@@ -4,6 +4,7 @@ import { useCallback, useEffect, useState } from "react";
import { ConflictCard } from "@/components/review/ConflictCard"; import { ConflictCard } from "@/components/review/ConflictCard";
import { useToast } from "@/components/Toast"; import { useToast } from "@/components/Toast";
import { Button } from "@/components/ui/Button";
import { api } from "@/lib/api/client"; import { api } from "@/lib/api/client";
import { buildResumeRequest } from "@/lib/chain/chain"; import { buildResumeRequest } from "@/lib/chain/chain";
import { friendlyFromApiError } from "@/lib/errors/messages"; import { friendlyFromApiError } from "@/lib/errors/messages";
@@ -159,14 +160,14 @@ export function ChainAdjudication({
</ul> </ul>
)} )}
<button <Button
type="button" variant="primary"
onClick={() => void onResume()} onClick={() => void onResume()}
disabled={resuming || !allResolved(drafts)} disabled={resuming || !allResolved(drafts)}
className="self-start rounded bg-cinnabar px-4 py-2 text-sm text-white disabled:opacity-50" className="self-start"
> >
{resuming ? "续跑中…" : "裁决完成,续跑"} {resuming ? "续跑中…" : "裁决完成,续跑"}
</button> </Button>
{pending.length > 0 ? ( {pending.length > 0 ? (
<p className="text-xs text-conflict"> <p className="text-xs text-conflict">
{pending.length} {pending.length}

View File

@@ -1,5 +1,6 @@
"use client"; "use client";
import { Card } from "@/components/ui/Card";
import type { ChainPhase, ChainResultView } from "@/lib/chain/chain"; import type { ChainPhase, ChainResultView } from "@/lib/chain/chain";
interface ChainProgressProps { interface ChainProgressProps {
@@ -18,8 +19,9 @@ const PHASE_LABEL: Record<ChainPhase, string> = {
// 链进度视图:相位徽标 + 进度条 + 已写章号清单token/正文绝不展示result 只含元数据)。 // 链进度视图:相位徽标 + 进度条 + 已写章号清单token/正文绝不展示result 只含元数据)。
export function ChainProgress({ phase, progress, result }: ChainProgressProps) { export function ChainProgress({ phase, progress, result }: ChainProgressProps) {
return ( return (
<section <Card
className="flex flex-col gap-3 rounded border border-line bg-panel p-5" as="section"
className="flex flex-col gap-3 p-5"
aria-label="链进度" aria-label="链进度"
> >
<div className="flex items-center justify-between gap-3"> <div className="flex items-center justify-between gap-3">
@@ -58,6 +60,6 @@ export function ChainProgress({ phase, progress, result }: ChainProgressProps) {
{result.awaitingChapter} {result.awaitingChapter}
</p> </p>
) : null} ) : null}
</section> </Card>
); );
} }

View File

@@ -6,6 +6,7 @@ import { Flag, Plus } from "lucide-react";
import { AppShell } from "@/components/AppShell"; import { AppShell } from "@/components/AppShell";
import { Badge } from "@/components/ui/Badge"; import { Badge } from "@/components/ui/Badge";
import { Button } from "@/components/ui/Button"; import { Button } from "@/components/ui/Button";
import { Card } from "@/components/ui/Card";
import { EmptyState } from "@/components/ui/EmptyState"; import { EmptyState } from "@/components/ui/EmptyState";
import { PageHeader } from "@/components/ui/PageHeader"; import { PageHeader } from "@/components/ui/PageHeader";
import type { ForeshadowView, ProjectResponse } from "@/lib/api/types"; import type { ForeshadowView, ProjectResponse } from "@/lib/api/types";
@@ -137,10 +138,7 @@ function ForeshadowBoardSummary({
className="mb-3 grid grid-cols-2 gap-2 sm:grid-cols-4" className="mb-3 grid grid-cols-2 gap-2 sm:grid-cols-4"
> >
{LANES.map((status) => ( {LANES.map((status) => (
<div <Card key={status} className="px-3 py-2">
key={status}
className="rounded border border-line bg-panel px-3 py-2"
>
<div className="flex items-center justify-between gap-2"> <div className="flex items-center justify-between gap-2">
<Badge variant={SUMMARY_VARIANTS[status]}> <Badge variant={SUMMARY_VARIANTS[status]}>
{LANE_LABELS[status]} {LANE_LABELS[status]}
@@ -152,7 +150,7 @@ function ForeshadowBoardSummary({
{" · "} {" · "}
{total > 0 ? `${Math.round((counts[status] / total) * 100)}%` : "0%"} {total > 0 ? `${Math.round((counts[status] / total) * 100)}%` : "0%"}
</p> </p>
</div> </Card>
))} ))}
</section> </section>
); );

View File

@@ -5,6 +5,7 @@ import { Plus } from "lucide-react";
import { Button } from "@/components/ui/Button"; import { Button } from "@/components/ui/Button";
import { TextInput } from "@/components/ui/TextInput"; import { TextInput } from "@/components/ui/TextInput";
import { cardClass } from "@/lib/ui/variants";
import type { RegisterInput } from "@/lib/foreshadow/board"; import type { RegisterInput } from "@/lib/foreshadow/board";
interface RegisterFormProps { interface RegisterFormProps {
@@ -79,7 +80,7 @@ export function RegisterForm({
e.preventDefault(); e.preventDefault();
void submit(); void submit();
}} }}
className="rounded border border-line bg-panel p-4" className={cardClass("p-4")}
> >
<div className="grid grid-cols-2 gap-3 sm:grid-cols-3"> <div className="grid grid-cols-2 gap-3 sm:grid-cols-3">
<FormSlot label="代号 *" id="f-code"> <FormSlot label="代号 *" id="f-code">

View File

@@ -1,5 +1,6 @@
"use client"; "use client";
import { Button } from "@/components/ui/Button";
import type { IngestConflicts } from "@/lib/generation/cards"; import type { IngestConflicts } from "@/lib/generation/cards";
interface ConflictAdjudicationProps { interface ConflictAdjudicationProps {
@@ -56,22 +57,22 @@ export function ConflictAdjudication({
))} ))}
</ul> </ul>
<div className="flex gap-2"> <div className="flex gap-2">
<button <Button
type="button" type="button"
variant="danger"
onClick={onAcknowledge} onClick={onAcknowledge}
disabled={busy} disabled={busy}
className="rounded bg-conflict px-3 py-1.5 text-sm text-white disabled:opacity-50"
> >
</button> </Button>
<button <Button
type="button" type="button"
variant="secondary"
onClick={onCancel} onClick={onCancel}
disabled={busy} disabled={busy}
className="rounded border border-line px-3 py-1.5 text-sm text-ink disabled:opacity-50"
> >
</button> </Button>
</div> </div>
</div> </div>
); );

View File

@@ -16,7 +16,7 @@ import {
type ProjectSort, type ProjectSort,
type ProjectViewMode, type ProjectViewMode,
} from "@/lib/projects/projects"; } from "@/lib/projects/projects";
import { buttonClass, cn } from "@/lib/ui/variants"; import { buttonClass, cardClass, cn } from "@/lib/ui/variants";
interface ProjectLibraryProps { interface ProjectLibraryProps {
projects: ProjectResponse[]; projects: ProjectResponse[];
@@ -47,7 +47,11 @@ export function ProjectLibrary({ projects }: ProjectLibraryProps) {
return ( return (
<div className="space-y-4"> <div className="space-y-4">
<div className="grid gap-3 rounded border border-line bg-panel p-3 md:grid-cols-[1fr_auto_auto_auto] md:items-center"> <div
className={cardClass(
"grid gap-3 p-3 md:grid-cols-[1fr_auto_auto_auto] md:items-center",
)}
>
<label className="relative block"> <label className="relative block">
<span className="sr-only"></span> <span className="sr-only"></span>
<Search <Search
@@ -155,7 +159,10 @@ export function ProjectLibrary({ projects }: ProjectLibraryProps) {
<li> <li>
<Link <Link
href="/projects/new" href="/projects/new"
className="flex items-center justify-center gap-2 rounded border border-dashed border-line bg-panel/70 px-4 py-3 text-sm text-cinnabar transition-colors hover:border-cinnabar hover:bg-panel" className={buttonClass({
variant: "outline",
className: "w-full border-dashed px-4 py-3",
})}
> >
<Plus className="h-4 w-4" aria-hidden="true" /> <Plus className="h-4 w-4" aria-hidden="true" />
@@ -200,7 +207,10 @@ function NewProjectCard() {
return ( return (
<Link <Link
href="/projects/new" href="/projects/new"
className="group flex h-full min-h-[176px] flex-col items-center justify-center rounded border border-dashed border-line bg-panel/70 text-center transition-colors hover:border-cinnabar hover:bg-panel" className={buttonClass({
variant: "outline",
className: "group h-full min-h-[176px] flex-col border-dashed text-center",
})}
> >
<span className="mb-3 flex h-10 w-10 items-center justify-center rounded bg-[var(--color-cinnabar-wash)] text-cinnabar transition-colors group-hover:bg-cinnabar group-hover:text-panel"> <span className="mb-3 flex h-10 w-10 items-center justify-center rounded bg-[var(--color-cinnabar-wash)] text-cinnabar transition-colors group-hover:bg-cinnabar group-hover:text-panel">
<Plus className="h-5 w-5" aria-hidden="true" /> <Plus className="h-5 w-5" aria-hidden="true" />

View File

@@ -18,6 +18,7 @@ import {
type RuleLevel, type RuleLevel,
} from "@/lib/rules/rules"; } from "@/lib/rules/rules";
import { useRules } from "@/lib/rules/useRules"; import { useRules } from "@/lib/rules/useRules";
import { cardClass } from "@/lib/ui/variants";
interface RulesPageProps { interface RulesPageProps {
project: ProjectResponse; project: ProjectResponse;
@@ -120,21 +121,24 @@ export function RulesPage({ project, initialRules }: RulesPageProps) {
{groups[lv].map((rule) => ( {groups[lv].map((rule) => (
<li <li
key={rule.id} key={rule.id}
className="flex items-start justify-between gap-3 rounded border border-line bg-panel px-3 py-2 text-sm text-ink" className={cardClass(
"flex items-start justify-between gap-3 p-3 text-sm text-ink",
)}
> >
<span className="min-w-0 flex-1 break-words"> <span className="min-w-0 flex-1 break-words">
{rule.content} {rule.content}
</span> </span>
<button <Button
type="button" variant="danger"
size="sm"
disabled={busy} disabled={busy}
onClick={() => void remove(project.id, rule.id)} onClick={() => void remove(project.id, rule.id)}
aria-label="删除规则" aria-label="删除规则"
className="inline-flex shrink-0 items-center gap-1 rounded border border-transparent px-2 py-1 text-xs text-ink-soft transition-colors hover:border-conflict/25 hover:bg-conflict/10 hover:text-conflict disabled:opacity-50" className="shrink-0"
> >
<Trash2 className="h-3 w-3" aria-hidden="true" /> <Trash2 className="h-3 w-3" aria-hidden="true" />
</button> </Button>
</li> </li>
))} ))}
</ul> </ul>

View File

@@ -46,14 +46,15 @@ export function RefineView({
> >
<div className="mb-2 flex items-center justify-between"> <div className="mb-2 flex items-center justify-between">
<h3 className="font-semibold text-ink"></h3> <h3 className="font-semibold text-ink"></h3>
<button <Button
type="button" type="button"
variant="ghost"
size="icon"
onClick={onClose} onClick={onClose}
className="rounded border border-transparent p-1 text-ink-soft hover:border-line hover:text-cinnabar" aria-label="关闭"
aria-label="关闭回炉对比"
> >
<X className="h-4 w-4" aria-hidden="true" /> <X className="h-4 w-4" aria-hidden="true" />
</button> </Button>
</div> </div>
{segment.trim().length === 0 ? ( {segment.trim().length === 0 ? (

View File

@@ -4,6 +4,7 @@ import { CheckCircle2, CircleDashed, RotateCcw } from "lucide-react";
import { Badge } from "@/components/ui/Badge"; import { Badge } from "@/components/ui/Badge";
import { Button } from "@/components/ui/Button"; import { Button } from "@/components/ui/Button";
import { cardClass } from "@/lib/ui/variants";
import type { StyleDriftReport, StyleDriftSegment } from "@/lib/review/sse"; import type { StyleDriftReport, StyleDriftSegment } from "@/lib/review/sse";
interface StylePanelProps { interface StylePanelProps {
@@ -63,7 +64,7 @@ export function StylePanel({ style, incomplete, onRefine }: StylePanelProps) {
{style.segments.map((seg, i) => ( {style.segments.map((seg, i) => (
<li <li
key={i} key={i}
className="rounded border border-line bg-panel p-2" className={cardClass("p-2")}
data-testid="drift-segment" data-testid="drift-segment"
> >
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">

View File

@@ -8,6 +8,7 @@ import { Button } from "@/components/ui/Button";
import { Field } from "@/components/ui/Field"; import { Field } from "@/components/ui/Field";
import { StatusNote } from "@/components/ui/StatusNote"; import { StatusNote } from "@/components/ui/StatusNote";
import { TextArea } from "@/components/ui/TextArea"; import { TextArea } from "@/components/ui/TextArea";
import { buttonClass } from "@/lib/ui/variants";
interface StyleUploadProps { interface StyleUploadProps {
busy: boolean; busy: boolean;
@@ -76,7 +77,7 @@ export function StyleUpload({
</Field> </Field>
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<label className="cursor-pointer rounded border border-line px-3 py-1.5 text-sm text-ink hover:border-cinnabar hover:text-cinnabar"> <label className={buttonClass({ variant: "secondary", className: "cursor-pointer" })}>
<input <input
type="file" type="file"

View File

@@ -16,6 +16,7 @@ import {
isTemplateDraftValid, isTemplateDraftValid,
type TemplateDraft, type TemplateDraft,
} from "@/lib/templates/templates"; } from "@/lib/templates/templates";
import { cardClass } from "@/lib/ui/variants";
interface TemplatesManagerProps { interface TemplatesManagerProps {
// 初始模板列表Server Component 预取;失败给空数组 + loadError 文案)。 // 初始模板列表Server Component 预取;失败给空数组 + loadError 文案)。
@@ -87,7 +88,7 @@ export function TemplatesManager({ initial }: TemplatesManagerProps) {
return ( return (
<div className="flex flex-col gap-6"> <div className="flex flex-col gap-6">
<form <form
className="flex flex-col gap-3 rounded border border-line bg-panel p-5" className={cardClass("flex flex-col gap-3 p-5")}
aria-label="新建模板" aria-label="新建模板"
onSubmit={(e) => { onSubmit={(e) => {
e.preventDefault(); e.preventDefault();

View File

@@ -3,8 +3,10 @@
import { useCallback, useEffect, useState } from "react"; import { useCallback, useEffect, useState } from "react";
import { useToast } from "@/components/Toast"; import { useToast } from "@/components/Toast";
import { Button } from "@/components/ui/Button";
import { api } from "@/lib/api/client"; import { api } from "@/lib/api/client";
import type { TemplateResponse } from "@/lib/api/types"; import type { TemplateResponse } from "@/lib/api/types";
import { buttonClass } from "@/lib/ui/variants";
interface TemplateFillerProps { interface TemplateFillerProps {
// 可填入的目标输入字段名(如 brief / text空 → 不渲染(无可填字段)。 // 可填入的目标输入字段名(如 brief / text空 → 不渲染(无可填字段)。
@@ -44,13 +46,14 @@ export function TemplateFiller({ targetField, onFill }: TemplateFillerProps) {
if (!open) { if (!open) {
return ( return (
<button <Button
type="button" variant="secondary"
size="sm"
onClick={() => setOpen(true)} onClick={() => setOpen(true)}
className="self-start rounded border border-line px-3 py-1 text-sm text-ink-soft hover:text-ink" className="self-start"
> >
</button> </Button>
); );
} }
@@ -61,13 +64,13 @@ export function TemplateFiller({ targetField, onFill }: TemplateFillerProps) {
> >
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<span className="text-sm text-ink-soft">{field}</span> <span className="text-sm text-ink-soft">{field}</span>
<button <Button
type="button" variant="ghost"
size="sm"
onClick={() => setOpen(false)} onClick={() => setOpen(false)}
className="text-xs text-ink-soft hover:text-ink"
> >
</button> </Button>
</div> </div>
{templates === null ? ( {templates === null ? (
<p className="text-sm text-ink-soft"></p> <p className="text-sm text-ink-soft"></p>
@@ -84,7 +87,10 @@ export function TemplateFiller({ targetField, onFill }: TemplateFillerProps) {
setOpen(false); setOpen(false);
toast(`已填入模板「${t.title}」。`, "success"); toast(`已填入模板「${t.title}」。`, "success");
}} }}
className="w-full rounded border border-line px-3 py-2 text-left text-sm text-ink hover:border-cinnabar" className={buttonClass({
variant: "secondary",
className: "w-full justify-start text-left text-ink",
})}
> >
<span className="font-serif">{t.title}</span> <span className="font-serif">{t.title}</span>
{t.tool_key ? ( {t.tool_key ? (