Add support to scroll to current chapter in volume list

This commit is contained in:
2026-03-01 23:51:56 +08:00
parent b9b637d5ac
commit 6cf7e3aa19
3 changed files with 70 additions and 8 deletions

View File

@@ -132,7 +132,7 @@ export default function BookChapter() {
<Splitter.Panel min='20%' max='40%' defaultSize='30%' collapsible className={styles.chs}>
{bookStatus.chapterShowMode != ChapterShowMode.All && listErr && <Result status="error" title="加载章节列表失败" subTitle={listErr} extra={<Button type="primary" onClick={handle_list_load}></Button>} />}
{bookStatus.chapterShowMode != ChapterShowMode.All && !bookStatus.chapterLists && !listErr && <Skeleton active />}
{vols.length > 0 && <VolumesList bookId={bookInfo.id} volumes={vols} oneLine key={bookInfo.id} />}
{vols.length > 0 && <VolumesList bookId={bookInfo.id} volumes={vols} oneLine key={bookInfo.id} current={id} />}
</Splitter.Panel>
<Splitter.Panel className={styles.chc}>
{err && <Result

View File

@@ -1,3 +1,16 @@
.c {
position: relative;
overflow-y: hidden;
height: 100%;
display: flex;
flex-direction: column;
}
.cl {
overflow-y: auto;
flex: 1;
}
.ch {
margin: 4px 8px;
width: min(calc((100% - 40px) / 4), 300px);
@@ -25,3 +38,13 @@
.open:hover {
color: inherit;
}
.current {
position: absolute;
right: 30px;
bottom: 50px;
padding: 4px;
background: white;
border-radius: 8px;
cursor: pointer;
}

View File

@@ -5,12 +5,14 @@ import { Link } from "react-router";
import { CheckCircleOutlined } from "@ant-design/icons";
import OpenInNewTab from "../../../node_modules/@material-icons/svg/svg/open_in_new/twotone.svg";
import Icon from "../../components/Icon";
import { useMemo } from "react";
import { useMemo, useState } from "react";
import LocationSearchingTwotone from "../../../node_modules/@material-icons/svg/svg/location_searching/twotone.svg";
export type VolumesListProps = {
volumes: Volume[];
bookId: number;
oneLine?: boolean;
current?: number;
}
async function open_in_qidian(bookId: number, chapterId: number) {
@@ -28,10 +30,42 @@ async function open_in_qidian(bookId: number, chapterId: number) {
}
}
export default function VolumesList({ volumes, bookId, oneLine }: VolumesListProps) {
export default function VolumesList({ volumes, bookId, oneLine, current }: VolumesListProps) {
const currentVolumeId = useMemo(() => {
if (!current) return null;
return volumes.find(v => v.chapters.some(ch => ch.id === current))?.id ?? null;
}, [volumes, current]);
const [activeKeys, setActiveKeys] = useState<string[]>([]);
const scrollToCurrent = () => {
if (!current || !currentVolumeId) return;
if (!activeKeys.includes(currentVolumeId)) {
setActiveKeys(prev => [...prev, currentVolumeId]);
function checkChapterInView() {
const el = document.getElementById(`chapter-${current}`);
if (el && el.offsetParent) {
// Wait for the collapse animation to finish before scrolling
setTimeout(() => {
el.scrollIntoView({ behavior: 'smooth', block: 'start' });
}, 300);
} else {
setTimeout(checkChapterInView, 100);
}
}
setTimeout(checkChapterInView, 20);
} else {
const el = document.getElementById(`chapter-${current}`);
if (el) {
el.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
}
};
const items = useMemo<CollapseProps['items']>(() => volumes.map(v => {
const children = v.chapters.map(chapter => (
<Flex className={oneLine ? styles.chone : styles.ch} key={chapter.id}>
<Flex className={oneLine ? styles.chone : styles.ch} key={chapter.id} id={`chapter-${chapter.id}`}>
<Link to={`/qd/book/${bookId}/chapter/${chapter.id}`}>{chapter.name}</Link>
<Flex className={styles.action}>
{chapter.isSaved && <CheckCircleOutlined className={styles.saved} />}
@@ -53,8 +87,13 @@ export default function VolumesList({ volumes, bookId, oneLine }: VolumesListPro
</Flex>
}
}), [volumes, bookId, oneLine]);
return (<Collapse
key={bookId}
items={items}
/>);
return (<div className={styles.c}>
<div className={styles.cl}><Collapse
key={bookId}
activeKey={activeKeys}
onChange={(keys) => setActiveKeys(keys as string[])}
items={items}
/></div>
{current && <Tooltip title="定位到当前章节" placement="left"><Icon cls={styles.current}><LocationSearchingTwotone fill="currentColor" onClick={scrollToCurrent} /></Icon></Tooltip>}
</div>);
}