1818
1919 <div class =" reader-main-layout" >
2020 <div
21- v-if =" showChapterPanel"
21+ v-if =" showChapterPanel && isMobileViewport() "
2222 class =" chapter-sidebar-mask"
2323 @click =" showChapterPanel = false"
2424 ></div >
3939 />
4040 </div >
4141
42- <div class =" chapter-sidebar-list" ref =" chapterPanelListRef" >
43- <button
44- v-for =" chapter in filteredChapters"
45- :key =" chapter.index"
46- type =" button"
47- class =" chapter-sidebar-item"
48- :class =" {
49- active: chapter.index === currentChapterIndex,
50- read: chapter.index < currentChapterIndex
42+ <div
43+ class =" chapter-sidebar-list"
44+ ref =" chapterPanelListRef"
45+ @scroll =" handleChapterListScroll"
46+ >
47+ <div
48+ class =" chapter-sidebar-spacer"
49+ :style =" {
50+ paddingTop: `${virtualTopPadding}px`,
51+ paddingBottom: `${virtualBottomPadding}px`
5152 }"
52- @click =" handlePanelChapterSelect(chapter.index)"
5353 >
54- <span class =" chapter-sidebar-number" >{{ chapter.index + 1 }}</span >
55- <span class =" chapter-sidebar-item-title" :title =" chapter.name" >{{ chapter.name }}</span >
56- <span class =" chapter-sidebar-current" v-if =" chapter.index === currentChapterIndex" >当前</span >
57- </button >
54+ <button
55+ v-for =" chapter in virtualChapters"
56+ :key =" chapter.index"
57+ type =" button"
58+ class =" chapter-sidebar-item"
59+ :class =" {
60+ active: chapter.index === currentChapterIndex,
61+ read: chapter.index < currentChapterIndex
62+ }"
63+ @click =" handlePanelChapterSelect(chapter.index)"
64+ >
65+ <span class =" chapter-sidebar-number" >{{ chapter.index + 1 }}</span >
66+ <span class =" chapter-sidebar-item-title" :title =" chapter.name" >{{ chapter.name }}</span >
67+ <span class =" chapter-sidebar-current" v-if =" chapter.index === currentChapterIndex" >当前</span >
68+ </button >
69+ </div >
5870 </div >
5971
6072 <div v-if =" filteredChapters.length === 0" class =" chapter-sidebar-empty" >
@@ -199,16 +211,20 @@ const loading = ref(false)
199211const error = ref (' ' )
200212const chapterContent = ref (null )
201213const showSettingsDialog = ref (false )
202- const showChapterPanel = ref (false )
214+ const showChapterPanel = ref (! isMobileViewport () )
203215const chapterSearchKeyword = ref (' ' )
204216const chapterPanelListRef = ref (null )
205217const readerContentRef = ref (null )
218+ const chapterListScrollTop = ref (0 )
219+ const chapterListViewportHeight = ref (0 )
206220const bottomSwipeProgress = ref (0 )
207221const touchStartY = ref (0 )
208222const touchLastY = ref (0 )
209223const bottomSwipeDistance = ref (0 )
210224const bottomSwipeTriggered = ref (false )
211225const lastAutoNextAt = ref (0 )
226+ const CHAPTER_ITEM_HEIGHT = 38
227+ const CHAPTER_LIST_BUFFER = 12
212228const AUTO_NEXT_COOLDOWN = 2000
213229
214230// 阅读设置
@@ -279,6 +295,30 @@ const filteredChapters = computed(() => {
279295 })
280296})
281297
298+ const virtualStartIndex = computed (() => {
299+ if (filteredChapters .value .length === 0 ) return 0
300+ return Math .max (0 , Math .floor (chapterListScrollTop .value / CHAPTER_ITEM_HEIGHT ) - CHAPTER_LIST_BUFFER )
301+ })
302+
303+ const virtualVisibleCount = computed (() => {
304+ const viewportRows = Math .ceil ((chapterListViewportHeight .value || 360 ) / CHAPTER_ITEM_HEIGHT )
305+ return viewportRows + CHAPTER_LIST_BUFFER * 2
306+ })
307+
308+ const virtualEndIndex = computed (() => {
309+ return Math .min (filteredChapters .value .length , virtualStartIndex .value + virtualVisibleCount .value )
310+ })
311+
312+ const virtualChapters = computed (() => {
313+ return filteredChapters .value .slice (virtualStartIndex .value , virtualEndIndex .value )
314+ })
315+
316+ const virtualTopPadding = computed (() => virtualStartIndex .value * CHAPTER_ITEM_HEIGHT )
317+
318+ const virtualBottomPadding = computed (() => {
319+ return Math .max (0 , (filteredChapters .value .length - virtualEndIndex .value ) * CHAPTER_ITEM_HEIGHT )
320+ })
321+
282322const canAutoNextChapter = computed (() => props .currentChapterIndex < props .chapters .length - 1 )
283323
284324// 格式化章节内容
@@ -382,13 +422,55 @@ const handleChapterSelected = (index) => {
382422 emit (' chapter-selected' , index)
383423}
384424
385- const isMobileViewport = () => window .matchMedia (' (max-width: 768px)' ).matches
425+ function isMobileViewport () {
426+ return window .matchMedia (' (max-width: 768px)' ).matches
427+ }
386428
387- const scrollActiveChapterIntoView = async () => {
429+ const updateChapterListViewport = async () => {
430+ await nextTick ()
431+ if (chapterPanelListRef .value ) {
432+ chapterListViewportHeight .value = chapterPanelListRef .value .clientHeight
433+ chapterListScrollTop .value = chapterPanelListRef .value .scrollTop
434+ }
435+ }
436+
437+ const handleChapterListScroll = () => {
438+ if (chapterPanelListRef .value ) {
439+ chapterListScrollTop .value = chapterPanelListRef .value .scrollTop
440+ chapterListViewportHeight .value = chapterPanelListRef .value .clientHeight
441+ }
442+ }
443+
444+ const findFilteredChapterPosition = (chapterIndex ) => {
445+ return filteredChapters .value .findIndex (chapter => chapter .index === chapterIndex)
446+ }
447+
448+ const scrollChapterPositionIntoView = async (position , block = ' center' ) => {
388449 await nextTick ()
389450 const list = chapterPanelListRef .value
390- const active = list? .querySelector (' .chapter-sidebar-item.active' )
391- active? .scrollIntoView ({ block: ' center' })
451+ if (! list || position < 0 ) return
452+
453+ const itemTop = position * CHAPTER_ITEM_HEIGHT
454+ const itemBottom = itemTop + CHAPTER_ITEM_HEIGHT
455+ const viewportHeight = list .clientHeight
456+ let nextScrollTop = list .scrollTop
457+
458+ if (block === ' center' ) {
459+ nextScrollTop = itemTop - viewportHeight / 2 + CHAPTER_ITEM_HEIGHT / 2
460+ } else if (itemTop < list .scrollTop ) {
461+ nextScrollTop = itemTop
462+ } else if (itemBottom > list .scrollTop + viewportHeight) {
463+ nextScrollTop = itemBottom - viewportHeight
464+ }
465+
466+ list .scrollTop = Math .max (0 , nextScrollTop)
467+ chapterListScrollTop .value = list .scrollTop
468+ chapterListViewportHeight .value = viewportHeight
469+ }
470+
471+ const scrollActiveChapterIntoView = async () => {
472+ await updateChapterListViewport ()
473+ await scrollChapterPositionIntoView (findFilteredChapterPosition (props .currentChapterIndex ), ' center' )
392474}
393475
394476const scrollReaderToTop = async () => {
@@ -464,14 +546,10 @@ const handleReaderWheel = (event) => {
464546}
465547
466548const handleToggleChapterPanel = async () => {
467- if (isMobileViewport ()) {
468- showChapterPanel .value = ! showChapterPanel .value
469- if (showChapterPanel .value ) {
470- await scrollActiveChapterIntoView ()
471- }
472- return
549+ showChapterPanel .value = ! showChapterPanel .value
550+ if (showChapterPanel .value ) {
551+ await scrollActiveChapterIntoView ()
473552 }
474- await scrollActiveChapterIntoView ()
475553}
476554
477555const handlePanelChapterSelect = (index ) => {
@@ -495,6 +573,15 @@ const handleSettingsChange = (newSettings) => {
495573 emit (' settings-change' , readingSettings .value )
496574}
497575
576+ watch (() => chapterSearchKeyword .value , async () => {
577+ chapterListScrollTop .value = 0
578+ await nextTick ()
579+ if (chapterPanelListRef .value ) {
580+ chapterPanelListRef .value .scrollTop = 0
581+ updateChapterListViewport ()
582+ }
583+ })
584+
498585// 监听章节变化
499586watch (() => props .currentChapterIndex , (newIndex ) => {
500587 if (props .visible && newIndex >= 0 ) {
@@ -507,6 +594,7 @@ watch(() => props.currentChapterIndex, (newIndex) => {
507594// 监听可见性变化
508595watch (() => props .visible , (visible ) => {
509596 if (visible && props .currentChapterIndex >= 0 ) {
597+ showChapterPanel .value = ! isMobileViewport ()
510598 loadChapterContent (props .currentChapterIndex )
511599 scrollActiveChapterIntoView ()
512600 } else {
@@ -578,15 +666,24 @@ onUnmounted(() => {
578666}
579667
580668.chapter - sidebar {
581- width: 320px ;
582- flex: 0 0 320px ;
669+ width: 0 ;
670+ flex: 0 0 0 ;
583671 min- height: 0 ;
584672 display: flex;
585673 flex- direction: column;
586- border- right: 1px solid var ( -- color - border - 2 ) ;
674+ border- right: 0 ;
587675 background: var (-- color- bg- 1 );
588- box- shadow: 2px 0 10px rgba (0 , 0 , 0 , 0.04 );
676+ box- shadow: none;
677+ overflow: hidden;
589678 z- index: 2 ;
679+ transition: width 0 .2s ease, flex- basis 0 .2s ease, border- color 0 .2s ease, box- shadow 0 .2s ease;
680+ }
681+
682+ .chapter - sidebar .open {
683+ width: 320px ;
684+ flex- basis: 320px ;
685+ border- right: 1px solid var (-- color- border- 2 );
686+ box- shadow: 2px 0 10px rgba (0 , 0 , 0 , 0.04 );
590687}
591688
592689.chapter - sidebar- header {
@@ -870,6 +967,7 @@ onUnmounted(() => {
870967 }
871968
872969 .chapter - sidebar- list {
970+ height: calc (100dvh - 172px );
873971 padding: 6px ;
874972 }
875973
0 commit comments