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 { 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} 条冲突未裁决。
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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" />
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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 ? (
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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 ? (
|
||||||
|
|||||||
Reference in New Issue
Block a user