mirror of
https://github.com/lifegpc/bookdownload.git
synced 2026-07-08 01:31:31 +08:00
Add support to scroll to current chapter in volume list
This commit is contained in:
@@ -132,7 +132,7 @@ export default function BookChapter() {
|
|||||||
<Splitter.Panel min='20%' max='40%' defaultSize='30%' collapsible className={styles.chs}>
|
<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 && 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 />}
|
{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>
|
||||||
<Splitter.Panel className={styles.chc}>
|
<Splitter.Panel className={styles.chc}>
|
||||||
{err && <Result
|
{err && <Result
|
||||||
|
|||||||
@@ -1,3 +1,16 @@
|
|||||||
|
.c {
|
||||||
|
position: relative;
|
||||||
|
overflow-y: hidden;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cl {
|
||||||
|
overflow-y: auto;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
.ch {
|
.ch {
|
||||||
margin: 4px 8px;
|
margin: 4px 8px;
|
||||||
width: min(calc((100% - 40px) / 4), 300px);
|
width: min(calc((100% - 40px) / 4), 300px);
|
||||||
@@ -25,3 +38,13 @@
|
|||||||
.open:hover {
|
.open:hover {
|
||||||
color: inherit;
|
color: inherit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.current {
|
||||||
|
position: absolute;
|
||||||
|
right: 30px;
|
||||||
|
bottom: 50px;
|
||||||
|
padding: 4px;
|
||||||
|
background: white;
|
||||||
|
border-radius: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|||||||
@@ -5,12 +5,14 @@ import { Link } from "react-router";
|
|||||||
import { CheckCircleOutlined } from "@ant-design/icons";
|
import { CheckCircleOutlined } from "@ant-design/icons";
|
||||||
import OpenInNewTab from "../../../node_modules/@material-icons/svg/svg/open_in_new/twotone.svg";
|
import OpenInNewTab from "../../../node_modules/@material-icons/svg/svg/open_in_new/twotone.svg";
|
||||||
import Icon from "../../components/Icon";
|
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 = {
|
export type VolumesListProps = {
|
||||||
volumes: Volume[];
|
volumes: Volume[];
|
||||||
bookId: number;
|
bookId: number;
|
||||||
oneLine?: boolean;
|
oneLine?: boolean;
|
||||||
|
current?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function open_in_qidian(bookId: number, chapterId: 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 items = useMemo<CollapseProps['items']>(() => volumes.map(v => {
|
||||||
const children = v.chapters.map(chapter => (
|
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>
|
<Link to={`/qd/book/${bookId}/chapter/${chapter.id}`}>{chapter.name}</Link>
|
||||||
<Flex className={styles.action}>
|
<Flex className={styles.action}>
|
||||||
{chapter.isSaved && <CheckCircleOutlined className={styles.saved} />}
|
{chapter.isSaved && <CheckCircleOutlined className={styles.saved} />}
|
||||||
@@ -53,8 +87,13 @@ export default function VolumesList({ volumes, bookId, oneLine }: VolumesListPro
|
|||||||
</Flex>
|
</Flex>
|
||||||
}
|
}
|
||||||
}), [volumes, bookId, oneLine]);
|
}), [volumes, bookId, oneLine]);
|
||||||
return (<Collapse
|
return (<div className={styles.c}>
|
||||||
key={bookId}
|
<div className={styles.cl}><Collapse
|
||||||
items={items}
|
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>);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user