- 后端新增 GET /projects/{id}/chapters(列真实存在的章 + has_draft/accepted/reviewed_at),TDD 10 测
- gen:api 重生成 TS 客户端(ChapterListItem)
- 选章下拉去掉 hidden(手机/窄屏也可切章);选项以真实章为准 + 大纲结构,
无草稿章置灰(仅当前章例外),覆盖『写过但不在大纲』的章
- 无 ?chapter 时默认落到最近一个有草稿的章,而非硬编码第 1 章
81 lines
2.9 KiB
TypeScript
81 lines
2.9 KiB
TypeScript
import type { ChapterListItem } from "@/lib/api/types";
|
||
import type { ChapterEntry } from "@/lib/workbench/chapter";
|
||
import { buildChapterEntries } from "@/lib/workbench/chapter";
|
||
|
||
export interface ReviewChapterOption {
|
||
no: number;
|
||
label: string;
|
||
// 有草稿正文=可审;无草稿章置灰(审稿依赖草稿,选空章会 422)。
|
||
disabled: boolean;
|
||
}
|
||
|
||
export function reviewChapterHref(projectId: string, chapterNo: number): string {
|
||
return `/projects/${projectId}/review?chapter=${chapterNo}`;
|
||
}
|
||
|
||
// 旧签名:仅按大纲章 + 当前章构建(无草稿状态)。保留兼容,无 chapterList 时回退用。
|
||
export function reviewChapterOptions(
|
||
chapters: readonly ChapterEntry[],
|
||
currentChapterNo: number,
|
||
): ReviewChapterOption[] {
|
||
return buildChapterEntries(chapters, currentChapterNo).map((chapter) => ({
|
||
no: chapter.no,
|
||
label: chapter.title
|
||
? `第 ${chapter.no} 章 · ${chapter.title}`
|
||
: `第 ${chapter.no} 章`,
|
||
disabled: false,
|
||
}));
|
||
}
|
||
|
||
function statusSuffix(item: ChapterListItem | undefined): string {
|
||
if (!item) return "";
|
||
if (item.accepted) return " · 已验收";
|
||
if (item.reviewed_at) return " · 已审";
|
||
if (item.has_draft) return " · 草稿";
|
||
return " · 空";
|
||
}
|
||
|
||
// 新签名:以「真实存在的章」(chapterList) 为准枚举,覆盖「写过但不在大纲」的章,
|
||
// 用大纲标题补短名,并标注可审/已审/已验收;无草稿章置灰(但当前章永不置灰)。
|
||
export function buildReviewChapterOptions(
|
||
chapterList: readonly ChapterListItem[],
|
||
outline: readonly ChapterEntry[],
|
||
currentChapterNo: number,
|
||
): ReviewChapterOption[] {
|
||
const titleByNo = new Map(outline.map((c) => [c.no, c.title]));
|
||
const itemByNo = new Map(chapterList.map((c) => [c.chapter_no, c]));
|
||
// 枚举真实写过的章(chapterList) ∪ 大纲章(outline,展示全书结构) ∪ 当前章。
|
||
const nos = new Set<number>([
|
||
...chapterList.map((c) => c.chapter_no),
|
||
...outline.map((c) => c.no),
|
||
currentChapterNo,
|
||
]);
|
||
return [...nos]
|
||
.sort((a, b) => a - b)
|
||
.map((no) => {
|
||
const item = itemByNo.get(no);
|
||
const title = titleByNo.get(no);
|
||
const base = title ? `第 ${no} 章 · ${title}` : `第 ${no} 章`;
|
||
return {
|
||
no,
|
||
label: base + statusSuffix(item),
|
||
// 无草稿的章不可审(审稿依赖草稿);仅当前章例外,始终可停留。
|
||
disabled: no !== currentChapterNo && !item?.has_draft,
|
||
};
|
||
});
|
||
}
|
||
|
||
// 无 ?chapter 时的默认章:优先最近一个有草稿的章,其次最大章号,兜底第 1 章。
|
||
export function defaultReviewChapter(
|
||
chapterList: readonly ChapterListItem[],
|
||
): number {
|
||
const withDraft = chapterList
|
||
.filter((c) => c.has_draft)
|
||
.map((c) => c.chapter_no);
|
||
if (withDraft.length > 0) return Math.max(...withDraft);
|
||
if (chapterList.length > 0) {
|
||
return Math.max(...chapterList.map((c) => c.chapter_no));
|
||
}
|
||
return 1;
|
||
}
|