fix(web): 审稿页支持切换章节并打通验收后审下一章

This commit is contained in:
Yaojia Wang
2026-06-29 16:52:43 +02:00
parent a2849bf6b7
commit ab3f9e17b1
3 changed files with 83 additions and 14 deletions

View File

@@ -1,8 +1,15 @@
import { notFound } from "next/navigation"; import { notFound } from "next/navigation";
import { ReviewReport } from "@/components/review/ReviewReport"; import { ReviewReport } from "@/components/review/ReviewReport";
import { fetchDraft, fetchProject, fetchReviews } from "@/lib/api/server"; import {
fetchDraft,
fetchOutline,
fetchProject,
fetchReviews,
} from "@/lib/api/server";
import type { ProjectResponse } from "@/lib/api/types";
import { latestReview } from "@/lib/review/history"; import { latestReview } from "@/lib/review/history";
import type { ChapterEntry } from "@/lib/workbench/chapter";
interface PageProps { interface PageProps {
params: Promise<{ id: string }>; params: Promise<{ id: string }>;
@@ -17,22 +24,51 @@ export default async function ReviewPage({ params, searchParams }: PageProps) {
const { id } = await params; const { id } = await params;
const { chapter } = await searchParams; const { chapter } = await searchParams;
const chapterNo = parsePositiveInt(chapter) ?? DEFAULT_CHAPTER_NO; const chapterNo = parsePositiveInt(chapter) ?? DEFAULT_CHAPTER_NO;
// 仅当项目确实取不到时才判 404审稿/草稿/大纲等次级拉取的瞬时错误不应把整页变成「找不到」。
let project: ProjectResponse;
try { try {
const project = await fetchProject(id); project = await fetchProject(id);
const history = await fetchReviews(id, chapterNo);
// 终稿初值取已存草稿GET .../draft404→null→"");否则 final_text 为空 → accept 422。
const draft = await fetchDraft(id, chapterNo);
return (
<ReviewReport
project={project}
chapterNo={chapterNo}
initialReview={latestReview(history.reviews)}
initialDraft={draft?.content ?? ""}
/>
);
} catch { } catch {
notFound(); notFound();
} }
// 审稿留痕GET .../reviews失败→空历史
let initialReview;
try {
const history = await fetchReviews(id, chapterNo);
initialReview = latestReview(history.reviews);
} catch {
initialReview = undefined;
}
// 终稿初值取已存草稿GET .../draft404→null→"");否则 final_text 为空 → accept 422。
let initialDraft = "";
try {
const draft = await fetchDraft(id, chapterNo);
initialDraft = draft?.content ?? "";
} catch {
initialDraft = "";
}
// 大纲章节作章节导航目录fetchOutline 已对空/错误降级为 []);首个节拍当短标题。
const chapters: ChapterEntry[] = (await fetchOutline(id)).map((c) => ({
no: c.no,
title: c.beats?.[0],
}));
return (
// key=章号:客户端切章(?chapter=N 变化)时强制重挂载,用新章审稿/草稿刷新状态,
// 避免 ReviewReport 内 useState 基线沿用上一章。
<ReviewReport
key={chapterNo}
project={project}
chapterNo={chapterNo}
initialReview={initialReview}
initialDraft={initialDraft}
chapters={chapters}
/>
);
} }
function parsePositiveInt(raw: string | undefined): number | null { function parsePositiveInt(raw: string | undefined): number | null {

View File

@@ -1,13 +1,14 @@
"use client"; "use client";
import Link from "next/link"; import Link from "next/link";
import { ArrowRight, CheckCircle2, ListTree } from "lucide-react"; import { ArrowRight, CheckCircle2, ClipboardCheck, ListTree } from "lucide-react";
import { Button } from "@/components/ui/Button"; import { Button } from "@/components/ui/Button";
import { StatusNote } from "@/components/ui/StatusNote"; import { StatusNote } from "@/components/ui/StatusNote";
import type { AcceptResponse } from "@/lib/api/types"; import type { AcceptResponse } from "@/lib/api/types";
import { buildAcceptPreview } from "@/lib/review/accept-preview"; import { buildAcceptPreview } from "@/lib/review/accept-preview";
import { ThinkingIndicator } from "@/components/ThinkingIndicator"; import { ThinkingIndicator } from "@/components/ThinkingIndicator";
import { reviewChapterHref } from "@/lib/review/chapterNav";
import { buttonClass } from "@/lib/ui/variants"; import { buttonClass } from "@/lib/ui/variants";
interface AcceptPanelProps { interface AcceptPanelProps {
@@ -85,6 +86,13 @@ export function AcceptPanel({
<ArrowRight className="h-4 w-4" aria-hidden="true" /> <ArrowRight className="h-4 w-4" aria-hidden="true" />
{chapterNo + 1} {chapterNo + 1}
</Link> </Link>
<Link
href={reviewChapterHref(projectId, chapterNo + 1)}
className={buttonClass({ variant: "secondary" })}
>
<ClipboardCheck className="h-4 w-4" aria-hidden="true" />
{chapterNo + 1}
</Link>
<Link <Link
href={`/projects/${projectId}/outline`} href={`/projects/${projectId}/outline`}
className={buttonClass({ variant: "secondary" })} className={buttonClass({ variant: "secondary" })}

View File

@@ -2,6 +2,7 @@
import { useEffect, useMemo, useRef, useState } from "react"; import { useEffect, useMemo, useRef, useState } from "react";
import Link from "next/link"; import Link from "next/link";
import { useRouter } from "next/navigation";
import { import {
AlertTriangle, AlertTriangle,
CheckCircle2, CheckCircle2,
@@ -12,6 +13,9 @@ import {
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 { Select } from "@/components/ui/Select";
import { reviewChapterHref, reviewChapterOptions } from "@/lib/review/chapterNav";
import type { ChapterEntry } from "@/lib/workbench/chapter";
import type { ProjectResponse, ReviewHistoryItem } from "@/lib/api/types"; import type { ProjectResponse, ReviewHistoryItem } from "@/lib/api/types";
import { friendlyError } from "@/lib/errors/messages"; import { friendlyError } from "@/lib/errors/messages";
import { import {
@@ -62,6 +66,8 @@ interface ReviewReportProps {
chapterNo: number; chapterNo: number;
initialReview: ReviewHistoryItem | undefined; initialReview: ReviewHistoryItem | undefined;
initialDraft: string; initialDraft: string;
// 章节导航目录(大纲章节):用于报告头部的「切换审稿章节」选择器。
chapters?: ChapterEntry[];
} }
// 审稿报告页主体UX §6.4 / §8.3 / §9 // 审稿报告页主体UX §6.4 / §8.3 / §9
@@ -72,7 +78,9 @@ export function ReviewReport({
chapterNo, chapterNo,
initialReview, initialReview,
initialDraft, initialDraft,
chapters,
}: ReviewReportProps) { }: ReviewReportProps) {
const router = useRouter();
const review = useReviewStream(); const review = useReviewStream();
const accept = useAccept(); const accept = useAccept();
const conflictFix = useRefine(); // 采纳无补丁冲突时,临时调 AI 重写定位段 const conflictFix = useRefine(); // 采纳无补丁冲突时,临时调 AI 重写定位段
@@ -403,6 +411,23 @@ export function ReviewReport({
<h1 className="min-w-0 flex-1 truncate font-serif text-lg text-ink"> <h1 className="min-w-0 flex-1 truncate font-serif text-lg text-ink">
{chapterNo} 稿 {chapterNo} 稿
</h1> </h1>
<Select
controlSize="sm"
aria-label="切换审稿章节"
className="hidden max-w-[12rem] shrink-0 sm:block"
value={chapterNo}
onChange={(e) =>
router.push(
reviewChapterHref(project.id, Number(e.target.value)),
)
}
>
{reviewChapterOptions(chapters ?? [], chapterNo).map((opt) => (
<option key={opt.no} value={opt.no}>
{opt.label}
</option>
))}
</Select>
<span className="hidden shrink-0 font-mono text-xs text-ink-soft sm:inline"> <span className="hidden shrink-0 font-mono text-xs text-ink-soft sm:inline">
{conflictCount} {conflictCount}
</span> </span>