@hanology/cham-browser 0.3.9 → 0.4.2
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 +303 -32
- 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 +319 -95
- package/template/src/components/BackToTop.vue +4 -0
- package/template/src/components/BookCard.vue +10 -11
- package/template/src/components/HorizontalDisplay.vue +56 -0
- package/template/src/components/PartBlock.vue +9 -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 +35 -0
- package/template/src/composables/useAnnotationRenderer.ts +57 -25
- package/template/src/composables/useData.ts +6 -1
- package/template/src/composables/useI18n.ts +36 -3
- package/template/src/composables/useReadingMode.ts +9 -4
- package/template/src/composables/useSiteConfig.ts +12 -1
- package/template/src/router.ts +0 -2
- package/template/src/styles/main.css +88 -0
- package/template/src/types.ts +12 -4
- package/template/src/views/AuthorView.vue +5 -5
- package/template/src/views/BookHome.vue +45 -21
- package/template/src/views/LibraryHome.vue +39 -41
- package/template/src/views/PieceView.vue +436 -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 selectedAuthorEra = computed(() => {
|
|
232
|
+
const name = selectedAuthorName.value
|
|
233
|
+
const a = getAuthor(name)
|
|
234
|
+
return a?.era || piece.value?.era || ''
|
|
235
|
+
})
|
|
236
|
+
|
|
237
|
+
const selectedAuthorWorkCount = computed(() => {
|
|
238
|
+
const name = selectedAuthorName.value
|
|
239
|
+
const a = getAuthor(name)
|
|
240
|
+
return a?.workCount || 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>
|
|
@@ -382,6 +435,7 @@ function tcy(n: number): string {
|
|
|
382
435
|
:annotations="interaction.items"
|
|
383
436
|
:layer-labels="layerLabels"
|
|
384
437
|
:style="interaction.style"
|
|
438
|
+
:vertical="true"
|
|
385
439
|
@close="interaction.dismiss"
|
|
386
440
|
@tooltip-enter="interaction.onTooltipEnter"
|
|
387
441
|
@tooltip-leave="interaction.onTooltipLeave"
|
|
@@ -394,12 +448,28 @@ function tcy(n: number): string {
|
|
|
394
448
|
<button class="v-pane-close" @click="closeAuthorPane">✕</button>
|
|
395
449
|
<div class="v-pane-header">
|
|
396
450
|
<div class="v-pane-name">{{ selectedAuthorName }}</div>
|
|
451
|
+
<div class="v-pane-meta">
|
|
452
|
+
<span v-if="selectedAuthorEra">{{ selectedAuthorEra }}</span>
|
|
453
|
+
<span v-if="authorLifespan">{{ authorLifespan }}</span>
|
|
454
|
+
<span v-if="selectedAuthorWorkCount" class="v-pane-count">{{ t('stat.pieceCount', { count: selectedAuthorWorkCount }) }}</span>
|
|
455
|
+
</div>
|
|
456
|
+
<div v-if="selectedAuthorData?.courtesyName || selectedAuthorData?.artName" class="v-pane-names">
|
|
457
|
+
<span v-if="selectedAuthorData?.courtesyName">字{{ selectedAuthorData.courtesyName }}</span>
|
|
458
|
+
<span v-if="selectedAuthorData?.artName">號{{ selectedAuthorData.artName }}</span>
|
|
459
|
+
</div>
|
|
460
|
+
</div>
|
|
461
|
+
<div class="v-pane-links">
|
|
462
|
+
<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>
|
|
463
|
+
<a v-if="selectedAuthorData?.wikipediaZh" :href="selectedAuthorData.wikipediaZh" target="_blank" rel="noopener" class="v-pane-link">維基</a>
|
|
464
|
+
<a v-if="selectedAuthorData?.wikipediaEn" :href="selectedAuthorData.wikipediaEn" target="_blank" rel="noopener" class="v-pane-link">Wikipedia</a>
|
|
465
|
+
<a v-if="selectedAuthorData?.wikidata" :href="`https://www.wikidata.org/wiki/${selectedAuthorData.wikidata}`" target="_blank" rel="noopener" class="v-pane-link">Wikidata</a>
|
|
397
466
|
</div>
|
|
398
467
|
<div v-if="selectedAuthorBio" class="v-pane-bio">
|
|
399
468
|
<div v-for="p in selectedAuthorBio.split('\n').filter(l => l.trim())" :key="p" class="v-pane-p">
|
|
400
469
|
{{ p.trim() }}
|
|
401
470
|
</div>
|
|
402
471
|
</div>
|
|
472
|
+
<div v-if="!selectedAuthorBio" class="v-pane-empty">{{ t('piece.noAuthorData') }}</div>
|
|
403
473
|
</div>
|
|
404
474
|
</div>
|
|
405
475
|
</Transition>
|
|
@@ -412,14 +482,16 @@ function tcy(n: number): string {
|
|
|
412
482
|
<div class="h-page">
|
|
413
483
|
<nav class="h-nav">
|
|
414
484
|
<div class="h-nav-inner">
|
|
415
|
-
<button class="h-back" @click="goBack">←
|
|
416
|
-
<div class="h-
|
|
417
|
-
<span v-if="piece.
|
|
418
|
-
|
|
485
|
+
<button class="h-back" @click="goBack">← {{ t('nav.back') }}</button>
|
|
486
|
+
<div class="h-nav-title-row">
|
|
487
|
+
<span v-if="piece.era" class="h-era">{{ piece.era }}</span>
|
|
488
|
+
<span class="h-breadcrumb">
|
|
489
|
+
<span v-if="piece.source?.textRef" class="h-source-link" @click="router.push(`/${piece.source.textRef}`)">
|
|
490
|
+
{{ meta?.title }} →
|
|
491
|
+
</span>
|
|
492
|
+
<span class="h-sep">{{ piece.num }}.</span>
|
|
493
|
+
{{ piece.title }}
|
|
419
494
|
</span>
|
|
420
|
-
<span class="h-sep">{{ piece.num }}.</span>
|
|
421
|
-
{{ piece.title }}
|
|
422
|
-
<span class="h-sep">·</span>
|
|
423
495
|
<template v-if="piece.contributors && piece.contributors.length > 1">
|
|
424
496
|
<template v-for="(group, gi) in contributorGroups" :key="group.title">
|
|
425
497
|
<span v-if="gi > 0" class="h-sep">|</span>
|
|
@@ -430,14 +502,17 @@ function tcy(n: number): string {
|
|
|
430
502
|
<span v-else class="h-author-link" @click="openAuthorPane">{{ piece.author }}</span>
|
|
431
503
|
</div>
|
|
432
504
|
<div class="h-controls">
|
|
505
|
+
<span class="h-tag h-tag-pager">{{ piece.num }} / {{ pieces.length }}</span>
|
|
433
506
|
<template v-if="isMultiPart">
|
|
434
|
-
<span class="h-tag">{{ piece.parts!.length }}
|
|
435
|
-
<span class="h-tag">{{ totalPartAnnotationCount > 0 ? totalPartAnnotationCount + '
|
|
507
|
+
<span class="h-tag">{{ piece.parts!.length }} {{ t('piece.stanzas') }}</span>
|
|
508
|
+
<span class="h-tag">{{ totalPartAnnotationCount > 0 ? totalPartAnnotationCount + ' ' + t('piece.notes') : t('piece.noNotes') }}</span>
|
|
436
509
|
</template>
|
|
437
510
|
<template v-else>
|
|
438
|
-
<span class="h-tag">{{ piece.verses.length }}
|
|
439
|
-
<span class="h-tag">{{ totalAnnotationCount > 0 ? totalAnnotationCount + '
|
|
511
|
+
<span class="h-tag">{{ piece.verses.length }} {{ t('piece.stanzas') }}</span>
|
|
512
|
+
<span class="h-tag">{{ totalAnnotationCount > 0 ? totalAnnotationCount + ' ' + t('piece.notes') : t('piece.noNotes') }}</span>
|
|
440
513
|
</template>
|
|
514
|
+
<button v-if="adjacent.prev !== null" class="h-nav-arrow" @click="navigate(-1)" :title="t('piece.previous')">←</button>
|
|
515
|
+
<button v-if="adjacent.next !== null" class="h-nav-arrow" @click="navigate(1)" :title="t('piece.next')">→</button>
|
|
441
516
|
</div>
|
|
442
517
|
</div>
|
|
443
518
|
</nav>
|
|
@@ -468,10 +543,10 @@ function tcy(n: number): string {
|
|
|
468
543
|
</div>
|
|
469
544
|
|
|
470
545
|
<div class="h-sections">
|
|
471
|
-
<div v-if="
|
|
546
|
+
<div v-if="piece.annotations.length > 0 || piece.sections.annotations || hasLayers" class="h-ann-section">
|
|
472
547
|
<AnnotationControlBar
|
|
473
548
|
:layers="annotationLayers"
|
|
474
|
-
:has-annotations="
|
|
549
|
+
:has-annotations="true"
|
|
475
550
|
v-model:active-ids="activeLayerIds"
|
|
476
551
|
v-model:annotations-visible="annotationsVisible"
|
|
477
552
|
style="margin-bottom: 16px"
|
|
@@ -479,12 +554,12 @@ function tcy(n: number): string {
|
|
|
479
554
|
<SectionBlock
|
|
480
555
|
v-if="annotationsVisible && piece.sections.annotations"
|
|
481
556
|
num=""
|
|
482
|
-
label="
|
|
557
|
+
:label="t('annotation.notes')"
|
|
483
558
|
:special="false"
|
|
484
559
|
:text="piece.sections.annotations"
|
|
485
560
|
:is-annotations="true"
|
|
486
561
|
/>
|
|
487
|
-
<template v-if="hasLayers">
|
|
562
|
+
<template v-if="hasLayers && annotationsVisible">
|
|
488
563
|
<SectionBlock
|
|
489
564
|
v-for="block in layerAnnotationBlocks"
|
|
490
565
|
:key="block.label"
|
|
@@ -511,12 +586,12 @@ function tcy(n: number): string {
|
|
|
511
586
|
|
|
512
587
|
<div class="h-nav-bottom">
|
|
513
588
|
<button v-if="adjacent.prev !== null" class="h-nav-btn" @click="navigate(-1)">
|
|
514
|
-
<div class="h-nav-label">←
|
|
589
|
+
<div class="h-nav-label">← {{ t('piece.previous') }}</div>
|
|
515
590
|
<div class="h-nav-title">{{ getPiece(adjacent.prev)?.title }}</div>
|
|
516
591
|
</button>
|
|
517
592
|
<div v-else />
|
|
518
593
|
<button v-if="adjacent.next !== null" class="h-nav-btn h-nav-next" @click="navigate(1)">
|
|
519
|
-
<div class="h-nav-label"
|
|
594
|
+
<div class="h-nav-label">{{ t('piece.next') }} →</div>
|
|
520
595
|
<div class="h-nav-title">{{ getPiece(adjacent.next)?.title }}</div>
|
|
521
596
|
</button>
|
|
522
597
|
</div>
|
|
@@ -528,6 +603,7 @@ function tcy(n: number): string {
|
|
|
528
603
|
:annotations="interaction.items"
|
|
529
604
|
:layer-labels="layerLabels"
|
|
530
605
|
:style="interaction.style"
|
|
606
|
+
:vertical="false"
|
|
531
607
|
@close="interaction.dismiss"
|
|
532
608
|
@tooltip-enter="interaction.onTooltipEnter"
|
|
533
609
|
@tooltip-leave="interaction.onTooltipLeave"
|
|
@@ -543,14 +619,37 @@ function tcy(n: number): string {
|
|
|
543
619
|
<div class="h-pane-header">
|
|
544
620
|
<div>
|
|
545
621
|
<div class="h-pane-name">{{ selectedAuthorName }}</div>
|
|
546
|
-
<div class="h-pane-meta">
|
|
622
|
+
<div class="h-pane-meta">
|
|
623
|
+
<span v-if="selectedAuthorEra" class="h-pane-era">{{ selectedAuthorEra }}</span>
|
|
624
|
+
<span v-if="authorLifespan" class="h-pane-lifespan">{{ authorLifespan }}</span>
|
|
625
|
+
<span v-if="selectedAuthorWorkCount" class="h-pane-count">{{ t('piece.collected', { count: selectedAuthorWorkCount }) }}</span>
|
|
626
|
+
</div>
|
|
627
|
+
<div v-if="selectedAuthorData?.courtesyName || selectedAuthorData?.artName" class="h-pane-alt-names">
|
|
628
|
+
<span v-if="selectedAuthorData?.courtesyName">字 {{ selectedAuthorData.courtesyName }}</span>
|
|
629
|
+
<span v-if="selectedAuthorData?.artName">號 {{ selectedAuthorData.artName }}</span>
|
|
630
|
+
</div>
|
|
547
631
|
</div>
|
|
548
632
|
</div>
|
|
633
|
+
<div class="h-pane-links">
|
|
634
|
+
<a v-if="selectedAuthorData?.ctextId" :href="`https://ctext.org/wiki.pl?if=en&res=${selectedAuthorData.ctextId}`" target="_blank" rel="noopener" class="h-pane-link">
|
|
635
|
+
<span class="link-icon">文</span> CTEXT
|
|
636
|
+
</a>
|
|
637
|
+
<a v-if="selectedAuthorData?.wikipediaZh" :href="selectedAuthorData.wikipediaZh" target="_blank" rel="noopener" class="h-pane-link">
|
|
638
|
+
<span class="link-icon">維</span> 維基百科
|
|
639
|
+
</a>
|
|
640
|
+
<a v-if="selectedAuthorData?.wikipediaEn" :href="selectedAuthorData.wikipediaEn" target="_blank" rel="noopener" class="h-pane-link">
|
|
641
|
+
<span class="link-icon">W</span> Wikipedia
|
|
642
|
+
</a>
|
|
643
|
+
<a v-if="selectedAuthorData?.wikidata" :href="`https://www.wikidata.org/wiki/${selectedAuthorData.wikidata}`" target="_blank" rel="noopener" class="h-pane-link">
|
|
644
|
+
<span class="link-icon">Q</span> Wikidata
|
|
645
|
+
</a>
|
|
646
|
+
</div>
|
|
549
647
|
<div v-if="selectedAuthorBio" class="h-pane-bio">
|
|
550
648
|
<div v-for="p in selectedAuthorBio.split('\n').filter(l => l.trim())" :key="p" class="h-pane-p">
|
|
551
649
|
{{ p.trim() }}
|
|
552
650
|
</div>
|
|
553
651
|
</div>
|
|
652
|
+
<div v-if="!selectedAuthorBio" class="h-pane-empty">暫無作者資料</div>
|
|
554
653
|
</div>
|
|
555
654
|
</div>
|
|
556
655
|
</Transition>
|
|
@@ -558,8 +657,9 @@ function tcy(n: number): string {
|
|
|
558
657
|
</div>
|
|
559
658
|
</div>
|
|
560
659
|
|
|
561
|
-
<div v-else class="loading">
|
|
562
|
-
<
|
|
660
|
+
<div v-else class="page-loading">
|
|
661
|
+
<img v-if="CHAM_LOGO_URL" :src="CHAM_LOGO_URL" alt="" class="page-loading-logo" />
|
|
662
|
+
<div v-else class="page-loading-seal">文</div>
|
|
563
663
|
</div>
|
|
564
664
|
</template>
|
|
565
665
|
|
|
@@ -567,20 +667,9 @@ function tcy(n: number): string {
|
|
|
567
667
|
/* ═══════ 直排模式 ═══════ */
|
|
568
668
|
|
|
569
669
|
.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
670
|
padding: 0;
|
|
577
671
|
background: var(--paper);
|
|
578
|
-
scrollbar-width: thin;
|
|
579
|
-
scrollbar-color: var(--gold) transparent;
|
|
580
|
-
scroll-snap-type: x proximity;
|
|
581
672
|
}
|
|
582
|
-
.v-page::-webkit-scrollbar { height: 4px; }
|
|
583
|
-
.v-page::-webkit-scrollbar-thumb { background: var(--gold); border-radius: 2px; }
|
|
584
673
|
|
|
585
674
|
.v-title-col {
|
|
586
675
|
writing-mode: vertical-rl;
|
|
@@ -739,7 +828,39 @@ function tcy(n: number): string {
|
|
|
739
828
|
transition: all 0.2s; white-space: nowrap;
|
|
740
829
|
}
|
|
741
830
|
.h-back:hover { background: var(--ink); color: var(--paper); border-color: var(--ink); }
|
|
742
|
-
.h-
|
|
831
|
+
.h-back:active { transform: scale(0.97); }
|
|
832
|
+
|
|
833
|
+
.h-nav-title-row {
|
|
834
|
+
display: flex;
|
|
835
|
+
align-items: baseline;
|
|
836
|
+
gap: 8px;
|
|
837
|
+
font-size: 15px;
|
|
838
|
+
font-weight: 600;
|
|
839
|
+
letter-spacing: 1px;
|
|
840
|
+
min-width: 0;
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
.h-era {
|
|
844
|
+
display: inline-flex;
|
|
845
|
+
align-items: center;
|
|
846
|
+
padding: 2px 8px;
|
|
847
|
+
background: var(--vermillion);
|
|
848
|
+
color: #fff;
|
|
849
|
+
font-family: var(--sans);
|
|
850
|
+
font-size: 11px;
|
|
851
|
+
font-weight: 700;
|
|
852
|
+
letter-spacing: 1px;
|
|
853
|
+
border-radius: 3px;
|
|
854
|
+
white-space: nowrap;
|
|
855
|
+
flex-shrink: 0;
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
.h-breadcrumb {
|
|
859
|
+
overflow: hidden;
|
|
860
|
+
text-overflow: ellipsis;
|
|
861
|
+
white-space: nowrap;
|
|
862
|
+
min-width: 0;
|
|
863
|
+
}
|
|
743
864
|
.h-sep { color: var(--ink-faint); font-weight: 300; margin: 0 8px; }
|
|
744
865
|
.h-author-link {
|
|
745
866
|
color: var(--ink-light); font-weight: 400;
|
|
@@ -757,6 +878,29 @@ function tcy(n: number): string {
|
|
|
757
878
|
border-radius: 2px; font-family: var(--sans);
|
|
758
879
|
font-size: 12px; color: var(--ink-light); letter-spacing: 1px;
|
|
759
880
|
}
|
|
881
|
+
.h-tag-pager {
|
|
882
|
+
background: var(--surface-warm);
|
|
883
|
+
font-weight: 600;
|
|
884
|
+
color: var(--ink-mid);
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
.h-nav-arrow {
|
|
888
|
+
width: 32px; height: 32px;
|
|
889
|
+
border: 1px solid var(--border);
|
|
890
|
+
border-radius: 4px;
|
|
891
|
+
background: none;
|
|
892
|
+
font-family: var(--sans);
|
|
893
|
+
font-size: 16px;
|
|
894
|
+
color: var(--ink-light);
|
|
895
|
+
cursor: pointer;
|
|
896
|
+
display: flex; align-items: center; justify-content: center;
|
|
897
|
+
transition: all 0.15s;
|
|
898
|
+
}
|
|
899
|
+
.h-nav-arrow:hover {
|
|
900
|
+
border-color: var(--vermillion);
|
|
901
|
+
color: var(--vermillion);
|
|
902
|
+
}
|
|
903
|
+
.h-nav-arrow:active { transform: scale(0.95); }
|
|
760
904
|
|
|
761
905
|
.h-content {
|
|
762
906
|
max-width: 1200px; margin: 0 auto; padding: 60px 40px;
|
|
@@ -781,6 +925,10 @@ function tcy(n: number): string {
|
|
|
781
925
|
|
|
782
926
|
.h-ann-section {
|
|
783
927
|
margin-bottom: 16px;
|
|
928
|
+
padding: 20px;
|
|
929
|
+
background: var(--surface);
|
|
930
|
+
border: 1px solid var(--border-light);
|
|
931
|
+
border-radius: 8px;
|
|
784
932
|
}
|
|
785
933
|
|
|
786
934
|
.h-layers-inline {
|
|
@@ -825,6 +973,7 @@ function tcy(n: number): string {
|
|
|
825
973
|
transform: translateY(-2px);
|
|
826
974
|
}
|
|
827
975
|
.h-nav-btn:hover::after { transform: scaleX(1); }
|
|
976
|
+
.h-nav-btn:active { transform: scale(0.98); }
|
|
828
977
|
.h-nav-btn.h-nav-next { text-align: right; }
|
|
829
978
|
.h-nav-label { font-size: 11px; color: var(--ink-faint); font-family: var(--sans); letter-spacing: 2px; margin-bottom: 4px; }
|
|
830
979
|
.h-nav-title { font-size: 16px; font-weight: 600; letter-spacing: 1px; color: var(--ink); }
|
|
@@ -876,7 +1025,83 @@ function tcy(n: number): string {
|
|
|
876
1025
|
color: var(--vermillion); flex-shrink: 0;
|
|
877
1026
|
}
|
|
878
1027
|
.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:
|
|
1028
|
+
.h-pane-meta { font-size: 14px; color: var(--ink-faint); letter-spacing: 2px; margin-top: 6px; display: flex; align-items: center; gap: 8px; }
|
|
1029
|
+
.h-pane-era {
|
|
1030
|
+
display: inline-flex;
|
|
1031
|
+
padding: 2px 8px;
|
|
1032
|
+
background: var(--vermillion);
|
|
1033
|
+
color: #fff;
|
|
1034
|
+
font-family: var(--sans);
|
|
1035
|
+
font-size: 11px;
|
|
1036
|
+
font-weight: 700;
|
|
1037
|
+
letter-spacing: 1px;
|
|
1038
|
+
border-radius: 3px;
|
|
1039
|
+
}
|
|
1040
|
+
.h-pane-count {
|
|
1041
|
+
font-family: var(--sans);
|
|
1042
|
+
font-size: 12px;
|
|
1043
|
+
color: var(--ink-faint);
|
|
1044
|
+
letter-spacing: 1px;
|
|
1045
|
+
}
|
|
1046
|
+
.h-pane-lifespan {
|
|
1047
|
+
font-family: var(--sans);
|
|
1048
|
+
font-size: 12px;
|
|
1049
|
+
color: var(--ink-light);
|
|
1050
|
+
letter-spacing: 1px;
|
|
1051
|
+
}
|
|
1052
|
+
.h-pane-alt-names {
|
|
1053
|
+
font-size: 14px;
|
|
1054
|
+
color: var(--ink-light);
|
|
1055
|
+
letter-spacing: 2px;
|
|
1056
|
+
margin-top: 6px;
|
|
1057
|
+
display: flex;
|
|
1058
|
+
gap: 12px;
|
|
1059
|
+
}
|
|
1060
|
+
.h-pane-links {
|
|
1061
|
+
display: flex;
|
|
1062
|
+
gap: 8px;
|
|
1063
|
+
flex-wrap: wrap;
|
|
1064
|
+
margin: 16px 0 0;
|
|
1065
|
+
padding-bottom: 16px;
|
|
1066
|
+
border-bottom: 1px solid var(--border-light);
|
|
1067
|
+
}
|
|
1068
|
+
.h-pane-link {
|
|
1069
|
+
display: inline-flex;
|
|
1070
|
+
align-items: center;
|
|
1071
|
+
gap: 4px;
|
|
1072
|
+
padding: 4px 10px;
|
|
1073
|
+
border: 1px solid var(--border);
|
|
1074
|
+
border-radius: 4px;
|
|
1075
|
+
font-family: var(--sans);
|
|
1076
|
+
font-size: 12px;
|
|
1077
|
+
color: var(--ink-mid);
|
|
1078
|
+
text-decoration: none;
|
|
1079
|
+
letter-spacing: 1px;
|
|
1080
|
+
transition: all 0.15s;
|
|
1081
|
+
}
|
|
1082
|
+
.h-pane-link:hover {
|
|
1083
|
+
border-color: var(--vermillion);
|
|
1084
|
+
color: var(--vermillion);
|
|
1085
|
+
}
|
|
1086
|
+
.h-pane-link .link-icon {
|
|
1087
|
+
display: inline-flex;
|
|
1088
|
+
align-items: center;
|
|
1089
|
+
justify-content: center;
|
|
1090
|
+
width: 18px;
|
|
1091
|
+
height: 18px;
|
|
1092
|
+
border-radius: 3px;
|
|
1093
|
+
background: var(--surface-warm);
|
|
1094
|
+
font-size: 10px;
|
|
1095
|
+
font-weight: 700;
|
|
1096
|
+
}
|
|
1097
|
+
.h-pane-empty {
|
|
1098
|
+
padding: 40px 0;
|
|
1099
|
+
text-align: center;
|
|
1100
|
+
color: var(--ink-faint);
|
|
1101
|
+
font-family: var(--sans);
|
|
1102
|
+
font-size: 14px;
|
|
1103
|
+
letter-spacing: 2px;
|
|
1104
|
+
}
|
|
880
1105
|
.h-pane-bio { border-top: 1px solid var(--border); padding-top: 24px; }
|
|
881
1106
|
.h-pane-p {
|
|
882
1107
|
font-size: 16px; line-height: 2.2;
|
|
@@ -931,6 +1156,60 @@ function tcy(n: number): string {
|
|
|
931
1156
|
font-size: 28px; font-weight: 900;
|
|
932
1157
|
letter-spacing: 6px; color: var(--ink);
|
|
933
1158
|
}
|
|
1159
|
+
.v-pane-meta {
|
|
1160
|
+
font-size: 13px;
|
|
1161
|
+
color: var(--ink-faint);
|
|
1162
|
+
font-family: var(--sans);
|
|
1163
|
+
letter-spacing: 2px;
|
|
1164
|
+
display: flex;
|
|
1165
|
+
gap: 8px;
|
|
1166
|
+
margin-left: 4px;
|
|
1167
|
+
}
|
|
1168
|
+
.v-pane-count {
|
|
1169
|
+
font-size: 12px;
|
|
1170
|
+
color: var(--ink-faint);
|
|
1171
|
+
letter-spacing: 1px;
|
|
1172
|
+
}
|
|
1173
|
+
.v-pane-names {
|
|
1174
|
+
font-size: 14px;
|
|
1175
|
+
color: var(--ink-light);
|
|
1176
|
+
letter-spacing: 2px;
|
|
1177
|
+
display: flex;
|
|
1178
|
+
gap: 8px;
|
|
1179
|
+
margin-left: 4px;
|
|
1180
|
+
}
|
|
1181
|
+
.v-pane-links {
|
|
1182
|
+
display: flex;
|
|
1183
|
+
gap: 8px;
|
|
1184
|
+
padding-left: 16px;
|
|
1185
|
+
border-left: 1px solid var(--border);
|
|
1186
|
+
margin-bottom: 16px;
|
|
1187
|
+
}
|
|
1188
|
+
.v-pane-link {
|
|
1189
|
+
display: inline-flex;
|
|
1190
|
+
align-items: center;
|
|
1191
|
+
padding: 4px 8px;
|
|
1192
|
+
border: 1px solid var(--border);
|
|
1193
|
+
border-radius: 3px;
|
|
1194
|
+
font-family: var(--sans);
|
|
1195
|
+
font-size: 11px;
|
|
1196
|
+
color: var(--ink-mid);
|
|
1197
|
+
text-decoration: none;
|
|
1198
|
+
letter-spacing: 1px;
|
|
1199
|
+
transition: all 0.15s;
|
|
1200
|
+
}
|
|
1201
|
+
.v-pane-link:hover {
|
|
1202
|
+
border-color: var(--vermillion);
|
|
1203
|
+
color: var(--vermillion);
|
|
1204
|
+
}
|
|
1205
|
+
.v-pane-empty {
|
|
1206
|
+
font-size: 14px;
|
|
1207
|
+
color: var(--ink-faint);
|
|
1208
|
+
font-family: var(--sans);
|
|
1209
|
+
letter-spacing: 2px;
|
|
1210
|
+
padding-left: 16px;
|
|
1211
|
+
border-left: 1px solid var(--border);
|
|
1212
|
+
}
|
|
934
1213
|
.v-pane-bio {
|
|
935
1214
|
font-size: 16px; line-height: 2.4;
|
|
936
1215
|
color: var(--ink-mid);
|
|
@@ -942,26 +1221,112 @@ function tcy(n: number): string {
|
|
|
942
1221
|
margin-left: 12px;
|
|
943
1222
|
}
|
|
944
1223
|
|
|
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
|
-
}
|
|
1224
|
+
|
|
1225
|
+
/* ─── 觸控回饋 ─── */
|
|
1226
|
+
.v-nav-btn:active { transform: scale(0.97); }
|
|
1227
|
+
.h-back:active { transform: scale(0.97); }
|
|
1228
|
+
.h-source-link:active { opacity: 1; }
|
|
1229
|
+
.v-source-link:active { opacity: 1; }
|
|
1230
|
+
.v-poem-author:active { color: var(--vermillion); }
|
|
1231
|
+
.h-author-link:active { color: var(--vermillion); }
|
|
1232
|
+
|
|
1233
|
+
/* ═══════ 行動裝置適配 ═══════ */
|
|
963
1234
|
|
|
964
1235
|
@media (max-width: 768px) {
|
|
965
|
-
|
|
1236
|
+
/* ─── 直排模式 ─── */
|
|
1237
|
+
.v-page { margin-right: var(--nav-width, 44px); }
|
|
1238
|
+
.v-title-col { padding: 24px 16px; }
|
|
1239
|
+
.v-poem-title { font-size: 32px; letter-spacing: 8px; }
|
|
1240
|
+
.v-poem-author { font-size: 20px; }
|
|
1241
|
+
.v-poem-col { padding: 16px; }
|
|
1242
|
+
|
|
1243
|
+
/* ─── 橫排模式導航 ─── */
|
|
1244
|
+
.h-nav { padding: 0 16px; }
|
|
1245
|
+
.h-nav-inner {
|
|
1246
|
+
height: auto;
|
|
1247
|
+
min-height: 48px;
|
|
1248
|
+
flex-wrap: wrap;
|
|
1249
|
+
padding: 8px 0;
|
|
1250
|
+
gap: 6px 12px;
|
|
1251
|
+
}
|
|
1252
|
+
.h-back {
|
|
1253
|
+
padding: 6px 10px;
|
|
1254
|
+
font-size: 12px;
|
|
1255
|
+
}
|
|
1256
|
+
.h-nav-title-row {
|
|
1257
|
+
font-size: 14px;
|
|
1258
|
+
order: 3;
|
|
1259
|
+
width: 100%;
|
|
1260
|
+
overflow: hidden;
|
|
1261
|
+
}
|
|
1262
|
+
.h-breadcrumb {
|
|
1263
|
+
overflow: hidden;
|
|
1264
|
+
text-overflow: ellipsis;
|
|
1265
|
+
white-space: nowrap;
|
|
1266
|
+
}
|
|
1267
|
+
.h-era { font-size: 10px; padding: 1px 6px; }
|
|
1268
|
+
.h-sep { margin: 0 4px; }
|
|
1269
|
+
.h-controls {
|
|
1270
|
+
margin-left: 0;
|
|
1271
|
+
gap: 4px;
|
|
1272
|
+
}
|
|
1273
|
+
.h-tag {
|
|
1274
|
+
padding: 3px 8px;
|
|
1275
|
+
font-size: 11px;
|
|
1276
|
+
}
|
|
1277
|
+
.h-nav-arrow { width: 28px; height: 28px; font-size: 14px; }
|
|
1278
|
+
|
|
1279
|
+
/* ─── 橫排內容 ─── */
|
|
1280
|
+
.h-content { padding: 24px 16px; }
|
|
1281
|
+
.h-poem-block { margin-bottom: 40px; }
|
|
1282
|
+
|
|
1283
|
+
.h-multipart {
|
|
1284
|
+
padding: 20px 16px;
|
|
1285
|
+
border-radius: 6px;
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1288
|
+
.h-sections {
|
|
1289
|
+
padding-bottom: 60px;
|
|
1290
|
+
}
|
|
1291
|
+
|
|
1292
|
+
.h-ann-section {
|
|
1293
|
+
margin-bottom: 12px;
|
|
1294
|
+
}
|
|
1295
|
+
|
|
1296
|
+
/* ─── 上/下篇導航 ─── */
|
|
1297
|
+
.h-nav-bottom {
|
|
1298
|
+
gap: 10px;
|
|
1299
|
+
margin: 0 auto 32px;
|
|
1300
|
+
}
|
|
1301
|
+
.h-nav-btn {
|
|
1302
|
+
padding: 16px;
|
|
1303
|
+
border-radius: 6px;
|
|
1304
|
+
}
|
|
1305
|
+
.h-nav-title { font-size: 14px; }
|
|
1306
|
+
.h-nav-label { font-size: 10px; }
|
|
1307
|
+
|
|
1308
|
+
/* ─── 作者面板 ─── */
|
|
1309
|
+
.h-overlay { justify-content: center; align-items: flex-end; }
|
|
1310
|
+
.h-pane {
|
|
1311
|
+
width: 100%;
|
|
1312
|
+
max-height: 85vh;
|
|
1313
|
+
height: auto;
|
|
1314
|
+
border-radius: 16px 16px 0 0;
|
|
1315
|
+
padding: 20px;
|
|
1316
|
+
}
|
|
1317
|
+
.overlay-enter-from .h-pane { transform: translateY(100%); }
|
|
1318
|
+
.overlay-leave-to .h-pane { transform: translateY(40px); }
|
|
1319
|
+
|
|
1320
|
+
.h-pane-name { font-size: 24px; }
|
|
1321
|
+
.h-pane-p { font-size: 15px; line-height: 2; }
|
|
1322
|
+
}
|
|
1323
|
+
|
|
1324
|
+
@media (max-width: 480px) {
|
|
1325
|
+
.h-nav-bottom {
|
|
1326
|
+
grid-template-columns: 1fr;
|
|
1327
|
+
}
|
|
1328
|
+
.h-nav-btn.h-nav-next { text-align: left; }
|
|
1329
|
+
.h-nav-title-row { font-size: 13px; }
|
|
1330
|
+
.h-nav-arrow { display: none; }
|
|
966
1331
|
}
|
|
967
1332
|
</style>
|