@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
|
@@ -15,6 +15,11 @@
|
|
|
15
15
|
--gold: #9a7d3a;
|
|
16
16
|
--gold-light: #c9a84c;
|
|
17
17
|
--jade: #3a6b5e;
|
|
18
|
+
--ann-person: #3a5a8c;
|
|
19
|
+
--ann-place: #8b6914;
|
|
20
|
+
--ann-event: #6b4c8a;
|
|
21
|
+
--ann-date: #2a7a7a;
|
|
22
|
+
--ann-allusion: #b5651d;
|
|
18
23
|
--border: #d8cdb8;
|
|
19
24
|
--border-light: #e8e0d0;
|
|
20
25
|
--shadow-rgb: 26,26,26;
|
|
@@ -36,6 +41,11 @@
|
|
|
36
41
|
--gold: #8a6d2a;
|
|
37
42
|
--gold-light: #b89540;
|
|
38
43
|
--jade: #2d5a4e;
|
|
44
|
+
--ann-person: #3a5a8c;
|
|
45
|
+
--ann-place: #8b6914;
|
|
46
|
+
--ann-event: #6b4c8a;
|
|
47
|
+
--ann-date: #2a7a7a;
|
|
48
|
+
--ann-allusion: #b5651d;
|
|
39
49
|
--border: #c9b896;
|
|
40
50
|
--border-light: #d8cab0;
|
|
41
51
|
--shadow-rgb: 74,63,46;
|
|
@@ -57,6 +67,11 @@
|
|
|
57
67
|
--gold: #c9a84c;
|
|
58
68
|
--gold-light: #d8b860;
|
|
59
69
|
--jade: #5aaa98;
|
|
70
|
+
--ann-person: #6a8ab4;
|
|
71
|
+
--ann-place: #b8943a;
|
|
72
|
+
--ann-event: #9a7cb4;
|
|
73
|
+
--ann-date: #5ab4b4;
|
|
74
|
+
--ann-allusion: #d4843a;
|
|
60
75
|
--border: #48484a;
|
|
61
76
|
--border-light: #555557;
|
|
62
77
|
--shadow-rgb: 0,0,0;
|
|
@@ -78,6 +93,11 @@
|
|
|
78
93
|
--gold: #e8c840;
|
|
79
94
|
--gold-light: #f0d860;
|
|
80
95
|
--jade: #40c8a8;
|
|
96
|
+
--ann-person: #6a8ab4;
|
|
97
|
+
--ann-place: #b8943a;
|
|
98
|
+
--ann-event: #9a7cb4;
|
|
99
|
+
--ann-date: #5ab4b4;
|
|
100
|
+
--ann-allusion: #d4843a;
|
|
81
101
|
--border: #333333;
|
|
82
102
|
--border-light: #444444;
|
|
83
103
|
--shadow-rgb: 0,0,0;
|
|
@@ -196,3 +216,71 @@ button { font-family: inherit; }
|
|
|
196
216
|
@media (max-width: 768px) {
|
|
197
217
|
:root { --nav-width: 44px; }
|
|
198
218
|
}
|
|
219
|
+
|
|
220
|
+
/* ===== VERTICAL PAGE LAYOUT ===== */
|
|
221
|
+
.v-page {
|
|
222
|
+
height: 100vh;
|
|
223
|
+
display: flex;
|
|
224
|
+
flex-direction: row-reverse;
|
|
225
|
+
overflow-x: auto;
|
|
226
|
+
overflow-y: hidden;
|
|
227
|
+
margin-right: var(--nav-width, 56px);
|
|
228
|
+
scrollbar-width: thin;
|
|
229
|
+
scrollbar-color: var(--gold) transparent;
|
|
230
|
+
scroll-snap-type: x proximity;
|
|
231
|
+
}
|
|
232
|
+
.v-page::-webkit-scrollbar { height: 4px; }
|
|
233
|
+
.v-page::-webkit-scrollbar-thumb { background: var(--gold); border-radius: 2px; }
|
|
234
|
+
|
|
235
|
+
/* ===== VIEW LOADING STATE ===== */
|
|
236
|
+
.v-loading {
|
|
237
|
+
display: flex;
|
|
238
|
+
flex-direction: column;
|
|
239
|
+
align-items: center;
|
|
240
|
+
justify-content: center;
|
|
241
|
+
min-height: 60vh;
|
|
242
|
+
gap: 16px;
|
|
243
|
+
}
|
|
244
|
+
.v-loading .seal {
|
|
245
|
+
width: 72px; height: 72px;
|
|
246
|
+
border: 2px solid var(--vermillion);
|
|
247
|
+
border-radius: 4px;
|
|
248
|
+
display: flex; align-items: center; justify-content: center;
|
|
249
|
+
animation: pulse 2s ease-in-out infinite;
|
|
250
|
+
}
|
|
251
|
+
.v-loading .char {
|
|
252
|
+
font-family: var(--serif);
|
|
253
|
+
font-size: 36px; font-weight: 900;
|
|
254
|
+
color: var(--vermillion);
|
|
255
|
+
line-height: 1;
|
|
256
|
+
}
|
|
257
|
+
.v-loading .label {
|
|
258
|
+
font-size: 13px;
|
|
259
|
+
color: var(--ink-faint);
|
|
260
|
+
letter-spacing: 4px;
|
|
261
|
+
}
|
|
262
|
+
@keyframes pulse {
|
|
263
|
+
0%, 100% { opacity: 0.6; transform: scale(0.96); }
|
|
264
|
+
50% { opacity: 1; transform: scale(1); }
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/* ===== FULL-PAGE LOADING (LibraryHome, PieceView redirect) ===== */
|
|
268
|
+
.page-loading {
|
|
269
|
+
display: flex; flex-direction: column;
|
|
270
|
+
align-items: center; justify-content: center;
|
|
271
|
+
height: 100vh;
|
|
272
|
+
}
|
|
273
|
+
.page-loading-seal {
|
|
274
|
+
width: 56px; height: 56px;
|
|
275
|
+
border: 2px solid var(--vermillion);
|
|
276
|
+
border-radius: 4px;
|
|
277
|
+
display: flex; align-items: center; justify-content: center;
|
|
278
|
+
font-size: 28px; font-weight: 900;
|
|
279
|
+
color: var(--vermillion);
|
|
280
|
+
animation: pulse 1.2s ease-in-out infinite;
|
|
281
|
+
}
|
|
282
|
+
.page-loading-logo {
|
|
283
|
+
width: 56px; height: auto;
|
|
284
|
+
object-fit: contain;
|
|
285
|
+
animation: pulse 1.2s ease-in-out infinite;
|
|
286
|
+
}
|
package/template/src/types.ts
CHANGED
|
@@ -140,7 +140,7 @@ export interface Piece {
|
|
|
140
140
|
title: string
|
|
141
141
|
author: string
|
|
142
142
|
authorId: string
|
|
143
|
-
|
|
143
|
+
era: string
|
|
144
144
|
genre: BookGenre
|
|
145
145
|
verses: VerseLine[]
|
|
146
146
|
sections: Record<string, string>
|
|
@@ -162,9 +162,17 @@ export interface Author {
|
|
|
162
162
|
'@id': string
|
|
163
163
|
'@type': string
|
|
164
164
|
name: string
|
|
165
|
-
|
|
166
|
-
|
|
165
|
+
era: string
|
|
166
|
+
workCount: number
|
|
167
167
|
bio?: string
|
|
168
|
+
born?: string
|
|
169
|
+
died?: string
|
|
170
|
+
courtesyName?: string
|
|
171
|
+
artName?: string
|
|
172
|
+
wikidata?: string
|
|
173
|
+
ctextId?: string
|
|
174
|
+
wikipediaZh?: string
|
|
175
|
+
wikipediaEn?: string
|
|
168
176
|
}
|
|
169
177
|
|
|
170
178
|
export interface Dynasty {
|
|
@@ -172,5 +180,5 @@ export interface Dynasty {
|
|
|
172
180
|
'@type': string
|
|
173
181
|
name: string
|
|
174
182
|
authors: string[]
|
|
175
|
-
|
|
183
|
+
workCount: number
|
|
176
184
|
}
|
|
@@ -50,7 +50,7 @@ function goHome() { router.push('/') }
|
|
|
50
50
|
<section class="v-author-info">
|
|
51
51
|
<div class="v-seal">{{ authorName.charAt(0) }}</div>
|
|
52
52
|
<h1 class="v-name">{{ authorName }}</h1>
|
|
53
|
-
<span v-if="author.
|
|
53
|
+
<span v-if="author.era" class="v-era">{{ author.era }}</span>
|
|
54
54
|
<span class="v-count">{{ authorPieces.length }} 篇收錄作品</span>
|
|
55
55
|
</section>
|
|
56
56
|
|
|
@@ -83,7 +83,7 @@ function goHome() { router.push('/') }
|
|
|
83
83
|
<span class="h-author-name">{{ authorName }}</span>
|
|
84
84
|
</div>
|
|
85
85
|
<div class="h-controls">
|
|
86
|
-
<span class="h-tag">{{ author.
|
|
86
|
+
<span class="h-tag">{{ author.era || '未知朝代' }}</span>
|
|
87
87
|
<span class="h-tag">{{ authorPieces.length }} 篇</span>
|
|
88
88
|
</div>
|
|
89
89
|
</div>
|
|
@@ -95,7 +95,7 @@ function goHome() { router.push('/') }
|
|
|
95
95
|
<div class="h-info">
|
|
96
96
|
<h1 class="h-name">{{ authorName }}</h1>
|
|
97
97
|
<div class="h-meta">
|
|
98
|
-
<span v-if="author.
|
|
98
|
+
<span v-if="author.era" class="h-era">{{ author.era }}</span>
|
|
99
99
|
<span class="h-count">{{ authorPieces.length }} 篇收錄作品</span>
|
|
100
100
|
</div>
|
|
101
101
|
</div>
|
|
@@ -174,7 +174,7 @@ function goHome() { router.push('/') }
|
|
|
174
174
|
letter-spacing: 10px; color: var(--ink);
|
|
175
175
|
margin-left: 20px;
|
|
176
176
|
}
|
|
177
|
-
.v-
|
|
177
|
+
.v-era {
|
|
178
178
|
font-size: 20px; color: var(--gold);
|
|
179
179
|
font-weight: 600; letter-spacing: 4px;
|
|
180
180
|
margin-left: 12px;
|
|
@@ -286,7 +286,7 @@ function goHome() { router.push('/') }
|
|
|
286
286
|
}
|
|
287
287
|
.h-name { font-size: 36px; font-weight: 900; letter-spacing: 6px; color: var(--ink); }
|
|
288
288
|
.h-meta { display: flex; gap: 16px; margin-top: 8px; font-family: var(--sans); font-size: 14px; }
|
|
289
|
-
.h-
|
|
289
|
+
.h-era { color: var(--gold); font-weight: 600; letter-spacing: 2px; }
|
|
290
290
|
.h-count { color: var(--ink-faint); letter-spacing: 1px; }
|
|
291
291
|
|
|
292
292
|
.h-bio {
|
|
@@ -5,6 +5,7 @@ import { useBook } from '../composables/useBook'
|
|
|
5
5
|
import { useTitle } from '../composables/useTitle'
|
|
6
6
|
import { useReadingMode } from '../composables/useReadingMode'
|
|
7
7
|
import { useHorizontalScroll } from '../composables/useHorizontalScroll'
|
|
8
|
+
import { useI18n } from '../composables/useI18n'
|
|
8
9
|
import PoemCard from '../components/PoemCard.vue'
|
|
9
10
|
import SideNav from '../components/SideNav.vue'
|
|
10
11
|
import ReadingToolbar from '../components/ReadingToolbar.vue'
|
|
@@ -22,6 +23,7 @@ const { layout } = useReadingMode()
|
|
|
22
23
|
const isVertical = computed(() => layout.value === 'vertical')
|
|
23
24
|
const vPageRef = ref<HTMLElement | null>(null)
|
|
24
25
|
const vScroll = useHorizontalScroll(vPageRef)
|
|
26
|
+
const { t } = useI18n()
|
|
25
27
|
|
|
26
28
|
const filtered = computed(() => {
|
|
27
29
|
const q = searchQuery.value.toLowerCase()
|
|
@@ -72,11 +74,11 @@ function scrollToCatalog() {
|
|
|
72
74
|
</section>
|
|
73
75
|
|
|
74
76
|
<section class="v-catalog-col">
|
|
75
|
-
<span class="v-ch-title"
|
|
77
|
+
<span class="v-ch-title">{{ t('catalog.title') }}</span>
|
|
76
78
|
<span class="v-ch-line"> </span>
|
|
77
|
-
<span class="v-count"
|
|
79
|
+
<span class="v-count">{{ t('catalog.total', { count: filtered.length }) }}</span>
|
|
78
80
|
<span class="v-search-wrap">
|
|
79
|
-
<input v-model="searchQuery" class="v-search" placeholder="
|
|
81
|
+
<input v-model="searchQuery" class="v-search" :placeholder="t('catalog.search')" />
|
|
80
82
|
</span>
|
|
81
83
|
</section>
|
|
82
84
|
|
|
@@ -104,28 +106,28 @@ function scrollToCatalog() {
|
|
|
104
106
|
<div class="h-stats">
|
|
105
107
|
<div class="h-stat-block">
|
|
106
108
|
<div class="h-stat-num">{{ pieces.length }}</div>
|
|
107
|
-
<div class="h-stat-label"
|
|
109
|
+
<div class="h-stat-label">{{ t('stat.piecePoems') }}</div>
|
|
108
110
|
</div>
|
|
109
111
|
<div class="h-stat-block">
|
|
110
112
|
<div class="h-stat-num">{{ authorCount }}</div>
|
|
111
|
-
<div class="h-stat-label"
|
|
113
|
+
<div class="h-stat-label">{{ t('stat.authorsLabel') }}</div>
|
|
112
114
|
</div>
|
|
113
115
|
</div>
|
|
114
116
|
<p v-if="meta?.publisher" class="h-publisher">{{ meta.publisher }}</p>
|
|
115
117
|
<button class="h-cta" @click="scrollToCatalog">
|
|
116
|
-
|
|
118
|
+
{{ t('catalog.enterLibrary') }}
|
|
117
119
|
</button>
|
|
118
120
|
</div>
|
|
119
121
|
</section>
|
|
120
122
|
|
|
121
123
|
<section class="h-catalog">
|
|
122
124
|
<div class="h-catalog-header">
|
|
123
|
-
<h2
|
|
125
|
+
<h2>{{ t('catalog.title') }}</h2>
|
|
124
126
|
<div class="h-line"></div>
|
|
125
127
|
<p v-if="meta?.publisher">{{ meta.publisher }}</p>
|
|
126
128
|
</div>
|
|
127
129
|
<div class="h-filter">
|
|
128
|
-
<input v-model="searchQuery" class="h-search" placeholder="
|
|
130
|
+
<input v-model="searchQuery" class="h-search" :placeholder="t('catalog.search')" />
|
|
129
131
|
</div>
|
|
130
132
|
<div class="h-grid">
|
|
131
133
|
<PoemCard
|
|
@@ -137,6 +139,10 @@ function scrollToCatalog() {
|
|
|
137
139
|
@click="openPiece(piece.num)"
|
|
138
140
|
/>
|
|
139
141
|
</div>
|
|
142
|
+
<div v-if="searchQuery && filtered.length === 0" class="h-empty">
|
|
143
|
+
<span class="h-empty-icon">🔍</span>
|
|
144
|
+
<p>{{ t('catalog.noResults', { query: searchQuery }) }}</p>
|
|
145
|
+
</div>
|
|
140
146
|
</section>
|
|
141
147
|
|
|
142
148
|
<BackToTop />
|
|
@@ -148,20 +154,9 @@ function scrollToCatalog() {
|
|
|
148
154
|
/* ═══════ 直排模式 ═══════ */
|
|
149
155
|
|
|
150
156
|
.v-page {
|
|
151
|
-
height: 100vh;
|
|
152
|
-
display: flex;
|
|
153
|
-
flex-direction: row-reverse;
|
|
154
|
-
overflow-x: auto;
|
|
155
|
-
overflow-y: hidden;
|
|
156
|
-
margin-right: var(--nav-width, 56px);
|
|
157
157
|
padding: 0 32px;
|
|
158
158
|
background: linear-gradient(90deg, var(--paper) 0%, var(--paper-warm) 100%);
|
|
159
|
-
scrollbar-width: thin;
|
|
160
|
-
scrollbar-color: var(--gold) transparent;
|
|
161
|
-
scroll-snap-type: x proximity;
|
|
162
159
|
}
|
|
163
|
-
.v-page::-webkit-scrollbar { height: 4px; }
|
|
164
|
-
.v-page::-webkit-scrollbar-thumb { background: var(--gold); border-radius: 2px; }
|
|
165
160
|
|
|
166
161
|
.v-hero {
|
|
167
162
|
writing-mode: vertical-rl;
|
|
@@ -367,10 +362,31 @@ function scrollToCatalog() {
|
|
|
367
362
|
animation: cardEnter 0.4s var(--ease-out-expo) both;
|
|
368
363
|
}
|
|
369
364
|
|
|
365
|
+
.h-empty {
|
|
366
|
+
text-align: center;
|
|
367
|
+
padding: 60px 20px;
|
|
368
|
+
color: var(--ink-faint);
|
|
369
|
+
font-family: var(--sans);
|
|
370
|
+
font-size: 15px;
|
|
371
|
+
letter-spacing: 1px;
|
|
372
|
+
}
|
|
373
|
+
.h-empty-icon {
|
|
374
|
+
display: block;
|
|
375
|
+
font-size: 40px;
|
|
376
|
+
margin-bottom: 16px;
|
|
377
|
+
opacity: 0.5;
|
|
378
|
+
}
|
|
379
|
+
|
|
370
380
|
@media (max-width: 768px) {
|
|
371
|
-
.h-
|
|
381
|
+
.h-hero { min-height: 80vh; height: auto; padding: 60px 16px; }
|
|
382
|
+
.h-ornament { font-size: 32px; letter-spacing: 12px; margin-bottom: 20px; }
|
|
383
|
+
.h-subtitle { margin-bottom: 32px; }
|
|
384
|
+
.h-divider { margin-bottom: 32px; }
|
|
385
|
+
.h-stats { gap: 24px; margin-bottom: 32px; }
|
|
372
386
|
.h-stat-num { font-size: 28px; }
|
|
373
|
-
.h-
|
|
387
|
+
.h-publisher { margin-bottom: 32px; }
|
|
388
|
+
.h-cta { padding: 12px 32px; font-size: 14px; letter-spacing: 2px; }
|
|
389
|
+
.h-catalog { padding: 40px 16px; }
|
|
374
390
|
.h-grid { grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); gap: 10px; }
|
|
375
391
|
.h-search { width: 100%; }
|
|
376
392
|
.v-page { padding: 0 16px; }
|
|
@@ -380,4 +396,12 @@ function scrollToCatalog() {
|
|
|
380
396
|
}
|
|
381
397
|
.v-search { height: 160px; }
|
|
382
398
|
}
|
|
399
|
+
|
|
400
|
+
@media (max-width: 480px) {
|
|
401
|
+
.h-hero { min-height: 70vh; }
|
|
402
|
+
.h-title { letter-spacing: 6px; }
|
|
403
|
+
.h-cta { width: 80%; justify-content: center; }
|
|
404
|
+
.h-grid { grid-template-columns: 1fr 1fr; gap: 8px; }
|
|
405
|
+
.h-catalog-header h2 { font-size: 22px; }
|
|
406
|
+
}
|
|
383
407
|
</style>
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import { computed, ref } from 'vue'
|
|
2
|
+
import { computed, ref, inject } from 'vue'
|
|
3
3
|
import { useRouter } from 'vue-router'
|
|
4
4
|
import { useLibrary } from '../composables/useLibrary'
|
|
5
5
|
import { useBook } from '../composables/useBook'
|
|
6
6
|
import { useTitle } from '../composables/useTitle'
|
|
7
7
|
import { useReadingMode } from '../composables/useReadingMode'
|
|
8
8
|
import { useHorizontalScroll } from '../composables/useHorizontalScroll'
|
|
9
|
+
import { useI18n } from '../composables/useI18n'
|
|
9
10
|
import BookCard from '../components/BookCard.vue'
|
|
10
11
|
import SideNav from '../components/SideNav.vue'
|
|
11
12
|
import ReadingToolbar from '../components/ReadingToolbar.vue'
|
|
@@ -13,10 +14,14 @@ import BackToTop from '../components/BackToTop.vue'
|
|
|
13
14
|
import { useSiteConfig } from '../composables/useSiteConfig'
|
|
14
15
|
import type { BookMeta } from '../types'
|
|
15
16
|
|
|
17
|
+
const aboutPane = inject<{ toggleAbout: () => void; closeAbout: () => void }>('aboutPane')
|
|
18
|
+
|
|
16
19
|
const { scale, books, singleBook, loadLibrary } = useLibrary()
|
|
17
20
|
await loadLibrary()
|
|
18
21
|
|
|
19
|
-
|
|
22
|
+
const { siteTitle, siteSubtitle, aboutHtml, logoUrl } = useSiteConfig()
|
|
23
|
+
const displayTitle = siteTitle || 'CHAM'
|
|
24
|
+
useTitle(displayTitle)
|
|
20
25
|
|
|
21
26
|
// Single-book: redirect to book home
|
|
22
27
|
if (scale.value === 'single-book' && singleBook.value) {
|
|
@@ -37,27 +42,20 @@ if (scale.value === 'single-piece' && singleBook.value) {
|
|
|
37
42
|
|
|
38
43
|
const router = useRouter()
|
|
39
44
|
const { layout } = useReadingMode()
|
|
40
|
-
const { logoUrl } = useSiteConfig()
|
|
41
45
|
const isVertical = computed(() => layout.value === 'vertical')
|
|
42
46
|
const vPageRef = ref<HTMLElement | null>(null)
|
|
43
47
|
const vScroll = useHorizontalScroll(vPageRef)
|
|
44
|
-
|
|
45
|
-
const genreLabel: Record<string, string> = {
|
|
46
|
-
poetry: '詩歌',
|
|
47
|
-
prose: '散文',
|
|
48
|
-
mixed: '綜合',
|
|
49
|
-
drama: '戲曲',
|
|
50
|
-
}
|
|
48
|
+
const { t } = useI18n()
|
|
51
49
|
|
|
52
50
|
function bookCategory(book: BookMeta): string {
|
|
53
|
-
if (book.id.startsWith('skqs-')) return '
|
|
54
|
-
if (book.id === 'primary' || book.id === 'primary-culture' || book.id === 'secondary' || book.id === 'nss') return '
|
|
55
|
-
return '
|
|
51
|
+
if (book.id.startsWith('skqs-')) return t('genre.fourTreasuries')
|
|
52
|
+
if (book.id === 'primary' || book.id === 'primary-culture' || book.id === 'secondary' || book.id === 'nss') return t('genre.textbooks')
|
|
53
|
+
return t('genre.classicalText')
|
|
56
54
|
}
|
|
57
55
|
|
|
58
56
|
const groupedBooks = computed(() => {
|
|
59
57
|
const groups = new Map<string, BookMeta[]>()
|
|
60
|
-
const order = ['
|
|
58
|
+
const order = [t('genre.textbooks'), t('genre.classicalText'), t('genre.fourTreasuries')]
|
|
61
59
|
for (const book of books.value) {
|
|
62
60
|
const cat = bookCategory(book)
|
|
63
61
|
if (!groups.has(cat)) groups.set(cat, [])
|
|
@@ -70,23 +68,29 @@ const groupedBooks = computed(() => {
|
|
|
70
68
|
|
|
71
69
|
const totalPieces = computed(() => books.value.reduce((sum, b) => sum + b.count, 0))
|
|
72
70
|
|
|
71
|
+
const spacedTitle = computed(() => displayTitle.split('').join(' '))
|
|
72
|
+
|
|
73
73
|
function openBook(bookId: string) {
|
|
74
74
|
router.push(`/${bookId}`)
|
|
75
75
|
}
|
|
76
76
|
</script>
|
|
77
77
|
|
|
78
78
|
<template>
|
|
79
|
-
<div v-if="scale
|
|
79
|
+
<div v-if="scale !== 'library'" class="page-loading">
|
|
80
|
+
<img v-if="logoUrl" :src="logoUrl" alt="" class="page-loading-logo" />
|
|
81
|
+
<div v-else class="page-loading-seal">文</div>
|
|
82
|
+
</div>
|
|
83
|
+
<div v-else>
|
|
80
84
|
<!-- ═══════ 直排模式 ═══════ -->
|
|
81
85
|
<div v-if="isVertical" class="v-root">
|
|
82
86
|
<SideNav @home="router.push('/')" @back="router.push('/')" />
|
|
83
87
|
<div ref="vPageRef" class="v-page">
|
|
84
|
-
<div class="v-about-col">
|
|
85
|
-
<
|
|
88
|
+
<div v-if="aboutHtml" class="v-about-col">
|
|
89
|
+
<button class="v-about-link" @click="aboutPane?.toggleAbout()">{{ t('nav.about') }}</button>
|
|
86
90
|
</div>
|
|
87
91
|
<section class="v-hero">
|
|
88
|
-
<h1 class="v-title"
|
|
89
|
-
<p class="v-subtitle">
|
|
92
|
+
<h1 class="v-title">{{ spacedTitle }}</h1>
|
|
93
|
+
<p v-if="siteSubtitle" class="v-subtitle">{{ siteSubtitle }}</p>
|
|
90
94
|
<div class="v-divider"></div>
|
|
91
95
|
</section>
|
|
92
96
|
|
|
@@ -101,7 +105,7 @@ function openBook(bookId: string) {
|
|
|
101
105
|
<h2 class="v-book-title">{{ book.title }}</h2>
|
|
102
106
|
<p v-if="book.subtitle" class="v-book-sub">{{ book.subtitle }}</p>
|
|
103
107
|
<div class="v-book-stats">
|
|
104
|
-
<span class="v-book-count">{{ book.count }}
|
|
108
|
+
<span class="v-book-count">{{ t('stat.pieceCount', { count: book.count }) }}</span>
|
|
105
109
|
</div>
|
|
106
110
|
</div>
|
|
107
111
|
</section>
|
|
@@ -112,15 +116,14 @@ function openBook(bookId: string) {
|
|
|
112
116
|
<div v-else class="lib-root">
|
|
113
117
|
<header class="lib-hero">
|
|
114
118
|
<img v-if="logoUrl" :src="logoUrl" alt="" class="lib-logo" />
|
|
115
|
-
<div v-else class="lib-seal"
|
|
116
|
-
<h1
|
|
117
|
-
<p class="lib-subtitle">
|
|
119
|
+
<div v-else class="lib-seal">{{ displayTitle.slice(0, 2) }}</div>
|
|
120
|
+
<h1>{{ displayTitle }} <button v-if="aboutHtml" class="lib-about-link" @click="aboutPane?.toggleAbout()">{{ t('nav.about') }}</button></h1>
|
|
121
|
+
<p v-if="siteSubtitle" class="lib-subtitle">{{ siteSubtitle }}</p>
|
|
118
122
|
<div class="lib-stats-bar">
|
|
119
|
-
<span class="lib-stat">{{ books.length }}
|
|
123
|
+
<span class="lib-stat">{{ books.length }} {{ t('stat.books') }}</span>
|
|
120
124
|
<span class="lib-stat-sep">·</span>
|
|
121
|
-
<span class="lib-stat">{{ totalPieces }}
|
|
125
|
+
<span class="lib-stat">{{ totalPieces }} {{ t('stat.pieces') }}</span>
|
|
122
126
|
</div>
|
|
123
|
-
<router-link to="/about" class="lib-about-link">關於</router-link>
|
|
124
127
|
</header>
|
|
125
128
|
<div v-for="group in groupedBooks" :key="group.category" class="lib-group">
|
|
126
129
|
<h2 class="lib-group-title">{{ group.category }}</h2>
|
|
@@ -136,11 +139,11 @@ function openBook(bookId: string) {
|
|
|
136
139
|
<div class="lib-card-body">
|
|
137
140
|
<div class="lib-card-top">
|
|
138
141
|
<h3 class="lib-card-title">{{ book.title }}</h3>
|
|
139
|
-
<span class="lib-card-genre">{{
|
|
142
|
+
<span class="lib-card-genre">{{ bookCategory(book) }}</span>
|
|
140
143
|
</div>
|
|
141
144
|
<p v-if="book.subtitle" class="lib-card-sub">{{ book.subtitle }}</p>
|
|
142
145
|
<div class="lib-card-stats">
|
|
143
|
-
<span class="lib-card-count">{{ book.count }}
|
|
146
|
+
<span class="lib-card-count">{{ t('stat.pieceCount', { count: book.count }) }}</span>
|
|
144
147
|
</div>
|
|
145
148
|
</div>
|
|
146
149
|
</div>
|
|
@@ -156,20 +159,9 @@ function openBook(bookId: string) {
|
|
|
156
159
|
/* ═══════ 直排模式 ═══════ */
|
|
157
160
|
|
|
158
161
|
.v-page {
|
|
159
|
-
height: 100vh;
|
|
160
|
-
display: flex;
|
|
161
|
-
flex-direction: row-reverse;
|
|
162
|
-
overflow-x: auto;
|
|
163
|
-
overflow-y: hidden;
|
|
164
|
-
margin-right: var(--nav-width, 56px);
|
|
165
162
|
padding: 0 32px;
|
|
166
163
|
background: linear-gradient(90deg, var(--paper) 0%, var(--paper-warm) 100%);
|
|
167
|
-
scrollbar-width: thin;
|
|
168
|
-
scrollbar-color: var(--gold) transparent;
|
|
169
|
-
scroll-snap-type: x proximity;
|
|
170
164
|
}
|
|
171
|
-
.v-page::-webkit-scrollbar { height: 4px; }
|
|
172
|
-
.v-page::-webkit-scrollbar-thumb { background: var(--gold); border-radius: 2px; }
|
|
173
165
|
|
|
174
166
|
.v-hero {
|
|
175
167
|
writing-mode: vertical-rl;
|
|
@@ -201,6 +193,8 @@ function openBook(bookId: string) {
|
|
|
201
193
|
padding: 12px 8px;
|
|
202
194
|
border: 1px solid var(--border-light);
|
|
203
195
|
border-radius: 2px;
|
|
196
|
+
background: none;
|
|
197
|
+
cursor: pointer;
|
|
204
198
|
transition: all 0.2s;
|
|
205
199
|
}
|
|
206
200
|
.v-about-link:hover {
|
|
@@ -353,16 +347,17 @@ function openBook(bookId: string) {
|
|
|
353
347
|
|
|
354
348
|
.lib-about-link {
|
|
355
349
|
display: inline-block;
|
|
356
|
-
margin-top: 16px;
|
|
357
350
|
font-family: var(--sans);
|
|
358
351
|
font-size: 13px;
|
|
359
352
|
color: var(--ink-faint);
|
|
360
353
|
letter-spacing: 2px;
|
|
361
|
-
text-decoration: none;
|
|
362
354
|
padding: 4px 12px;
|
|
363
355
|
border: 1px solid var(--border-light);
|
|
364
356
|
border-radius: 4px;
|
|
357
|
+
background: none;
|
|
358
|
+
cursor: pointer;
|
|
365
359
|
transition: all 0.2s;
|
|
360
|
+
vertical-align: middle;
|
|
366
361
|
}
|
|
367
362
|
.lib-about-link:hover {
|
|
368
363
|
color: var(--ink);
|
|
@@ -455,12 +450,15 @@ function openBook(bookId: string) {
|
|
|
455
450
|
.v-page { padding: 0 16px; }
|
|
456
451
|
.v-title { font-size: 36px; letter-spacing: 10px; }
|
|
457
452
|
.lib-root { padding: 40px 16px 80px; }
|
|
453
|
+
.lib-hero { margin-bottom: 32px; }
|
|
458
454
|
.lib-hero h1 { font-size: 28px; letter-spacing: 4px; }
|
|
455
|
+
.lib-logo { height: 48px; margin-bottom: 16px; }
|
|
459
456
|
.lib-grid {
|
|
460
457
|
grid-template-columns: 1fr 1fr;
|
|
461
458
|
gap: 8px;
|
|
462
459
|
}
|
|
463
460
|
.lib-card { padding: 14px; }
|
|
461
|
+
.lib-card:active { transform: scale(0.98); }
|
|
464
462
|
.lib-card-title { font-size: 18px; letter-spacing: 2px; }
|
|
465
463
|
.lib-card-genre { display: none; }
|
|
466
464
|
.lib-card-sub { font-size: 12px; margin-bottom: 8px; }
|