@hanology/cham-browser 0.3.8 → 0.4.1
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.
- package/dist/cli.js +158 -29
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
- package/template/index.html +4 -8
- package/template/src/App.vue +101 -17
- package/template/src/components/AnnotationControlBar.vue +119 -49
- package/template/src/components/AnnotationTooltip.vue +278 -99
- package/template/src/components/BackToTop.vue +4 -0
- package/template/src/components/BookCard.vue +10 -11
- package/template/src/components/HorizontalDisplay.vue +52 -0
- package/template/src/components/PoemCard.vue +1 -0
- package/template/src/components/PronunciationGroup.vue +27 -18
- package/template/src/components/ReadingToolbar.vue +20 -0
- package/template/src/components/SectionBlock.vue +91 -12
- package/template/src/components/SideNav.vue +5 -4
- package/template/src/components/VerticalScroll.vue +30 -0
- package/template/src/composables/useData.ts +6 -1
- package/template/src/composables/useI18n.ts +33 -0
- package/template/src/composables/useReadingMode.ts +9 -4
- package/template/src/composables/useSiteConfig.ts +27 -6
- package/template/src/router.ts +0 -2
- package/template/src/styles/main.css +88 -0
- package/template/src/types.ts +8 -0
- package/template/src/views/BookHome.vue +45 -21
- package/template/src/views/LibraryHome.vue +39 -41
- package/template/src/views/PieceView.vue +434 -71
- package/template/src/views/AboutView.vue +0 -191
|
@@ -7,6 +7,7 @@ import { useReadingMode } from '../composables/useReadingMode'
|
|
|
7
7
|
import { useHorizontalScroll } from '../composables/useHorizontalScroll'
|
|
8
8
|
import { useAnnotationInteraction } from '../composables/useAnnotationInteraction'
|
|
9
9
|
import { useData } from '../composables/useData'
|
|
10
|
+
import { useI18n } from '../composables/useI18n'
|
|
10
11
|
import VerticalScroll from '../components/VerticalScroll.vue'
|
|
11
12
|
import HorizontalDisplay from '../components/HorizontalDisplay.vue'
|
|
12
13
|
import SectionBlock from '../components/SectionBlock.vue'
|
|
@@ -26,6 +27,7 @@ await load(props.bookId)
|
|
|
26
27
|
const { layout } = useReadingMode()
|
|
27
28
|
const vPageRef = ref<HTMLElement | null>(null)
|
|
28
29
|
const vScroll = useHorizontalScroll(vPageRef)
|
|
30
|
+
const { t } = useI18n()
|
|
29
31
|
|
|
30
32
|
const authorPaneOpen = ref(false)
|
|
31
33
|
const selectedAuthorId = ref('')
|
|
@@ -43,6 +45,24 @@ onMounted(() => {
|
|
|
43
45
|
onUnmounted(() => observer.disconnect())
|
|
44
46
|
})
|
|
45
47
|
|
|
48
|
+
// Keyboard navigation
|
|
49
|
+
onMounted(() => {
|
|
50
|
+
function onKey(e: KeyboardEvent) {
|
|
51
|
+
if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) return
|
|
52
|
+
const adj = adjacent.value
|
|
53
|
+
if (isVertical.value) {
|
|
54
|
+
if (e.key === 'ArrowLeft' && adj.next !== null) { e.preventDefault(); navigate(1) }
|
|
55
|
+
if (e.key === 'ArrowRight' && adj.prev !== null) { e.preventDefault(); navigate(-1) }
|
|
56
|
+
} else {
|
|
57
|
+
if (e.key === 'ArrowRight' && adj.next !== null) { e.preventDefault(); navigate(1) }
|
|
58
|
+
if (e.key === 'ArrowLeft' && adj.prev !== null) { e.preventDefault(); navigate(-1) }
|
|
59
|
+
}
|
|
60
|
+
if (e.key === 'Escape') { e.preventDefault(); goBack() }
|
|
61
|
+
}
|
|
62
|
+
window.addEventListener('keydown', onKey)
|
|
63
|
+
onUnmounted(() => window.removeEventListener('keydown', onKey))
|
|
64
|
+
})
|
|
65
|
+
|
|
46
66
|
const piece = computed<Piece | undefined>(() => {
|
|
47
67
|
const n = typeof props.num === 'string' ? parseInt(props.num, 10) : props.num
|
|
48
68
|
return getPiece(n)
|
|
@@ -86,6 +106,7 @@ function initLayers() {
|
|
|
86
106
|
}
|
|
87
107
|
|
|
88
108
|
const mergedAnnotations = computed<Annotation[]>(() => {
|
|
109
|
+
if (!annotationsVisible.value) return []
|
|
89
110
|
if (!hasLayers.value) return piece.value?.annotations || []
|
|
90
111
|
const result: Annotation[] = []
|
|
91
112
|
for (const layer of annotationLayers.value) {
|
|
@@ -194,6 +215,8 @@ const proseSections = computed(() => {
|
|
|
194
215
|
const { getAuthor, loadShared } = useData()
|
|
195
216
|
await loadShared()
|
|
196
217
|
|
|
218
|
+
const CHAM_LOGO_URL = import.meta.env.CHAM_LOGO_URL || ''
|
|
219
|
+
|
|
197
220
|
const selectedAuthorName = computed(() => {
|
|
198
221
|
if (!selectedAuthorId.value) return piece.value?.author || ''
|
|
199
222
|
const c = piece.value?.contributors?.find(x => x.id === selectedAuthorId.value)
|
|
@@ -205,6 +228,30 @@ const selectedAuthorBio = computed(() => {
|
|
|
205
228
|
return a?.bio || piece.value?.sections?.author_bio || ''
|
|
206
229
|
})
|
|
207
230
|
|
|
231
|
+
const selectedAuthorDynasty = computed(() => {
|
|
232
|
+
const name = selectedAuthorName.value
|
|
233
|
+
const a = getAuthor(name)
|
|
234
|
+
return a?.dynasty || piece.value?.dynasty || ''
|
|
235
|
+
})
|
|
236
|
+
|
|
237
|
+
const selectedAuthorPoemCount = computed(() => {
|
|
238
|
+
const name = selectedAuthorName.value
|
|
239
|
+
const a = getAuthor(name)
|
|
240
|
+
return a?.poemCount || 0
|
|
241
|
+
})
|
|
242
|
+
|
|
243
|
+
const selectedAuthorData = computed(() => {
|
|
244
|
+
const name = selectedAuthorName.value
|
|
245
|
+
return getAuthor(name)
|
|
246
|
+
})
|
|
247
|
+
|
|
248
|
+
const authorLifespan = computed(() => {
|
|
249
|
+
const a = selectedAuthorData.value
|
|
250
|
+
if (!a?.born && !a?.died) return ''
|
|
251
|
+
if (a.born && a.died) return `${a.born}–${a.died}`
|
|
252
|
+
return a.born ? `${a.born}–` : `?–${a.died}`
|
|
253
|
+
})
|
|
254
|
+
|
|
208
255
|
function openAuthorPane(id?: string) {
|
|
209
256
|
selectedAuthorId.value = id || piece.value?.authorId || ''
|
|
210
257
|
authorPaneOpen.value = true
|
|
@@ -219,12 +266,20 @@ function navigate(delta: number) {
|
|
|
219
266
|
if (target !== null) router.push(`/${props.bookId}/${target}`)
|
|
220
267
|
}
|
|
221
268
|
|
|
269
|
+
const ROLE_LABELS: Record<string, string> = {
|
|
270
|
+
author: t('role.author'),
|
|
271
|
+
commentator: t('role.commentator'),
|
|
272
|
+
editor: t('role.editor'),
|
|
273
|
+
translator: t('role.translator'),
|
|
274
|
+
annotator: t('role.annotator'),
|
|
275
|
+
}
|
|
276
|
+
|
|
222
277
|
const contributorGroups = computed(() => {
|
|
223
278
|
const c = piece.value?.contributors
|
|
224
279
|
if (!c || c.length <= 1) return []
|
|
225
280
|
const groups = new Map<string, string[]>()
|
|
226
281
|
for (const x of c) {
|
|
227
|
-
const t = x.title || '作者'
|
|
282
|
+
const t = x.title || ROLE_LABELS[x.role] || '作者'
|
|
228
283
|
if (!groups.has(t)) groups.set(t, [])
|
|
229
284
|
groups.get(t)!.push(x.name)
|
|
230
285
|
}
|
|
@@ -271,12 +326,12 @@ function tcy(n: number): string {
|
|
|
271
326
|
</div>
|
|
272
327
|
<div class="v-poem-meta">
|
|
273
328
|
<template v-if="isMultiPart">
|
|
274
|
-
<span class="v-meta-item" v-html="tcy(piece.parts!.length) + '
|
|
275
|
-
<span class="v-meta-item" v-html="totalPartAnnotationCount > 0 ? tcy(totalPartAnnotationCount) + '
|
|
329
|
+
<span class="v-meta-item" v-html="tcy(piece.parts!.length) + ' ' + t('piece.stanzas')" />
|
|
330
|
+
<span class="v-meta-item" v-html="totalPartAnnotationCount > 0 ? tcy(totalPartAnnotationCount) + ' ' + t('piece.notes') : t('piece.noNotes')" />
|
|
276
331
|
</template>
|
|
277
332
|
<template v-else>
|
|
278
|
-
<span class="v-meta-item" v-html="tcy(piece.verses.length) + '
|
|
279
|
-
<span class="v-meta-item" v-html="totalAnnotationCount > 0 ? tcy(totalAnnotationCount) + '
|
|
333
|
+
<span class="v-meta-item" v-html="tcy(piece.verses.length) + ' ' + t('piece.stanzas')" />
|
|
334
|
+
<span class="v-meta-item" v-html="totalAnnotationCount > 0 ? tcy(totalAnnotationCount) + ' ' + t('piece.notes') : t('piece.noNotes')" />
|
|
280
335
|
</template>
|
|
281
336
|
</div>
|
|
282
337
|
</section>
|
|
@@ -311,7 +366,7 @@ function tcy(n: number): string {
|
|
|
311
366
|
<SectionBlock
|
|
312
367
|
v-if="!isMultiPart && annotationsVisible && piece.sections.annotations"
|
|
313
368
|
num=""
|
|
314
|
-
label="
|
|
369
|
+
:label="t('annotation.notes')"
|
|
315
370
|
:special="false"
|
|
316
371
|
:text="piece.sections.annotations"
|
|
317
372
|
:is-annotations="true"
|
|
@@ -322,7 +377,7 @@ function tcy(n: number): string {
|
|
|
322
377
|
<div class="v-layers-inline v-section">
|
|
323
378
|
<AnnotationControlBar
|
|
324
379
|
:layers="annotationLayers"
|
|
325
|
-
:has-annotations="
|
|
380
|
+
:has-annotations="true"
|
|
326
381
|
v-model:active-ids="activeLayerIds"
|
|
327
382
|
v-model:annotations-visible="annotationsVisible"
|
|
328
383
|
/>
|
|
@@ -339,16 +394,14 @@ function tcy(n: number): string {
|
|
|
339
394
|
class="v-section"
|
|
340
395
|
/>
|
|
341
396
|
</template>
|
|
342
|
-
<
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
class="v-section"
|
|
351
|
-
/>
|
|
397
|
+
<div v-else-if="piece.annotations.length > 0 || piece.sections.annotations" class="v-layers-inline v-section">
|
|
398
|
+
<AnnotationControlBar
|
|
399
|
+
:layers="annotationLayers"
|
|
400
|
+
:has-annotations="true"
|
|
401
|
+
v-model:active-ids="activeLayerIds"
|
|
402
|
+
v-model:annotations-visible="annotationsVisible"
|
|
403
|
+
/>
|
|
404
|
+
</div>
|
|
352
405
|
|
|
353
406
|
<SectionBlock
|
|
354
407
|
v-for="(sec, idx) in proseSections"
|
|
@@ -365,13 +418,13 @@ function tcy(n: number): string {
|
|
|
365
418
|
<nav class="v-nav">
|
|
366
419
|
<button v-if="adjacent.prev !== null" class="v-nav-btn" @click="navigate(-1)">
|
|
367
420
|
<span class="v-nav-dir">▲</span>
|
|
368
|
-
<span class="v-nav-label"
|
|
421
|
+
<span class="v-nav-label">{{ t('piece.previous') }}</span>
|
|
369
422
|
<span class="v-nav-title">{{ getPiece(adjacent.prev)?.title }}</span>
|
|
370
423
|
</button>
|
|
371
424
|
<div v-else class="v-nav-spacer" />
|
|
372
425
|
<button v-if="adjacent.next !== null" class="v-nav-btn" @click="navigate(1)">
|
|
373
426
|
<span class="v-nav-dir">▼</span>
|
|
374
|
-
<span class="v-nav-label"
|
|
427
|
+
<span class="v-nav-label">{{ t('piece.next') }}</span>
|
|
375
428
|
<span class="v-nav-title">{{ getPiece(adjacent.next)?.title }}</span>
|
|
376
429
|
</button>
|
|
377
430
|
</nav>
|
|
@@ -394,12 +447,28 @@ function tcy(n: number): string {
|
|
|
394
447
|
<button class="v-pane-close" @click="closeAuthorPane">✕</button>
|
|
395
448
|
<div class="v-pane-header">
|
|
396
449
|
<div class="v-pane-name">{{ selectedAuthorName }}</div>
|
|
450
|
+
<div class="v-pane-meta">
|
|
451
|
+
<span v-if="selectedAuthorDynasty">{{ selectedAuthorDynasty }}</span>
|
|
452
|
+
<span v-if="authorLifespan">{{ authorLifespan }}</span>
|
|
453
|
+
<span v-if="selectedAuthorPoemCount" class="v-pane-count">{{ t('stat.pieceCount', { count: selectedAuthorPoemCount }) }}</span>
|
|
454
|
+
</div>
|
|
455
|
+
<div v-if="selectedAuthorData?.courtesyName || selectedAuthorData?.artName" class="v-pane-names">
|
|
456
|
+
<span v-if="selectedAuthorData?.courtesyName">字{{ selectedAuthorData.courtesyName }}</span>
|
|
457
|
+
<span v-if="selectedAuthorData?.artName">號{{ selectedAuthorData.artName }}</span>
|
|
458
|
+
</div>
|
|
459
|
+
</div>
|
|
460
|
+
<div class="v-pane-links">
|
|
461
|
+
<a v-if="selectedAuthorData?.ctextId" :href="`https://ctext.org/wiki.pl?if=en&res=${selectedAuthorData.ctextId}`" target="_blank" rel="noopener" class="v-pane-link">CTEXT</a>
|
|
462
|
+
<a v-if="selectedAuthorData?.wikipediaZh" :href="selectedAuthorData.wikipediaZh" target="_blank" rel="noopener" class="v-pane-link">維基</a>
|
|
463
|
+
<a v-if="selectedAuthorData?.wikipediaEn" :href="selectedAuthorData.wikipediaEn" target="_blank" rel="noopener" class="v-pane-link">Wikipedia</a>
|
|
464
|
+
<a v-if="selectedAuthorData?.wikidata" :href="`https://www.wikidata.org/wiki/${selectedAuthorData.wikidata}`" target="_blank" rel="noopener" class="v-pane-link">Wikidata</a>
|
|
397
465
|
</div>
|
|
398
466
|
<div v-if="selectedAuthorBio" class="v-pane-bio">
|
|
399
467
|
<div v-for="p in selectedAuthorBio.split('\n').filter(l => l.trim())" :key="p" class="v-pane-p">
|
|
400
468
|
{{ p.trim() }}
|
|
401
469
|
</div>
|
|
402
470
|
</div>
|
|
471
|
+
<div v-if="!selectedAuthorBio" class="v-pane-empty">{{ t('piece.noAuthorData') }}</div>
|
|
403
472
|
</div>
|
|
404
473
|
</div>
|
|
405
474
|
</Transition>
|
|
@@ -412,14 +481,16 @@ function tcy(n: number): string {
|
|
|
412
481
|
<div class="h-page">
|
|
413
482
|
<nav class="h-nav">
|
|
414
483
|
<div class="h-nav-inner">
|
|
415
|
-
<button class="h-back" @click="goBack">←
|
|
416
|
-
<div class="h-
|
|
417
|
-
<span v-if="piece.
|
|
418
|
-
|
|
484
|
+
<button class="h-back" @click="goBack">← {{ t('nav.back') }}</button>
|
|
485
|
+
<div class="h-nav-title-row">
|
|
486
|
+
<span v-if="piece.dynasty" class="h-dynasty">{{ piece.dynasty }}</span>
|
|
487
|
+
<span class="h-breadcrumb">
|
|
488
|
+
<span v-if="piece.source?.textRef" class="h-source-link" @click="router.push(`/${piece.source.textRef}`)">
|
|
489
|
+
{{ meta?.title }} →
|
|
490
|
+
</span>
|
|
491
|
+
<span class="h-sep">{{ piece.num }}.</span>
|
|
492
|
+
{{ piece.title }}
|
|
419
493
|
</span>
|
|
420
|
-
<span class="h-sep">{{ piece.num }}.</span>
|
|
421
|
-
{{ piece.title }}
|
|
422
|
-
<span class="h-sep">·</span>
|
|
423
494
|
<template v-if="piece.contributors && piece.contributors.length > 1">
|
|
424
495
|
<template v-for="(group, gi) in contributorGroups" :key="group.title">
|
|
425
496
|
<span v-if="gi > 0" class="h-sep">|</span>
|
|
@@ -430,14 +501,17 @@ function tcy(n: number): string {
|
|
|
430
501
|
<span v-else class="h-author-link" @click="openAuthorPane">{{ piece.author }}</span>
|
|
431
502
|
</div>
|
|
432
503
|
<div class="h-controls">
|
|
504
|
+
<span class="h-tag h-tag-pager">{{ piece.num }} / {{ pieces.length }}</span>
|
|
433
505
|
<template v-if="isMultiPart">
|
|
434
|
-
<span class="h-tag">{{ piece.parts!.length }}
|
|
435
|
-
<span class="h-tag">{{ totalPartAnnotationCount > 0 ? totalPartAnnotationCount + '
|
|
506
|
+
<span class="h-tag">{{ piece.parts!.length }} {{ t('piece.stanzas') }}</span>
|
|
507
|
+
<span class="h-tag">{{ totalPartAnnotationCount > 0 ? totalPartAnnotationCount + ' ' + t('piece.notes') : t('piece.noNotes') }}</span>
|
|
436
508
|
</template>
|
|
437
509
|
<template v-else>
|
|
438
|
-
<span class="h-tag">{{ piece.verses.length }}
|
|
439
|
-
<span class="h-tag">{{ totalAnnotationCount > 0 ? totalAnnotationCount + '
|
|
510
|
+
<span class="h-tag">{{ piece.verses.length }} {{ t('piece.stanzas') }}</span>
|
|
511
|
+
<span class="h-tag">{{ totalAnnotationCount > 0 ? totalAnnotationCount + ' ' + t('piece.notes') : t('piece.noNotes') }}</span>
|
|
440
512
|
</template>
|
|
513
|
+
<button v-if="adjacent.prev !== null" class="h-nav-arrow" @click="navigate(-1)" :title="t('piece.previous')">←</button>
|
|
514
|
+
<button v-if="adjacent.next !== null" class="h-nav-arrow" @click="navigate(1)" :title="t('piece.next')">→</button>
|
|
441
515
|
</div>
|
|
442
516
|
</div>
|
|
443
517
|
</nav>
|
|
@@ -468,10 +542,10 @@ function tcy(n: number): string {
|
|
|
468
542
|
</div>
|
|
469
543
|
|
|
470
544
|
<div class="h-sections">
|
|
471
|
-
<div v-if="
|
|
545
|
+
<div v-if="piece.annotations.length > 0 || piece.sections.annotations || hasLayers" class="h-ann-section">
|
|
472
546
|
<AnnotationControlBar
|
|
473
547
|
:layers="annotationLayers"
|
|
474
|
-
:has-annotations="
|
|
548
|
+
:has-annotations="true"
|
|
475
549
|
v-model:active-ids="activeLayerIds"
|
|
476
550
|
v-model:annotations-visible="annotationsVisible"
|
|
477
551
|
style="margin-bottom: 16px"
|
|
@@ -479,12 +553,12 @@ function tcy(n: number): string {
|
|
|
479
553
|
<SectionBlock
|
|
480
554
|
v-if="annotationsVisible && piece.sections.annotations"
|
|
481
555
|
num=""
|
|
482
|
-
label="
|
|
556
|
+
:label="t('annotation.notes')"
|
|
483
557
|
:special="false"
|
|
484
558
|
:text="piece.sections.annotations"
|
|
485
559
|
:is-annotations="true"
|
|
486
560
|
/>
|
|
487
|
-
<template v-if="hasLayers">
|
|
561
|
+
<template v-if="hasLayers && annotationsVisible">
|
|
488
562
|
<SectionBlock
|
|
489
563
|
v-for="block in layerAnnotationBlocks"
|
|
490
564
|
:key="block.label"
|
|
@@ -511,12 +585,12 @@ function tcy(n: number): string {
|
|
|
511
585
|
|
|
512
586
|
<div class="h-nav-bottom">
|
|
513
587
|
<button v-if="adjacent.prev !== null" class="h-nav-btn" @click="navigate(-1)">
|
|
514
|
-
<div class="h-nav-label">←
|
|
588
|
+
<div class="h-nav-label">← {{ t('piece.previous') }}</div>
|
|
515
589
|
<div class="h-nav-title">{{ getPiece(adjacent.prev)?.title }}</div>
|
|
516
590
|
</button>
|
|
517
591
|
<div v-else />
|
|
518
592
|
<button v-if="adjacent.next !== null" class="h-nav-btn h-nav-next" @click="navigate(1)">
|
|
519
|
-
<div class="h-nav-label"
|
|
593
|
+
<div class="h-nav-label">{{ t('piece.next') }} →</div>
|
|
520
594
|
<div class="h-nav-title">{{ getPiece(adjacent.next)?.title }}</div>
|
|
521
595
|
</button>
|
|
522
596
|
</div>
|
|
@@ -543,14 +617,37 @@ function tcy(n: number): string {
|
|
|
543
617
|
<div class="h-pane-header">
|
|
544
618
|
<div>
|
|
545
619
|
<div class="h-pane-name">{{ selectedAuthorName }}</div>
|
|
546
|
-
<div class="h-pane-meta">
|
|
620
|
+
<div class="h-pane-meta">
|
|
621
|
+
<span v-if="selectedAuthorDynasty" class="h-pane-dynasty">{{ selectedAuthorDynasty }}</span>
|
|
622
|
+
<span v-if="authorLifespan" class="h-pane-lifespan">{{ authorLifespan }}</span>
|
|
623
|
+
<span v-if="selectedAuthorPoemCount" class="h-pane-count">{{ t('piece.collected', { count: selectedAuthorPoemCount }) }}</span>
|
|
624
|
+
</div>
|
|
625
|
+
<div v-if="selectedAuthorData?.courtesyName || selectedAuthorData?.artName" class="h-pane-alt-names">
|
|
626
|
+
<span v-if="selectedAuthorData?.courtesyName">字 {{ selectedAuthorData.courtesyName }}</span>
|
|
627
|
+
<span v-if="selectedAuthorData?.artName">號 {{ selectedAuthorData.artName }}</span>
|
|
628
|
+
</div>
|
|
547
629
|
</div>
|
|
548
630
|
</div>
|
|
631
|
+
<div class="h-pane-links">
|
|
632
|
+
<a v-if="selectedAuthorData?.ctextId" :href="`https://ctext.org/wiki.pl?if=en&res=${selectedAuthorData.ctextId}`" target="_blank" rel="noopener" class="h-pane-link">
|
|
633
|
+
<span class="link-icon">文</span> CTEXT
|
|
634
|
+
</a>
|
|
635
|
+
<a v-if="selectedAuthorData?.wikipediaZh" :href="selectedAuthorData.wikipediaZh" target="_blank" rel="noopener" class="h-pane-link">
|
|
636
|
+
<span class="link-icon">維</span> 維基百科
|
|
637
|
+
</a>
|
|
638
|
+
<a v-if="selectedAuthorData?.wikipediaEn" :href="selectedAuthorData.wikipediaEn" target="_blank" rel="noopener" class="h-pane-link">
|
|
639
|
+
<span class="link-icon">W</span> Wikipedia
|
|
640
|
+
</a>
|
|
641
|
+
<a v-if="selectedAuthorData?.wikidata" :href="`https://www.wikidata.org/wiki/${selectedAuthorData.wikidata}`" target="_blank" rel="noopener" class="h-pane-link">
|
|
642
|
+
<span class="link-icon">Q</span> Wikidata
|
|
643
|
+
</a>
|
|
644
|
+
</div>
|
|
549
645
|
<div v-if="selectedAuthorBio" class="h-pane-bio">
|
|
550
646
|
<div v-for="p in selectedAuthorBio.split('\n').filter(l => l.trim())" :key="p" class="h-pane-p">
|
|
551
647
|
{{ p.trim() }}
|
|
552
648
|
</div>
|
|
553
649
|
</div>
|
|
650
|
+
<div v-if="!selectedAuthorBio" class="h-pane-empty">暫無作者資料</div>
|
|
554
651
|
</div>
|
|
555
652
|
</div>
|
|
556
653
|
</Transition>
|
|
@@ -558,8 +655,9 @@ function tcy(n: number): string {
|
|
|
558
655
|
</div>
|
|
559
656
|
</div>
|
|
560
657
|
|
|
561
|
-
<div v-else class="loading">
|
|
562
|
-
<
|
|
658
|
+
<div v-else class="page-loading">
|
|
659
|
+
<img v-if="CHAM_LOGO_URL" :src="CHAM_LOGO_URL" alt="" class="page-loading-logo" />
|
|
660
|
+
<div v-else class="page-loading-seal">文</div>
|
|
563
661
|
</div>
|
|
564
662
|
</template>
|
|
565
663
|
|
|
@@ -567,20 +665,9 @@ function tcy(n: number): string {
|
|
|
567
665
|
/* ═══════ 直排模式 ═══════ */
|
|
568
666
|
|
|
569
667
|
.v-page {
|
|
570
|
-
height: 100vh;
|
|
571
|
-
display: flex;
|
|
572
|
-
flex-direction: row-reverse;
|
|
573
|
-
overflow-x: auto;
|
|
574
|
-
overflow-y: hidden;
|
|
575
|
-
margin-right: var(--nav-width, 56px);
|
|
576
668
|
padding: 0;
|
|
577
669
|
background: var(--paper);
|
|
578
|
-
scrollbar-width: thin;
|
|
579
|
-
scrollbar-color: var(--gold) transparent;
|
|
580
|
-
scroll-snap-type: x proximity;
|
|
581
670
|
}
|
|
582
|
-
.v-page::-webkit-scrollbar { height: 4px; }
|
|
583
|
-
.v-page::-webkit-scrollbar-thumb { background: var(--gold); border-radius: 2px; }
|
|
584
671
|
|
|
585
672
|
.v-title-col {
|
|
586
673
|
writing-mode: vertical-rl;
|
|
@@ -739,7 +826,39 @@ function tcy(n: number): string {
|
|
|
739
826
|
transition: all 0.2s; white-space: nowrap;
|
|
740
827
|
}
|
|
741
828
|
.h-back:hover { background: var(--ink); color: var(--paper); border-color: var(--ink); }
|
|
742
|
-
.h-
|
|
829
|
+
.h-back:active { transform: scale(0.97); }
|
|
830
|
+
|
|
831
|
+
.h-nav-title-row {
|
|
832
|
+
display: flex;
|
|
833
|
+
align-items: baseline;
|
|
834
|
+
gap: 8px;
|
|
835
|
+
font-size: 15px;
|
|
836
|
+
font-weight: 600;
|
|
837
|
+
letter-spacing: 1px;
|
|
838
|
+
min-width: 0;
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
.h-dynasty {
|
|
842
|
+
display: inline-flex;
|
|
843
|
+
align-items: center;
|
|
844
|
+
padding: 2px 8px;
|
|
845
|
+
background: var(--vermillion);
|
|
846
|
+
color: #fff;
|
|
847
|
+
font-family: var(--sans);
|
|
848
|
+
font-size: 11px;
|
|
849
|
+
font-weight: 700;
|
|
850
|
+
letter-spacing: 1px;
|
|
851
|
+
border-radius: 3px;
|
|
852
|
+
white-space: nowrap;
|
|
853
|
+
flex-shrink: 0;
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
.h-breadcrumb {
|
|
857
|
+
overflow: hidden;
|
|
858
|
+
text-overflow: ellipsis;
|
|
859
|
+
white-space: nowrap;
|
|
860
|
+
min-width: 0;
|
|
861
|
+
}
|
|
743
862
|
.h-sep { color: var(--ink-faint); font-weight: 300; margin: 0 8px; }
|
|
744
863
|
.h-author-link {
|
|
745
864
|
color: var(--ink-light); font-weight: 400;
|
|
@@ -757,6 +876,29 @@ function tcy(n: number): string {
|
|
|
757
876
|
border-radius: 2px; font-family: var(--sans);
|
|
758
877
|
font-size: 12px; color: var(--ink-light); letter-spacing: 1px;
|
|
759
878
|
}
|
|
879
|
+
.h-tag-pager {
|
|
880
|
+
background: var(--surface-warm);
|
|
881
|
+
font-weight: 600;
|
|
882
|
+
color: var(--ink-mid);
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
.h-nav-arrow {
|
|
886
|
+
width: 32px; height: 32px;
|
|
887
|
+
border: 1px solid var(--border);
|
|
888
|
+
border-radius: 4px;
|
|
889
|
+
background: none;
|
|
890
|
+
font-family: var(--sans);
|
|
891
|
+
font-size: 16px;
|
|
892
|
+
color: var(--ink-light);
|
|
893
|
+
cursor: pointer;
|
|
894
|
+
display: flex; align-items: center; justify-content: center;
|
|
895
|
+
transition: all 0.15s;
|
|
896
|
+
}
|
|
897
|
+
.h-nav-arrow:hover {
|
|
898
|
+
border-color: var(--vermillion);
|
|
899
|
+
color: var(--vermillion);
|
|
900
|
+
}
|
|
901
|
+
.h-nav-arrow:active { transform: scale(0.95); }
|
|
760
902
|
|
|
761
903
|
.h-content {
|
|
762
904
|
max-width: 1200px; margin: 0 auto; padding: 60px 40px;
|
|
@@ -781,6 +923,10 @@ function tcy(n: number): string {
|
|
|
781
923
|
|
|
782
924
|
.h-ann-section {
|
|
783
925
|
margin-bottom: 16px;
|
|
926
|
+
padding: 20px;
|
|
927
|
+
background: var(--surface);
|
|
928
|
+
border: 1px solid var(--border-light);
|
|
929
|
+
border-radius: 8px;
|
|
784
930
|
}
|
|
785
931
|
|
|
786
932
|
.h-layers-inline {
|
|
@@ -825,6 +971,7 @@ function tcy(n: number): string {
|
|
|
825
971
|
transform: translateY(-2px);
|
|
826
972
|
}
|
|
827
973
|
.h-nav-btn:hover::after { transform: scaleX(1); }
|
|
974
|
+
.h-nav-btn:active { transform: scale(0.98); }
|
|
828
975
|
.h-nav-btn.h-nav-next { text-align: right; }
|
|
829
976
|
.h-nav-label { font-size: 11px; color: var(--ink-faint); font-family: var(--sans); letter-spacing: 2px; margin-bottom: 4px; }
|
|
830
977
|
.h-nav-title { font-size: 16px; font-weight: 600; letter-spacing: 1px; color: var(--ink); }
|
|
@@ -876,7 +1023,83 @@ function tcy(n: number): string {
|
|
|
876
1023
|
color: var(--vermillion); flex-shrink: 0;
|
|
877
1024
|
}
|
|
878
1025
|
.h-pane-name { font-size: 28px; font-weight: 900; letter-spacing: 4px; color: var(--ink); }
|
|
879
|
-
.h-pane-meta { font-size: 14px; color: var(--ink-faint); letter-spacing: 2px; margin-top:
|
|
1026
|
+
.h-pane-meta { font-size: 14px; color: var(--ink-faint); letter-spacing: 2px; margin-top: 6px; display: flex; align-items: center; gap: 8px; }
|
|
1027
|
+
.h-pane-dynasty {
|
|
1028
|
+
display: inline-flex;
|
|
1029
|
+
padding: 2px 8px;
|
|
1030
|
+
background: var(--vermillion);
|
|
1031
|
+
color: #fff;
|
|
1032
|
+
font-family: var(--sans);
|
|
1033
|
+
font-size: 11px;
|
|
1034
|
+
font-weight: 700;
|
|
1035
|
+
letter-spacing: 1px;
|
|
1036
|
+
border-radius: 3px;
|
|
1037
|
+
}
|
|
1038
|
+
.h-pane-count {
|
|
1039
|
+
font-family: var(--sans);
|
|
1040
|
+
font-size: 12px;
|
|
1041
|
+
color: var(--ink-faint);
|
|
1042
|
+
letter-spacing: 1px;
|
|
1043
|
+
}
|
|
1044
|
+
.h-pane-lifespan {
|
|
1045
|
+
font-family: var(--sans);
|
|
1046
|
+
font-size: 12px;
|
|
1047
|
+
color: var(--ink-light);
|
|
1048
|
+
letter-spacing: 1px;
|
|
1049
|
+
}
|
|
1050
|
+
.h-pane-alt-names {
|
|
1051
|
+
font-size: 14px;
|
|
1052
|
+
color: var(--ink-light);
|
|
1053
|
+
letter-spacing: 2px;
|
|
1054
|
+
margin-top: 6px;
|
|
1055
|
+
display: flex;
|
|
1056
|
+
gap: 12px;
|
|
1057
|
+
}
|
|
1058
|
+
.h-pane-links {
|
|
1059
|
+
display: flex;
|
|
1060
|
+
gap: 8px;
|
|
1061
|
+
flex-wrap: wrap;
|
|
1062
|
+
margin: 16px 0 0;
|
|
1063
|
+
padding-bottom: 16px;
|
|
1064
|
+
border-bottom: 1px solid var(--border-light);
|
|
1065
|
+
}
|
|
1066
|
+
.h-pane-link {
|
|
1067
|
+
display: inline-flex;
|
|
1068
|
+
align-items: center;
|
|
1069
|
+
gap: 4px;
|
|
1070
|
+
padding: 4px 10px;
|
|
1071
|
+
border: 1px solid var(--border);
|
|
1072
|
+
border-radius: 4px;
|
|
1073
|
+
font-family: var(--sans);
|
|
1074
|
+
font-size: 12px;
|
|
1075
|
+
color: var(--ink-mid);
|
|
1076
|
+
text-decoration: none;
|
|
1077
|
+
letter-spacing: 1px;
|
|
1078
|
+
transition: all 0.15s;
|
|
1079
|
+
}
|
|
1080
|
+
.h-pane-link:hover {
|
|
1081
|
+
border-color: var(--vermillion);
|
|
1082
|
+
color: var(--vermillion);
|
|
1083
|
+
}
|
|
1084
|
+
.h-pane-link .link-icon {
|
|
1085
|
+
display: inline-flex;
|
|
1086
|
+
align-items: center;
|
|
1087
|
+
justify-content: center;
|
|
1088
|
+
width: 18px;
|
|
1089
|
+
height: 18px;
|
|
1090
|
+
border-radius: 3px;
|
|
1091
|
+
background: var(--surface-warm);
|
|
1092
|
+
font-size: 10px;
|
|
1093
|
+
font-weight: 700;
|
|
1094
|
+
}
|
|
1095
|
+
.h-pane-empty {
|
|
1096
|
+
padding: 40px 0;
|
|
1097
|
+
text-align: center;
|
|
1098
|
+
color: var(--ink-faint);
|
|
1099
|
+
font-family: var(--sans);
|
|
1100
|
+
font-size: 14px;
|
|
1101
|
+
letter-spacing: 2px;
|
|
1102
|
+
}
|
|
880
1103
|
.h-pane-bio { border-top: 1px solid var(--border); padding-top: 24px; }
|
|
881
1104
|
.h-pane-p {
|
|
882
1105
|
font-size: 16px; line-height: 2.2;
|
|
@@ -931,6 +1154,60 @@ function tcy(n: number): string {
|
|
|
931
1154
|
font-size: 28px; font-weight: 900;
|
|
932
1155
|
letter-spacing: 6px; color: var(--ink);
|
|
933
1156
|
}
|
|
1157
|
+
.v-pane-meta {
|
|
1158
|
+
font-size: 13px;
|
|
1159
|
+
color: var(--ink-faint);
|
|
1160
|
+
font-family: var(--sans);
|
|
1161
|
+
letter-spacing: 2px;
|
|
1162
|
+
display: flex;
|
|
1163
|
+
gap: 8px;
|
|
1164
|
+
margin-left: 4px;
|
|
1165
|
+
}
|
|
1166
|
+
.v-pane-count {
|
|
1167
|
+
font-size: 12px;
|
|
1168
|
+
color: var(--ink-faint);
|
|
1169
|
+
letter-spacing: 1px;
|
|
1170
|
+
}
|
|
1171
|
+
.v-pane-names {
|
|
1172
|
+
font-size: 14px;
|
|
1173
|
+
color: var(--ink-light);
|
|
1174
|
+
letter-spacing: 2px;
|
|
1175
|
+
display: flex;
|
|
1176
|
+
gap: 8px;
|
|
1177
|
+
margin-left: 4px;
|
|
1178
|
+
}
|
|
1179
|
+
.v-pane-links {
|
|
1180
|
+
display: flex;
|
|
1181
|
+
gap: 8px;
|
|
1182
|
+
padding-left: 16px;
|
|
1183
|
+
border-left: 1px solid var(--border);
|
|
1184
|
+
margin-bottom: 16px;
|
|
1185
|
+
}
|
|
1186
|
+
.v-pane-link {
|
|
1187
|
+
display: inline-flex;
|
|
1188
|
+
align-items: center;
|
|
1189
|
+
padding: 4px 8px;
|
|
1190
|
+
border: 1px solid var(--border);
|
|
1191
|
+
border-radius: 3px;
|
|
1192
|
+
font-family: var(--sans);
|
|
1193
|
+
font-size: 11px;
|
|
1194
|
+
color: var(--ink-mid);
|
|
1195
|
+
text-decoration: none;
|
|
1196
|
+
letter-spacing: 1px;
|
|
1197
|
+
transition: all 0.15s;
|
|
1198
|
+
}
|
|
1199
|
+
.v-pane-link:hover {
|
|
1200
|
+
border-color: var(--vermillion);
|
|
1201
|
+
color: var(--vermillion);
|
|
1202
|
+
}
|
|
1203
|
+
.v-pane-empty {
|
|
1204
|
+
font-size: 14px;
|
|
1205
|
+
color: var(--ink-faint);
|
|
1206
|
+
font-family: var(--sans);
|
|
1207
|
+
letter-spacing: 2px;
|
|
1208
|
+
padding-left: 16px;
|
|
1209
|
+
border-left: 1px solid var(--border);
|
|
1210
|
+
}
|
|
934
1211
|
.v-pane-bio {
|
|
935
1212
|
font-size: 16px; line-height: 2.4;
|
|
936
1213
|
color: var(--ink-mid);
|
|
@@ -942,26 +1219,112 @@ function tcy(n: number): string {
|
|
|
942
1219
|
margin-left: 12px;
|
|
943
1220
|
}
|
|
944
1221
|
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
}
|
|
950
|
-
.
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
font-size: 28px; font-weight: 900;
|
|
956
|
-
color: var(--vermillion);
|
|
957
|
-
animation: pulse 1.2s ease-in-out infinite;
|
|
958
|
-
}
|
|
959
|
-
@keyframes pulse {
|
|
960
|
-
0%, 100% { opacity: 0.3; }
|
|
961
|
-
50% { opacity: 1; }
|
|
962
|
-
}
|
|
1222
|
+
|
|
1223
|
+
/* ─── 觸控回饋 ─── */
|
|
1224
|
+
.v-nav-btn:active { transform: scale(0.97); }
|
|
1225
|
+
.h-back:active { transform: scale(0.97); }
|
|
1226
|
+
.h-source-link:active { opacity: 1; }
|
|
1227
|
+
.v-source-link:active { opacity: 1; }
|
|
1228
|
+
.v-poem-author:active { color: var(--vermillion); }
|
|
1229
|
+
.h-author-link:active { color: var(--vermillion); }
|
|
1230
|
+
|
|
1231
|
+
/* ═══════ 行動裝置適配 ═══════ */
|
|
963
1232
|
|
|
964
1233
|
@media (max-width: 768px) {
|
|
965
|
-
|
|
1234
|
+
/* ─── 直排模式 ─── */
|
|
1235
|
+
.v-page { margin-right: var(--nav-width, 44px); }
|
|
1236
|
+
.v-title-col { padding: 24px 16px; }
|
|
1237
|
+
.v-poem-title { font-size: 32px; letter-spacing: 8px; }
|
|
1238
|
+
.v-poem-author { font-size: 20px; }
|
|
1239
|
+
.v-poem-col { padding: 16px; }
|
|
1240
|
+
|
|
1241
|
+
/* ─── 橫排模式導航 ─── */
|
|
1242
|
+
.h-nav { padding: 0 16px; }
|
|
1243
|
+
.h-nav-inner {
|
|
1244
|
+
height: auto;
|
|
1245
|
+
min-height: 48px;
|
|
1246
|
+
flex-wrap: wrap;
|
|
1247
|
+
padding: 8px 0;
|
|
1248
|
+
gap: 6px 12px;
|
|
1249
|
+
}
|
|
1250
|
+
.h-back {
|
|
1251
|
+
padding: 6px 10px;
|
|
1252
|
+
font-size: 12px;
|
|
1253
|
+
}
|
|
1254
|
+
.h-nav-title-row {
|
|
1255
|
+
font-size: 14px;
|
|
1256
|
+
order: 3;
|
|
1257
|
+
width: 100%;
|
|
1258
|
+
overflow: hidden;
|
|
1259
|
+
}
|
|
1260
|
+
.h-breadcrumb {
|
|
1261
|
+
overflow: hidden;
|
|
1262
|
+
text-overflow: ellipsis;
|
|
1263
|
+
white-space: nowrap;
|
|
1264
|
+
}
|
|
1265
|
+
.h-dynasty { font-size: 10px; padding: 1px 6px; }
|
|
1266
|
+
.h-sep { margin: 0 4px; }
|
|
1267
|
+
.h-controls {
|
|
1268
|
+
margin-left: 0;
|
|
1269
|
+
gap: 4px;
|
|
1270
|
+
}
|
|
1271
|
+
.h-tag {
|
|
1272
|
+
padding: 3px 8px;
|
|
1273
|
+
font-size: 11px;
|
|
1274
|
+
}
|
|
1275
|
+
.h-nav-arrow { width: 28px; height: 28px; font-size: 14px; }
|
|
1276
|
+
|
|
1277
|
+
/* ─── 橫排內容 ─── */
|
|
1278
|
+
.h-content { padding: 24px 16px; }
|
|
1279
|
+
.h-poem-block { margin-bottom: 40px; }
|
|
1280
|
+
|
|
1281
|
+
.h-multipart {
|
|
1282
|
+
padding: 20px 16px;
|
|
1283
|
+
border-radius: 6px;
|
|
1284
|
+
}
|
|
1285
|
+
|
|
1286
|
+
.h-sections {
|
|
1287
|
+
padding-bottom: 60px;
|
|
1288
|
+
}
|
|
1289
|
+
|
|
1290
|
+
.h-ann-section {
|
|
1291
|
+
margin-bottom: 12px;
|
|
1292
|
+
}
|
|
1293
|
+
|
|
1294
|
+
/* ─── 上/下篇導航 ─── */
|
|
1295
|
+
.h-nav-bottom {
|
|
1296
|
+
gap: 10px;
|
|
1297
|
+
margin: 0 auto 32px;
|
|
1298
|
+
}
|
|
1299
|
+
.h-nav-btn {
|
|
1300
|
+
padding: 16px;
|
|
1301
|
+
border-radius: 6px;
|
|
1302
|
+
}
|
|
1303
|
+
.h-nav-title { font-size: 14px; }
|
|
1304
|
+
.h-nav-label { font-size: 10px; }
|
|
1305
|
+
|
|
1306
|
+
/* ─── 作者面板 ─── */
|
|
1307
|
+
.h-overlay { justify-content: center; align-items: flex-end; }
|
|
1308
|
+
.h-pane {
|
|
1309
|
+
width: 100%;
|
|
1310
|
+
max-height: 85vh;
|
|
1311
|
+
height: auto;
|
|
1312
|
+
border-radius: 16px 16px 0 0;
|
|
1313
|
+
padding: 20px;
|
|
1314
|
+
}
|
|
1315
|
+
.overlay-enter-from .h-pane { transform: translateY(100%); }
|
|
1316
|
+
.overlay-leave-to .h-pane { transform: translateY(40px); }
|
|
1317
|
+
|
|
1318
|
+
.h-pane-name { font-size: 24px; }
|
|
1319
|
+
.h-pane-p { font-size: 15px; line-height: 2; }
|
|
1320
|
+
}
|
|
1321
|
+
|
|
1322
|
+
@media (max-width: 480px) {
|
|
1323
|
+
.h-nav-bottom {
|
|
1324
|
+
grid-template-columns: 1fr;
|
|
1325
|
+
}
|
|
1326
|
+
.h-nav-btn.h-nav-next { text-align: left; }
|
|
1327
|
+
.h-nav-title-row { font-size: 13px; }
|
|
1328
|
+
.h-nav-arrow { display: none; }
|
|
966
1329
|
}
|
|
967
1330
|
</style>
|