@airhang/vue-book-reader 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,3003 @@
1
+ <template>
2
+ <div
3
+ class="photo-album-container"
4
+ @touchstart="onContainerTouchStart"
5
+ @touchmove="onContainerTouchMove"
6
+ @touchend="onContainerTouchEnd"
7
+ @dblclick="onContainerDoubleClick"
8
+ @click="onContentClick"
9
+ :style="{ transform: `scale(${zoomScale}) translate(${translateX}px, ${translateY}px)` }"
10
+ >
11
+ <div class="album-header" v-if="false">
12
+ <h1>氪氪</h1>
13
+ <!-- <p>双页模式 - 双击图片放大,滚轮缩放,拖拽翻页</p> -->
14
+ </div>
15
+
16
+ <div
17
+ v-if="contentReady"
18
+ class="flipbook-wrapper"
19
+ @dblclick="onFlipbookDoubleClick"
20
+ @touchstart.capture="onFlipbookTouchStart"
21
+ @touchend.capture="onFlipbookTouchEnd"
22
+ >
23
+ <!-- 仿真翻页模式 -->
24
+ <flipbook
25
+ v-if="flipMode === 'flip' && flag"
26
+ :class="['flipbook', isMobile ? 'mobile-mode mobile-optimized' : 'desktop-mode desktop-optimized']"
27
+ :pages="pages"
28
+ :pagesHiRes="pagesHiRes"
29
+ :startPage="startPage"
30
+ v-slot="{ page, canFlipLeft, canFlipRight }"
31
+ ref="flipbook"
32
+ @flip-left-end="onFlipLeftEnd"
33
+ @flip-right-end="onFlipRightEnd"
34
+ :zooms="null"
35
+ :ambient-light="0.6"
36
+ :gloss="0.4"
37
+ :single-page="isMobile"
38
+ :click-to-flip="false"
39
+ :wheel-to-flip="!isMobile"
40
+ :swipe-to-flip="true"
41
+ :center-pages="true"
42
+ >
43
+ </flipbook>
44
+
45
+ <!-- 滑动翻页模式 -->
46
+ <div
47
+ v-else-if="flipMode === 'slide'"
48
+ :class="['slide-viewer', isMobile ? 'mobile-mode mobile-optimized' : 'desktop-mode desktop-optimized']"
49
+ ref="slideViewer"
50
+ @touchstart="onSlideViewerTouchStart"
51
+ @touchmove="onSlideViewerTouchMove"
52
+ @touchend="onSlideViewerTouchEnd"
53
+ >
54
+ <div class="slide-container" :style="slideContainerStyle">
55
+ <div
56
+ v-for="(page, index) in pages"
57
+ :key="index"
58
+ class="slide-page"
59
+ :class="{ 'slide-page-active': index + 1 === currentPage }"
60
+ >
61
+ <img v-if="page" :src="page" alt="" class="slide-page-image" />
62
+ <div v-else class="slide-page-placeholder">封面/封底</div>
63
+ </div>
64
+ </div>
65
+ </div>
66
+
67
+ <!-- 淡入淡出翻页模式 -->
68
+ <div
69
+ v-else-if="flipMode === 'fade'"
70
+ :class="['fade-viewer', isMobile ? 'mobile-mode mobile-optimized' : 'desktop-mode desktop-optimized']"
71
+ ref="fadeViewer"
72
+ >
73
+ <transition name="fade-page" mode="out-in">
74
+ <div :key="currentPage" class="fade-page-container">
75
+ <img v-if="pages[currentPage - 1]" :src="pages[currentPage - 1]" alt="" class="fade-page-image" />
76
+ <div v-else class="fade-page-placeholder">封面/封底</div>
77
+ </div>
78
+ </transition>
79
+ </div>
80
+
81
+ <!-- 垂直滚动模式 -->
82
+ <div
83
+ v-else-if="flipMode === 'scroll'"
84
+ :class="['scroll-viewer', isMobile ? 'mobile-mode mobile-optimized' : 'desktop-mode desktop-optimized']"
85
+ ref="scrollViewer"
86
+ @scroll="onScrollViewerScroll"
87
+ >
88
+ <div class="scroll-container">
89
+ <div
90
+ v-for="(page, index) in pages"
91
+ :key="index"
92
+ class="scroll-page"
93
+ :ref="'scrollPage' + index"
94
+ >
95
+ <img v-if="page" :src="page" alt="" class="scroll-page-image" />
96
+ <div v-else class="scroll-page-placeholder">封面/封底</div>
97
+ </div>
98
+ </div>
99
+ </div>
100
+
101
+ <!-- 截断特效模式 -->
102
+ <div
103
+ v-else-if="flipMode === 'clip'"
104
+ :class="['clip-viewer', isMobile ? 'mobile-mode mobile-optimized' : 'desktop-mode desktop-optimized']"
105
+ ref="clipViewer"
106
+ >
107
+ <div class="clip-pages-wrapper">
108
+ <!-- 当前页 -->
109
+ <transition name="clip-current">
110
+ <div :key="'current-' + currentPage" class="clip-page clip-page-current">
111
+ <img v-if="pages[currentPage - 1]" :src="pages[currentPage - 1]" alt="" class="clip-page-image" />
112
+ <div v-else class="clip-page-placeholder">封面/封底</div>
113
+ </div>
114
+ </transition>
115
+ <!-- 下一页预览(部分可见) -->
116
+ <div v-if="currentPage < totalPages" class="clip-page clip-page-next">
117
+ <img v-if="pages[currentPage]" :src="pages[currentPage]" alt="" class="clip-page-image" />
118
+ <div v-else class="clip-page-placeholder">封面/封底</div>
119
+ </div>
120
+ </div>
121
+ </div>
122
+
123
+ <!-- 卡片风格模式 -->
124
+ <div
125
+ v-else-if="flipMode === 'card'"
126
+ :class="['card-viewer', isMobile ? 'mobile-mode mobile-optimized' : 'desktop-mode desktop-optimized']"
127
+ ref="cardViewer"
128
+ >
129
+ <div class="card-stack">
130
+ <!-- 显示前后各一页的卡片堆叠效果 -->
131
+ <transition-group name="card-flip" tag="div" class="card-transition-group">
132
+ <div
133
+ v-for="(page, index) in visibleCards"
134
+ :key="'card-' + page.originalIndex"
135
+ class="card-item"
136
+ :class="{
137
+ 'card-item-prev': page.position === 'prev',
138
+ 'card-item-current': page.position === 'current',
139
+ 'card-item-next': page.position === 'next'
140
+ }"
141
+ :style="getCardStyle(page.position)"
142
+ >
143
+ <div class="card-content">
144
+ <img v-if="page.src" :src="page.src" alt="" class="card-image" />
145
+ <div v-else class="card-placeholder">封面/封底</div>
146
+ </div>
147
+ <div class="card-page-number">{{ page.originalIndex + 1 }} / {{ totalPages }}</div>
148
+ </div>
149
+ </transition-group>
150
+ </div>
151
+ </div>
152
+ </div>
153
+
154
+ <div class="controls" :class="{ 'mobile-controls': isMobile, 'desktop-controls': !isMobile, 'controls-visible': showControls }" @click.stop>
155
+ <button @click="flipLeft" class="btn btn-prev">
156
+ ← 上一页
157
+ </button>
158
+ <span class="page-indicator">
159
+ <span v-if="totalPages === 0">加载中...</span>
160
+ <span v-else-if="currentPage === 1 && totalPages > 1">封面</span>
161
+ <span v-else-if="currentPage === totalPages && totalPages > 1">封底</span>
162
+ <span v-else-if="!isMobile && totalPages > 2">第 {{ Math.ceil((currentPage - 1) / 2) }} 组 / 共 {{ Math.ceil((totalPages - 2) / 2) }} 组</span>
163
+ <span v-else-if="isMobile && totalPages > 2">第 {{ Math.max(1, currentPage ) }} 页 / 共 {{ Math.max(0, totalPages - 2) }} 页</span>
164
+ <span v-else-if="totalPages === 1">第 1 页 / 共 1 页</span>
165
+ <span v-else>第 {{ currentPage }} 页 / 共 {{ totalPages }} 页</span>
166
+ </span>
167
+ <button @click="flipRight" class="btn btn-next">
168
+ 下一页 →
169
+ </button>
170
+ </div>
171
+
172
+ <!-- 页码跳转 - 放在翻页控件下方 -->
173
+ <!-- <div class="page-jump-container" :class="{ 'mobile-page-jump': isMobile }" @click.stop @touchstart.stop @touchmove.stop @touchend.stop @mousedown.stop @mouseup.stop>
174
+ <input
175
+ type="text"
176
+ :value="jumpPageInput"
177
+ @input="handlePageInput"
178
+ :placeholder="`搜索跳转`"
179
+ class="page-jump-input"
180
+ @click.stop
181
+ @mousedown.stop
182
+ @mouseup.stop
183
+ />
184
+ <button @click.stop="handleJumpToPage" @mousedown.stop @mouseup.stop class="btn btn-jump">
185
+ 跳转
186
+ </button>
187
+ </div> -->
188
+
189
+ <!-- 翻页模式切换按钮(当URL没有指定模式时显示) -->
190
+ <div v-if="showFlipModeSelector" class="flip-mode-selector" :class="{ 'mobile-flip-mode-selector': isMobile }">
191
+ <button @click="toggleFlipModeMenu" class="btn btn-flip-mode">
192
+ <span class="flip-mode-icon">{{ flipModeIcon }}</span>
193
+ <span class="flip-mode-text">{{ flipModeLabel }}</span>
194
+ </button>
195
+ <div v-if="showFlipModeMenu" class="flip-mode-menu">
196
+ <div
197
+ v-for="mode in flipModes"
198
+ :key="mode.value"
199
+ class="flip-mode-item"
200
+ :class="{ 'flip-mode-item-active': flipMode === mode.value }"
201
+ @click="selectFlipMode(mode.value)"
202
+ >
203
+ <span class="flip-mode-item-icon">{{ mode.icon }}</span>
204
+ <span class="flip-mode-item-label">{{ mode.label }}</span>
205
+ </div>
206
+ </div>
207
+ </div>
208
+
209
+ <!-- 目录按钮 -->
210
+ <div
211
+ class="catalogue-button"
212
+ :class="{
213
+ 'mobile-catalogue-button': isMobile,
214
+ 'dragging': catalogueButtonDragging,
215
+ 'catalogue-visible': showControls
216
+ }"
217
+ :style="{
218
+ left: catalogueButtonPosition.x + 'px',
219
+ top: catalogueButtonPosition.y + 'px'
220
+ }"
221
+ @mousedown="onCatalogueMouseDown"
222
+ @touchstart="onCatalogueTouchStart"
223
+ @touchmove="onCatalogueTouchMove"
224
+ @touchend="onCatalogueTouchEnd"
225
+ @click.stop
226
+ >
227
+ <button @click="openCatalogue" class="btn btn-catalogue">
228
+ <!-- <span class="drag-handle">⋮⋮</span> -->
229
+ <!-- <span class="catalogue-icon">☰</span> -->
230
+ <span class="catalogue-text">目录</span>
231
+ </button>
232
+ </div>
233
+
234
+ <!-- 缩放提示 -->
235
+ <div class="zoom-hint" :class="{ 'show': zoomScale !== 1 }">
236
+ <div class="zoom-info">
237
+ <span>缩放: {{ Math.round(zoomScale * 100) }}%</span>
238
+ <button @click="resetZoom" class="btn-reset-zoom">重置</button>
239
+ </div>
240
+ </div>
241
+
242
+ <!-- 加载状态覆盖层 -->
243
+ <div v-if="loading" class="loading-overlay">
244
+ <div class="loading-container">
245
+ <div class="loading-spinner">
246
+ <div class="spinner-ring"></div>
247
+ <div class="spinner-ring"></div>
248
+ <div class="spinner-ring"></div>
249
+ </div>
250
+ <div class="loading-text">
251
+ <h3>正在加载书籍</h3>
252
+ <p>请稍候,正在获取精彩内容...</p>
253
+ <div class="loading-progress">
254
+ <div class="progress-bar"></div>
255
+ </div>
256
+ </div>
257
+ </div>
258
+ </div>
259
+
260
+ <!-- 错误状态 -->
261
+ <div v-if="error && !loading" class="error-container">
262
+ <div class="error-content">
263
+ <div class="error-icon">⚠️</div>
264
+ <h3>加载失败</h3>
265
+ <p>{{ error.message || '网络连接异常,请检查网络后重试' }}</p>
266
+ <button @click="fetchBooksData" class="retry-btn">
267
+ <span class="retry-icon">🔄</span>
268
+ 重新加载
269
+ </button>
270
+ </div>
271
+ </div>
272
+
273
+ <!-- 成功状态提示(短暂显示) -->
274
+ <div v-if="booksData && !loading && !error" class="success-toast" :class="{ 'show': showSuccessToast }">
275
+ 书籍加载完成
276
+ </div>
277
+
278
+ <!-- 书籍目录抽屉 -->
279
+ <book-catalogue-drawer
280
+ v-model="showCatalogueDrawer"
281
+ :book-id="bookId"
282
+ :catalogue="catalogue"
283
+ :loading="catalogueLoading"
284
+ @catalogue-click="onCatalogueClick"
285
+ @page-jump="onPageJump"
286
+ @Focus="onFocus"
287
+ @fetch-catalogue="onFetchCatalogue"
288
+ />
289
+
290
+ <!-- 调试信息 -->
291
+ <!-- <div class="debug-info" style="margin-top: 20px; color: #6c757d; text-align: center; font-size: 0.9rem;">
292
+ 当前页: {{ currentPage }} / 总页数: {{ totalPages }} | 设备: {{ isMobile ? '移动端' : 'PC端' }}
293
+ <br>
294
+ <div v-if="booksData" style="margin-top: 10px; padding: 10px; background: #f8f9fa; border-radius: 5px; font-family: monospace; font-size: 0.8rem; text-align: left; max-height: 200px; overflow-y: auto;">
295
+ <strong>接口返回数据:</strong><br>
296
+ {{ JSON.stringify(booksData, null, 2) }}
297
+ </div>
298
+ </div> -->
299
+ <!-- <div class="debug-info" style="margin-top: 20px; color: #6c757d; text-align: center; font-size: 0.9rem;">
300
+ 当前页: {{ currentPage }} / 总页数: {{ totalPages }} | 设备: {{ isMobile ? '移动端' : 'PC端' }} | 模式: {{ isMobile ? '单页' : '双页' }}
301
+ <br>
302
+ </div> -->
303
+
304
+ </div>
305
+ </template>
306
+
307
+ <script>
308
+ import Flipbook from 'flipbook-vue/vue2'
309
+ import BookCatalogueDrawer from './BookCatalogueDrawer.vue'
310
+
311
+ export default {
312
+ name: 'PhotoAlbumView',
313
+ components: {
314
+ Flipbook,
315
+ BookCatalogueDrawer
316
+ },
317
+ props: {
318
+ pages: {
319
+ type: Array,
320
+ default: () => []
321
+ },
322
+ bookId: {
323
+ type: String,
324
+ default: ''
325
+ },
326
+ startPage: {
327
+ type: Number,
328
+ default: 1
329
+ },
330
+ catalogue: {
331
+ type: Array,
332
+ default: () => []
333
+ },
334
+ catalogueLoading: {
335
+ type: Boolean,
336
+ default: false
337
+ }
338
+ },
339
+ data() {
340
+ return {
341
+ currentPage: 1,
342
+ flag: true,
343
+ // 设备检测
344
+ isMobile: false,
345
+ // 接口相关数据
346
+ booksData: null,
347
+ loading: false,
348
+ error: null,
349
+ showSuccessToast: false,
350
+ // 内容就绪状态
351
+ contentReady: false,
352
+ // 手势缩放相关
353
+ zoomScale: 1,
354
+ translateX: 0,
355
+ translateY: 0,
356
+ lastTouchDistance: 0,
357
+ lastTouchCenter: { x: 0, y: 0 },
358
+ isZooming: false,
359
+ isPanning: false,
360
+ touchStartTime: 0,
361
+ initialTouches: [],
362
+ // Flipbook区域双击检测
363
+ flipbookTouchStartTime: 0,
364
+ flipbookLastTapTime: 0,
365
+ // 目录相关
366
+ showCatalogueDrawer: false,
367
+ // 目录按钮拖拽相关
368
+ catalogueButtonDragging: false,
369
+ catalogueButtonPosition: { x: 20, y: 20 },
370
+ dragStartPosition: { x: 0, y: 0 },
371
+ dragOffset: { x: 0, y: 0 },
372
+ // 翻页模式相关
373
+ flipMode: 'flip', // flip: 仿真翻页, slide: 滑动翻页, fade: 淡入淡出, scroll: 垂直滚动
374
+ showFlipModeMenu: false,
375
+ flipModes: [
376
+ { value: 'flip', label: '仿真翻页', icon: '📖' },
377
+ { value: 'slide', label: '滑动翻页', icon: '↔️' },
378
+ { value: 'fade', label: '淡入淡出', icon: '✨' },
379
+ { value: 'scroll', label: '垂直滚动', icon: '📜' },
380
+ { value: 'clip', label: '截断特效', icon: '✂️' },
381
+ { value: 'card', label: '卡片风格', icon: '🃏' }
382
+ ],
383
+ // 滑动模式相关
384
+ slideStartX: 0,
385
+ slideCurrentX: 0,
386
+ slideIsDragging: false,
387
+ // URL参数控制
388
+ flipModeFromUrl: false, // 翻页模式是否由URL参数指定
389
+ // 控件自动隐藏相关
390
+ showControls: false,
391
+ hideControlsTimer: null,
392
+ // 页码跳转相关
393
+ jumpPageInput: ''
394
+ }
395
+ },
396
+ computed: {
397
+ totalPages() {
398
+ return this.pages.length
399
+ },
400
+ pagesHiRes() {
401
+ return this.pages
402
+ },
403
+ /**
404
+ * 翻页模式图标
405
+ */
406
+ flipModeIcon() {
407
+ const mode = this.flipModes.find(m => m.value === this.flipMode)
408
+ return mode ? mode.icon : '📖'
409
+ },
410
+ /**
411
+ * 翻页模式标签
412
+ */
413
+ flipModeLabel() {
414
+ const mode = this.flipModes.find(m => m.value === this.flipMode)
415
+ return mode ? mode.label : '仿真翻页'
416
+ },
417
+ /**
418
+ * 滑动容器样式
419
+ */
420
+ slideContainerStyle() {
421
+ const offset = -(this.currentPage - 1) * 100
422
+ const dragOffset = this.slideIsDragging ? (this.slideCurrentX - this.slideStartX) / 5 : 0
423
+ return {
424
+ transform: `translateX(calc(${offset}% + ${dragOffset}px))`,
425
+ transition: this.slideIsDragging ? 'none' : 'transform 0.3s ease-out'
426
+ }
427
+ },
428
+ /**
429
+ * 是否显示翻页模式选择器
430
+ */
431
+ showFlipModeSelector() {
432
+ // 如果翻页模式是由URL参数指定的,则隐藏选择器
433
+ return !this.flipModeFromUrl
434
+ },
435
+ /**
436
+ * 卡片模式可见卡片
437
+ */
438
+ visibleCards() {
439
+ const cards = []
440
+ // 上一页
441
+ if (this.currentPage > 1) {
442
+ cards.push({
443
+ src: this.pages[this.currentPage - 2],
444
+ originalIndex: this.currentPage - 2,
445
+ position: 'prev'
446
+ })
447
+ }
448
+ // 当前页
449
+ cards.push({
450
+ src: this.pages[this.currentPage - 1],
451
+ originalIndex: this.currentPage - 1,
452
+ position: 'current'
453
+ })
454
+ // 下一页
455
+ if (this.currentPage < this.totalPages) {
456
+ cards.push({
457
+ src: this.pages[this.currentPage],
458
+ originalIndex: this.currentPage,
459
+ position: 'next'
460
+ })
461
+ }
462
+ return cards
463
+ },
464
+ /**
465
+ * 计算当前页面显示文本
466
+ */
467
+ pageDisplayText() {
468
+ if (this.totalPages === 0) return '加载中...'
469
+ if (this.currentPage === 1 && this.totalPages > 1) return '封面'
470
+ if (this.currentPage === this.totalPages && this.totalPages > 1) return '封底'
471
+
472
+ if (this.isMobile) {
473
+ if (this.totalPages <= 2) return `第 ${this.currentPage} 页 / 共 ${this.totalPages} 页`
474
+ return `第 ${Math.max(1, this.currentPage - 1)} 页 / 共 ${Math.max(0, this.totalPages - 2)} 页`
475
+ } else {
476
+ if (this.totalPages <= 2) return `第 ${this.currentPage} 页 / 共 ${this.totalPages} 页`
477
+ return `第 ${Math.ceil((this.currentPage - 1) / 2)} 组 / 共 ${Math.ceil((this.totalPages - 2) / 2)} 组`
478
+ }
479
+ }
480
+ },
481
+ methods: {
482
+ /**
483
+ * 获取书籍数据
484
+ */
485
+ async fetchBooksData() {
486
+ try {
487
+ this.loading = true
488
+ this.error = null
489
+
490
+ // 注释:原来从URL参数获取fileManagementId,现在改为通过props传入
491
+ // const fileManagementId = this.$route.params.id || this.$route.query.id || ''
492
+ // this.bookId = fileManagementId
493
+
494
+ // 注释:原来调用API获取数据,现在改为通过props传入
495
+ // const response = await booksApi.getBooksSlice(fileManagementId, textContent, 1, 9999)
496
+
497
+ // 触发事件让父组件处理数据获取
498
+ this.$emit('fetch-books-data', this.bookId)
499
+
500
+ // 如果通过props传入了pages数据,直接使用
501
+ if (this.pages && this.pages.length > 0) {
502
+ this.booksData = { data: { records: this.pages.map(url => ({ imageUrl: url })) } }
503
+
504
+ // 显示成功提示
505
+ this.showSuccessToast = true
506
+ setTimeout(() => {
507
+ this.showSuccessToast = false
508
+ }, 3000)
509
+
510
+ // 检查URL参数并跳转到指定页码
511
+ this.checkUrlPageParameter()
512
+ }
513
+
514
+ } catch (error) {
515
+ console.error('获取书籍数据失败:', error)
516
+ this.error = error
517
+
518
+ // 显示错误信息
519
+ if (error.response) {
520
+ console.error('API错误响应:', error.response.data)
521
+ } else if (error.request) {
522
+ console.error('网络请求失败:', error.request)
523
+ } else {
524
+ console.error('请求配置错误:', error.message)
525
+ }
526
+ } finally {
527
+ this.loading = false
528
+ }
529
+ },
530
+
531
+ /**
532
+ * 根据API数据更新页面
533
+ * @param {Array} apiData - API返回的数据
534
+ */
535
+ updatePagesFromApiData(apiData) {
536
+ if (!Array.isArray(apiData)) {
537
+ console.warn('API数据格式不正确,期望数组格式')
538
+ return
539
+ }
540
+
541
+ console.log('开始更新页面数据,数据长度:', apiData.length)
542
+
543
+ // 这里可以根据实际的API数据结构来处理
544
+ // 假设API返回的数据包含图片URL
545
+ const newPages = [null] // 保留封面页
546
+
547
+ apiData.forEach((item, index) => {
548
+ if (item.imageUrl) {
549
+ newPages.push(item.imageUrl)
550
+ console.log(`添加页面 ${index + 1}:`, item.imageUrl)
551
+ }
552
+ })
553
+
554
+ if (newPages.length > 1) {
555
+ this.pages = newPages
556
+ console.log('页面数据更新完成,总页数:', this.pages.length)
557
+ }
558
+ },
559
+
560
+ /**
561
+ * 检测是否为移动设备
562
+ */
563
+ detectMobile() {
564
+ const userAgent = navigator.userAgent.toLowerCase()
565
+ const mobileKeywords = ['mobile', 'android', 'iphone', 'ipad', 'ipod', 'blackberry', 'windows phone']
566
+
567
+ // 检测 User Agent
568
+ const isMobileUA = mobileKeywords.some(keyword => userAgent.includes(keyword))
569
+
570
+ // 检测屏幕尺寸
571
+ const isMobileScreen = window.innerWidth <= 768
572
+
573
+ // 检测触摸支持
574
+ const isTouchDevice = 'ontouchstart' in window || navigator.maxTouchPoints > 0
575
+
576
+ // 综合判断
577
+ this.isMobile = isMobileUA || (isMobileScreen && isTouchDevice)
578
+
579
+ console.log('设备检测结果:', {
580
+ userAgent: isMobileUA,
581
+ screenSize: isMobileScreen,
582
+ touchSupport: isTouchDevice,
583
+ finalResult: this.isMobile
584
+ })
585
+ },
586
+
587
+ /**
588
+ * 预加载首页图片
589
+ */
590
+ async preloadFirstPage() {
591
+ if (!this.pages || this.pages.length === 0) {
592
+ this.contentReady = true
593
+ return
594
+ }
595
+
596
+ const firstPageIndex = this.startPage - 1
597
+ const firstPageUrl = this.pages[firstPageIndex]
598
+
599
+ if (!firstPageUrl) {
600
+ this.contentReady = true
601
+ return
602
+ }
603
+
604
+ try {
605
+ await new Promise((resolve, reject) => {
606
+ const img = new Image()
607
+ img.onload = () => resolve()
608
+ img.onerror = () => resolve() // 即使失败也继续
609
+ img.src = firstPageUrl
610
+
611
+ // 超时保护
612
+ setTimeout(() => resolve(), 3000)
613
+ })
614
+ } catch (error) {
615
+ console.warn('首页图片预加载失败:', error)
616
+ } finally {
617
+ this.contentReady = true
618
+ }
619
+ },
620
+
621
+ /**
622
+ * 监听窗口尺寸变化
623
+ */
624
+ handleResize() {
625
+ const wasMobile = this.isMobile
626
+ this.detectMobile()
627
+
628
+ // 如果设备类型发生变化,重新初始化flipbook
629
+ if (wasMobile !== this.isMobile) {
630
+ console.log('设备类型变化,重新初始化')
631
+ this.$nextTick(() => {
632
+ if (this.$refs.flipbook) {
633
+ // 重置到第一页
634
+ this.currentPage = 1
635
+ this.startPage = 1
636
+ }
637
+ })
638
+ }
639
+ },
640
+ flipLeft() {
641
+ console.log('尝试向左翻页,当前页:', this.currentPage)
642
+ if (this.flipMode === 'flip') {
643
+ // 仿真翻页模式
644
+ if (this.$refs.flipbook && this.currentPage > 1) {
645
+ try {
646
+ this.$refs.flipbook.flipLeft()
647
+ console.log('向左翻页命令已发送')
648
+ } catch (error) {
649
+ console.error('向左翻页失败:', error)
650
+ }
651
+ } else {
652
+ console.log('无法向左翻页 - 已在第一页或组件未就绪')
653
+ }
654
+ } else {
655
+ // 其他模式直接切换页码
656
+ if (this.currentPage > 1) {
657
+ this.currentPage--
658
+ this.onFlipLeftEnd(this.currentPage)
659
+ }
660
+ }
661
+ },
662
+ flipRight() {
663
+ console.log('尝试向右翻页,当前页:', this.currentPage)
664
+ if (this.flipMode === 'flip') {
665
+ // 仿真翻页模式
666
+ if (this.$refs.flipbook && this.currentPage < this.totalPages) {
667
+ try {
668
+ this.$refs.flipbook.flipRight()
669
+ console.log('向右翻页命令已发送')
670
+ } catch (error) {
671
+ console.error('向右翻页失败:', error)
672
+ }
673
+ } else {
674
+ console.log('无法向右翻页 - 已在最后一页或组件未就绪')
675
+ }
676
+ } else {
677
+ // 其他模式直接切换页码
678
+ if (this.currentPage < this.totalPages) {
679
+ this.currentPage++
680
+ this.onFlipRightEnd(this.currentPage)
681
+ }
682
+ }
683
+ },
684
+ onFlipLeftEnd(page) {
685
+ console.log('向左翻页完成,新页面:', page)
686
+ this.currentPage = page
687
+ // 触发页码变化事件
688
+ this.$emit('page-change', page)
689
+ // 记录阅读进度
690
+ this.recordReadingProgress(page)
691
+ },
692
+ onFlipRightEnd(page) {
693
+ console.log('向右翻页完成,新页面:', page)
694
+ this.currentPage = page
695
+ // 触发页码变化事件
696
+ this.$emit('page-change', page)
697
+ // 记录阅读进度
698
+ this.recordReadingProgress(page)
699
+ },
700
+
701
+ /**
702
+ * 容器触摸开始事件
703
+ */
704
+ onContainerTouchStart(e) {
705
+ this.touchStartTime = Date.now()
706
+ this.initialTouches = Array.from(e.touches)
707
+
708
+ if (e.touches.length === 2) {
709
+ // 双指缩放
710
+ this.isZooming = true
711
+ this.lastTouchDistance = this.getTouchDistance(e.touches[0], e.touches[1])
712
+ this.lastTouchCenter = this.getTouchCenter(e.touches[0], e.touches[1])
713
+ e.preventDefault()
714
+ } else if (e.touches.length === 1 && this.zoomScale > 1) {
715
+ // 单指拖拽(仅在放大状态下)
716
+ this.isPanning = true
717
+ this.lastTouchCenter = { x: e.touches[0].clientX, y: e.touches[0].clientY }
718
+ }
719
+ },
720
+
721
+ /**
722
+ * 容器触摸移动事件
723
+ */
724
+ onContainerTouchMove(e) {
725
+ if (this.isZooming && e.touches.length === 2) {
726
+ // 双指缩放
727
+ const currentDistance = this.getTouchDistance(e.touches[0], e.touches[1])
728
+ const currentCenter = this.getTouchCenter(e.touches[0], e.touches[1])
729
+
730
+ // 计算缩放比例
731
+ const scaleChange = currentDistance / this.lastTouchDistance
732
+ let newScale = this.zoomScale * scaleChange
733
+
734
+ // 限制缩放范围
735
+ newScale = Math.max(0.5, Math.min(3, newScale))
736
+
737
+ // 计算缩放中心点的偏移
738
+ const deltaX = currentCenter.x - this.lastTouchCenter.x
739
+ const deltaY = currentCenter.y - this.lastTouchCenter.y
740
+
741
+ this.zoomScale = newScale
742
+ this.translateX += deltaX
743
+ this.translateY += deltaY
744
+
745
+ this.lastTouchDistance = currentDistance
746
+ this.lastTouchCenter = currentCenter
747
+
748
+ e.preventDefault()
749
+ } else if (this.isPanning && e.touches.length === 1 && this.zoomScale > 1) {
750
+ // 单指拖拽
751
+ const deltaX = e.touches[0].clientX - this.lastTouchCenter.x
752
+ const deltaY = e.touches[0].clientY - this.lastTouchCenter.y
753
+
754
+ this.translateX += deltaX
755
+ this.translateY += deltaY
756
+
757
+ this.lastTouchCenter = { x: e.touches[0].clientX, y: e.touches[0].clientY }
758
+
759
+ e.preventDefault()
760
+ }
761
+ },
762
+
763
+ /**
764
+ * 容器触摸结束事件
765
+ */
766
+ onContainerTouchEnd(e) {
767
+ const touchDuration = Date.now() - this.touchStartTime
768
+ const wasZooming = this.isZooming
769
+ const wasPanning = this.isPanning
770
+
771
+ // 先重置状态
772
+ this.isZooming = false
773
+ this.isPanning = false
774
+
775
+ // 双击重置缩放 - 只在单指快速点击时检测
776
+ if (e.changedTouches.length === 1 && touchDuration < 300 && !wasZooming && !wasPanning) {
777
+ const now = Date.now()
778
+ if (this.lastTapTime && now - this.lastTapTime < 400) {
779
+ // 检测到双击
780
+ this.resetZoom()
781
+ console.log('移动端双击重置显示比例')
782
+ this.lastTapTime = 0 // 重置,避免三击触发
783
+ } else {
784
+ this.lastTapTime = now
785
+ }
786
+ }
787
+
788
+ // 边界检查和回弹
789
+ this.constrainPosition()
790
+ },
791
+
792
+ /**
793
+ * 获取两点间距离
794
+ */
795
+ getTouchDistance(touch1, touch2) {
796
+ const dx = touch1.clientX - touch2.clientX
797
+ const dy = touch1.clientY - touch2.clientY
798
+ return Math.sqrt(dx * dx + dy * dy)
799
+ },
800
+
801
+ /**
802
+ * 获取两点中心
803
+ */
804
+ getTouchCenter(touch1, touch2) {
805
+ return {
806
+ x: (touch1.clientX + touch2.clientX) / 2,
807
+ y: (touch1.clientY + touch2.clientY) / 2
808
+ }
809
+ },
810
+
811
+ /**
812
+ * 重置缩放
813
+ */
814
+ resetZoom() {
815
+ this.zoomScale = 1
816
+ this.translateX = 0
817
+ this.translateY = 0
818
+ },
819
+
820
+ /**
821
+ * PC端双击事件处理
822
+ */
823
+ onContainerDoubleClick(e) {
824
+ // 阻止事件冒泡和默认行为,避免触发翻页
825
+ e.preventDefault()
826
+ e.stopPropagation()
827
+
828
+ // 重置缩放比例
829
+ this.resetZoom()
830
+ console.log('容器双击重置显示比例,当前缩放:', this.zoomScale)
831
+ },
832
+
833
+ /**
834
+ * Flipbook区域双击事件处理
835
+ */
836
+ onFlipbookDoubleClick(e) {
837
+ // 阻止事件冒泡和默认行为,避免触发翻页
838
+ e.preventDefault()
839
+ e.stopPropagation()
840
+
841
+ // 重置缩放比例
842
+ this.resetZoom()
843
+ console.log('Flipbook双击重置显示比例,当前缩放:', this.zoomScale)
844
+ },
845
+
846
+ /**
847
+ * Flipbook区域触摸开始事件
848
+ */
849
+ onFlipbookTouchStart(e) {
850
+ this.flipbookTouchStartTime = Date.now()
851
+ },
852
+
853
+ /**
854
+ * Flipbook区域触摸结束事件(移动端双击检测)
855
+ */
856
+ onFlipbookTouchEnd(e) {
857
+ const touchDuration = Date.now() - this.flipbookTouchStartTime
858
+
859
+ // 双击重置缩放 - 只在单指快速点击时检测
860
+ if (e.changedTouches.length === 1 && touchDuration < 300) {
861
+ const now = Date.now()
862
+ if (this.flipbookLastTapTime && now - this.flipbookLastTapTime < 400) {
863
+ // 检测到双击
864
+ e.preventDefault()
865
+ e.stopPropagation()
866
+ this.resetZoom()
867
+ console.log('Flipbook区域移动端双击重置显示比例')
868
+ this.flipbookLastTapTime = 0 // 重置,避免三击触发
869
+ } else {
870
+ this.flipbookLastTapTime = now
871
+ }
872
+ }
873
+ },
874
+
875
+ /**
876
+ * 约束位置在边界内
877
+ */
878
+ constrainPosition() {
879
+ if (this.zoomScale <= 1) {
880
+ this.translateX = 0
881
+ this.translateY = 0
882
+ return
883
+ }
884
+
885
+ const maxTranslate = (this.zoomScale - 1) * 200
886
+ this.translateX = Math.max(-maxTranslate, Math.min(maxTranslate, this.translateX))
887
+ this.translateY = Math.max(-maxTranslate, Math.min(maxTranslate, this.translateY))
888
+ },
889
+
890
+
891
+ /**
892
+ * 打开目录抽屉
893
+ */
894
+ openCatalogue() {
895
+ console.log('点击目录按钮', {
896
+ dragging: this.catalogueButtonDragging,
897
+ showDrawer: this.showCatalogueDrawer
898
+ })
899
+
900
+ // 只有在不是拖拽状态下才打开目录
901
+ if (!this.catalogueButtonDragging) {
902
+ this.showCatalogueDrawer = true
903
+ console.log('目录抽屉已打开')
904
+ } else {
905
+ console.log('拖拽状态中,不打开目录')
906
+ }
907
+ },
908
+
909
+ /**
910
+ * 目录按钮拖拽开始
911
+ */
912
+ onCatalogueMouseDown(e) {
913
+ // 不立即设置为拖拽状态,等待移动再判断
914
+ this.catalogueButtonDragging = false
915
+
916
+ const rect = e.target.closest('.catalogue-button').getBoundingClientRect()
917
+ this.dragStartPosition = {
918
+ x: e.clientX,
919
+ y: e.clientY
920
+ }
921
+ this.dragOffset = {
922
+ x: e.clientX - rect.left,
923
+ y: e.clientY - rect.top
924
+ }
925
+
926
+ document.addEventListener('mousemove', this.onCatalogueMouseMove)
927
+ document.addEventListener('mouseup', this.onCatalogueMouseUp)
928
+ },
929
+
930
+ /**
931
+ * 目录按钮拖拽移动
932
+ */
933
+ onCatalogueMouseMove(e) {
934
+ // 检查是否开始拖拽
935
+ const dragDistance = Math.sqrt(
936
+ Math.pow(e.clientX - this.dragStartPosition.x, 2) +
937
+ Math.pow(e.clientY - this.dragStartPosition.y, 2)
938
+ )
939
+
940
+ // 只有移动距离超过5px才认为是拖拽
941
+ if (dragDistance > 5) {
942
+ this.catalogueButtonDragging = true
943
+ }
944
+
945
+ if (!this.catalogueButtonDragging) return
946
+
947
+ e.preventDefault()
948
+
949
+ const newX = e.clientX - this.dragOffset.x
950
+ const newY = e.clientY - this.dragOffset.y
951
+
952
+ // 限制在视窗范围内
953
+ const maxX = window.innerWidth - 120 // 按钮宽度约120px
954
+ const maxY = window.innerHeight - 50 // 按钮高度约50px
955
+
956
+ this.catalogueButtonPosition = {
957
+ x: Math.max(0, Math.min(maxX, newX)),
958
+ y: Math.max(0, Math.min(maxY, newY))
959
+ }
960
+ },
961
+
962
+ /**
963
+ * 目录按钮拖拽结束
964
+ */
965
+ onCatalogueMouseUp(e) {
966
+ // 立即重置拖拽状态
967
+ this.catalogueButtonDragging = false
968
+
969
+ document.removeEventListener('mousemove', this.onCatalogueMouseMove)
970
+ document.removeEventListener('mouseup', this.onCatalogueMouseUp)
971
+ },
972
+
973
+ /**
974
+ * 目录按钮触摸开始(移动端)
975
+ */
976
+ onCatalogueTouchStart(e) {
977
+ // 不立即设置为拖拽状态
978
+ this.catalogueButtonDragging = false
979
+
980
+ const touch = e.touches[0]
981
+ const rect = e.target.closest('.catalogue-button').getBoundingClientRect()
982
+
983
+ this.dragStartPosition = {
984
+ x: touch.clientX,
985
+ y: touch.clientY
986
+ }
987
+ this.dragOffset = {
988
+ x: touch.clientX - rect.left,
989
+ y: touch.clientY - rect.top
990
+ }
991
+ },
992
+
993
+ /**
994
+ * 目录按钮触摸移动(移动端)
995
+ */
996
+ onCatalogueTouchMove(e) {
997
+ const touch = e.touches[0]
998
+
999
+ // 检查是否开始拖拽
1000
+ const dragDistance = Math.sqrt(
1001
+ Math.pow(touch.clientX - this.dragStartPosition.x, 2) +
1002
+ Math.pow(touch.clientY - this.dragStartPosition.y, 2)
1003
+ )
1004
+
1005
+ // 只有移动距离超过5px才认为是拖拽
1006
+ if (dragDistance > 5) {
1007
+ this.catalogueButtonDragging = true
1008
+ }
1009
+
1010
+ if (!this.catalogueButtonDragging) return
1011
+
1012
+ e.preventDefault()
1013
+
1014
+ const newX = touch.clientX - this.dragOffset.x
1015
+ const newY = touch.clientY - this.dragOffset.y
1016
+
1017
+ // 限制在视窗范围内,考虑安全区域
1018
+ const maxX = window.innerWidth - 120
1019
+ const maxY = window.innerHeight - 80
1020
+
1021
+ this.catalogueButtonPosition = {
1022
+ x: Math.max(15, Math.min(maxX, newX)),
1023
+ y: Math.max(15, Math.min(maxY, newY))
1024
+ }
1025
+ },
1026
+
1027
+ /**
1028
+ * 目录按钮触摸结束(移动端)
1029
+ */
1030
+ onCatalogueTouchEnd(e) {
1031
+ // 立即重置拖拽状态
1032
+ this.catalogueButtonDragging = false
1033
+ },
1034
+
1035
+ /**
1036
+ * 切换翻页模式菜单显示
1037
+ */
1038
+ toggleFlipModeMenu() {
1039
+ this.showFlipModeMenu = !this.showFlipModeMenu
1040
+ },
1041
+
1042
+ /**
1043
+ * 选择翻页模式
1044
+ * @param {string} mode - 翻页模式
1045
+ */
1046
+ selectFlipMode(mode) {
1047
+ const oldMode = this.flipMode
1048
+ this.flipMode = mode
1049
+ this.showFlipModeMenu = false
1050
+
1051
+ // 保存翻页模式到本地存储
1052
+ localStorage.setItem('book-viewer-flip-mode', mode)
1053
+
1054
+ console.log('切换翻页模式:', { from: oldMode, to: mode })
1055
+
1056
+ // 如果切换到滚动模式,需要滚动到当前页
1057
+ if (mode === 'scroll') {
1058
+ this.$nextTick(() => {
1059
+ this.scrollToCurrentPage()
1060
+ })
1061
+ }
1062
+ },
1063
+
1064
+ /**
1065
+ * 滑动模式触摸开始
1066
+ */
1067
+ onSlideViewerTouchStart(e) {
1068
+ if (e.touches.length === 1) {
1069
+ this.slideStartX = e.touches[0].clientX
1070
+ this.slideCurrentX = e.touches[0].clientX
1071
+ this.slideIsDragging = true
1072
+ }
1073
+ },
1074
+
1075
+ /**
1076
+ * 滑动模式触摸移动
1077
+ */
1078
+ onSlideViewerTouchMove(e) {
1079
+ if (this.slideIsDragging && e.touches.length === 1) {
1080
+ this.slideCurrentX = e.touches[0].clientX
1081
+ e.preventDefault()
1082
+ }
1083
+ },
1084
+
1085
+ /**
1086
+ * 滑动模式触摸结束
1087
+ */
1088
+ onSlideViewerTouchEnd(e) {
1089
+ if (this.slideIsDragging) {
1090
+ const deltaX = this.slideCurrentX - this.slideStartX
1091
+ const threshold = 50 // 滑动阈值
1092
+
1093
+ if (deltaX > threshold && this.currentPage > 1) {
1094
+ // 向右滑动,上一页
1095
+ this.currentPage--
1096
+ this.recordReadingProgress(this.currentPage)
1097
+ } else if (deltaX < -threshold && this.currentPage < this.totalPages) {
1098
+ // 向左滑动,下一页
1099
+ this.currentPage++
1100
+ this.recordReadingProgress(this.currentPage)
1101
+ }
1102
+
1103
+ this.slideIsDragging = false
1104
+ }
1105
+ },
1106
+
1107
+ /**
1108
+ * 滚动模式滚动事件
1109
+ */
1110
+ onScrollViewerScroll(e) {
1111
+ const container = this.$refs.scrollViewer
1112
+ if (!container) return
1113
+
1114
+ const scrollTop = container.scrollTop
1115
+ const containerHeight = container.clientHeight
1116
+
1117
+ // 计算当前页码
1118
+ for (let i = 0; i < this.pages.length; i++) {
1119
+ const pageRef = this.$refs['scrollPage' + i]
1120
+ if (pageRef && pageRef[0]) {
1121
+ const pageTop = pageRef[0].offsetTop
1122
+ const pageBottom = pageTop + pageRef[0].offsetHeight
1123
+
1124
+ if (scrollTop >= pageTop - containerHeight / 2 && scrollTop < pageBottom - containerHeight / 2) {
1125
+ if (this.currentPage !== i + 1) {
1126
+ this.currentPage = i + 1
1127
+ this.recordReadingProgress(this.currentPage)
1128
+ }
1129
+ break
1130
+ }
1131
+ }
1132
+ }
1133
+ },
1134
+
1135
+ /**
1136
+ * 滚动到当前页
1137
+ */
1138
+ scrollToCurrentPage() {
1139
+ if (this.flipMode !== 'scroll') return
1140
+
1141
+ const pageRef = this.$refs['scrollPage' + (this.currentPage - 1)]
1142
+ if (pageRef && pageRef[0]) {
1143
+ pageRef[0].scrollIntoView({ behavior: 'smooth', block: 'start' })
1144
+ }
1145
+ },
1146
+
1147
+ /**
1148
+ * 关闭翻页模式菜单(点击外部)
1149
+ */
1150
+ closeFlipModeMenu(e) {
1151
+ if (!e.target.closest('.flip-mode-selector')) {
1152
+ this.showFlipModeMenu = false
1153
+ }
1154
+ },
1155
+
1156
+ /**
1157
+ * 获取卡片样式
1158
+ * @param {string} position - 卡片位置:prev, current, next
1159
+ */
1160
+ getCardStyle(position) {
1161
+ switch (position) {
1162
+ case 'prev':
1163
+ return {
1164
+ transform: 'translateX(-70%) scale(0.85) rotateY(15deg)',
1165
+ zIndex: 1,
1166
+ opacity: 0.6,
1167
+ filter: 'brightness(0.8)'
1168
+ }
1169
+ case 'current':
1170
+ return {
1171
+ transform: 'translateX(0) scale(1) rotateY(0deg)',
1172
+ zIndex: 3,
1173
+ opacity: 1,
1174
+ filter: 'brightness(1)'
1175
+ }
1176
+ case 'next':
1177
+ return {
1178
+ transform: 'translateX(70%) scale(0.85) rotateY(-15deg)',
1179
+ zIndex: 1,
1180
+ opacity: 0.6,
1181
+ filter: 'brightness(0.8)'
1182
+ }
1183
+ default:
1184
+ return {}
1185
+ }
1186
+ },
1187
+
1188
+ /**
1189
+ * 目录项点击事件
1190
+ * @param {Object} item - 点击的目录项
1191
+ */
1192
+ onCatalogueClick(item) {
1193
+ console.log('目录项被点击:', item)
1194
+ this.$emit('catalogue-click', item)
1195
+ },
1196
+
1197
+ /**
1198
+ * 获取目录数据事件
1199
+ * @param {String} bookId - 书籍ID
1200
+ */
1201
+ onFetchCatalogue(bookId) {
1202
+ this.$emit('fetch-catalogue', bookId)
1203
+ },
1204
+
1205
+ /**
1206
+ * 页面跳转事件
1207
+ * @param {number} pageNumber - 目标页码
1208
+ */
1209
+ onFocus() {
1210
+ // 使用flipbook的方法跳转到指定页面
1211
+ if (this.flipMode === 'flip') {
1212
+ this.flag = false
1213
+ setTimeout(() => {
1214
+ this.flag = true
1215
+ }, 500)
1216
+ } else if (this.flipMode === 'scroll') {
1217
+ this.scrollToCurrentPage()
1218
+ }
1219
+ },
1220
+ onPageJump(pageNumber) {
1221
+ console.log('跳转到页面:', pageNumber)
1222
+
1223
+ // 关闭目录抽屉
1224
+ this.showCatalogueDrawer = false
1225
+
1226
+ // 跳转到指定页面
1227
+ if (pageNumber && pageNumber >= 1 && pageNumber <= this.totalPages) {
1228
+ this.currentPage = pageNumber
1229
+ this.startPage = pageNumber
1230
+
1231
+ // 记录阅读进度
1232
+ this.recordReadingProgress(pageNumber)
1233
+
1234
+ // 使用flipbook的方法跳转到指定页面
1235
+ if (this.flipMode === 'flip') {
1236
+ this.flag = false
1237
+ setTimeout(() => {
1238
+ this.flag = true
1239
+ this.$nextTick(() => {
1240
+ this.$refs.flipbook.goToPage(pageNumber)
1241
+ })
1242
+ }, 500)
1243
+ } else if (this.flipMode === 'scroll') {
1244
+ this.scrollToCurrentPage()
1245
+ }
1246
+ } else {
1247
+ console.warn('无效的页码:', pageNumber)
1248
+ }
1249
+ },
1250
+
1251
+ /**
1252
+ * 检查URL参数并跳转到指定页码
1253
+ */
1254
+ checkUrlPageParameter() {
1255
+ // 从URL查询参数中获取页码
1256
+ const pageParam = this.$route.query.page
1257
+
1258
+ if (pageParam) {
1259
+ const targetPage = parseInt(pageParam, 10)
1260
+
1261
+ console.log('检测到URL页码参数:', {
1262
+ pageParam,
1263
+ targetPage,
1264
+ totalPages: this.totalPages
1265
+ })
1266
+
1267
+ // 验证页码是否有效
1268
+ if (!isNaN(targetPage) && targetPage >= 1 && targetPage <= this.totalPages) {
1269
+ console.log('准备跳转到页码:', targetPage)
1270
+
1271
+ // 使用nextTick确保DOM已更新
1272
+ this.$nextTick(() => {
1273
+ this.jumpToPage(targetPage)
1274
+ })
1275
+ } else {
1276
+ console.warn('无效的页码参数:', {
1277
+ pageParam,
1278
+ targetPage,
1279
+ totalPages: this.totalPages,
1280
+ isValid: !isNaN(targetPage) && targetPage >= 1 && targetPage <= this.totalPages
1281
+ })
1282
+ }
1283
+ } else {
1284
+ console.log('URL中未检测到页码参数')
1285
+ }
1286
+ },
1287
+
1288
+ /**
1289
+ * 跳转到指定页码
1290
+ * @param {number} pageNumber - 目标页码
1291
+ */
1292
+ jumpToPage(pageNumber) {
1293
+ if (!pageNumber || pageNumber < 1 || pageNumber > this.totalPages) {
1294
+ console.warn('无效的页码:', pageNumber)
1295
+ return
1296
+ }
1297
+
1298
+ console.log('跳转到页面:', pageNumber)
1299
+
1300
+ // 更新当前页码和起始页码
1301
+ this.currentPage = pageNumber
1302
+ this.startPage = pageNumber
1303
+
1304
+ // 记录阅读进度
1305
+ this.recordReadingProgress(pageNumber)
1306
+
1307
+ // 使用flipbook的方法跳转到指定页面
1308
+ if (this.flipMode === 'flip') {
1309
+ if (this.$refs.flipbook && this.$refs.flipbook.goToPage) {
1310
+ try {
1311
+ this.$refs.flipbook.goToPage(pageNumber)
1312
+ console.log('页面跳转成功:', pageNumber)
1313
+ } catch (error) {
1314
+ console.error('页面跳转失败:', error)
1315
+ }
1316
+ } else {
1317
+ console.warn('Flipbook组件未就绪,无法跳转页面')
1318
+ }
1319
+ } else if (this.flipMode === 'scroll') {
1320
+ this.scrollToCurrentPage()
1321
+ }
1322
+ // slide 和 fade 模式直接通过 currentPage 变化来切换
1323
+ },
1324
+
1325
+ /**
1326
+ * 记录阅读进度
1327
+ * @param {number} pageNumber - 当前页码
1328
+ */
1329
+ async recordReadingProgress(pageNumber) {
1330
+ if (!this.bookId || !pageNumber) {
1331
+ console.warn('缺少书籍ID或页码,无法记录阅读进度')
1332
+ return
1333
+ }
1334
+
1335
+ try {
1336
+ console.log('记录阅读进度:', {
1337
+ bookId: this.bookId,
1338
+ pageNumber: pageNumber
1339
+ })
1340
+
1341
+ // 注释:原来调用API记录进度,现在改为触发事件让父组件处理
1342
+ // await booksApi.recordReadProgress(this.bookId, pageNumber)
1343
+ this.$emit('record-progress', { bookId: this.bookId, pageNumber })
1344
+
1345
+ console.log('阅读进度记录成功')
1346
+
1347
+ } catch (error) {
1348
+ console.error('记录阅读进度失败:', error)
1349
+ // 阅读记录失败不影响用户体验,只记录日志
1350
+ }
1351
+ },
1352
+
1353
+ /**
1354
+ * 显示控件
1355
+ */
1356
+ showControlsTemporarily() {
1357
+ this.showControls = true
1358
+
1359
+ // 清除之前的定时器
1360
+ if (this.hideControlsTimer) {
1361
+ clearTimeout(this.hideControlsTimer)
1362
+ }
1363
+
1364
+ // 3秒后自动隐藏
1365
+ this.hideControlsTimer = setTimeout(() => {
1366
+ this.showControls = false
1367
+ }, 3000)
1368
+ },
1369
+
1370
+ /**
1371
+ * 内容区域点击事件
1372
+ */
1373
+ onContentClick() {
1374
+ this.showControlsTemporarily()
1375
+ },
1376
+
1377
+ /**
1378
+ * 处理页码输入
1379
+ */
1380
+ handlePageInput(e) {
1381
+ const value = e.target.value
1382
+ // 只允许输入数字
1383
+ const numValue = value.replace(/[^\d]/g, '')
1384
+ // 限制不超过总页数
1385
+ if (numValue && parseInt(numValue) > this.totalPages) {
1386
+ this.jumpPageInput = this.totalPages.toString()
1387
+ } else {
1388
+ this.jumpPageInput = numValue
1389
+ }
1390
+ },
1391
+
1392
+ /**
1393
+ * 执行页码跳转
1394
+ */
1395
+ handleJumpToPage() {
1396
+ const pageNum = parseInt(this.jumpPageInput)
1397
+ if (pageNum && pageNum >= 1 && pageNum <= this.totalPages) {
1398
+ // 重置缩放和位移,避免内容区域缩小
1399
+ this.resetZoom()
1400
+ this.jumpToPage(pageNum)
1401
+ this.jumpPageInput = ''
1402
+ }
1403
+ }
1404
+ },
1405
+ watch: {
1406
+ async pages(newPages) {
1407
+ if (newPages && newPages.length > 0 && this.isMobile) {
1408
+ this.contentReady = false
1409
+ await this.$nextTick()
1410
+ await this.preloadFirstPage()
1411
+ } else if (newPages && newPages.length > 0) {
1412
+ this.contentReady = true
1413
+ }
1414
+ }
1415
+ },
1416
+ async mounted() {
1417
+ console.log('组件已挂载,总页数:', this.totalPages)
1418
+
1419
+ // 检测设备类型
1420
+ this.detectMobile()
1421
+
1422
+ // 检查URL参数中的翻页模式
1423
+ const urlFlipMode = this.$route.query.mode || this.$route.query.flipMode
1424
+ if (urlFlipMode && this.flipModes.some(m => m.value === urlFlipMode)) {
1425
+ // URL参数指定的翻页模式优先级最高,并隐藏切换按钮
1426
+ this.flipMode = urlFlipMode
1427
+ this.flipModeFromUrl = true
1428
+ console.log('从URL参数读取翻页模式:', urlFlipMode)
1429
+ } else {
1430
+ // 否则从本地存储恢复
1431
+ const savedFlipMode = localStorage.getItem('book-viewer-flip-mode')
1432
+ if (savedFlipMode && this.flipModes.some(m => m.value === savedFlipMode)) {
1433
+ this.flipMode = savedFlipMode
1434
+ }
1435
+ }
1436
+
1437
+ // 预加载首页图片(移动端优化)
1438
+ if (this.isMobile && this.pages && this.pages.length > 0) {
1439
+ await this.$nextTick()
1440
+ await this.preloadFirstPage()
1441
+ } else if (this.pages && this.pages.length > 0) {
1442
+ this.contentReady = true
1443
+ }
1444
+
1445
+ // 监听窗口尺寸变化
1446
+ window.addEventListener('resize', this.handleResize)
1447
+
1448
+ // 监听点击事件,用于关闭翻页模式菜单
1449
+ document.addEventListener('click', this.closeFlipModeMenu)
1450
+ },
1451
+
1452
+ beforeDestroy() {
1453
+ // 清理事件监听
1454
+ window.removeEventListener('resize', this.handleResize)
1455
+ // 清理拖拽事件监听
1456
+ document.removeEventListener('mousemove', this.onCatalogueMouseMove)
1457
+ document.removeEventListener('mouseup', this.onCatalogueMouseUp)
1458
+ // 清理翻页模式菜单事件监听
1459
+ document.removeEventListener('click', this.closeFlipModeMenu)
1460
+ // 清理控件自动隐藏定时器
1461
+ if (this.hideControlsTimer) {
1462
+ clearTimeout(this.hideControlsTimer)
1463
+ }
1464
+ }
1465
+ }
1466
+ </script>
1467
+
1468
+ <style scoped>
1469
+ .photo-album-container {
1470
+ /* min-height: 100vh; */
1471
+ background: transparent;
1472
+ padding: 5px 5px 80px 5px;
1473
+ display: flex;
1474
+ flex-direction: column;
1475
+ align-items: center;
1476
+ justify-content: center;
1477
+ transform-origin: center center;
1478
+ transition: transform 0.2s ease-out;
1479
+ overflow: hidden;
1480
+ position: relative;
1481
+ }
1482
+
1483
+ .album-header {
1484
+ text-align: center;
1485
+ color: #495057;
1486
+ margin-bottom: 30px;
1487
+ }
1488
+
1489
+ .album-header h1 {
1490
+ font-size: 2.5rem;
1491
+ margin-bottom: 10px;
1492
+ text-shadow: 1px 1px 2px rgba(0,0,0,0.1);
1493
+ font-weight: 600;
1494
+ }
1495
+
1496
+ .album-header p {
1497
+ font-size: 1.1rem;
1498
+ opacity: 0.9;
1499
+ }
1500
+
1501
+ .flipbook-wrapper {
1502
+ perspective: 2000px;
1503
+ margin-bottom: 20px;
1504
+ display: flex;
1505
+ justify-content: center;
1506
+ align-items: center;
1507
+ width: 100%;
1508
+ }
1509
+
1510
+ .flipbook {
1511
+ width: 1600px;
1512
+ height: 1200px;
1513
+ box-shadow: none;
1514
+ border-radius: 0;
1515
+ overflow: hidden;
1516
+ transition: all 0.3s ease;
1517
+ }
1518
+
1519
+ /* 隐藏标题时的调整 */
1520
+ .album-header[style*="display: none"],
1521
+ .album-header[v-if="false"] {
1522
+ display: none !important;
1523
+ }
1524
+
1525
+ .photo-album-container:has(.album-header[v-if="false"]) .flipbook,
1526
+ .photo-album-container .flipbook {
1527
+ max-height: calc(100vh - 100px);
1528
+ }
1529
+
1530
+ /* 移动端单页模式样式 */
1531
+ .flipbook.mobile-mode {
1532
+ width: 100%;
1533
+ max-width: none;
1534
+ height: 92vh;
1535
+ max-height: 92vh;
1536
+ margin: 0 auto;
1537
+ }
1538
+
1539
+ /* PC端双页模式样式 */
1540
+ .flipbook.desktop-mode {
1541
+ width: 1600px;
1542
+ height: 1200px;
1543
+ margin: 0 auto;
1544
+ }
1545
+
1546
+ .page {
1547
+ background: white;
1548
+ display: flex;
1549
+ flex-direction: column;
1550
+ justify-content: center;
1551
+ align-items: center;
1552
+ position: relative;
1553
+ border-radius: 5px;
1554
+ }
1555
+
1556
+ .page--cover {
1557
+ background: linear-gradient(45deg, #e9ecef 0%, #dee2e6 50%, #ced4da 100%);
1558
+ color: #495057;
1559
+ }
1560
+
1561
+ .cover-page, .back-cover {
1562
+ text-align: center;
1563
+ height: 100%;
1564
+ display: flex;
1565
+ flex-direction: column;
1566
+ justify-content: center;
1567
+ align-items: center;
1568
+ padding: 40px;
1569
+ }
1570
+
1571
+ .cover-page h2 {
1572
+ font-size: 3rem;
1573
+ margin-bottom: 20px;
1574
+ text-shadow: 2px 2px 4px rgba(0,0,0,0.3);
1575
+ font-weight: bold;
1576
+ }
1577
+
1578
+ .cover-page p {
1579
+ font-size: 1.5rem;
1580
+ opacity: 0.9;
1581
+ margin-bottom: 30px;
1582
+ }
1583
+
1584
+ .cover-decoration {
1585
+ display: flex;
1586
+ align-items: center;
1587
+ gap: 15px;
1588
+ margin-top: 20px;
1589
+ }
1590
+
1591
+ .decoration-line {
1592
+ width: 60px;
1593
+ height: 2px;
1594
+ background: rgba(73,80,87,0.6);
1595
+ border-radius: 1px;
1596
+ }
1597
+
1598
+ .cover-decoration span {
1599
+ font-size: 1.1rem;
1600
+ opacity: 0.9;
1601
+ white-space: nowrap;
1602
+ }
1603
+
1604
+ .back-cover h3 {
1605
+ font-size: 2.5rem;
1606
+ margin-bottom: 15px;
1607
+ text-shadow: 2px 2px 4px rgba(0,0,0,0.3);
1608
+ }
1609
+
1610
+ .back-cover p {
1611
+ font-size: 1.2rem;
1612
+ opacity: 0.8;
1613
+ margin-bottom: 30px;
1614
+ }
1615
+
1616
+ .back-decoration p {
1617
+ font-size: 1.5rem;
1618
+ margin-bottom: 10px;
1619
+ }
1620
+
1621
+ .back-decoration small {
1622
+ opacity: 0.7;
1623
+ font-size: 1rem;
1624
+ }
1625
+
1626
+ .content-page {
1627
+ padding: 20px;
1628
+ height: 100%;
1629
+ width: 100%;
1630
+ box-sizing: border-box;
1631
+ display: flex;
1632
+ flex-direction: column;
1633
+ justify-content: space-between;
1634
+ }
1635
+
1636
+ .image-container {
1637
+ flex: 1;
1638
+ display: flex;
1639
+ justify-content: center;
1640
+ align-items: center;
1641
+ margin-bottom: 0;
1642
+ width: 100%;
1643
+ height: 100%;
1644
+ overflow: hidden;
1645
+ }
1646
+
1647
+ .image-container img {
1648
+ max-width: 100%;
1649
+ max-height: 100%;
1650
+ width: auto;
1651
+ height: auto;
1652
+ object-fit: contain;
1653
+ border-radius: 8px;
1654
+ box-shadow: 0 4px 12px rgba(0,0,0,0.1);
1655
+ transition: transform 0.3s ease;
1656
+ }
1657
+
1658
+ .image-container img:hover {
1659
+ transform: scale(1.02);
1660
+ }
1661
+
1662
+ .page-content-info {
1663
+ background: rgba(255,255,255,0.95);
1664
+ padding: 8px;
1665
+ border-radius: 12px;
1666
+ box-shadow: 0 2px 8px rgba(0,0,0,0.1);
1667
+ backdrop-filter: blur(10px);
1668
+ margin-top: 4px;
1669
+ flex-shrink: 0;
1670
+ }
1671
+
1672
+ .image-title {
1673
+ font-size: 1.2rem;
1674
+ font-weight: bold;
1675
+ color: #2c3e50;
1676
+ margin-bottom: 8px;
1677
+ text-align: center;
1678
+ display: block;
1679
+ }
1680
+
1681
+ .image-description {
1682
+ font-size: 0.9rem;
1683
+ color: #5a6c7d;
1684
+ line-height: 1.4;
1685
+ text-align: center;
1686
+ margin-bottom: 10px;
1687
+ display: block;
1688
+ }
1689
+
1690
+ .page-number {
1691
+ text-align: center;
1692
+ padding-top: 10px;
1693
+ border-top: 1px solid rgba(0,0,0,0.1);
1694
+ }
1695
+
1696
+ .page-number span {
1697
+ background: linear-gradient(45deg, #667eea, #764ba2);
1698
+ color: white;
1699
+ padding: 4px 12px;
1700
+ border-radius: 12px;
1701
+ font-size: 0.8rem;
1702
+ font-weight: bold;
1703
+ }
1704
+
1705
+ .controls {
1706
+ display: flex;
1707
+ align-items: center;
1708
+ justify-content: center;
1709
+ gap: 12px;
1710
+ background: transparent;
1711
+ padding: 12px 20px;
1712
+ border-radius: 0;
1713
+ backdrop-filter: none;
1714
+ box-shadow: none;
1715
+ border: none;
1716
+ width: fit-content;
1717
+ margin: 20px auto;
1718
+ z-index: 1000;
1719
+ }
1720
+
1721
+ .desktop-controls {
1722
+ position: relative;
1723
+ margin: 20px auto;
1724
+ }
1725
+
1726
+ .mobile-controls {
1727
+ position: fixed;
1728
+ bottom: 20px;
1729
+ left: 50%;
1730
+ transform: translateX(-50%);
1731
+ margin: 0;
1732
+ max-width: 90vw;
1733
+ }
1734
+
1735
+ .btn {
1736
+ background: #333;
1737
+ color: white;
1738
+ border: 1px solid #666;
1739
+ padding: 8px 16px;
1740
+ border-radius: 4px;
1741
+ cursor: pointer;
1742
+ font-size: 0.9rem;
1743
+ transition: all 0.3s ease;
1744
+ box-shadow: none;
1745
+ }
1746
+
1747
+ .btn:hover:not(:disabled) {
1748
+ background: #555;
1749
+ transform: none;
1750
+ box-shadow: none;
1751
+ }
1752
+
1753
+ .btn:disabled {
1754
+ opacity: 0.5;
1755
+ cursor: not-allowed;
1756
+ transform: none;
1757
+ }
1758
+
1759
+ .page-indicator {
1760
+ color: #495057;
1761
+ font-size: 0.95rem;
1762
+ font-weight: bold;
1763
+ text-shadow: none;
1764
+ min-width: 70px;
1765
+ text-align: center;
1766
+ }
1767
+
1768
+ /* 页码跳转容器样式 */
1769
+ .page-jump-container {
1770
+ display: flex;
1771
+ align-items: center;
1772
+ justify-content: center;
1773
+ gap: 8px;
1774
+ background: rgba(255, 255, 255, 0.95);
1775
+ padding: 10px 16px;
1776
+ border-radius: 8px;
1777
+ box-shadow: 0 2px 12px rgba(0, 0, 0, 0.15);
1778
+ z-index: 1000;
1779
+ width: fit-content;
1780
+ margin: 10px auto 0;
1781
+ }
1782
+
1783
+ .mobile-page-jump {
1784
+ padding: 8px 12px;
1785
+ margin: 10px auto 0;
1786
+ }
1787
+
1788
+ .page-jump-input {
1789
+ width: 120px;
1790
+ padding: 8px 12px;
1791
+ border: 1px solid #ddd;
1792
+ border-radius: 6px;
1793
+ background: #fff;
1794
+ color: #333;
1795
+ font-size: 16px;
1796
+ text-align: center;
1797
+ outline: none;
1798
+ transition: border-color 0.2s ease, box-shadow 0.2s ease;
1799
+ }
1800
+
1801
+ .page-jump-input:focus {
1802
+ border-color: #333;
1803
+ box-shadow: 0 0 0 2px rgba(51, 51, 51, 0.1);
1804
+ }
1805
+
1806
+ .page-jump-input::placeholder {
1807
+ color: #999;
1808
+ font-size: 0.85rem;
1809
+ }
1810
+
1811
+ .btn-jump {
1812
+ padding: 8px 16px;
1813
+ white-space: nowrap;
1814
+ background: #333;
1815
+ color: white;
1816
+ border: none;
1817
+ border-radius: 6px;
1818
+ font-size: 0.9rem;
1819
+ cursor: pointer;
1820
+ transition: background 0.2s ease;
1821
+ }
1822
+
1823
+ .btn-jump:hover {
1824
+ background: #555;
1825
+ }
1826
+
1827
+ /* PC端响应式设计 */
1828
+ @media (max-width: 1800px) and (min-width: 769px) {
1829
+ .flipbook.desktop-mode {
1830
+ width: 95vw;
1831
+ height: calc(95vw * 0.625);
1832
+ max-width: 1600px;
1833
+ max-height: 1000px;
1834
+ }
1835
+ }
1836
+
1837
+ /* 移动端样式 */
1838
+ @media (max-width: 768px) {
1839
+ .photo-album-container {
1840
+ padding: 5px 5px 120px 5px;
1841
+ align-items: center;
1842
+ touch-action: none;
1843
+ overflow: hidden;
1844
+ }
1845
+
1846
+ .flipbook-wrapper {
1847
+ width: 100%;
1848
+ display: flex;
1849
+ justify-content: center;
1850
+ }
1851
+
1852
+ .flipbook.mobile-mode {
1853
+ width: 98vw;
1854
+ height: 85vh;
1855
+ max-width: none;
1856
+ max-height: 85vh;
1857
+ margin: 0 auto;
1858
+ }
1859
+
1860
+ .album-header {
1861
+ width: 100%;
1862
+ text-align: center;
1863
+ }
1864
+
1865
+ .album-header h1 {
1866
+ font-size: 2rem;
1867
+ }
1868
+
1869
+
1870
+ .btn {
1871
+ padding: 8px 16px;
1872
+ font-size: 0.9rem;
1873
+ }
1874
+
1875
+ .page-indicator {
1876
+ font-size: 0.85rem;
1877
+ min-width: 120px;
1878
+ text-align: center;
1879
+ }
1880
+
1881
+ /* 移动端页码跳转 */
1882
+ .page-jump-container {
1883
+ top: 10px;
1884
+ right: 10px;
1885
+ padding: 8px 10px;
1886
+ gap: 6px;
1887
+ }
1888
+
1889
+ .page-jump-input {
1890
+ width: 100px;
1891
+ padding: 6px 10px;
1892
+ font-size: 0.85rem;
1893
+ }
1894
+
1895
+ .btn-jump {
1896
+ padding: 6px 12px;
1897
+ font-size: 0.85rem;
1898
+ }
1899
+
1900
+ /* 移动端图片优化 */
1901
+ .image-container {
1902
+ margin-bottom: 0;
1903
+ }
1904
+
1905
+ .image-container img {
1906
+ max-width: 100%;
1907
+ max-height: 100%;
1908
+ border-radius: 6px;
1909
+ }
1910
+
1911
+ .page-content-info {
1912
+ padding: 6px;
1913
+ margin-top: 2px;
1914
+ }
1915
+ }
1916
+
1917
+ @media (max-width: 480px) {
1918
+ .flipbook.mobile-mode {
1919
+ width: 98vw;
1920
+ height: 80vh;
1921
+ max-height: 80vh;
1922
+ margin: 0 auto;
1923
+ }
1924
+
1925
+ .album-header h1 {
1926
+ font-size: 1.8rem;
1927
+ }
1928
+
1929
+ .mobile-controls {
1930
+ gap: 6px;
1931
+ padding: 8px 12px;
1932
+ justify-content: center;
1933
+ align-items: center;
1934
+ bottom: 15px;
1935
+ width: 95%;
1936
+ /* max-width: 95vw; */
1937
+ }
1938
+
1939
+ .btn {
1940
+ padding: 6px 12px;
1941
+ font-size: 0.8rem;
1942
+ }
1943
+
1944
+ .page-indicator {
1945
+ text-align: center;
1946
+ min-width: 100px;
1947
+ }
1948
+
1949
+ /* 小屏幕图片优化 */
1950
+ .image-container {
1951
+ margin-bottom: 0;
1952
+ }
1953
+
1954
+ .image-container img {
1955
+ max-width: 100%;
1956
+ max-height: 100%;
1957
+ border-radius: 4px;
1958
+ }
1959
+
1960
+ .page-content-info {
1961
+ padding: 4px;
1962
+ margin-top: 1px;
1963
+ }
1964
+ }
1965
+
1966
+ /* 设备特定的优化 */
1967
+ .mobile-optimized {
1968
+ /* 移动端优化 */
1969
+ -webkit-tap-highlight-color: transparent;
1970
+ -webkit-touch-callout: none;
1971
+ -webkit-user-select: none;
1972
+ user-select: none;
1973
+ }
1974
+
1975
+ .desktop-optimized {
1976
+ /* PC端优化 */
1977
+ cursor: pointer;
1978
+ }
1979
+
1980
+ /* 翻页按钮在不同设备上的样式 */
1981
+ .btn {
1982
+ transition: all 0.2s ease;
1983
+ }
1984
+
1985
+ .btn:hover:not(:disabled) {
1986
+ background: #555;
1987
+ transform: none;
1988
+ box-shadow: none;
1989
+ }
1990
+
1991
+ /* 缩放提示样式 */
1992
+ .zoom-hint {
1993
+ position: fixed;
1994
+ top: 20px;
1995
+ right: 20px;
1996
+ background: rgba(0, 0, 0, 0.85);
1997
+ color: white;
1998
+ padding: 10px 16px;
1999
+ border-radius: 25px;
2000
+ font-size: 0.85rem;
2001
+ opacity: 0;
2002
+ transform: translateY(-10px);
2003
+ transition: all 0.3s ease;
2004
+ z-index: 10001;
2005
+ pointer-events: none;
2006
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
2007
+ }
2008
+
2009
+ .zoom-hint.show {
2010
+ opacity: 1;
2011
+ transform: translateY(0);
2012
+ pointer-events: auto;
2013
+ }
2014
+
2015
+ .zoom-info {
2016
+ display: flex;
2017
+ flex-direction: row;
2018
+ align-items: center;
2019
+ gap: 12px;
2020
+ }
2021
+
2022
+ .zoom-info span {
2023
+ font-weight: 500;
2024
+ }
2025
+
2026
+ .btn-reset-zoom {
2027
+ background: rgba(255, 255, 255, 0.2);
2028
+ color: white;
2029
+ border: 1px solid rgba(255, 255, 255, 0.3);
2030
+ padding: 4px 12px;
2031
+ border-radius: 15px;
2032
+ font-size: 0.75rem;
2033
+ cursor: pointer;
2034
+ transition: all 0.2s ease;
2035
+ font-weight: 600;
2036
+ white-space: nowrap;
2037
+ }
2038
+
2039
+ .btn-reset-zoom:hover {
2040
+ background: rgba(255, 255, 255, 0.3);
2041
+ border-color: rgba(255, 255, 255, 0.5);
2042
+ transform: scale(1.05);
2043
+ }
2044
+
2045
+ .btn-reset-zoom:active {
2046
+ transform: scale(0.95);
2047
+ }
2048
+
2049
+ /* 控件显示/隐藏动画 */
2050
+ .controls {
2051
+ opacity: 0;
2052
+ visibility: hidden;
2053
+ transform: translateY(20px);
2054
+ transition: opacity 0.3s ease, visibility 0.3s ease, transform 0.3s ease;
2055
+ display: flex;
2056
+ background: rgba(255, 255, 255, 0.9);
2057
+ border-radius: 8px;
2058
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
2059
+ }
2060
+
2061
+ .controls.controls-visible {
2062
+ opacity: 1;
2063
+ visibility: visible;
2064
+ transform: translateY(0);
2065
+ }
2066
+
2067
+ .mobile-controls {
2068
+ position: fixed !important;
2069
+ bottom: 76px !important;
2070
+ left: 50% !important;
2071
+ transform: translateX(-50%) !important;
2072
+ z-index: 10000 !important;
2073
+ }
2074
+
2075
+ .desktop-controls {
2076
+ position: relative !important;
2077
+ margin: 20px auto !important;
2078
+ }
2079
+
2080
+ /* 目录按钮样式 */
2081
+ .catalogue-button {
2082
+ position: fixed;
2083
+ z-index: 9999999999;
2084
+ opacity: 0;
2085
+ visibility: hidden;
2086
+ transform: translateX(-20px);
2087
+ transition: opacity 0.3s ease, visibility 0.3s ease, transform 0.3s ease;
2088
+ user-select: none;
2089
+ }
2090
+
2091
+ .catalogue-button.catalogue-visible {
2092
+ opacity: 1;
2093
+ visibility: visible;
2094
+ transform: translateX(0);
2095
+ }
2096
+
2097
+ .catalogue-button.dragging {
2098
+ transform: scale(1.05);
2099
+ /* box-shadow: 0 8px 25px rgba(0, 0, 0, 0.2); */
2100
+ z-index: 999999;
2101
+ }
2102
+
2103
+
2104
+ .btn-catalogue {
2105
+ background: linear-gradient(45deg, #1989fa, #0066cc);
2106
+ color: white;
2107
+ border: 2px solid rgba(255, 255, 255, 0.3);
2108
+ padding: 10px 16px;
2109
+ border-radius: 20px;
2110
+ font-size: 0.9rem;
2111
+ font-weight: 600;
2112
+ box-shadow: 0 4px 12px rgba(25, 137, 250, 0.4), 0 0 0 1px rgba(255, 255, 255, 0.1);
2113
+ transition: all 0.2s ease;
2114
+ display: flex;
2115
+ align-items: center;
2116
+ gap: 6px;
2117
+ cursor: grab;
2118
+ white-space: nowrap;
2119
+ pointer-events: all;
2120
+ }
2121
+
2122
+ .catalogue-button.dragging .btn-catalogue {
2123
+ cursor: grabbing;
2124
+ /* background: linear-gradient(45deg, #0066cc, #004499); */
2125
+ }
2126
+
2127
+ .btn-catalogue:active {
2128
+ transform: scale(0.98);
2129
+ }
2130
+
2131
+ /* 拖拽手柄样式 */
2132
+ .btn-catalogue .drag-handle {
2133
+ color: rgba(255, 255, 255, 0.7);
2134
+ font-size: 14px;
2135
+ margin-right: 2px;
2136
+ user-select: none;
2137
+ }
2138
+
2139
+ .catalogue-icon {
2140
+ font-size: 1rem;
2141
+ flex-shrink: 0;
2142
+ }
2143
+
2144
+ .catalogue-text {
2145
+ font-size: 0.9rem;
2146
+ flex-shrink: 0;
2147
+ }
2148
+
2149
+ .btn-catalogue:hover {
2150
+ background: linear-gradient(45deg, #0066cc, #004499);
2151
+ transform: translateY(-2px);
2152
+ box-shadow: 0 6px 16px rgba(25, 137, 250, 0.4);
2153
+ }
2154
+
2155
+ .btn-catalogue:hover .drag-handle {
2156
+ color: rgba(255, 255, 255, 0.9);
2157
+ }
2158
+
2159
+ /* 移动端按钮优化 */
2160
+ @media (max-width: 768px) {
2161
+ .btn:active {
2162
+ background: #555;
2163
+ transform: none;
2164
+ }
2165
+
2166
+ .btn:hover:not(:disabled) {
2167
+ background: #555;
2168
+ transform: none;
2169
+ }
2170
+
2171
+ .btn-catalogue {
2172
+ padding: 8px 12px;
2173
+ font-size: 0.8rem;
2174
+ border-radius: 16px;
2175
+ }
2176
+
2177
+ .btn-catalogue .drag-handle {
2178
+ font-size: 16px;
2179
+ }
2180
+
2181
+ .catalogue-icon {
2182
+ font-size: 0.9rem;
2183
+ }
2184
+
2185
+ .catalogue-text {
2186
+ font-size: 0.8rem;
2187
+ }
2188
+
2189
+ .zoom-hint {
2190
+ top: 10px;
2191
+ right: 10px;
2192
+ font-size: 0.75rem;
2193
+ }
2194
+ }
2195
+
2196
+ /* 加载状态样式 */
2197
+ .loading-overlay {
2198
+ position: fixed;
2199
+ top: 0;
2200
+ left: 0;
2201
+ width: 100%;
2202
+ height: 100%;
2203
+ background: rgba(255, 255, 255, 0.95);
2204
+ backdrop-filter: blur(10px);
2205
+ display: flex;
2206
+ justify-content: center;
2207
+ align-items: center;
2208
+ z-index: 9999;
2209
+ animation: fadeIn 0.3s ease-out;
2210
+ }
2211
+
2212
+ .loading-container {
2213
+ text-align: center;
2214
+ padding: 40px;
2215
+ background: white;
2216
+ border-radius: 20px;
2217
+ box-shadow: 0 20px 60px rgba(0, 0, 0, 0.1);
2218
+ max-width: 400px;
2219
+ width: 90%;
2220
+ }
2221
+
2222
+ .loading-spinner {
2223
+ position: relative;
2224
+ width: 80px;
2225
+ height: 80px;
2226
+ margin: 0 auto 30px;
2227
+ }
2228
+
2229
+ .spinner-ring {
2230
+ position: absolute;
2231
+ width: 100%;
2232
+ height: 100%;
2233
+ border: 3px solid transparent;
2234
+ border-top: 3px solid #007bff;
2235
+ border-radius: 50%;
2236
+ animation: spin 1.2s linear infinite;
2237
+ }
2238
+
2239
+ .spinner-ring:nth-child(2) {
2240
+ width: 60px;
2241
+ height: 60px;
2242
+ top: 10px;
2243
+ left: 10px;
2244
+ border-top-color: #28a745;
2245
+ animation-delay: -0.4s;
2246
+ }
2247
+
2248
+ .spinner-ring:nth-child(3) {
2249
+ width: 40px;
2250
+ height: 40px;
2251
+ top: 20px;
2252
+ left: 20px;
2253
+ border-top-color: #ffc107;
2254
+ animation-delay: -0.8s;
2255
+ }
2256
+
2257
+ @keyframes spin {
2258
+ 0% { transform: rotate(0deg); }
2259
+ 100% { transform: rotate(360deg); }
2260
+ }
2261
+
2262
+ .loading-text h3 {
2263
+ color: #333;
2264
+ font-size: 1.5rem;
2265
+ margin-bottom: 10px;
2266
+ font-weight: 600;
2267
+ }
2268
+
2269
+ .loading-text p {
2270
+ color: #666;
2271
+ font-size: 1rem;
2272
+ margin-bottom: 20px;
2273
+ line-height: 1.5;
2274
+ }
2275
+
2276
+ .loading-progress {
2277
+ width: 100%;
2278
+ height: 4px;
2279
+ background: #f0f0f0;
2280
+ border-radius: 2px;
2281
+ overflow: hidden;
2282
+ margin-top: 20px;
2283
+ }
2284
+
2285
+ .progress-bar {
2286
+ height: 100%;
2287
+ background: linear-gradient(90deg, #007bff, #28a745, #ffc107);
2288
+ background-size: 200% 100%;
2289
+ animation: progressMove 2s ease-in-out infinite;
2290
+ border-radius: 2px;
2291
+ }
2292
+
2293
+ @keyframes progressMove {
2294
+ 0% {
2295
+ transform: translateX(-100%);
2296
+ background-position: 200% 0;
2297
+ }
2298
+ 100% {
2299
+ transform: translateX(100%);
2300
+ background-position: -200% 0;
2301
+ }
2302
+ }
2303
+
2304
+ /* 错误状态样式 */
2305
+ .error-container {
2306
+ position: fixed;
2307
+ top: 0;
2308
+ left: 0;
2309
+ width: 100%;
2310
+ height: 100%;
2311
+ background: rgba(255, 255, 255, 0.95);
2312
+ backdrop-filter: blur(10px);
2313
+ display: flex;
2314
+ justify-content: center;
2315
+ align-items: center;
2316
+ z-index: 9999;
2317
+ animation: fadeIn 0.3s ease-out;
2318
+ }
2319
+
2320
+ .error-content {
2321
+ text-align: center;
2322
+ padding: 40px;
2323
+ background: white;
2324
+ border-radius: 20px;
2325
+ box-shadow: 0 20px 60px rgba(0, 0, 0, 0.1);
2326
+ max-width: 400px;
2327
+ width: 90%;
2328
+ }
2329
+
2330
+ .error-icon {
2331
+ font-size: 4rem;
2332
+ margin-bottom: 20px;
2333
+ display: block;
2334
+ }
2335
+
2336
+ .error-content h3 {
2337
+ color: #dc3545;
2338
+ font-size: 1.5rem;
2339
+ margin-bottom: 15px;
2340
+ font-weight: 600;
2341
+ }
2342
+
2343
+ .error-content p {
2344
+ color: #666;
2345
+ font-size: 1rem;
2346
+ margin-bottom: 30px;
2347
+ line-height: 1.5;
2348
+ }
2349
+
2350
+ .retry-btn {
2351
+ background: linear-gradient(45deg, #007bff, #0056b3);
2352
+ color: white;
2353
+ border: none;
2354
+ padding: 12px 30px;
2355
+ border-radius: 25px;
2356
+ font-size: 1rem;
2357
+ font-weight: 600;
2358
+ cursor: pointer;
2359
+ transition: all 0.3s ease;
2360
+ display: inline-flex;
2361
+ align-items: center;
2362
+ gap: 8px;
2363
+ box-shadow: 0 4px 15px rgba(0, 123, 255, 0.3);
2364
+ }
2365
+
2366
+ .retry-btn:hover {
2367
+ transform: translateY(-2px);
2368
+ box-shadow: 0 6px 20px rgba(0, 123, 255, 0.4);
2369
+ }
2370
+
2371
+ .retry-btn:active {
2372
+ transform: translateY(0);
2373
+ }
2374
+
2375
+ .retry-icon {
2376
+ display: inline-block;
2377
+ animation: rotateIcon 0.5s ease-in-out;
2378
+ }
2379
+
2380
+ .retry-btn:hover .retry-icon {
2381
+ animation: rotateIcon 0.5s ease-in-out infinite;
2382
+ }
2383
+
2384
+ @keyframes rotateIcon {
2385
+ from { transform: rotate(0deg); }
2386
+ to { transform: rotate(360deg); }
2387
+ }
2388
+
2389
+ /* 成功提示样式 */
2390
+ .success-toast {
2391
+ position: fixed;
2392
+ top: 30px;
2393
+ right: 30px;
2394
+ background: linear-gradient(45deg, #28a745, #20c997);
2395
+ color: white;
2396
+ padding: 15px 25px;
2397
+ border-radius: 25px;
2398
+ font-size: 1rem;
2399
+ font-weight: 600;
2400
+ box-shadow: 0 8px 25px rgba(40, 167, 69, 0.3);
2401
+ z-index: 10000;
2402
+ opacity: 0;
2403
+ transform: translateX(100px);
2404
+ transition: all 0.4s cubic-bezier(0.68, -0.55, 0.265, 1.55);
2405
+ display: flex;
2406
+ align-items: center;
2407
+ gap: 10px;
2408
+ }
2409
+
2410
+ .success-toast.show {
2411
+ opacity: 1;
2412
+ transform: translateX(0);
2413
+ }
2414
+
2415
+ .success-icon {
2416
+ font-size: 1.2rem;
2417
+ }
2418
+
2419
+ @keyframes fadeIn {
2420
+ from {
2421
+ opacity: 0;
2422
+ transform: scale(0.9);
2423
+ }
2424
+ to {
2425
+ opacity: 1;
2426
+ transform: scale(1);
2427
+ }
2428
+ }
2429
+
2430
+ /* 移动端适配 */
2431
+ /* 翻页模式选择器样式 */
2432
+ .flip-mode-selector {
2433
+ position: fixed;
2434
+ top: 20px;
2435
+ left: 20px;
2436
+ z-index: 10000;
2437
+ }
2438
+
2439
+ .mobile-flip-mode-selector {
2440
+ top: 15px;
2441
+ left: 15px;
2442
+ }
2443
+
2444
+ .btn-flip-mode {
2445
+ background: linear-gradient(45deg, #6c5ce7, #a29bfe);
2446
+ color: white;
2447
+ border: 2px solid rgba(255, 255, 255, 0.3);
2448
+ padding: 10px 16px;
2449
+ border-radius: 20px;
2450
+ font-size: 0.9rem;
2451
+ font-weight: 600;
2452
+ box-shadow: 0 4px 12px rgba(108, 92, 231, 0.4);
2453
+ transition: all 0.2s ease;
2454
+ display: flex;
2455
+ align-items: center;
2456
+ gap: 6px;
2457
+ cursor: pointer;
2458
+ }
2459
+
2460
+ .btn-flip-mode:hover {
2461
+ background: linear-gradient(45deg, #5f4ed5, #8c7ae6);
2462
+ transform: translateY(-2px);
2463
+ box-shadow: 0 6px 16px rgba(108, 92, 231, 0.5);
2464
+ }
2465
+
2466
+ .flip-mode-icon {
2467
+ font-size: 1rem;
2468
+ }
2469
+
2470
+ .flip-mode-text {
2471
+ font-size: 0.85rem;
2472
+ }
2473
+
2474
+ .flip-mode-menu {
2475
+ position: absolute;
2476
+ top: 100%;
2477
+ left: 0;
2478
+ margin-top: 8px;
2479
+ background: white;
2480
+ border-radius: 12px;
2481
+ box-shadow: 0 8px 25px rgba(0, 0, 0, 0.15);
2482
+ overflow: hidden;
2483
+ min-width: 150px;
2484
+ animation: slideDown 0.2s ease-out;
2485
+ }
2486
+
2487
+ @keyframes slideDown {
2488
+ from {
2489
+ opacity: 0;
2490
+ transform: translateY(-10px);
2491
+ }
2492
+ to {
2493
+ opacity: 1;
2494
+ transform: translateY(0);
2495
+ }
2496
+ }
2497
+
2498
+ .flip-mode-item {
2499
+ display: flex;
2500
+ align-items: center;
2501
+ gap: 10px;
2502
+ padding: 12px 16px;
2503
+ cursor: pointer;
2504
+ transition: background 0.2s ease;
2505
+ }
2506
+
2507
+ .flip-mode-item:hover {
2508
+ background: #f5f5f5;
2509
+ }
2510
+
2511
+ .flip-mode-item-active {
2512
+ background: linear-gradient(45deg, #6c5ce7, #a29bfe);
2513
+ color: white;
2514
+ }
2515
+
2516
+ .flip-mode-item-active:hover {
2517
+ background: linear-gradient(45deg, #5f4ed5, #8c7ae6);
2518
+ }
2519
+
2520
+ .flip-mode-item-icon {
2521
+ font-size: 1.1rem;
2522
+ }
2523
+
2524
+ .flip-mode-item-label {
2525
+ font-size: 0.9rem;
2526
+ font-weight: 500;
2527
+ }
2528
+
2529
+ /* 滑动模式样式 */
2530
+ .slide-viewer {
2531
+ width: 1600px;
2532
+ height: 1000px;
2533
+ overflow: hidden;
2534
+ position: relative;
2535
+ }
2536
+
2537
+ .slide-viewer.mobile-mode {
2538
+ width: 100%;
2539
+ max-width: none;
2540
+ height: 85vh;
2541
+ max-height: 85vh;
2542
+ }
2543
+
2544
+ .slide-container {
2545
+ display: flex;
2546
+ height: 100%;
2547
+ will-change: transform;
2548
+ }
2549
+
2550
+ .slide-page {
2551
+ flex: 0 0 100%;
2552
+ width: 100%;
2553
+ height: 100%;
2554
+ display: flex;
2555
+ justify-content: center;
2556
+ align-items: center;
2557
+ background: #f8f9fa;
2558
+ }
2559
+
2560
+ .slide-page-image {
2561
+ max-width: 100%;
2562
+ max-height: 100%;
2563
+ object-fit: contain;
2564
+ }
2565
+
2566
+ .slide-page-placeholder {
2567
+ font-size: 2rem;
2568
+ color: #666;
2569
+ text-align: center;
2570
+ }
2571
+
2572
+ /* 淡入淡出模式样式 */
2573
+ .fade-viewer {
2574
+ width: 1600px;
2575
+ height: 1000px;
2576
+ overflow: hidden;
2577
+ position: relative;
2578
+ }
2579
+
2580
+ .fade-viewer.mobile-mode {
2581
+ width: 100%;
2582
+ max-width: none;
2583
+ height: 85vh;
2584
+ max-height: 85vh;
2585
+ }
2586
+
2587
+ .fade-page-container {
2588
+ width: 100%;
2589
+ height: 100%;
2590
+ display: flex;
2591
+ justify-content: center;
2592
+ align-items: center;
2593
+ background: #f8f9fa;
2594
+ }
2595
+
2596
+ .fade-page-image {
2597
+ max-width: 100%;
2598
+ max-height: 100%;
2599
+ object-fit: contain;
2600
+ }
2601
+
2602
+ .fade-page-placeholder {
2603
+ font-size: 2rem;
2604
+ color: #666;
2605
+ text-align: center;
2606
+ }
2607
+
2608
+ .fade-page-enter-active,
2609
+ .fade-page-leave-active {
2610
+ transition: opacity 0.3s ease;
2611
+ }
2612
+
2613
+ .fade-page-enter,
2614
+ .fade-page-leave-to {
2615
+ opacity: 0;
2616
+ }
2617
+
2618
+ /* 垂直滚动模式样式 */
2619
+ .scroll-viewer {
2620
+ width: 1600px;
2621
+ height: 1000px;
2622
+ overflow-y: auto;
2623
+ overflow-x: hidden;
2624
+ position: relative;
2625
+ scroll-behavior: smooth;
2626
+ }
2627
+
2628
+ .scroll-viewer.mobile-mode {
2629
+ width: 100%;
2630
+ max-width: none;
2631
+ height: 85vh;
2632
+ max-height: 85vh;
2633
+ }
2634
+
2635
+ .scroll-container {
2636
+ width: 100%;
2637
+ }
2638
+
2639
+ .scroll-page {
2640
+ width: 100%;
2641
+ min-height: 100%;
2642
+ display: flex;
2643
+ justify-content: center;
2644
+ align-items: center;
2645
+ background: #f8f9fa;
2646
+ padding: 20px 0;
2647
+ box-sizing: border-box;
2648
+ }
2649
+
2650
+ .scroll-page-image {
2651
+ max-width: 100%;
2652
+ max-height: none;
2653
+ width: auto;
2654
+ }
2655
+
2656
+ .scroll-page-placeholder {
2657
+ font-size: 2rem;
2658
+ color: #666;
2659
+ text-align: center;
2660
+ min-height: 300px;
2661
+ display: flex;
2662
+ align-items: center;
2663
+ justify-content: center;
2664
+ }
2665
+
2666
+ /* 滚动条样式 */
2667
+ .scroll-viewer::-webkit-scrollbar {
2668
+ width: 8px;
2669
+ }
2670
+
2671
+ .scroll-viewer::-webkit-scrollbar-track {
2672
+ background: #f1f1f1;
2673
+ border-radius: 4px;
2674
+ }
2675
+
2676
+ .scroll-viewer::-webkit-scrollbar-thumb {
2677
+ background: #c1c1c1;
2678
+ border-radius: 4px;
2679
+ }
2680
+
2681
+ .scroll-viewer::-webkit-scrollbar-thumb:hover {
2682
+ background: #a1a1a1;
2683
+ }
2684
+
2685
+ /* 截断特效模式样式 */
2686
+ .clip-viewer {
2687
+ width: 1600px;
2688
+ height: 1000px;
2689
+ overflow: hidden;
2690
+ position: relative;
2691
+ perspective: 1200px;
2692
+ }
2693
+
2694
+ .clip-viewer.mobile-mode {
2695
+ width: 100%;
2696
+ max-width: none;
2697
+ height: 85vh;
2698
+ max-height: 85vh;
2699
+ }
2700
+
2701
+ .clip-pages-wrapper {
2702
+ width: 100%;
2703
+ height: 100%;
2704
+ position: relative;
2705
+ display: flex;
2706
+ justify-content: center;
2707
+ align-items: center;
2708
+ }
2709
+
2710
+ .clip-page {
2711
+ position: absolute;
2712
+ width: 100%;
2713
+ height: 100%;
2714
+ display: flex;
2715
+ justify-content: center;
2716
+ align-items: center;
2717
+ background: #f8f9fa;
2718
+ }
2719
+
2720
+ .clip-page-current {
2721
+ z-index: 2;
2722
+ clip-path: inset(0 0 0 0);
2723
+ }
2724
+
2725
+ .clip-page-next {
2726
+ z-index: 1;
2727
+ opacity: 0.5;
2728
+ transform: scale(0.95);
2729
+ filter: blur(2px);
2730
+ }
2731
+
2732
+ .clip-page-image {
2733
+ max-width: 100%;
2734
+ max-height: 100%;
2735
+ object-fit: contain;
2736
+ border-radius: 8px;
2737
+ box-shadow: 0 10px 40px rgba(0, 0, 0, 0.2);
2738
+ }
2739
+
2740
+ .clip-page-placeholder {
2741
+ font-size: 2rem;
2742
+ color: #666;
2743
+ text-align: center;
2744
+ }
2745
+
2746
+ /* 截断动画 */
2747
+ .clip-current-enter-active {
2748
+ animation: clipIn 0.5s cubic-bezier(0.4, 0, 0.2, 1);
2749
+ }
2750
+
2751
+ .clip-current-leave-active {
2752
+ animation: clipOut 0.5s cubic-bezier(0.4, 0, 0.2, 1);
2753
+ }
2754
+
2755
+ @keyframes clipIn {
2756
+ 0% {
2757
+ clip-path: inset(0 100% 0 0);
2758
+ transform: translateX(50px);
2759
+ opacity: 0;
2760
+ }
2761
+ 100% {
2762
+ clip-path: inset(0 0 0 0);
2763
+ transform: translateX(0);
2764
+ opacity: 1;
2765
+ }
2766
+ }
2767
+
2768
+ @keyframes clipOut {
2769
+ 0% {
2770
+ clip-path: inset(0 0 0 0);
2771
+ transform: translateX(0);
2772
+ opacity: 1;
2773
+ }
2774
+ 100% {
2775
+ clip-path: inset(0 0 0 100%);
2776
+ transform: translateX(-50px);
2777
+ opacity: 0;
2778
+ }
2779
+ }
2780
+
2781
+ /* 卡片风格模式样式 */
2782
+ .card-viewer {
2783
+ width: 1600px;
2784
+ height: 1000px;
2785
+ overflow: visible;
2786
+ position: relative;
2787
+ perspective: 1500px;
2788
+ }
2789
+
2790
+ .card-viewer.mobile-mode {
2791
+ width: 100%;
2792
+ max-width: none;
2793
+ height: 85vh;
2794
+ max-height: 85vh;
2795
+ }
2796
+
2797
+ .card-stack {
2798
+ width: 100%;
2799
+ height: 100%;
2800
+ position: relative;
2801
+ display: flex;
2802
+ justify-content: center;
2803
+ align-items: center;
2804
+ transform-style: preserve-3d;
2805
+ }
2806
+
2807
+ .card-transition-group {
2808
+ width: 100%;
2809
+ height: 100%;
2810
+ position: relative;
2811
+ display: flex;
2812
+ justify-content: center;
2813
+ align-items: center;
2814
+ }
2815
+
2816
+ .card-item {
2817
+ position: absolute;
2818
+ width: 70%;
2819
+ height: 90%;
2820
+ transition: all 0.5s cubic-bezier(0.4, 0, 0.2, 1);
2821
+ transform-style: preserve-3d;
2822
+ cursor: pointer;
2823
+ }
2824
+
2825
+ .card-content {
2826
+ width: 100%;
2827
+ height: 100%;
2828
+ background: white;
2829
+ border-radius: 16px;
2830
+ box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3), 0 8px 25px rgba(0, 0, 0, 0.2);
2831
+ overflow: hidden;
2832
+ display: flex;
2833
+ justify-content: center;
2834
+ align-items: center;
2835
+ position: relative;
2836
+ }
2837
+
2838
+ .card-item-current .card-content {
2839
+ box-shadow: 0 30px 80px rgba(0, 0, 0, 0.35), 0 15px 40px rgba(0, 0, 0, 0.25);
2840
+ }
2841
+
2842
+ .card-image {
2843
+ max-width: 100%;
2844
+ max-height: 100%;
2845
+ object-fit: contain;
2846
+ }
2847
+
2848
+ .card-placeholder {
2849
+ font-size: 2rem;
2850
+ color: #666;
2851
+ text-align: center;
2852
+ }
2853
+
2854
+ .card-page-number {
2855
+ position: absolute;
2856
+ bottom: 15px;
2857
+ left: 50%;
2858
+ transform: translateX(-50%);
2859
+ background: rgba(0, 0, 0, 0.7);
2860
+ color: white;
2861
+ padding: 6px 16px;
2862
+ border-radius: 20px;
2863
+ font-size: 0.85rem;
2864
+ font-weight: 500;
2865
+ }
2866
+
2867
+ .card-item-prev .card-page-number,
2868
+ .card-item-next .card-page-number {
2869
+ opacity: 0;
2870
+ }
2871
+
2872
+ /* 卡片切换动画 */
2873
+ .card-flip-enter-active,
2874
+ .card-flip-leave-active {
2875
+ transition: all 0.5s cubic-bezier(0.4, 0, 0.2, 1);
2876
+ }
2877
+
2878
+ .card-flip-enter {
2879
+ opacity: 0;
2880
+ transform: translateX(100%) scale(0.8) rotateY(-30deg);
2881
+ }
2882
+
2883
+ .card-flip-leave-to {
2884
+ opacity: 0;
2885
+ transform: translateX(-100%) scale(0.8) rotateY(30deg);
2886
+ }
2887
+
2888
+ /* 卡片悬停效果 */
2889
+ .card-item-current:hover .card-content {
2890
+ box-shadow: 0 35px 90px rgba(0, 0, 0, 0.4), 0 18px 50px rgba(0, 0, 0, 0.3);
2891
+ }
2892
+
2893
+ /* 移动端卡片样式 */
2894
+ @media (max-width: 768px) {
2895
+ .card-item {
2896
+ width: 85%;
2897
+ height: 85%;
2898
+ }
2899
+
2900
+ .card-item-prev,
2901
+ .card-item-next {
2902
+ display: none;
2903
+ }
2904
+
2905
+ .card-content {
2906
+ border-radius: 12px;
2907
+ }
2908
+
2909
+ .card-page-number {
2910
+ font-size: 0.75rem;
2911
+ padding: 4px 12px;
2912
+ }
2913
+
2914
+ .clip-page-next {
2915
+ display: none;
2916
+ }
2917
+ }
2918
+
2919
+ /* 移动端翻页模式选择器样式 */
2920
+ @media (max-width: 768px) {
2921
+ .btn-flip-mode {
2922
+ padding: 8px 12px;
2923
+ font-size: 0.8rem;
2924
+ border-radius: 16px;
2925
+ }
2926
+
2927
+ .flip-mode-icon {
2928
+ font-size: 0.9rem;
2929
+ }
2930
+
2931
+ .flip-mode-text {
2932
+ font-size: 0.75rem;
2933
+ }
2934
+
2935
+ .flip-mode-menu {
2936
+ min-width: 130px;
2937
+ }
2938
+
2939
+ .flip-mode-item {
2940
+ padding: 10px 14px;
2941
+ }
2942
+
2943
+ .flip-mode-item-icon {
2944
+ font-size: 1rem;
2945
+ }
2946
+
2947
+ .flip-mode-item-label {
2948
+ font-size: 0.8rem;
2949
+ }
2950
+ }
2951
+
2952
+ @media (max-width: 768px) {
2953
+ .loading-container,
2954
+ .error-content {
2955
+ padding: 30px 20px;
2956
+ margin: 20px;
2957
+ }
2958
+
2959
+ .loading-spinner {
2960
+ width: 60px;
2961
+ height: 60px;
2962
+ }
2963
+
2964
+ .spinner-ring:nth-child(2) {
2965
+ width: 45px;
2966
+ height: 45px;
2967
+ top: 7.5px;
2968
+ left: 7.5px;
2969
+ }
2970
+
2971
+ .spinner-ring:nth-child(3) {
2972
+ width: 30px;
2973
+ height: 30px;
2974
+ top: 15px;
2975
+ left: 15px;
2976
+ }
2977
+
2978
+ .loading-text h3,
2979
+ .error-content h3 {
2980
+ font-size: 1.3rem;
2981
+ }
2982
+
2983
+ .loading-text p,
2984
+ .error-content p {
2985
+ font-size: 0.9rem;
2986
+ }
2987
+
2988
+ .success-toast {
2989
+ top: 20px;
2990
+ right: 20px;
2991
+ left: 20px;
2992
+ right: 20px;
2993
+ font-size: 0.9rem;
2994
+ padding: 12px 20px;
2995
+ }
2996
+
2997
+ .retry-btn {
2998
+ padding: 10px 25px;
2999
+ font-size: 0.9rem;
3000
+ }
3001
+ }
3002
+
3003
+ </style>