From cd251b940c8d9fd5a1bd6fe3c1ae33e632f3f7c0 Mon Sep 17 00:00:00 2001 From: XingfenD Date: Tue, 8 Sep 2026 14:56:46 +0800 Subject: [PATCH] feat(cbz): pure chapter-window helpers (chapterIndexAt/chapterWin) with table tests --- frontend/src/lib/virt.ts | 30 ++++++++++++++++++++++++++++++ frontend/test/virt.test.ts | 34 +++++++++++++++++++++++++++++++++- 2 files changed, 63 insertions(+), 1 deletion(-) diff --git a/frontend/src/lib/virt.ts b/frontend/src/lib/virt.ts index 03a75e5..64a8962 100644 --- a/frontend/src/lib/virt.ts +++ b/frontend/src/lib/virt.ts @@ -76,3 +76,33 @@ export class PageHeights { return [a, b]; } } + +/** 页号 → 所在章下标;无章(扁平包或单组)恒 0;首章 start 之前的散页归首章。 */ +export function chapterIndexAt(chs: { start: number }[] | null, page: number): number { + if (!chs || chs.length < 2) return 0; + return chs.reduce((acc, c, i) => (c.start <= page ? i : acc), 0); +} + +export interface ChapterWindow { + start: number; // 窗口首页(含)——滚动/挂载下界 + end: number; // 窗口末页(不含)——锁章时的滚动边界 + mountEnd: number; // 挂载上界(含预读章),恒 ≤ count 且 ≥ end +} + +/** 阅读窗口:连读或扁平包 = 全书;锁章 = 本章滚动、本章 + prefetch 章挂载。 */ +export function chapterWin( + chs: { start: number }[] | null, + count: number, + ci: number, + continuous: boolean, + prefetch: number, +): ChapterWindow { + if (!chs || chs.length < 2 || continuous) return { start: 0, end: count, mountEnd: count }; + const c = Math.max(0, Math.min(ci, chs.length - 1)); + const end = chs[c + 1]?.start ?? count; + return { + start: c === 0 ? 0 : chs[c].start, + end, + mountEnd: chs[c + 1 + Math.max(0, prefetch)]?.start ?? count, + }; +} diff --git a/frontend/test/virt.test.ts b/frontend/test/virt.test.ts index 59214bf..ac140c5 100644 --- a/frontend/test/virt.test.ts +++ b/frontend/test/virt.test.ts @@ -1,5 +1,5 @@ import { expect, it } from "vitest"; -import { PageHeights } from "../src/lib/virt"; +import { chapterIndexAt, chapterWin, PageHeights } from "../src/lib/virt"; it("估算高度:offset/total/pageAt/range(带 overscan、夹边)", () => { const ph = new PageHeights(10, 100); @@ -37,3 +37,35 @@ it("n=0 时一切归空", () => { expect(ph.range(0, 100, 2)).toEqual([0, 0]); expect(ph.set(0, 50)).toBe(0); }); + +const CH4 = [{ start: 0 }, { start: 10 }, { start: 20 }, { start: 30 }]; + +it("chapterIndexAt:无章/单章恒 0;页号归章;首章 start 之前的散页归 0", () => { + expect(chapterIndexAt(null, 7)).toBe(0); + expect(chapterIndexAt([{ start: 0 }], 7)).toBe(0); + expect(chapterIndexAt(CH4, 0)).toBe(0); + expect(chapterIndexAt(CH4, 9)).toBe(0); + expect(chapterIndexAt(CH4, 10)).toBe(1); + expect(chapterIndexAt(CH4, 999)).toBe(3); + expect(chapterIndexAt([{ start: 5 }, { start: 10 }], 3)).toBe(0); // 根散页 +}); + +it("chapterWin:扁平/连读 = 全书窗口", () => { + expect(chapterWin(null, 40, 0, false, 2)).toEqual({ start: 0, end: 40, mountEnd: 40 }); + expect(chapterWin(CH4, 40, 2, true, 2)).toEqual({ start: 0, end: 40, mountEnd: 40 }); +}); + +it("chapterWin:锁章窗口 = 本章;mountEnd 按预读章数延伸、夹到 count", () => { + expect(chapterWin(CH4, 40, 1, false, 0)).toEqual({ start: 10, end: 20, mountEnd: 20 }); + expect(chapterWin(CH4, 40, 1, false, 1)).toEqual({ start: 10, end: 20, mountEnd: 30 }); + expect(chapterWin(CH4, 40, 1, false, 2)).toEqual({ start: 10, end: 20, mountEnd: 40 }); + expect(chapterWin(CH4, 40, 1, false, 3)).toEqual({ start: 10, end: 20, mountEnd: 40 }); // 越界夹住 + expect(chapterWin(CH4, 40, 3, false, 2)).toEqual({ start: 30, end: 40, mountEnd: 40 }); // 末章 +}); + +it("chapterWin:ci 越界夹边;首章窗口含首章 start 之前的散页;prefetch 负数按 0", () => { + expect(chapterWin(CH4, 40, 9, false, 1)).toEqual({ start: 30, end: 40, mountEnd: 40 }); + expect(chapterWin(CH4, 40, -2, false, 1)).toEqual({ start: 0, end: 10, mountEnd: 20 }); + expect(chapterWin([{ start: 5 }, { start: 10 }], 20, 0, false, 0)).toEqual({ start: 0, end: 10, mountEnd: 10 }); + expect(chapterWin(CH4, 40, 1, false, -1)).toEqual({ start: 10, end: 20, mountEnd: 20 }); +});