fix(web): 链相位语义色/失败可重试 + 模板填入中文标签/删除可撤销/工具卡文案 + 模板库 PageHeader
This commit is contained in:
@@ -5,6 +5,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 { StatusNote } from "@/components/ui/StatusNote";
|
||||
import { api } from "@/lib/api/client";
|
||||
import { buildResumeRequest } from "@/lib/chain/chain";
|
||||
import { friendlyFromApiError } from "@/lib/errors/messages";
|
||||
@@ -46,36 +47,44 @@ export function ChainAdjudication({
|
||||
const [drafts, setDrafts] = useState<DecisionDraft[]>([]);
|
||||
const [resuming, setResuming] = useState(false);
|
||||
|
||||
// 拉取 awaiting 章最近审稿冲突。抽成可复用回调,供进场 effect 与「重新加载」按钮共用。
|
||||
const load = useCallback(async (): Promise<boolean> => {
|
||||
setLoadState("loading");
|
||||
try {
|
||||
const { data, error } = await api.GET(
|
||||
"/projects/{project_id}/chapters/{chapter_no}/reviews",
|
||||
{
|
||||
params: {
|
||||
path: { project_id: projectId, chapter_no: chapterNo },
|
||||
},
|
||||
},
|
||||
);
|
||||
if (error || !data) {
|
||||
setLoadState("error");
|
||||
return false;
|
||||
}
|
||||
const next = normalizeConflicts(latestReview(data.reviews));
|
||||
setConflicts(next);
|
||||
setDrafts(emptyDecisions(next.length));
|
||||
setLoadState("ready");
|
||||
return true;
|
||||
} catch {
|
||||
setLoadState("error");
|
||||
return false;
|
||||
}
|
||||
}, [projectId, chapterNo]);
|
||||
|
||||
useEffect(() => {
|
||||
let active = true;
|
||||
setLoadState("loading");
|
||||
void (async () => {
|
||||
try {
|
||||
const { data, error } = await api.GET(
|
||||
"/projects/{project_id}/chapters/{chapter_no}/reviews",
|
||||
{
|
||||
params: {
|
||||
path: { project_id: projectId, chapter_no: chapterNo },
|
||||
},
|
||||
},
|
||||
);
|
||||
if (!active) return;
|
||||
if (error || !data) {
|
||||
setLoadState("error");
|
||||
return;
|
||||
}
|
||||
const next = normalizeConflicts(latestReview(data.reviews));
|
||||
setConflicts(next);
|
||||
setDrafts(emptyDecisions(next.length));
|
||||
setLoadState("ready");
|
||||
} catch {
|
||||
if (active) setLoadState("error");
|
||||
}
|
||||
// 进场加载:组件已卸载则丢弃结果(避免 setState-after-unmount)。
|
||||
if (!active) return;
|
||||
await load();
|
||||
})();
|
||||
return () => {
|
||||
active = false;
|
||||
};
|
||||
}, [projectId, chapterNo]);
|
||||
}, [load]);
|
||||
|
||||
const onVerdict = useCallback((index: number, verdict: Verdict): void => {
|
||||
setDrafts((prev) => setVerdict(prev, index, verdict));
|
||||
@@ -116,9 +125,17 @@ export function ChainAdjudication({
|
||||
}
|
||||
if (loadState === "error") {
|
||||
return (
|
||||
<p className="text-sm text-conflict">
|
||||
加载第 {chapterNo} 章审稿冲突失败,请刷新重试。
|
||||
</p>
|
||||
<StatusNote variant="danger" title={`加载第 ${chapterNo} 章审稿冲突失败`}>
|
||||
<p>请检查网络后重新加载。</p>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => void load()}
|
||||
className="mt-3"
|
||||
>
|
||||
重新加载
|
||||
</Button>
|
||||
</StatusNote>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,8 @@ import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
|
||||
import { AppShell } from "@/components/AppShell";
|
||||
import { useToast } from "@/components/Toast";
|
||||
import { Button } from "@/components/ui/Button";
|
||||
import { StatusNote } from "@/components/ui/StatusNote";
|
||||
import { api } from "@/lib/api/client";
|
||||
import { chainPhase, chainResultView, type ChainKind } from "@/lib/chain/chain";
|
||||
import { friendlyFromApiError } from "@/lib/errors/messages";
|
||||
@@ -77,6 +79,11 @@ export function ChainPage({ project }: ChainPageProps) {
|
||||
if (jobId) poll.poll(jobId);
|
||||
}, [jobId, poll]);
|
||||
|
||||
// 链失败重试:重启轮询同一 job(覆盖轮询/网络抖动这类瞬时失败;图从检查点续)。
|
||||
const onRetry = useCallback((): void => {
|
||||
if (jobId) poll.poll(jobId);
|
||||
}, [jobId, poll]);
|
||||
|
||||
const showProgress = jobId !== null && result !== null;
|
||||
// 链运行中/等待裁决时禁止重复发起。
|
||||
const busy =
|
||||
@@ -112,13 +119,25 @@ export function ChainPage({ project }: ChainPageProps) {
|
||||
) : null}
|
||||
|
||||
{phase === "failed" ? (
|
||||
<p className="text-sm text-conflict">
|
||||
{poll.error ?? "链运行失败,请重试。"}
|
||||
</p>
|
||||
<StatusNote variant="danger" title="链运行失败">
|
||||
<p>{poll.error ?? "请稍后重试。"}</p>
|
||||
{jobId !== null ? (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={onRetry}
|
||||
className="mt-3"
|
||||
>
|
||||
重试
|
||||
</Button>
|
||||
) : null}
|
||||
</StatusNote>
|
||||
) : null}
|
||||
|
||||
{phase === "completed" ? (
|
||||
<p className="text-sm text-pass">本链已全部跑完。</p>
|
||||
<StatusNote variant="success" title="本链已全部跑完">
|
||||
<p>各章已写完并验收,可在大纲或正文里继续推进。</p>
|
||||
</StatusNote>
|
||||
) : null}
|
||||
</div>
|
||||
</AppShell>
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
"use client";
|
||||
|
||||
import { Badge } from "@/components/ui/Badge";
|
||||
import { Card } from "@/components/ui/Card";
|
||||
import type { BadgeVariant } from "@/lib/ui/variants";
|
||||
import type { ChainPhase, ChainResultView } from "@/lib/chain/chain";
|
||||
|
||||
interface ChainProgressProps {
|
||||
@@ -16,6 +18,14 @@ const PHASE_LABEL: Record<ChainPhase, string> = {
|
||||
failed: "已失败",
|
||||
};
|
||||
|
||||
// 相位 → 语义色:失败用危险色而非朱砂品牌色,运行/等待/完成各取信息/警示/成功。
|
||||
const PHASE_VARIANT: Record<ChainPhase, BadgeVariant> = {
|
||||
running: "info",
|
||||
awaiting: "warning",
|
||||
completed: "success",
|
||||
failed: "danger",
|
||||
};
|
||||
|
||||
// 链进度视图:相位徽标 + 进度条 + 已写章号清单(token/正文绝不展示,result 只含元数据)。
|
||||
export function ChainProgress({ phase, progress, result }: ChainProgressProps) {
|
||||
return (
|
||||
@@ -26,23 +36,22 @@ export function ChainProgress({ phase, progress, result }: ChainProgressProps) {
|
||||
>
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<h2 className="font-serif text-lg text-ink">链进度</h2>
|
||||
<span
|
||||
className="rounded bg-cinnabar/10 px-2 py-0.5 text-xs text-cinnabar"
|
||||
aria-live="polite"
|
||||
>
|
||||
<Badge variant={PHASE_VARIANT[phase]} aria-live="polite">
|
||||
{PHASE_LABEL[phase]}
|
||||
</span>
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="h-2 w-full overflow-hidden rounded bg-bg"
|
||||
role="progressbar"
|
||||
aria-label="链写章进度"
|
||||
aria-valuenow={progress}
|
||||
aria-valuemin={0}
|
||||
aria-valuemax={100}
|
||||
aria-valuetext={`已完成 ${result.written.length} 章`}
|
||||
>
|
||||
<div
|
||||
className="h-full bg-cinnabar transition-all"
|
||||
className="h-full bg-cinnabar motion-safe:transition-all"
|
||||
style={{ width: `${progress}%` }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user