Files
Yaojia Wang 9aa0cddeaf feat(review): 审稿选章可用——列章端点 + 选章下拉全章可见 + 默认最近章
- 后端新增 GET /projects/{id}/chapters(列真实存在的章 + has_draft/accepted/reviewed_at),TDD 10 测
- gen:api 重生成 TS 客户端(ChapterListItem)
- 选章下拉去掉 hidden(手机/窄屏也可切章);选项以真实章为准 + 大纲结构,
  无草稿章置灰(仅当前章例外),覆盖『写过但不在大纲』的章
- 无 ?chapter 时默认落到最近一个有草稿的章,而非硬编码第 1 章
2026-07-12 17:52:40 +02:00

94 lines
3.1 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { notFound } from "next/navigation";
import { ReviewReport } from "@/components/review/ReviewReport";
import {
fetchChapters,
fetchDraft,
fetchOutline,
fetchProject,
fetchReviews,
} from "@/lib/api/server";
import type { ProjectResponse } from "@/lib/api/types";
import {
buildReviewChapterOptions,
defaultReviewChapter,
} from "@/lib/review/chapterNav";
import { latestReview } from "@/lib/review/history";
import type { ChapterEntry } from "@/lib/workbench/chapter";
interface PageProps {
params: Promise<{ id: string }>;
searchParams: Promise<{ chapter?: string }>;
}
// 审稿报告页UX §6.4。Server Component 取项目 + 审稿留痕历史(新→旧);
// ReviewReportClient承载 SSE 重审 / 裁决 / 验收交互。
export default async function ReviewPage({ params, searchParams }: PageProps) {
const { id } = await params;
const { chapter } = await searchParams;
// 仅当项目确实取不到时才判 404审稿/草稿/大纲等次级拉取的瞬时错误不应把整页变成「找不到」。
let project: ProjectResponse;
try {
project = await fetchProject(id);
} catch {
notFound();
}
// 真实存在的章(含可审/已审标记,错误→[]):供选章枚举 + 决定默认章。
const chapterList = await fetchChapters(id);
// 无 ?chapter 时默认落到最近一个有草稿的章,而非硬编码第 1 章。
const chapterNo = parsePositiveInt(chapter) ?? defaultReviewChapter(chapterList);
// 审稿留痕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],
}));
// 选章下拉选项:以真实章为准(覆盖「写过但不在大纲」的章),大纲标题补短名,标注可审/已审。
const chapterOptions = buildReviewChapterOptions(
chapterList,
chapters,
chapterNo,
);
return (
// key=章号:客户端切章(?chapter=N 变化)时强制重挂载,用新章审稿/草稿刷新状态,
// 避免 ReviewReport 内 useState 基线沿用上一章。
<ReviewReport
key={chapterNo}
project={project}
chapterNo={chapterNo}
initialReview={initialReview}
initialDraft={initialDraft}
chapters={chapters}
chapterOptions={chapterOptions}
/>
);
}
function parsePositiveInt(raw: string | undefined): number | null {
if (!raw) return null;
const n = Number.parseInt(raw, 10);
return Number.isInteger(n) && n > 0 ? n : null;
}