feat(web): unify reader TOC and bookmarks into one left nav drawer with tabs

This commit is contained in:
2026-09-08 21:08:42 +08:00
parent 4f4c7e5434
commit 48161ea1ff
7 changed files with 340 additions and 211 deletions
+55 -34
View File
@@ -2,7 +2,8 @@ import { useQuery } from "@tanstack/react-query";
import { useEffect, useMemo, useRef, useState, type MouseEvent } from "react";
import { api, formatPageUrl } from "../api/client";
import { btn } from "../components/ui";
import { useBookmarks } from "../components/Bookmarks";
import { BookmarksTab } from "../components/Bookmarks";
import { ReaderNav, type NavTab } from "../components/reader-nav";
import { fetchObjectUrl } from "../lib/authImage";
import { useProgressSaver, type ReaderProps } from "../lib/useProgress";
import { PageHeights, chapterIndexAt, chapterWin } from "../lib/virt";
@@ -79,10 +80,9 @@ export default function CbzReader({ book, initialLocator, initialPercent, chrome
});
const count = countQ.data?.count ?? 0;
const chapters = countQ.data?.chapters ?? null; // 包内目录结构(第N話/上中下卷);null/单组=扁平整本一章
const [toc, setToc] = useState(false);
const [nav, setNav] = useState<null | NavTab>(null);
const activeRow = useRef<HTMLButtonElement>(null);
const saver = useProgressSaver(book.id, { locator: initialLocator, percent: initialPercent });
const bm = useBookmarks({ book, capture: saver.capture, seek: (l) => goPage(Number(l.page) || 0) });
const boxRef = useRef<HTMLDivElement>(null);
const [width, setWidth] = useState(480);
const [vh, setVh] = useState(600);
@@ -190,8 +190,8 @@ export default function CbzReader({ book, initialLocator, initialPercent, chrome
const cur = locked ? Math.min(curRaw, w.end - 1) : curRaw; // 锁章:视口落在章末卡片上时读数不越章(HUD/滑条/进度)
useEffect(() => {
if (toc) activeRow.current?.scrollIntoView({ block: "center" });
}, [toc]);
if (nav === "toc") activeRow.current?.scrollIntoView({ block: "center" });
}, [nav]);
function scrollToPage(i: number, smooth: boolean) {
const el = boxRef.current;
@@ -358,10 +358,22 @@ export default function CbzReader({ book, initialLocator, initialPercent, chrome
预读 {prefetch} 章
</button>
)}
{bm.btn}
<button
className="rd-btn"
aria-expanded={nav === "bm"}
aria-controls="reader-nav"
onClick={() => setNav(nav === "bm" ? null : "bm")}
>
书签
</button>
{chapters && (
<>
<button className="rd-btn" aria-expanded={toc} aria-controls="cbz-toc" onClick={() => setToc((v) => !v)}>
<button
className="rd-btn"
aria-expanded={nav === "toc"}
aria-controls="reader-nav"
onClick={() => setNav(nav === "toc" ? null : "toc")}
>
目录
</button>
<span className="text-xs tabular-nums opacity-60">
@@ -389,34 +401,43 @@ export default function CbzReader({ book, initialLocator, initialPercent, chrome
<span className="opacity-60">{Math.round(((cur + 1) / Math.max(1, count)) * 100)}%</span>
</div>
)}
{toc && chapters && (
<>
<div className="fixed inset-0 z-10" aria-hidden="true" onClick={() => setToc(false)} />
<nav
id="cbz-toc"
aria-label="章节目录"
className="rd-divider absolute inset-y-0 left-0 z-20 w-64 overflow-y-auto border-r bg-[var(--rd-bg)] p-2 shadow-2xl"
>
<ul className="space-y-0.5">
{chapters.map((c, i) => (
<li key={i}>
<button
ref={i === ci ? activeRow : undefined}
className={"rd-row" + (i === ci ? " rd-btn-on" : "")}
onClick={() => {
goPage(c.start);
setToc(false);
}}
>
{i + 1}. {c.title}
</button>
</li>
))}
</ul>
</nav>
</>
{nav && (
<ReaderNav
tab={chapters ? nav : "bm"}
onTab={setNav}
onClose={() => setNav(null)}
bookmarks={
<BookmarksTab
book={book}
capture={saver.capture}
seek={(l) => {
goPage(Number(l.page) || 0);
setNav(null);
}}
/>
}
toc={
chapters && (
<ul className="space-y-0.5">
{chapters.map((c, i) => (
<li key={i}>
<button
ref={i === ci ? activeRow : undefined}
className={"rd-row" + (i === ci ? " rd-btn-on" : "")}
onClick={() => {
goPage(c.start);
setNav(null);
}}
>
{i + 1}. {c.title}
</button>
</li>
))}
</ul>
)
}
/>
)}
{bm.panel}
</div>
);
}