fix(web): 大纲生成只替换目标卷+重排确认+窗口徽标用lucide + 伏笔看板中文化/单一登记入口/Field表单
This commit is contained in:
@@ -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("⚑");
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -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 ⚑ 可回收)。
|
||||
|
||||
@@ -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([]));
|
||||
|
||||
@@ -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");
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user