refactor(web): 采用 Button/Card 原语替换多处手搓样式
This commit is contained in:
@@ -4,6 +4,7 @@ import { useCallback, useEffect, useState } from "react";
|
||||
|
||||
import { ConflictCard } from "@/components/review/ConflictCard";
|
||||
import { useToast } from "@/components/Toast";
|
||||
import { Button } from "@/components/ui/Button";
|
||||
import { api } from "@/lib/api/client";
|
||||
import { buildResumeRequest } from "@/lib/chain/chain";
|
||||
import { friendlyFromApiError } from "@/lib/errors/messages";
|
||||
@@ -159,14 +160,14 @@ export function ChainAdjudication({
|
||||
</ul>
|
||||
)}
|
||||
|
||||
<button
|
||||
type="button"
|
||||
<Button
|
||||
variant="primary"
|
||||
onClick={() => void onResume()}
|
||||
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 ? "续跑中…" : "裁决完成,续跑"}
|
||||
</button>
|
||||
</Button>
|
||||
{pending.length > 0 ? (
|
||||
<p className="text-xs text-conflict">
|
||||
还有 {pending.length} 条冲突未裁决。
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { Card } from "@/components/ui/Card";
|
||||
import type { ChainPhase, ChainResultView } from "@/lib/chain/chain";
|
||||
|
||||
interface ChainProgressProps {
|
||||
@@ -18,8 +19,9 @@ const PHASE_LABEL: Record<ChainPhase, string> = {
|
||||
// 链进度视图:相位徽标 + 进度条 + 已写章号清单(token/正文绝不展示,result 只含元数据)。
|
||||
export function ChainProgress({ phase, progress, result }: ChainProgressProps) {
|
||||
return (
|
||||
<section
|
||||
className="flex flex-col gap-3 rounded border border-line bg-panel p-5"
|
||||
<Card
|
||||
as="section"
|
||||
className="flex flex-col gap-3 p-5"
|
||||
aria-label="链进度"
|
||||
>
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
@@ -58,6 +60,6 @@ export function ChainProgress({ phase, progress, result }: ChainProgressProps) {
|
||||
第 {result.awaitingChapter} 章存在未决冲突,请在下方逐条裁决后续跑。
|
||||
</p>
|
||||
) : null}
|
||||
</section>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import { Flag, Plus } from "lucide-react";
|
||||
import { AppShell } from "@/components/AppShell";
|
||||
import { Badge } from "@/components/ui/Badge";
|
||||
import { Button } from "@/components/ui/Button";
|
||||
import { Card } from "@/components/ui/Card";
|
||||
import { EmptyState } from "@/components/ui/EmptyState";
|
||||
import { PageHeader } from "@/components/ui/PageHeader";
|
||||
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"
|
||||
>
|
||||
{LANES.map((status) => (
|
||||
<div
|
||||
key={status}
|
||||
className="rounded border border-line bg-panel px-3 py-2"
|
||||
>
|
||||
<Card key={status} className="px-3 py-2">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<Badge variant={SUMMARY_VARIANTS[status]}>
|
||||
{LANE_LABELS[status]}
|
||||
@@ -152,7 +150,7 @@ function ForeshadowBoardSummary({
|
||||
{" · "}
|
||||
{total > 0 ? `${Math.round((counts[status] / total) * 100)}%` : "0%"}
|
||||
</p>
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
</section>
|
||||
);
|
||||
|
||||
@@ -5,6 +5,7 @@ import { Plus } from "lucide-react";
|
||||
|
||||
import { Button } from "@/components/ui/Button";
|
||||
import { TextInput } from "@/components/ui/TextInput";
|
||||
import { cardClass } from "@/lib/ui/variants";
|
||||
import type { RegisterInput } from "@/lib/foreshadow/board";
|
||||
|
||||
interface RegisterFormProps {
|
||||
@@ -79,7 +80,7 @@ export function RegisterForm({
|
||||
e.preventDefault();
|
||||
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">
|
||||
<FormSlot label="代号 *" id="f-code">
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { Button } from "@/components/ui/Button";
|
||||
import type { IngestConflicts } from "@/lib/generation/cards";
|
||||
|
||||
interface ConflictAdjudicationProps {
|
||||
@@ -56,22 +57,22 @@ export function ConflictAdjudication({
|
||||
))}
|
||||
</ul>
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
<Button
|
||||
type="button"
|
||||
variant="danger"
|
||||
onClick={onAcknowledge}
|
||||
disabled={busy}
|
||||
className="rounded bg-conflict px-3 py-1.5 text-sm text-white disabled:opacity-50"
|
||||
>
|
||||
已知悉,强制入库
|
||||
</button>
|
||||
<button
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="secondary"
|
||||
onClick={onCancel}
|
||||
disabled={busy}
|
||||
className="rounded border border-line px-3 py-1.5 text-sm text-ink disabled:opacity-50"
|
||||
>
|
||||
取消,回去改卡
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -16,7 +16,7 @@ import {
|
||||
type ProjectSort,
|
||||
type ProjectViewMode,
|
||||
} from "@/lib/projects/projects";
|
||||
import { buttonClass, cn } from "@/lib/ui/variants";
|
||||
import { buttonClass, cardClass, cn } from "@/lib/ui/variants";
|
||||
|
||||
interface ProjectLibraryProps {
|
||||
projects: ProjectResponse[];
|
||||
@@ -47,7 +47,11 @@ export function ProjectLibrary({ projects }: ProjectLibraryProps) {
|
||||
|
||||
return (
|
||||
<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">
|
||||
<span className="sr-only">搜索作品</span>
|
||||
<Search
|
||||
@@ -155,7 +159,10 @@ export function ProjectLibrary({ projects }: ProjectLibraryProps) {
|
||||
<li>
|
||||
<Link
|
||||
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" />
|
||||
新建作品
|
||||
@@ -200,7 +207,10 @@ function NewProjectCard() {
|
||||
return (
|
||||
<Link
|
||||
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">
|
||||
<Plus className="h-5 w-5" aria-hidden="true" />
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
type RuleLevel,
|
||||
} from "@/lib/rules/rules";
|
||||
import { useRules } from "@/lib/rules/useRules";
|
||||
import { cardClass } from "@/lib/ui/variants";
|
||||
|
||||
interface RulesPageProps {
|
||||
project: ProjectResponse;
|
||||
@@ -120,21 +121,24 @@ export function RulesPage({ project, initialRules }: RulesPageProps) {
|
||||
{groups[lv].map((rule) => (
|
||||
<li
|
||||
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">
|
||||
{rule.content}
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
<Button
|
||||
variant="danger"
|
||||
size="sm"
|
||||
disabled={busy}
|
||||
onClick={() => void remove(project.id, rule.id)}
|
||||
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" />
|
||||
删除
|
||||
</button>
|
||||
</Button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
@@ -46,14 +46,15 @@ export function RefineView({
|
||||
>
|
||||
<div className="mb-2 flex items-center justify-between">
|
||||
<h3 className="font-semibold text-ink">回炉对比</h3>
|
||||
<button
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
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" />
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{segment.trim().length === 0 ? (
|
||||
|
||||
@@ -4,6 +4,7 @@ import { CheckCircle2, CircleDashed, RotateCcw } from "lucide-react";
|
||||
|
||||
import { Badge } from "@/components/ui/Badge";
|
||||
import { Button } from "@/components/ui/Button";
|
||||
import { cardClass } from "@/lib/ui/variants";
|
||||
import type { StyleDriftReport, StyleDriftSegment } from "@/lib/review/sse";
|
||||
|
||||
interface StylePanelProps {
|
||||
@@ -63,7 +64,7 @@ export function StylePanel({ style, incomplete, onRefine }: StylePanelProps) {
|
||||
{style.segments.map((seg, i) => (
|
||||
<li
|
||||
key={i}
|
||||
className="rounded border border-line bg-panel p-2"
|
||||
className={cardClass("p-2")}
|
||||
data-testid="drift-segment"
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
|
||||
@@ -8,6 +8,7 @@ import { Button } from "@/components/ui/Button";
|
||||
import { Field } from "@/components/ui/Field";
|
||||
import { StatusNote } from "@/components/ui/StatusNote";
|
||||
import { TextArea } from "@/components/ui/TextArea";
|
||||
import { buttonClass } from "@/lib/ui/variants";
|
||||
|
||||
interface StyleUploadProps {
|
||||
busy: boolean;
|
||||
@@ -76,7 +77,7 @@ export function StyleUpload({
|
||||
</Field>
|
||||
|
||||
<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
|
||||
type="file"
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
isTemplateDraftValid,
|
||||
type TemplateDraft,
|
||||
} from "@/lib/templates/templates";
|
||||
import { cardClass } from "@/lib/ui/variants";
|
||||
|
||||
interface TemplatesManagerProps {
|
||||
// 初始模板列表(Server Component 预取;失败给空数组 + loadError 文案)。
|
||||
@@ -87,7 +88,7 @@ export function TemplatesManager({ initial }: TemplatesManagerProps) {
|
||||
return (
|
||||
<div className="flex flex-col gap-6">
|
||||
<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="新建模板"
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
|
||||
@@ -3,8 +3,10 @@
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
|
||||
import { useToast } from "@/components/Toast";
|
||||
import { Button } from "@/components/ui/Button";
|
||||
import { api } from "@/lib/api/client";
|
||||
import type { TemplateResponse } from "@/lib/api/types";
|
||||
import { buttonClass } from "@/lib/ui/variants";
|
||||
|
||||
interface TemplateFillerProps {
|
||||
// 可填入的目标输入字段名(如 brief / text);空 → 不渲染(无可填字段)。
|
||||
@@ -44,13 +46,14 @@ export function TemplateFiller({ targetField, onFill }: TemplateFillerProps) {
|
||||
|
||||
if (!open) {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
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">
|
||||
<span className="text-sm text-ink-soft">选一个模板填入「{field}」</span>
|
||||
<button
|
||||
type="button"
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => setOpen(false)}
|
||||
className="text-xs text-ink-soft hover:text-ink"
|
||||
>
|
||||
收起
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
{templates === null ? (
|
||||
<p className="text-sm text-ink-soft">加载中…</p>
|
||||
@@ -84,7 +87,10 @@ export function TemplateFiller({ targetField, onFill }: TemplateFillerProps) {
|
||||
setOpen(false);
|
||||
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>
|
||||
{t.tool_key ? (
|
||||
|
||||
Reference in New Issue
Block a user