@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,839 @@
1
+ <template>
2
+ <div class="catalogue-drawer-container">
3
+ <!-- 遮罩层 -->
4
+ <div v-if="visible" class="drawer-overlay" @click="closeDrawer"></div>
5
+
6
+ <!-- 抽屉主体 -->
7
+ <div
8
+ class="catalogue-drawer"
9
+ :class="{ 'drawer-open': visible }"
10
+ :style="{ width: drawerWidth }"
11
+ >
12
+ <div class="drawer-header">
13
+ <h3 class="drawer-title">目录</h3>
14
+ <button class="close-button" @click="closeDrawer">
15
+ <span class="close-icon">×</span>
16
+ </button>
17
+ </div>
18
+
19
+ <!-- 页码跳转 -->
20
+ <div class="page-jump-section">
21
+ <input
22
+ type="text"
23
+ v-model="jumpPageInput"
24
+ @input="handlePageInput"
25
+ placeholder="请输入页码"
26
+ class="page-jump-input"
27
+ @focus="onInputFocus"
28
+ @blur="onInputBlur"
29
+ />
30
+ <button @click="handleJumpToPage" class="btn-jump">
31
+ 跳转
32
+ </button>
33
+ </div>
34
+
35
+ <div class="drawer-content">
36
+ <!-- 加载状态 -->
37
+ <div v-if="loading" class="loading-container">
38
+ <div class="loading-spinner"></div>
39
+ <p>加载目录中...</p>
40
+ </div>
41
+
42
+ <!-- 错误状态 -->
43
+ <div v-else-if="error" class="error-container">
44
+ <div class="error-icon">⚠️</div>
45
+ <p>目录加载失败</p>
46
+ <button class="retry-button" @click="fetchCatalogue">
47
+ 重新加载
48
+ </button>
49
+ </div>
50
+
51
+ <!-- 目录内容 -->
52
+ <div v-else-if="catalogueData && catalogueData.length > 0" class="catalogue-list">
53
+ <catalogue-item
54
+ v-for="(item, index) in catalogueData"
55
+ :key="`catalogue-${index}`"
56
+ :item="item || {}"
57
+ :level="0"
58
+ @item-click="onItemClick"
59
+ />
60
+ </div>
61
+
62
+ <!-- 空状态 -->
63
+ <div v-else class="empty-container">
64
+ <p>暂无目录</p>
65
+ </div>
66
+ </div>
67
+ </div>
68
+ </div>
69
+ </template>
70
+
71
+ <script>
72
+ // 目录项组件
73
+ const CatalogueItem = {
74
+ name: 'CatalogueItem',
75
+ props: {
76
+ item: {
77
+ type: Object,
78
+ required: true
79
+ },
80
+ level: {
81
+ type: Number,
82
+ default: 0
83
+ }
84
+ },
85
+ data() {
86
+ return {
87
+ expanded: false
88
+ }
89
+ },
90
+ computed: {
91
+ hasChildren() {
92
+ const children = this.item.childrenList || this.item.children || this.item.subItems || this.item.items
93
+ return children && children.length > 0
94
+ },
95
+ childrenData() {
96
+ return this.item.childrenList || this.item.children || this.item.subItems || this.item.items || []
97
+ },
98
+ itemStyle() {
99
+ // 移除缩进,所有层级都左对齐
100
+ return {
101
+ paddingLeft: '12px',
102
+ paddingRight: '12px'
103
+ }
104
+ },
105
+ levelClass() {
106
+ return `level-${this.level}`
107
+ }
108
+ },
109
+ methods: {
110
+ toggleExpand() {
111
+ if (this.hasChildren) {
112
+ this.expanded = !this.expanded
113
+ } else {
114
+ this.handleClick()
115
+ }
116
+ },
117
+ handleClick() {
118
+ this.$emit('item-click', this.item)
119
+ }
120
+ },
121
+ render(h) {
122
+ const item = this.item
123
+ const titleText = item.titleName || item.title || item.name || item.text || item.label || '未命名章节'
124
+ const pageNum = item.startPageNum || item.pageNumber || item.page || item.pageNum
125
+
126
+ // 创建拖拽手柄
127
+ const dragHandle = h('span', {
128
+ class: 'drag-handle'
129
+ }, '')
130
+
131
+ // 创建展开图标
132
+ const expandIcon = this.hasChildren ? h('span', {
133
+ class: 'expand-icon'
134
+ }, this.expanded ? '▼' : '▶') : null
135
+
136
+ // 创建层级指示器
137
+ const levelIndicator = this.level > 0 ? h('span', {
138
+ class: `level-indicator level-${this.level}`
139
+ }, '•'.repeat(this.level)) : null
140
+
141
+ // 创建标题
142
+ const titleSpan = h('span', {
143
+ class: 'item-title'
144
+ }, titleText)
145
+
146
+ // 创建页码(放在标题后面)
147
+ // const pageSpan = pageNum ? h('span', {
148
+ // class: 'page-number'
149
+ // }, `第${pageNum}页`) : null
150
+ const pageSpan = null
151
+ // 创建子级目录
152
+ const childrenDiv = (this.hasChildren && this.expanded) ? h('div', {
153
+ class: 'children-container'
154
+ }, this.childrenData.map((child, index) => {
155
+ return h('catalogue-item', {
156
+ key: index,
157
+ props: {
158
+ item: child,
159
+ level: this.level + 1
160
+ },
161
+ on: {
162
+ 'item-click': (item) => this.$emit('item-click', item)
163
+ }
164
+ })
165
+ })) : null
166
+
167
+ return h('div', {
168
+ class: 'catalogue-item-wrapper'
169
+ }, [
170
+ h('div', {
171
+ class: [
172
+ 'catalogue-item',
173
+ this.levelClass,
174
+ {
175
+ 'has-children': this.hasChildren,
176
+ 'expanded': this.expanded
177
+ }
178
+ ],
179
+ style: this.itemStyle,
180
+ on: {
181
+ click: this.toggleExpand
182
+ }
183
+ }, [
184
+ h('div', {
185
+ class: 'item-content'
186
+ }, [dragHandle, expandIcon, levelIndicator, titleSpan, pageSpan].filter(Boolean))
187
+ ]),
188
+ childrenDiv
189
+ ].filter(Boolean))
190
+ }
191
+ }
192
+
193
+ export default {
194
+ name: 'BookCatalogueDrawer',
195
+ components: {
196
+ CatalogueItem
197
+ },
198
+ props: {
199
+ value: {
200
+ type: Boolean,
201
+ default: false
202
+ },
203
+ bookId: {
204
+ type: String,
205
+ default: ''
206
+ },
207
+ catalogue: {
208
+ type: Array,
209
+ default: () => []
210
+ },
211
+ loading: {
212
+ type: Boolean,
213
+ default: false
214
+ }
215
+ },
216
+ data() {
217
+ return {
218
+ catalogueData: [],
219
+ error: null,
220
+ jumpPageInput: ''
221
+ }
222
+ },
223
+ computed: {
224
+ visible: {
225
+ get() {
226
+ return this.value
227
+ },
228
+ set(val) {
229
+ this.$emit('input', val)
230
+ }
231
+ },
232
+ drawerWidth() {
233
+ // 响应式宽度
234
+ return window.innerWidth <= 768 ? '80%' : '400px'
235
+ }
236
+ },
237
+ watch: {
238
+ visible(newVal) {
239
+ if (newVal && this.bookId && !this.catalogueData.length) {
240
+ this.$emit('fetch-catalogue', this.bookId)
241
+ }
242
+ },
243
+ bookId(newVal) {
244
+ if (newVal && this.visible) {
245
+ this.$emit('fetch-catalogue', newVal)
246
+ }
247
+ },
248
+ catalogue: {
249
+ handler(newVal) {
250
+ if (newVal && newVal.length > 0) {
251
+ this.catalogueData = this.processCatalogueData(newVal)
252
+ }
253
+ },
254
+ immediate: true
255
+ }
256
+ },
257
+ methods: {
258
+ /**
259
+ * 获取目录数据 - 触发事件让父组件处理
260
+ */
261
+ fetchCatalogue() {
262
+ if (!this.bookId) {
263
+ console.warn('缺少 bookId,无法获取目录')
264
+ return
265
+ }
266
+ this.$emit('fetch-catalogue', this.bookId)
267
+ },
268
+
269
+ /**
270
+ * 处理目录数据,确保数据结构正确
271
+ * @param {Array} data - 原始目录数据
272
+ * @returns {Array} 处理后的目录数据
273
+ */
274
+ processCatalogueData(data) {
275
+ if (!Array.isArray(data)) {
276
+ return []
277
+ }
278
+
279
+ return data.map(item => {
280
+ if (!item || typeof item !== 'object') {
281
+ return {
282
+ titleName: '未知章节',
283
+ startPageNum: null,
284
+ childrenList: []
285
+ }
286
+ }
287
+
288
+ // 确保必要的字段存在
289
+ const processedItem = {
290
+ ...item,
291
+ titleName: item.titleName || item.title || item.name || '未命名章节',
292
+ startPageNum: item.startPageNum || item.pageNumber || item.page || item.pageNum || null,
293
+ childrenList: item.childrenList || item.children || item.subItems || item.items || []
294
+ }
295
+
296
+ // 递归处理子级
297
+ if (processedItem.childrenList && processedItem.childrenList.length > 0) {
298
+ processedItem.childrenList = this.processCatalogueData(processedItem.childrenList)
299
+ }
300
+
301
+ return processedItem
302
+ })
303
+ },
304
+
305
+ /**
306
+ * 目录项点击事件
307
+ * @param {Object} item - 点击的目录项
308
+ */
309
+ onItemClick(item) {
310
+ console.log('目录项被点击:', item)
311
+
312
+ // 发送事件给父组件
313
+ this.$emit('catalogue-click', item)
314
+
315
+ // 如果有页码信息,可以进行跳转
316
+ const pageNumber = item.startPageNum || item.pageNumber || item.page || item.pageNum
317
+ if (pageNumber) {
318
+ this.$emit('page-jump', pageNumber)
319
+ }
320
+ },
321
+
322
+ /**
323
+ * 关闭抽屉
324
+ */
325
+ closeDrawer() {
326
+ this.$emit('input', false)
327
+ },
328
+
329
+ /**
330
+ * 处理页码输入
331
+ */
332
+ handlePageInput(e) {
333
+ const value = e.target.value
334
+ const numValue = value.replace(/[^\d]/g, '')
335
+ this.jumpPageInput = numValue
336
+ },
337
+
338
+ /**
339
+ * 执行页码跳转
340
+ */
341
+ handleJumpToPage() {
342
+ const pageNum = parseInt(this.jumpPageInput)
343
+ if (pageNum && pageNum >= 1) {
344
+ this.$emit('page-jump', pageNum)
345
+ this.jumpPageInput = ''
346
+ this.closeDrawer()
347
+ }
348
+ },
349
+
350
+ /**
351
+ * 输入框获得焦点
352
+ */
353
+ onInputFocus() {
354
+ // 可以在这里添加焦点处理逻辑
355
+ },
356
+
357
+ /**
358
+ * 输入框失去焦点
359
+ */
360
+ onInputBlur() {
361
+ this.$emit('Focus')
362
+ },
363
+
364
+ /**
365
+ * 重置数据
366
+ */
367
+ resetData() {
368
+ this.catalogueData = []
369
+ this.error = null
370
+ this.loading = false
371
+ },
372
+
373
+ /**
374
+ * 处理窗口大小变化
375
+ */
376
+ handleResize() {
377
+ // 可以在这里添加响应式逻辑
378
+ }
379
+ },
380
+
381
+ mounted() {
382
+ // 监听窗口大小变化
383
+ window.addEventListener('resize', this.handleResize)
384
+ },
385
+
386
+ beforeDestroy() {
387
+ window.removeEventListener('resize', this.handleResize)
388
+ }
389
+ }
390
+ </script>
391
+
392
+ <style scoped>
393
+ /* 容器 */
394
+ .catalogue-drawer-container {
395
+ position: relative;
396
+ }
397
+
398
+ /* 遮罩层 */
399
+ .drawer-overlay {
400
+ position: fixed;
401
+ top: 0;
402
+ left: 0;
403
+ width: 100%;
404
+ height: 100%;
405
+ background: rgba(0, 0, 0, 0.5);
406
+ z-index: 19998;
407
+ transition: opacity 0.3s ease;
408
+ }
409
+
410
+ /* 抽屉主体 */
411
+ .catalogue-drawer {
412
+ position: fixed;
413
+ top: 0;
414
+ left: 0;
415
+ height: 100%;
416
+ background: #ffffff;
417
+ box-shadow: 2px 0 10px rgba(0, 0, 0, 0.1);
418
+ transform: translateX(-100%);
419
+ transition: transform 0.3s ease;
420
+ z-index: 19999;
421
+ overflow: hidden;
422
+ /* 处理安全区域 */
423
+ padding-top: env(safe-area-inset-top);
424
+ padding-bottom: env(safe-area-inset-bottom);
425
+ }
426
+
427
+ .catalogue-drawer.drawer-open {
428
+ transform: translateX(0);
429
+ }
430
+
431
+ .drawer-header {
432
+ display: flex;
433
+ justify-content: space-between;
434
+ align-items: center;
435
+ padding: 16px 20px;
436
+ border-bottom: 1px solid #e5e5e5;
437
+ background: #ffffff;
438
+ position: sticky;
439
+ top: 0;
440
+ z-index: 100;
441
+ }
442
+
443
+ .drawer-title {
444
+ font-size: 18px;
445
+ font-weight: 600;
446
+ color: #333333;
447
+ margin: 0;
448
+ }
449
+
450
+ .close-button {
451
+ background: none;
452
+ border: none;
453
+ cursor: pointer;
454
+ padding: 6px;
455
+ border-radius: 4px;
456
+ transition: background-color 0.2s ease;
457
+ width: 32px;
458
+ height: 32px;
459
+ display: flex;
460
+ align-items: center;
461
+ justify-content: center;
462
+ }
463
+
464
+ .close-button:hover {
465
+ background: #f5f5f5;
466
+ }
467
+
468
+ .close-icon {
469
+ font-size: 18px;
470
+ color: #666;
471
+ line-height: 1;
472
+ display: block;
473
+ }
474
+
475
+ .close-button:hover .close-icon {
476
+ color: #333;
477
+ }
478
+
479
+ /* 页码跳转区域 */
480
+ .page-jump-section {
481
+ display: flex;
482
+ align-items: center;
483
+ gap: 10px;
484
+ padding: 12px 16px;
485
+ border-bottom: 1px solid #e5e5e5;
486
+ background: #f8f9fa;
487
+ }
488
+
489
+ .page-jump-input {
490
+ flex: 1;
491
+ min-width: 0;
492
+ padding: 10px 12px;
493
+ border: 1px solid #ddd;
494
+ border-radius: 6px;
495
+ background: #fff;
496
+ color: #333;
497
+ font-size: 16px;
498
+ text-align: center;
499
+ outline: none;
500
+ transition: border-color 0.2s ease, box-shadow 0.2s ease;
501
+ }
502
+
503
+ .page-jump-input:focus {
504
+ border-color: #1989fa;
505
+ box-shadow: 0 0 0 2px rgba(25, 137, 250, 0.1);
506
+ }
507
+
508
+ .page-jump-input::placeholder {
509
+ color: #999;
510
+ font-size: 14px;
511
+ }
512
+
513
+ .btn-jump {
514
+ flex-shrink: 0;
515
+ padding: 10px 20px;
516
+ background: #1989fa;
517
+ color: white;
518
+ border: none;
519
+ border-radius: 6px;
520
+ font-size: 14px;
521
+ font-weight: 500;
522
+ cursor: pointer;
523
+ transition: background 0.2s ease;
524
+ white-space: nowrap;
525
+ }
526
+
527
+ .btn-jump:hover {
528
+ background: #0066cc;
529
+ }
530
+
531
+ .btn-jump:active {
532
+ background: #004499;
533
+ }
534
+
535
+ .drawer-content {
536
+ height: calc(100% - 65px);
537
+ overflow-y: auto;
538
+ -webkit-overflow-scrolling: touch;
539
+ }
540
+
541
+ .loading-container {
542
+ display: flex;
543
+ justify-content: center;
544
+ align-items: center;
545
+ height: 200px;
546
+ flex-direction: column;
547
+ gap: 12px;
548
+ }
549
+
550
+ .loading-spinner {
551
+ width: 40px;
552
+ height: 40px;
553
+ border: 3px solid #f3f3f3;
554
+ border-top: 3px solid #1989fa;
555
+ border-radius: 50%;
556
+ animation: spin 1s linear infinite;
557
+ }
558
+
559
+ @keyframes spin {
560
+ 0% { transform: rotate(0deg); }
561
+ 100% { transform: rotate(360deg); }
562
+ }
563
+
564
+ .error-container,
565
+ .empty-container {
566
+ display: flex;
567
+ flex-direction: column;
568
+ align-items: center;
569
+ justify-content: center;
570
+ padding: 40px 20px;
571
+ text-align: left;
572
+ }
573
+
574
+ .error-icon {
575
+ font-size: 48px;
576
+ margin-bottom: 16px;
577
+ }
578
+
579
+ .retry-button {
580
+ margin-top: 16px;
581
+ background: #1989fa;
582
+ color: white;
583
+ border: none;
584
+ padding: 8px 16px;
585
+ border-radius: 4px;
586
+ cursor: pointer;
587
+ transition: background-color 0.2s ease;
588
+ }
589
+
590
+ .retry-button:hover {
591
+ background: #0066cc;
592
+ }
593
+
594
+ .catalogue-list {
595
+ padding: 0;
596
+ }
597
+
598
+ .catalogue-item-wrapper {
599
+ border-bottom: 1px solid #f0f0f0;
600
+ }
601
+
602
+ .catalogue-item-wrapper:last-child {
603
+ border-bottom: none;
604
+ }
605
+
606
+ .catalogue-item {
607
+ padding: 12px;
608
+ cursor: pointer;
609
+ transition: all 0.2s ease;
610
+ position: relative;
611
+ min-height: 44px;
612
+ border-left: 3px solid transparent;
613
+ }
614
+
615
+ .catalogue-item:hover {
616
+ background-color: #f8f9fa;
617
+ }
618
+
619
+ .catalogue-item:active {
620
+ background-color: #e9ecef;
621
+ }
622
+
623
+ .item-content {
624
+ display: flex;
625
+ align-items: center;
626
+ flex: 1;
627
+ width: 100%;
628
+ justify-content: flex-start;
629
+ gap: 8px;
630
+ }
631
+
632
+ /* 拖拽手柄样式 */
633
+ .drag-handle {
634
+ color: #ccc;
635
+ font-size: 14px;
636
+ cursor: grab;
637
+ transition: color 0.2s ease;
638
+ flex-shrink: 0;
639
+ width: 20px;
640
+ text-align: center;
641
+ line-height: 1;
642
+ user-select: none;
643
+ }
644
+
645
+ .drag-handle:hover {
646
+ color: #999;
647
+ }
648
+
649
+ .drag-handle:active {
650
+ cursor: grabbing;
651
+ color: #666;
652
+ }
653
+
654
+ /* 层级指示器样式 */
655
+ .level-indicator {
656
+ font-size: 10px;
657
+ color: #999;
658
+ flex-shrink: 0;
659
+ line-height: 1;
660
+ margin-right: 4px;
661
+ }
662
+
663
+ .level-indicator.level-1 {
664
+ color: #007bff;
665
+ }
666
+
667
+ .level-indicator.level-2 {
668
+ color: #28a745;
669
+ }
670
+
671
+ .level-indicator.level-3 {
672
+ color: #ffc107;
673
+ }
674
+
675
+ .expand-icon {
676
+ font-size: 12px;
677
+ color: #999;
678
+ transition: color 0.2s ease;
679
+ flex-shrink: 0;
680
+ width: 16px;
681
+ text-align: center;
682
+ }
683
+
684
+ .catalogue-item:hover .expand-icon {
685
+ color: #666;
686
+ }
687
+
688
+ .item-title {
689
+ font-size: 14px;
690
+ color: #333;
691
+ line-height: 1.4;
692
+ font-weight: 400;
693
+ }
694
+
695
+ .page-number {
696
+ font-size: 12px;
697
+ color: #999;
698
+ margin-left: 6px;
699
+ }
700
+
701
+ /* 不同层级的样式 - 用颜色和字体大小区分层级 */
702
+ .catalogue-item.level-0 {
703
+ background: linear-gradient(90deg, transparent, rgba(0, 123, 255, 0.05), transparent);
704
+ }
705
+
706
+ .catalogue-item.level-0 .item-title {
707
+ font-size: 15px;
708
+ color: #333;
709
+ font-weight: 600;
710
+ }
711
+
712
+ .catalogue-item.level-1 {
713
+ background: linear-gradient(90deg, transparent, rgba(40, 167, 69, 0.03), transparent);
714
+ }
715
+
716
+ .catalogue-item.level-1 .item-title {
717
+ font-size: 14px;
718
+ color: #555;
719
+ font-weight: 500;
720
+ }
721
+
722
+ .catalogue-item.level-2 {
723
+ background: linear-gradient(90deg, transparent, rgba(255, 193, 7, 0.03), transparent);
724
+ }
725
+
726
+ .catalogue-item.level-2 .item-title {
727
+ font-size: 13px;
728
+ color: #666;
729
+ font-weight: 400;
730
+ }
731
+
732
+ .catalogue-item.level-3 .item-title {
733
+ font-size: 12px;
734
+ color: #777;
735
+ font-weight: 400;
736
+ }
737
+
738
+ .children-container {
739
+ /* 移除背景色,保持一致性 */
740
+ background: transparent;
741
+ }
742
+
743
+ /* 移动端适配 */
744
+ @media (max-width: 768px) {
745
+ .catalogue-drawer {
746
+ /* 移动端安全区域适配 */
747
+ padding-top: max(env(safe-area-inset-top), 20px);
748
+ padding-bottom: max(env(safe-area-inset-bottom), 20px);
749
+ }
750
+
751
+ .drawer-header {
752
+ padding: 14px 16px;
753
+ }
754
+
755
+ .drawer-title {
756
+ font-size: 16px;
757
+ }
758
+
759
+ .catalogue-item {
760
+ padding: 10px 8px;
761
+ min-height: 44px; /* 增加触摸区域 */
762
+ }
763
+
764
+ .drag-handle {
765
+ font-size: 16px; /* 移动端增大手柄 */
766
+ width: 24px;
767
+ }
768
+
769
+ .item-title {
770
+ font-size: 13px;
771
+ }
772
+
773
+ .catalogue-item.level-0 .item-title {
774
+ font-size: 14px;
775
+ }
776
+
777
+ .page-number {
778
+ font-size: 11px;
779
+ }
780
+
781
+ .level-indicator {
782
+ font-size: 12px; /* 移动端增大指示器 */
783
+ }
784
+ }
785
+
786
+ /* 滚动条样式 */
787
+ .drawer-content::-webkit-scrollbar {
788
+ width: 4px;
789
+ }
790
+
791
+ .drawer-content::-webkit-scrollbar-track {
792
+ background: #f5f5f5;
793
+ }
794
+
795
+ .drawer-content::-webkit-scrollbar-thumb {
796
+ background: #ccc;
797
+ border-radius: 2px;
798
+ }
799
+
800
+ .drawer-content::-webkit-scrollbar-thumb:hover {
801
+ background: #999;
802
+ }
803
+
804
+ /* 拖拽状态样式 */
805
+ .catalogue-item.dragging {
806
+ opacity: 0.5;
807
+ transform: scale(0.95);
808
+ background: #f0f8ff;
809
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
810
+ }
811
+
812
+ /* 拖拽悬停目标样式 */
813
+ .catalogue-item.drag-over {
814
+ border-top: 2px solid #007bff;
815
+ background: rgba(0, 123, 255, 0.1);
816
+ }
817
+
818
+ /* 增强视觉层次 */
819
+ .catalogue-item {
820
+ border-left: 3px solid transparent;
821
+ transition: all 0.2s ease;
822
+ }
823
+
824
+ .catalogue-item.level-0 {
825
+ border-left-color: #007bff;
826
+ }
827
+
828
+ .catalogue-item.level-1 {
829
+ border-left-color: #28a745;
830
+ }
831
+
832
+ .catalogue-item.level-2 {
833
+ border-left-color: #ffc107;
834
+ }
835
+
836
+ .catalogue-item.level-3 {
837
+ border-left-color: #dc3545;
838
+ }
839
+ </style>