Skip to content

Commit 6753d34

Browse files
author
Taois
committed
feat: 小说漫画阅读界面优化
1 parent 72e6db1 commit 6753d34

9 files changed

Lines changed: 927 additions & 445 deletions

File tree

dashboard/src/components/players/LiveProxySelector.vue

Lines changed: 39 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,14 @@
1212
size="small"
1313
placeholder="选择代理播放地址"
1414
>
15-
<a-option value="disabled" title="关闭代理播放功能">代理播放:关闭</a-option>
15+
<a-option value="disabled" title="关闭代理播放功能">代理 · 关闭</a-option>
1616
<a-option
1717
v-for="option in proxyOptions"
1818
:key="option.value"
1919
:value="option.value"
2020
:title="`${option.label}\n完整链接: ${option.url || option.value}`"
2121
>
22-
代理播放:{{ option.label }}
22+
代理 · {{ option.label }}
2323
</a-option>
2424
</a-select>
2525
</div>
@@ -174,47 +174,51 @@ defineExpose({
174174

175175
<style scoped>
176176
.live-proxy-selector {
177-
display: flex;
177+
display: inline-flex;
178178
align-items: center;
179-
gap: 4px;
180-
padding: 6px 10px;
181-
border-radius: 4px;
179+
gap: 6px;
180+
width: 100%;
181+
min-width: 0;
182+
min-height: 32px;
183+
padding: 4px 8px;
184+
border-radius: 8px;
182185
cursor: pointer;
183-
transition: all 0.2s ease;
186+
transition: background 0.2s ease, color 0.2s ease;
184187
background: transparent;
185188
border: none;
186189
font-size: 12px;
187190
font-weight: 500;
188-
color: #495057;
189-
min-height: 28px;
191+
color: var(--color-text-2);
190192
position: relative;
191193
}
192194
193195
.live-proxy-selector:hover {
194-
background: #e9ecef;
195-
color: #212529;
196-
transform: translateY(-1px);
196+
background: var(--color-fill-2);
197+
color: var(--color-text-1);
197198
}
198199
199200
.selector-icon {
200201
width: 14px;
201202
height: 14px;
202-
flex-shrink: 0;
203+
flex: 0 0 auto;
203204
}
204205
205206
.proxy-select {
207+
width: 142px;
208+
min-width: 142px;
206209
border: none !important;
207210
background: transparent !important;
208211
box-shadow: none !important;
209-
min-width: 120px;
210212
}
211213
212214
.proxy-select :deep(.arco-select-view) {
215+
height: 32px;
213216
border: none !important;
214217
background: transparent !important;
215-
padding: 0;
216-
font-size: 11px;
218+
padding: 0 8px 0 0;
219+
font-size: 12px;
217220
font-weight: 500;
221+
border-radius: 8px;
218222
}
219223
220224
.proxy-select :deep(.arco-select-view-suffix) {
@@ -223,5 +227,24 @@ defineExpose({
223227
224228
.proxy-select :deep(.arco-select-view-value) {
225229
color: currentColor;
230+
overflow: hidden;
231+
text-overflow: ellipsis;
232+
white-space: nowrap;
226233
}
234+
235+
@media (max-width: 768px) {
236+
.live-proxy-selector {
237+
flex: 0 0 auto;
238+
width: auto;
239+
min-height: 32px;
240+
padding: 4px;
241+
border-radius: 10px;
242+
}
243+
244+
.proxy-select {
245+
width: 142px;
246+
min-width: 142px;
247+
}
248+
}
249+
227250
</style>

dashboard/src/components/readers/BookReader.vue

Lines changed: 129 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
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>
@@ -39,22 +39,34 @@
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)
199211
const error = ref('')
200212
const chapterContent = ref(null)
201213
const showSettingsDialog = ref(false)
202-
const showChapterPanel = ref(false)
214+
const showChapterPanel = ref(!isMobileViewport())
203215
const chapterSearchKeyword = ref('')
204216
const chapterPanelListRef = ref(null)
205217
const readerContentRef = ref(null)
218+
const chapterListScrollTop = ref(0)
219+
const chapterListViewportHeight = ref(0)
206220
const bottomSwipeProgress = ref(0)
207221
const touchStartY = ref(0)
208222
const touchLastY = ref(0)
209223
const bottomSwipeDistance = ref(0)
210224
const bottomSwipeTriggered = ref(false)
211225
const lastAutoNextAt = ref(0)
226+
const CHAPTER_ITEM_HEIGHT = 38
227+
const CHAPTER_LIST_BUFFER = 12
212228
const 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+
282322
const 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
394476
const scrollReaderToTop = async () => {
@@ -464,14 +546,10 @@ const handleReaderWheel = (event) => {
464546
}
465547
466548
const 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
477555
const 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
// 监听章节变化
499586
watch(() => props.currentChapterIndex, (newIndex) => {
500587
if (props.visible && newIndex >= 0) {
@@ -507,6 +594,7 @@ watch(() => props.currentChapterIndex, (newIndex) => {
507594
// 监听可见性变化
508595
watch(() => 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

Comments
 (0)