fix(web): 审稿页支持切换章节并打通验收后审下一章
This commit is contained in:
@@ -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 .../draft,404→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 .../draft,404→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 {
|
||||||
|
|||||||
@@ -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" })}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user