feat(review): 审稿选章可用——列章端点 + 选章下拉全章可见 + 默认最近章

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

View File

@@ -17,7 +17,11 @@ import { AppShell } from "@/components/AppShell";
import { Badge } from "@/components/ui/Badge";
import { Button } from "@/components/ui/Button";
import { Select } from "@/components/ui/Select";
import { reviewChapterHref, reviewChapterOptions } from "@/lib/review/chapterNav";
import {
reviewChapterHref,
reviewChapterOptions,
type ReviewChapterOption,
} from "@/lib/review/chapterNav";
import type { ChapterEntry } from "@/lib/workbench/chapter";
import type { ProjectResponse, ReviewHistoryItem } from "@/lib/api/types";
import { friendlyError } from "@/lib/errors/messages";
@@ -74,8 +78,10 @@ interface ReviewReportProps {
chapterNo: number;
initialReview: ReviewHistoryItem | undefined;
initialDraft: string;
// 章节导航目录(大纲章节):用于报告头部的「切换审稿章节」选择器
// 章节导航目录(大纲章节):无 chapterOptions 时回退用
chapters?: ChapterEntry[];
// 选章选项(以真实章为准,含可审/已审标记);优先于 chapters 渲染选择器。
chapterOptions?: ReviewChapterOption[];
}
// 审稿报告页主体UX §6.4 / §8.3 / §9
@@ -87,6 +93,7 @@ export function ReviewReport({
initialReview,
initialDraft,
chapters,
chapterOptions,
}: ReviewReportProps) {
const router = useRouter();
const review = useReviewStream();
@@ -460,7 +467,7 @@ export function ReviewReport({
<Select
controlSize="sm"
aria-label="切换审稿章节"
className="hidden max-w-[12rem] shrink-0 sm:block"
className="max-w-[12rem] shrink-0"
value={chapterNo}
onChange={(e) =>
router.push(
@@ -468,8 +475,10 @@ export function ReviewReport({
)
}
>
{reviewChapterOptions(chapters ?? [], chapterNo).map((opt) => (
<option key={opt.no} value={opt.no}>
{(
chapterOptions ?? reviewChapterOptions(chapters ?? [], chapterNo)
).map((opt: ReviewChapterOption) => (
<option key={opt.no} value={opt.no} disabled={opt.disabled}>
{opt.label}
</option>
))}