fix(web): 大纲生成只替换目标卷+重排确认+窗口徽标用lucide + 伏笔看板中文化/单一登记入口/Field表单

This commit is contained in:
Yaojia Wang
2026-06-30 08:56:23 +02:00
parent f0f925b195
commit 68606b84dc
9 changed files with 214 additions and 142 deletions

View File

@@ -83,17 +83,21 @@ describe("filterByViewVolume", () => {
});
describe("windowBadgeLabel", () => {
it("renders a range, half-open, or bare badge", () => {
it("renders a range, half-open, or bare badge without any glyph", () => {
expect(
windowBadgeLabel(win({ expected_close_from: 40, expected_close_to: 60 })),
).toBe("F-012 (40-60)");
expect(windowBadgeLabel(win({ expected_close_to: 60 }))).toBe(
"⚑ F-012 (≤60)",
);
).toBe("F-012 (40-60)");
expect(windowBadgeLabel(win({ expected_close_to: 60 }))).toBe("F-012 (≤60)");
expect(windowBadgeLabel(win({ expected_close_from: 40 }))).toBe(
"F-012 (≥40)",
"F-012 (≥40)",
);
expect(windowBadgeLabel(win({}))).toBe("F-012");
expect(windowBadgeLabel(win({}))).toBe("F-012");
});
it("never embeds the raw ⚑ glyph (icon comes from the render layer)", () => {
expect(
windowBadgeLabel(win({ expected_close_from: 40, expected_close_to: 60 })),
).not.toContain("⚑");
});
});

View File

@@ -47,16 +47,17 @@ export function filterByViewVolume(
return all.filter((ch) => ch.volume === view);
}
// 伏笔窗口徽标文案`⚑ F-012 (40-60)` / `⚑ F-012 (≤60)` / `⚑ F-012`。
// 伏笔窗口徽标文案(纯文本,旗标图标由渲染层的 lucide Flag 承载,不掺字形):
// `F-012 (40-60)` / `F-012 (≤60)` / `F-012 (≥40)` / `F-012`。
export function windowBadgeLabel(window: ForeshadowWindowView): string {
const from = window.expected_close_from;
const to = window.expected_close_to;
if (typeof from === "number" && typeof to === "number") {
return `${window.code} (${from}-${to})`;
return `${window.code} (${from}-${to})`;
}
if (typeof to === "number") return `${window.code} (≤${to})`;
if (typeof from === "number") return `${window.code} (≥${from})`;
return `${window.code}`;
if (typeof to === "number") return `${window.code} (≤${to})`;
if (typeof from === "number") return `${window.code} (≥${from})`;
return window.code;
}
// 接近回收:本章号落在窗口 [from,to] 内提示「本章可回收」UX §6.7 ⚑ 可回收)。

View File

@@ -11,8 +11,8 @@ const toast = vi.fn();
vi.mock("@/lib/api/client", () => ({ api: { POST: (...a: unknown[]) => post(...a) } }));
vi.mock("@/components/Toast", () => ({ useToast: () => toast }));
function makeChapter(no: number): OutlineChapterView {
return { no, volume: 1 };
function makeChapter(no: number, volume = 1): OutlineChapterView {
return { no, volume };
}
describe("useOutline", () => {
@@ -51,6 +51,25 @@ describe("useOutline", () => {
expect(toast).toHaveBeenCalledWith("大纲已生成", "success");
});
it("生成只替换目标卷,保留其它卷的已存章节", async () => {
const initial = [makeChapter(1, 1), makeChapter(2, 1), makeChapter(80, 2)];
const regenerated = [makeChapter(1, 1), makeChapter(2, 1), makeChapter(3, 1)];
post.mockResolvedValue({ data: { chapters: regenerated }, error: null });
const { result } = renderHook(() => useOutline(initial));
await act(async () => {
await result.current.generate("p1", 1);
});
// 卷 2 的第 80 章不被整列覆盖;卷 1 替换为新生成的三章。
expect(result.current.chapters).toEqual([
makeChapter(80, 2),
makeChapter(1, 1),
makeChapter(2, 1),
makeChapter(3, 1),
]);
});
it("data.chapters 为空时章节回退为空数组", async () => {
post.mockResolvedValue({ data: { chapters: null }, error: null });
const { result } = renderHook(() => useOutline([]));

View File

@@ -52,7 +52,12 @@ export function useOutline(initial: OutlineChapterView[]): UseOutline {
toast(friendly.text, "error");
return;
}
setChapters(data.chapters ?? []);
// 只替换目标卷的条目保留其它卷「AI 排大纲」是单卷操作,整列覆盖会误删别卷)。
const generated = data.chapters ?? [];
setChapters((prev) => [
...prev.filter((ch) => ch.volume !== volume),
...generated,
]);
setStatus("ready");
toast("大纲已生成", "success");
},