eithney commited on
Commit
bc2665f
·
1 Parent(s): dc3bb4a

Add static files with LFS support for fonts

Browse files
.gitattributes CHANGED
@@ -37,3 +37,5 @@ FZKT_GBK.woff filter=lfs diff=lfs merge=lfs -text
37
  *.mp3 filter=lfs diff=lfs merge=lfs -text
38
  *.jpg filter=lfs diff=lfs merge=lfs -text
39
  *.woff filter=lfs diff=lfs merge=lfs -text
 
 
 
37
  *.mp3 filter=lfs diff=lfs merge=lfs -text
38
  *.jpg filter=lfs diff=lfs merge=lfs -text
39
  *.woff filter=lfs diff=lfs merge=lfs -text
40
+ *.ttf filter=lfs diff=lfs merge=lfs -text
41
+ *.woff2 filter=lfs diff=lfs merge=lfs -text
download_resources.py CHANGED
@@ -96,16 +96,16 @@ def extract_urls_from_json(json_file):
96
  # 提取图片URL
97
  if 'originImgUrl' in page:
98
  urls['images'].add(page['originImgUrl'])
99
- if 'encryptImgUrl' in page:
100
- urls['images'].add(page['encryptImgUrl'])
101
 
102
  # 提取音频URL
103
  if 'pieces' in page:
104
  for piece in page['pieces']:
105
  if 'originSoundUrl' in piece:
106
  urls['audios'].add(piece['originSoundUrl'])
107
- if 'encryptSoundUrl' in piece:
108
- urls['audios'].add(piece['encryptSoundUrl'])
109
 
110
  return urls
111
 
 
96
  # 提取图片URL
97
  if 'originImgUrl' in page:
98
  urls['images'].add(page['originImgUrl'])
99
+ # if 'encryptImgUrl' in page:
100
+ # urls['images'].add(page['encryptImgUrl'])
101
 
102
  # 提取音频URL
103
  if 'pieces' in page:
104
  for piece in page['pieces']:
105
  if 'originSoundUrl' in piece:
106
  urls['audios'].add(piece['originSoundUrl'])
107
+ # if 'encryptSoundUrl' in piece:
108
+ # urls['audios'].add(piece['encryptSoundUrl'])
109
 
110
  return urls
111
 
index.html CHANGED
@@ -4,11 +4,9 @@
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
  <title>交互式英语学习应用</title>
7
- <link rel="stylesheet" href="style.css">
8
- <link rel="preconnect" href="https://fonts.googleapis.com">
9
- <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
10
- <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
11
- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
12
  </head>
13
  <body>
14
  <div class="app-container">
@@ -184,6 +182,6 @@
184
  <div class="toast" id="toast"></div>
185
  </div>
186
 
187
- <script src="script.js"></script>
188
  </body>
189
  </html>
 
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
  <title>交互式英语学习应用</title>
7
+ <link rel="stylesheet" href="static/css/style.css">
8
+ <link rel="stylesheet" href="static/css/inter.css">
9
+ <link rel="stylesheet" href="static/css/all.min.css">
 
 
10
  </head>
11
  <body>
12
  <div class="app-container">
 
182
  <div class="toast" id="toast"></div>
183
  </div>
184
 
185
+ <script src="static/js/script.js"></script>
186
  </body>
187
  </html>
static/css/all.min.css ADDED
The diff for this file is too large to render. See raw diff
 
static/css/inter.css ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @font-face {
2
+ font-family: 'Inter';
3
+ font-style: normal;
4
+ font-weight: 300;
5
+ font-display: swap;
6
+ src: url(../fonts/inter/Inter-300.ttf) format('truetype');
7
+ }
8
+ @font-face {
9
+ font-family: 'Inter';
10
+ font-style: normal;
11
+ font-weight: 400;
12
+ font-display: swap;
13
+ src: url(../fonts/inter/Inter-400.ttf) format('truetype');
14
+ }
15
+ @font-face {
16
+ font-family: 'Inter';
17
+ font-style: normal;
18
+ font-weight: 500;
19
+ font-display: swap;
20
+ src: url(../fonts/inter/Inter-500.ttf) format('truetype');
21
+ }
22
+ @font-face {
23
+ font-family: 'Inter';
24
+ font-style: normal;
25
+ font-weight: 600;
26
+ font-display: swap;
27
+ src: url(../fonts/inter/Inter-600.ttf) format('truetype');
28
+ }
29
+ @font-face {
30
+ font-family: 'Inter';
31
+ font-style: normal;
32
+ font-weight: 700;
33
+ font-display: swap;
34
+ src: url(../fonts/inter/Inter-700.ttf) format('truetype');
35
+ }
static/css/style.css ADDED
@@ -0,0 +1,910 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* 全局样式重置和基础设置 */
2
+ * {
3
+ margin: 0;
4
+ padding: 0;
5
+ box-sizing: border-box;
6
+ }
7
+
8
+ :root {
9
+ --primary-color: #2563eb;
10
+ --primary-hover: #1d4ed8;
11
+ --secondary-color: #64748b;
12
+ --success-color: #059669;
13
+ --warning-color: #d97706;
14
+ --error-color: #dc2626;
15
+
16
+ --bg-primary: #ffffff;
17
+ --bg-secondary: #f8fafc;
18
+ --bg-tertiary: #e2e8f0;
19
+
20
+ --text-primary: #1e293b;
21
+ --text-secondary: #475569;
22
+ --text-muted: #64748b;
23
+
24
+ --border-color: #e2e8f0;
25
+ --shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.05);
26
+ --shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);
27
+ --shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);
28
+
29
+ --border-radius: 8px;
30
+ --border-radius-lg: 12px;
31
+ --border-radius-xl: 16px;
32
+
33
+ --font-size-sm: 0.875rem;
34
+ --font-size-base: 1rem;
35
+ --font-size-lg: 1.125rem;
36
+ --font-size-xl: 1.25rem;
37
+ --font-size-2xl: 1.5rem;
38
+
39
+ --z-index-dropdown: 1000;
40
+ --z-index-overlay: 2000;
41
+ --z-index-modal: 3000;
42
+ --z-index-toast: 4000;
43
+ }
44
+
45
+ body {
46
+ font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Microsoft YaHei', '微软雅黑', Roboto, sans-serif;
47
+ background-color: var(--bg-secondary);
48
+ color: var(--text-primary);
49
+ line-height: 1.6;
50
+ }
51
+
52
+ .app-container {
53
+ min-height: 100vh;
54
+ display: flex;
55
+ flex-direction: column;
56
+ }
57
+
58
+ /* 顶部导航栏 */
59
+ .header {
60
+ background: var(--bg-primary);
61
+ border-bottom: 1px solid var(--border-color);
62
+ box-shadow: var(--shadow-sm);
63
+ position: sticky;
64
+ top: 0;
65
+ z-index: var(--z-index-dropdown);
66
+ }
67
+
68
+ .header-content {
69
+ max-width: 1200px;
70
+ margin: 0 auto;
71
+ padding: 1rem 1.5rem;
72
+ display: flex;
73
+ justify-content: space-between;
74
+ align-items: center;
75
+ }
76
+
77
+ .app-title {
78
+ font-size: var(--font-size-xl);
79
+ font-weight: 600;
80
+ color: var(--primary-color);
81
+ display: flex;
82
+ align-items: center;
83
+ gap: 0.75rem;
84
+ }
85
+
86
+ .app-title i {
87
+ font-size: var(--font-size-2xl);
88
+ }
89
+
90
+ .controls {
91
+ display: flex;
92
+ gap: 0.75rem;
93
+ }
94
+
95
+ .control-btn {
96
+ display: flex;
97
+ align-items: center;
98
+ gap: 0.5rem;
99
+ padding: 0.5rem 1rem;
100
+ background: var(--bg-secondary);
101
+ border: 1px solid var(--border-color);
102
+ border-radius: var(--border-radius);
103
+ color: var(--text-secondary);
104
+ font-size: var(--font-size-sm);
105
+ cursor: pointer;
106
+ transition: all 0.2s ease;
107
+ }
108
+
109
+ .control-btn:hover {
110
+ background: var(--primary-color);
111
+ color: white;
112
+ border-color: var(--primary-color);
113
+ }
114
+
115
+ .control-btn.active {
116
+ background: var(--primary-color);
117
+ color: white;
118
+ border-color: var(--primary-color);
119
+ }
120
+
121
+ /* 主要内容区域 */
122
+ .main-content {
123
+ flex: 1;
124
+ max-width: 1200px;
125
+ margin: 0 auto;
126
+ width: 100%;
127
+ padding: 1.5rem;
128
+ }
129
+
130
+ /* 页面导航 */
131
+ .page-navigation {
132
+ display: flex;
133
+ align-items: center;
134
+ justify-content: center;
135
+ gap: 1rem;
136
+ margin-bottom: 1.5rem;
137
+ padding: 1rem;
138
+ background: var(--bg-primary);
139
+ border-radius: var(--border-radius-lg);
140
+ box-shadow: var(--shadow-sm);
141
+ }
142
+
143
+ .nav-btn {
144
+ width: 2.5rem;
145
+ height: 2.5rem;
146
+ border: 1px solid var(--border-color);
147
+ background: var(--bg-primary);
148
+ border-radius: 50%;
149
+ display: flex;
150
+ align-items: center;
151
+ justify-content: center;
152
+ cursor: pointer;
153
+ transition: all 0.2s ease;
154
+ color: var(--text-secondary);
155
+ }
156
+
157
+ .nav-btn:hover {
158
+ background: var(--primary-color);
159
+ color: white;
160
+ border-color: var(--primary-color);
161
+ }
162
+
163
+ .nav-btn:disabled {
164
+ opacity: 0.5;
165
+ cursor: not-allowed;
166
+ }
167
+
168
+ .nav-btn:disabled:hover {
169
+ background: var(--bg-primary);
170
+ color: var(--text-secondary);
171
+ border-color: var(--border-color);
172
+ }
173
+
174
+ .page-info {
175
+ font-size: var(--font-size-lg);
176
+ font-weight: 500;
177
+ color: var(--text-primary);
178
+ min-width: 4rem;
179
+ text-align: center;
180
+ }
181
+
182
+ .progress-bar {
183
+ flex: 1;
184
+ max-width: 200px;
185
+ height: 4px;
186
+ background: var(--bg-tertiary);
187
+ border-radius: 2px;
188
+ overflow: hidden;
189
+ margin-left: 1rem;
190
+ }
191
+
192
+ .progress-fill {
193
+ height: 100%;
194
+ background: var(--primary-color);
195
+ border-radius: 2px;
196
+ transition: width 0.3s ease;
197
+ }
198
+
199
+ /* 页面容器 */
200
+ .page-container {
201
+ background: var(--bg-primary);
202
+ border-radius: var(--border-radius-xl);
203
+ box-shadow: var(--shadow-lg);
204
+ overflow: hidden;
205
+ min-height: 500px;
206
+ display: flex;
207
+ align-items: center;
208
+ justify-content: center;
209
+ }
210
+
211
+ .loading {
212
+ text-align: center;
213
+ color: var(--text-muted);
214
+ }
215
+
216
+ .loading i {
217
+ font-size: 2rem;
218
+ margin-bottom: 1rem;
219
+ color: var(--primary-color);
220
+ }
221
+
222
+ .page-content {
223
+ width: 100%;
224
+ height: 100%;
225
+ }
226
+
227
+ .page-image-container {
228
+ position: relative;
229
+ width: 100%;
230
+ height: 100%;
231
+ min-height: 500px;
232
+ display: flex;
233
+ justify-content: center;
234
+ align-items: center;
235
+ overflow: hidden;
236
+ }
237
+
238
+ .page-image {
239
+ max-width: 100%;
240
+ max-height: 80vh;
241
+ height: auto;
242
+ border-radius: var(--border-radius);
243
+ box-shadow: var(--shadow-md);
244
+ display: block;
245
+ }
246
+
247
+ /* 文本覆盖层 */
248
+ .text-overlays {
249
+ position: absolute;
250
+ top: 0;
251
+ left: 0;
252
+ width: 100%;
253
+ height: 100%;
254
+ pointer-events: none;
255
+ }
256
+
257
+ .text-piece {
258
+ position: absolute;
259
+ background: transparent;
260
+ border: 1px solid transparent;
261
+ border-radius: 4px;
262
+ cursor: pointer;
263
+ pointer-events: all;
264
+ transition: all 0.3s ease;
265
+ padding: 0;
266
+ }
267
+
268
+ .text-piece:hover {
269
+ background: rgba(37, 99, 235, 0.08);
270
+ border-color: rgba(37, 99, 235, 0.8);
271
+ border-width: 2px;
272
+ box-shadow: 0 0 0 1px rgba(37, 99, 235, 0.2);
273
+ }
274
+
275
+ .text-piece.active {
276
+ background: rgba(37, 99, 235, 0.12);
277
+ border-color: var(--primary-color);
278
+ border-width: 2px;
279
+ box-shadow: 0 0 0 2px rgba(37, 99, 235, 0.4);
280
+ animation: activeFlash 1s ease-in-out;
281
+ }
282
+
283
+ .text-piece.playing {
284
+ border-color: var(--success-color);
285
+ box-shadow: 0 0 0 2px rgba(5, 150, 105, 0.4);
286
+ animation: playingPulse 2s ease-in-out infinite;
287
+ }
288
+
289
+ /* 交互模式下显示所有区域的轮廓 */
290
+ .show-interactive-areas .text-piece {
291
+ background: rgba(37, 99, 235, 0.05);
292
+ border-color: rgba(37, 99, 235, 0.3);
293
+ border-width: 1px;
294
+ }
295
+
296
+ .show-interactive-areas .text-piece:hover {
297
+ background: rgba(37, 99, 235, 0.1);
298
+ border-color: var(--primary-color);
299
+ border-width: 2px;
300
+ }
301
+
302
+ .piece-text {
303
+ font-size: var(--font-size-sm);
304
+ font-weight: 500;
305
+ color: var(--text-primary);
306
+ text-shadow: 0 1px 2px rgba(255, 255, 255, 1);
307
+ line-height: 1.3;
308
+ white-space: nowrap;
309
+ overflow: hidden;
310
+ text-overflow: ellipsis;
311
+ font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Microsoft YaHei', '微软雅黑', sans-serif;
312
+ opacity: 0;
313
+ pointer-events: none;
314
+ display: none;
315
+ }
316
+
317
+ .piece-translation {
318
+ font-size: 0.75rem;
319
+ color: var(--text-secondary);
320
+ margin-top: 0.125rem;
321
+ text-shadow: 0 1px 2px rgba(255, 255, 255, 1);
322
+ font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Microsoft YaHei', '微软雅黑', sans-serif;
323
+ opacity: 0;
324
+ pointer-events: none;
325
+ display: none;
326
+ }
327
+
328
+ /* 交互模式下显示文本(仅用于调试) */
329
+ .show-interactive-areas .piece-text {
330
+ opacity: 0.6;
331
+ display: block;
332
+ background: rgba(255, 255, 255, 0.9);
333
+ padding: 0.125rem 0.25rem;
334
+ border-radius: 2px;
335
+ font-size: 0.7rem;
336
+ }
337
+
338
+ .show-interactive-areas .piece-translation {
339
+ opacity: 0.5;
340
+ display: block;
341
+ background: rgba(255, 255, 255, 0.8);
342
+ padding: 0.125rem 0.25rem;
343
+ border-radius: 2px;
344
+ font-size: 0.6rem;
345
+ }
346
+
347
+ /* 底部控制面板 */
348
+ .bottom-panel {
349
+ margin-top: 1.5rem;
350
+ }
351
+
352
+ .audio-player {
353
+ background: var(--bg-primary);
354
+ border-radius: var(--border-radius-lg);
355
+ padding: 1rem;
356
+ box-shadow: var(--shadow-md);
357
+ }
358
+
359
+ .audio-info {
360
+ display: flex;
361
+ flex-direction: column;
362
+ gap: 1rem;
363
+ }
364
+
365
+ .audio-text {
366
+ font-size: var(--font-size-lg);
367
+ font-weight: 500;
368
+ color: var(--text-primary);
369
+ text-align: center;
370
+ font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Microsoft YaHei', '微软雅黑', sans-serif;
371
+ }
372
+
373
+ .audio-controls {
374
+ display: flex;
375
+ align-items: center;
376
+ gap: 1rem;
377
+ }
378
+
379
+ .audio-btn {
380
+ width: 2.5rem;
381
+ height: 2.5rem;
382
+ border: none;
383
+ background: var(--primary-color);
384
+ color: white;
385
+ border-radius: 50%;
386
+ cursor: pointer;
387
+ transition: all 0.2s ease;
388
+ display: flex;
389
+ align-items: center;
390
+ justify-content: center;
391
+ }
392
+
393
+ .audio-btn:hover {
394
+ background: var(--primary-hover);
395
+ transform: scale(1.05);
396
+ }
397
+
398
+ .audio-progress {
399
+ flex: 1;
400
+ display: flex;
401
+ flex-direction: column;
402
+ gap: 0.5rem;
403
+ }
404
+
405
+ .audio-timeline {
406
+ height: 4px;
407
+ background: var(--bg-tertiary);
408
+ border-radius: 2px;
409
+ cursor: pointer;
410
+ position: relative;
411
+ }
412
+
413
+ .audio-progress-bar {
414
+ height: 100%;
415
+ background: var(--primary-color);
416
+ border-radius: 2px;
417
+ transition: width 0.1s ease;
418
+ }
419
+
420
+ .audio-time {
421
+ display: flex;
422
+ justify-content: space-between;
423
+ font-size: var(--font-size-sm);
424
+ color: var(--text-muted);
425
+ }
426
+
427
+ /* 搜索面板 */
428
+ .search-panel {
429
+ position: fixed;
430
+ top: 0;
431
+ left: 0;
432
+ width: 100%;
433
+ height: 100%;
434
+ background: rgba(0, 0, 0, 0.5);
435
+ z-index: var(--z-index-modal);
436
+ display: flex;
437
+ align-items: center;
438
+ justify-content: center;
439
+ padding: 1rem;
440
+ }
441
+
442
+ .search-content {
443
+ background: var(--bg-primary);
444
+ border-radius: var(--border-radius-xl);
445
+ box-shadow: var(--shadow-lg);
446
+ width: 100%;
447
+ max-width: 600px;
448
+ max-height: 80vh;
449
+ overflow: hidden;
450
+ }
451
+
452
+ .search-header {
453
+ padding: 1.5rem;
454
+ border-bottom: 1px solid var(--border-color);
455
+ display: flex;
456
+ justify-content: space-between;
457
+ align-items: center;
458
+ }
459
+
460
+ .search-body {
461
+ padding: 1.5rem;
462
+ max-height: 60vh;
463
+ overflow-y: auto;
464
+ }
465
+
466
+ .search-input-container {
467
+ display: flex;
468
+ gap: 0.5rem;
469
+ margin-bottom: 1.5rem;
470
+ }
471
+
472
+ .search-input-container input {
473
+ flex: 1;
474
+ padding: 0.75rem 1rem;
475
+ border: 1px solid var(--border-color);
476
+ border-radius: var(--border-radius);
477
+ font-size: var(--font-size-base);
478
+ outline: none;
479
+ transition: border-color 0.2s ease;
480
+ }
481
+
482
+ .search-input-container input:focus {
483
+ border-color: var(--primary-color);
484
+ }
485
+
486
+ .search-btn {
487
+ padding: 0.75rem 1rem;
488
+ background: var(--primary-color);
489
+ color: white;
490
+ border: none;
491
+ border-radius: var(--border-radius);
492
+ cursor: pointer;
493
+ transition: background-color 0.2s ease;
494
+ }
495
+
496
+ .search-btn:hover {
497
+ background: var(--primary-hover);
498
+ }
499
+
500
+ .search-results {
501
+ min-height: 200px;
502
+ }
503
+
504
+ .search-empty {
505
+ text-align: center;
506
+ color: var(--text-muted);
507
+ padding: 2rem;
508
+ font-style: italic;
509
+ }
510
+
511
+ .search-result-item {
512
+ padding: 1rem;
513
+ border: 1px solid var(--border-color);
514
+ border-radius: var(--border-radius);
515
+ margin-bottom: 0.75rem;
516
+ cursor: pointer;
517
+ transition: all 0.2s ease;
518
+ }
519
+
520
+ .search-result-item:hover {
521
+ border-color: var(--primary-color);
522
+ background: var(--bg-secondary);
523
+ }
524
+
525
+ .search-result-page {
526
+ font-size: var(--font-size-sm);
527
+ color: var(--text-muted);
528
+ margin-bottom: 0.5rem;
529
+ }
530
+
531
+ .search-result-text {
532
+ font-weight: 500;
533
+ margin-bottom: 0.25rem;
534
+ font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Microsoft YaHei', '微软雅黑', sans-serif;
535
+ }
536
+
537
+ .search-result-translation {
538
+ color: var(--text-secondary);
539
+ font-size: var(--font-size-sm);
540
+ font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Microsoft YaHei', '微软雅黑', sans-serif;
541
+ }
542
+
543
+ .search-highlight {
544
+ background: yellow;
545
+ padding: 0.125rem 0.25rem;
546
+ border-radius: 2px;
547
+ }
548
+
549
+ /* 书签面板 */
550
+ .bookmark-panel {
551
+ position: fixed;
552
+ top: 0;
553
+ left: 0;
554
+ width: 100%;
555
+ height: 100%;
556
+ background: rgba(0, 0, 0, 0.5);
557
+ z-index: var(--z-index-modal);
558
+ display: flex;
559
+ align-items: center;
560
+ justify-content: center;
561
+ padding: 1rem;
562
+ }
563
+
564
+ .bookmark-content {
565
+ background: var(--bg-primary);
566
+ border-radius: var(--border-radius-xl);
567
+ box-shadow: var(--shadow-lg);
568
+ width: 100%;
569
+ max-width: 500px;
570
+ max-height: 80vh;
571
+ overflow: hidden;
572
+ }
573
+
574
+ .bookmark-header {
575
+ padding: 1.5rem;
576
+ border-bottom: 1px solid var(--border-color);
577
+ display: flex;
578
+ justify-content: space-between;
579
+ align-items: center;
580
+ }
581
+
582
+ .bookmark-body {
583
+ padding: 1.5rem;
584
+ max-height: 60vh;
585
+ overflow-y: auto;
586
+ }
587
+
588
+ .bookmark-empty {
589
+ text-align: center;
590
+ color: var(--text-muted);
591
+ padding: 2rem;
592
+ font-style: italic;
593
+ }
594
+
595
+ .bookmark-item {
596
+ display: flex;
597
+ justify-content: space-between;
598
+ align-items: center;
599
+ padding: 1rem;
600
+ border: 1px solid var(--border-color);
601
+ border-radius: var(--border-radius);
602
+ margin-bottom: 0.75rem;
603
+ cursor: pointer;
604
+ transition: all 0.2s ease;
605
+ }
606
+
607
+ .bookmark-item:hover {
608
+ border-color: var(--primary-color);
609
+ background: var(--bg-secondary);
610
+ }
611
+
612
+ .bookmark-info {
613
+ flex: 1;
614
+ }
615
+
616
+ .bookmark-page {
617
+ font-weight: 500;
618
+ color: var(--text-primary);
619
+ margin-bottom: 0.25rem;
620
+ }
621
+
622
+ .bookmark-time {
623
+ font-size: var(--font-size-sm);
624
+ color: var(--text-muted);
625
+ }
626
+
627
+ .bookmark-actions {
628
+ display: flex;
629
+ gap: 0.5rem;
630
+ }
631
+
632
+ .bookmark-delete {
633
+ width: 1.5rem;
634
+ height: 1.5rem;
635
+ border: none;
636
+ background: var(--error-color);
637
+ color: white;
638
+ border-radius: 50%;
639
+ cursor: pointer;
640
+ transition: all 0.2s ease;
641
+ display: flex;
642
+ align-items: center;
643
+ justify-content: center;
644
+ font-size: 0.75rem;
645
+ }
646
+
647
+ .bookmark-delete:hover {
648
+ transform: scale(1.1);
649
+ }
650
+
651
+ /* 设置面板 */
652
+ .settings-panel {
653
+ position: fixed;
654
+ top: 0;
655
+ left: 0;
656
+ width: 100%;
657
+ height: 100%;
658
+ background: rgba(0, 0, 0, 0.5);
659
+ z-index: var(--z-index-modal);
660
+ display: flex;
661
+ align-items: center;
662
+ justify-content: center;
663
+ padding: 1rem;
664
+ }
665
+
666
+ .settings-content {
667
+ background: var(--bg-primary);
668
+ border-radius: var(--border-radius-xl);
669
+ box-shadow: var(--shadow-lg);
670
+ width: 100%;
671
+ max-width: 400px;
672
+ max-height: 80vh;
673
+ overflow-y: auto;
674
+ }
675
+
676
+ .settings-header {
677
+ padding: 1.5rem;
678
+ border-bottom: 1px solid var(--border-color);
679
+ display: flex;
680
+ justify-content: space-between;
681
+ align-items: center;
682
+ }
683
+
684
+ .settings-header h3 {
685
+ font-size: var(--font-size-xl);
686
+ font-weight: 600;
687
+ color: var(--text-primary);
688
+ }
689
+
690
+ .close-btn {
691
+ width: 2rem;
692
+ height: 2rem;
693
+ border: none;
694
+ background: none;
695
+ color: var(--text-muted);
696
+ cursor: pointer;
697
+ border-radius: 4px;
698
+ transition: all 0.2s ease;
699
+ }
700
+
701
+ .close-btn:hover {
702
+ background: var(--bg-secondary);
703
+ color: var(--text-primary);
704
+ }
705
+
706
+ .settings-body {
707
+ padding: 1.5rem;
708
+ }
709
+
710
+ .setting-item {
711
+ display: flex;
712
+ justify-content: space-between;
713
+ align-items: center;
714
+ padding: 1rem 0;
715
+ border-bottom: 1px solid var(--border-color);
716
+ }
717
+
718
+ .setting-item:last-child {
719
+ border-bottom: none;
720
+ }
721
+
722
+ .setting-item label {
723
+ font-weight: 500;
724
+ color: var(--text-primary);
725
+ }
726
+
727
+ .setting-item input[type="checkbox"] {
728
+ width: 1rem;
729
+ height: 1rem;
730
+ accent-color: var(--primary-color);
731
+ }
732
+
733
+ .setting-item select {
734
+ padding: 0.25rem 0.5rem;
735
+ border: 1px solid var(--border-color);
736
+ border-radius: var(--border-radius);
737
+ background: var(--bg-primary);
738
+ color: var(--text-primary);
739
+ }
740
+
741
+ /* 消息提示 */
742
+ .toast {
743
+ position: fixed;
744
+ bottom: 2rem;
745
+ left: 50%;
746
+ transform: translateX(-50%);
747
+ background: var(--text-primary);
748
+ color: white;
749
+ padding: 0.75rem 1.5rem;
750
+ border-radius: var(--border-radius);
751
+ box-shadow: var(--shadow-lg);
752
+ z-index: var(--z-index-toast);
753
+ opacity: 0;
754
+ visibility: hidden;
755
+ transition: all 0.3s ease;
756
+ }
757
+
758
+ .toast.show {
759
+ opacity: 1;
760
+ visibility: visible;
761
+ }
762
+
763
+ .toast.success {
764
+ background: var(--success-color);
765
+ }
766
+
767
+ .toast.warning {
768
+ background: var(--warning-color);
769
+ }
770
+
771
+ .toast.error {
772
+ background: var(--error-color);
773
+ }
774
+
775
+ /* 响应式设计 */
776
+ @media (max-width: 768px) {
777
+ .header-content {
778
+ padding: 1rem;
779
+ flex-direction: column;
780
+ gap: 1rem;
781
+ }
782
+
783
+ .controls {
784
+ width: 100%;
785
+ justify-content: center;
786
+ }
787
+
788
+ .main-content {
789
+ padding: 1rem;
790
+ }
791
+
792
+ .page-navigation {
793
+ flex-wrap: wrap;
794
+ gap: 0.75rem;
795
+ }
796
+
797
+ .progress-bar {
798
+ width: 100%;
799
+ margin-left: 0;
800
+ margin-top: 0.5rem;
801
+ }
802
+
803
+ .audio-controls {
804
+ flex-direction: column;
805
+ gap: 0.75rem;
806
+ }
807
+
808
+ .control-btn span {
809
+ display: none;
810
+ }
811
+
812
+ .text-piece {
813
+ min-width: 2rem;
814
+ min-height: 1.5rem;
815
+ }
816
+
817
+ .piece-text {
818
+ font-size: 0.75rem;
819
+ }
820
+
821
+ .piece-translation {
822
+ font-size: 0.625rem;
823
+ }
824
+ }
825
+
826
+ @media (max-width: 480px) {
827
+ .app-title {
828
+ font-size: var(--font-size-lg);
829
+ }
830
+
831
+ .page-info {
832
+ font-size: var(--font-size-base);
833
+ }
834
+
835
+ .settings-content {
836
+ margin: 0.5rem;
837
+ }
838
+ }
839
+
840
+ /* 动画效果 */
841
+ @keyframes fadeIn {
842
+ from {
843
+ opacity: 0;
844
+ transform: translateY(1rem);
845
+ }
846
+ to {
847
+ opacity: 1;
848
+ transform: translateY(0);
849
+ }
850
+ }
851
+
852
+ @keyframes pulse {
853
+ 0%, 100% {
854
+ transform: scale(1);
855
+ }
856
+ 50% {
857
+ transform: scale(1.05);
858
+ }
859
+ }
860
+
861
+ @keyframes activeFlash {
862
+ 0% {
863
+ box-shadow: 0 0 0 2px rgba(37, 99, 235, 0.4);
864
+ }
865
+ 50% {
866
+ box-shadow: 0 0 0 4px rgba(37, 99, 235, 0.6);
867
+ }
868
+ 100% {
869
+ box-shadow: 0 0 0 2px rgba(37, 99, 235, 0.4);
870
+ }
871
+ }
872
+
873
+ @keyframes playingPulse {
874
+ 0% {
875
+ box-shadow: 0 0 0 2px rgba(5, 150, 105, 0.4);
876
+ }
877
+ 50% {
878
+ box-shadow: 0 0 0 4px rgba(5, 150, 105, 0.6);
879
+ }
880
+ 100% {
881
+ box-shadow: 0 0 0 2px rgba(5, 150, 105, 0.4);
882
+ }
883
+ }
884
+
885
+ .page-content {
886
+ animation: fadeIn 0.3s ease;
887
+ }
888
+
889
+ .text-piece.playing {
890
+ animation: pulse 1s ease-in-out infinite;
891
+ }
892
+
893
+ /* 滚动条样式 */
894
+ ::-webkit-scrollbar {
895
+ width: 6px;
896
+ height: 6px;
897
+ }
898
+
899
+ ::-webkit-scrollbar-track {
900
+ background: var(--bg-secondary);
901
+ }
902
+
903
+ ::-webkit-scrollbar-thumb {
904
+ background: var(--border-color);
905
+ border-radius: 3px;
906
+ }
907
+
908
+ ::-webkit-scrollbar-thumb:hover {
909
+ background: var(--text-muted);
910
+ }
static/fonts/fa-brands-400.ttf ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:20c4a58bc9d1d69e935d06f1528923646a715be5e218665655cade8f5f1b8c00
3
+ size 187208
static/fonts/fa-brands-400.woff2 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:748332090c4b8e20f95d0ff59f0be20fa9c889359d3b36d4b886d73376054207
3
+ size 108020
static/fonts/fa-regular-400.ttf ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:528d022dce6725f8a0811fd91d8e6513445c81ef33353a5c3234eab932551abf
3
+ size 63952
static/fonts/fa-regular-400.woff2 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:8e7e5ea1b15f62ab14dbd41768e8fbcd21cc859a4ea5da812457ee714299fb35
3
+ size 24948
static/fonts/fa-solid-900.ttf ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:67a65763c7f80903d81603bbeb9049fc2bf28508479b83ed011fe24c71fa950a
3
+ size 394628
static/fonts/fa-solid-900.woff2 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:7152a6933ee3d690ec2af3d09da9d701723d16aa3410a6d80f28ff8866f3b880
3
+ size 150124
static/fonts/fa-v4compatibility.ttf ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:0515a423f828ce4e6accf92a2ea0b03d19d31cc86d9af0373291e1fd4db5f348
3
+ size 10172
static/fonts/fa-v4compatibility.woff2 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:694a17c3d9d6c05f8aac63c544615552a4b220e9a4de863d87341a6bcfc1bc8d
3
+ size 4564
static/fonts/inter/Inter-300.ttf ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:d0f4bc7faca468376e3db9b5e57afcdc2192134c9ac82a9511f32767b56853a4
3
+ size 325748
static/fonts/inter/Inter-400.ttf ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:1b08e7fc267a5c7e1d614100f604b83e7e8a0be241f0f288faa2b3ac93a683ba
3
+ size 324820
static/fonts/inter/Inter-500.ttf ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:8c883f63b2c4157d997319f2c8bc6995ed4357ef371940d31ca159004a4aae63
3
+ size 325304
static/fonts/inter/Inter-600.ttf ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:e7a1aaf7eda9f2fad4131725fa556265ec75ca7b2d756260173a040363e8d4f7
3
+ size 326048
static/fonts/inter/Inter-700.ttf ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:b37284b5701b6b168dfc770aa1a4ac492106422fd3ba76bc7641e37434e8019c
3
+ size 326468
static/fonts/inter/Inter-Light.woff2 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:2301bb030a2bcaa9c763cc4771bd717aac16709c29eaba00673fcbe7cdf99a59
3
+ size 21564
static/fonts/inter/inter.css ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @font-face {
2
+ font-family: 'Inter';
3
+ font-style: normal;
4
+ font-weight: 300;
5
+ font-display: swap;
6
+ src: url(../fonts/inter/Inter-300.ttf) format('truetype');
7
+ }
8
+ @font-face {
9
+ font-family: 'Inter';
10
+ font-style: normal;
11
+ font-weight: 400;
12
+ font-display: swap;
13
+ src: url(../fonts/inter/Inter-400.ttf) format('truetype');
14
+ }
15
+ @font-face {
16
+ font-family: 'Inter';
17
+ font-style: normal;
18
+ font-weight: 500;
19
+ font-display: swap;
20
+ src: url(../fonts/inter/Inter-500.ttf) format('truetype');
21
+ }
22
+ @font-face {
23
+ font-family: 'Inter';
24
+ font-style: normal;
25
+ font-weight: 600;
26
+ font-display: swap;
27
+ src: url(../fonts/inter/Inter-600.ttf) format('truetype');
28
+ }
29
+ @font-face {
30
+ font-family: 'Inter';
31
+ font-style: normal;
32
+ font-weight: 700;
33
+ font-display: swap;
34
+ src: url(../fonts/inter/Inter-700.ttf) format('truetype');
35
+ }
static/js/script.js ADDED
@@ -0,0 +1,952 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * 交互式英语学习应用 - 主要逻辑
3
+ * 支持点读、翻译显示和音频播放功能
4
+ */
5
+
6
+ class InteractiveLearningApp {
7
+ constructor() {
8
+ this.bookData = null;
9
+ this.currentPageIndex = 0;
10
+ this.currentAudio = null;
11
+ this.isPlaying = false;
12
+ this.showTranslation = false;
13
+ this.showInteractiveAreas = false;
14
+ this.bookmarks = [];
15
+ this.searchResults = [];
16
+ this.debugMode = false; // 调试模式
17
+ this.useLocalResources = true; // 使用本地资源
18
+ this.settings = {
19
+ autoTranslation: false,
20
+ playbackSpeed: 1,
21
+ autoPlayNext: false
22
+ };
23
+
24
+ this.init();
25
+ }
26
+
27
+ async init() {
28
+ try {
29
+ await this.loadBookData();
30
+ this.setupEventListeners();
31
+ this.loadSettings();
32
+ this.loadBookmarks();
33
+ this.renderCurrentPage();
34
+ this.updateUI();
35
+ } catch (error) {
36
+ console.error('应用初始化失败:', error);
37
+ this.showToast('应用初始化失败,请刷新页面重试', 'error');
38
+ }
39
+ }
40
+
41
+ /**
42
+ * 从URL中提取文件名
43
+ */
44
+ extractFilenameFromUrl(url) {
45
+ if (!url) return null;
46
+
47
+ try {
48
+ const urlObj = new URL(url);
49
+ const pathname = urlObj.pathname;
50
+ const filename = pathname.split('/').pop();
51
+
52
+ // 移除URL解码
53
+ return decodeURIComponent(filename);
54
+ } catch (error) {
55
+ console.error('URL解析失败:', error);
56
+ return null;
57
+ }
58
+ }
59
+
60
+ /**
61
+ * 将线上URL转换为本地资源路径
62
+ */
63
+ mapToLocalResource(url, type) {
64
+ if (!this.useLocalResources || !url) {
65
+ return url;
66
+ }
67
+
68
+ const filename = this.extractFilenameFromUrl(url);
69
+ if (!filename) {
70
+ return url;
71
+ }
72
+
73
+ if (type === 'image') {
74
+ return `./assets/images/${filename}`;
75
+ } else if (type === 'audio') {
76
+ return `./assets/audios/${filename}`;
77
+ }
78
+
79
+ return url;
80
+ }
81
+
82
+ async loadBookData() {
83
+ try {
84
+ const response = await fetch('./book_10242.json');
85
+ if (!response.ok) {
86
+ throw new Error('数据文件加载失败');
87
+ }
88
+ const jsonData = await response.json();
89
+
90
+ if (!jsonData.Data) {
91
+ throw new Error('数据格式不正确');
92
+ }
93
+
94
+ this.bookData = JSON.parse(jsonData.Data);
95
+ console.log('书籍数据加载成功:', this.bookData.length, '页');
96
+ } catch (error) {
97
+ console.error('数据加载失败:', error);
98
+ throw error;
99
+ }
100
+ }
101
+
102
+ setupEventListeners() {
103
+ // 页面导航
104
+ document.getElementById('prevBtn').addEventListener('click', () => this.previousPage());
105
+ document.getElementById('nextBtn').addEventListener('click', () => this.nextPage());
106
+
107
+ // 控制按钮
108
+ document.getElementById('translationToggle').addEventListener('click', () => this.toggleTranslation());
109
+ document.getElementById('interactiveToggle').addEventListener('click', () => this.toggleInteractiveAreas());
110
+ document.getElementById('playAllBtn').addEventListener('click', () => this.playAllPieces());
111
+ document.getElementById('bookmarkBtn').addEventListener('click', () => this.toggleBookmark());
112
+ document.getElementById('searchBtn').addEventListener('click', () => this.showSearch());
113
+ document.getElementById('settingsBtn').addEventListener('click', () => this.showSettings());
114
+
115
+ // 音频控制
116
+ document.getElementById('playPauseBtn').addEventListener('click', () => this.togglePlayPause());
117
+ document.getElementById('repeatBtn').addEventListener('click', () => this.repeatAudio());
118
+ document.getElementById('audioTimeline').addEventListener('click', (e) => this.seekAudio(e));
119
+
120
+ // 设置面板
121
+ document.getElementById('closeSettings').addEventListener('click', () => this.hideSettings());
122
+
123
+ // 搜索面板
124
+ document.getElementById('closeSearch').addEventListener('click', () => this.hideSearch());
125
+ document.getElementById('doSearch').addEventListener('click', () => this.performSearch());
126
+ document.getElementById('searchInput').addEventListener('keypress', (e) => {
127
+ if (e.key === 'Enter') this.performSearch();
128
+ });
129
+
130
+ // 书签面板
131
+ document.getElementById('closeBookmark').addEventListener('click', () => this.hideBookmarks());
132
+ document.getElementById('autoTranslation').addEventListener('change', (e) => {
133
+ this.settings.autoTranslation = e.target.checked;
134
+ this.saveSettings();
135
+ });
136
+ document.getElementById('playbackSpeed').addEventListener('change', (e) => {
137
+ this.settings.playbackSpeed = parseFloat(e.target.value);
138
+ this.saveSettings();
139
+ if (this.currentAudio) {
140
+ this.currentAudio.playbackRate = this.settings.playbackSpeed;
141
+ }
142
+ });
143
+ document.getElementById('autoPlayNext').addEventListener('change', (e) => {
144
+ this.settings.autoPlayNext = e.target.checked;
145
+ this.saveSettings();
146
+ });
147
+
148
+ // 键盘快捷键
149
+ document.addEventListener('keydown', (e) => this.handleKeyPress(e));
150
+
151
+ // 点击设置面板外部关闭
152
+ document.getElementById('settingsPanel').addEventListener('click', (e) => {
153
+ if (e.target.id === 'settingsPanel') {
154
+ this.hideSettings();
155
+ }
156
+ });
157
+
158
+ // 音频事件监听
159
+ const audio = document.getElementById('audio');
160
+ audio.addEventListener('loadedmetadata', () => this.updateAudioUI());
161
+ audio.addEventListener('timeupdate', () => this.updateAudioProgress());
162
+ audio.addEventListener('ended', () => this.onAudioEnded());
163
+ audio.addEventListener('error', (e) => {
164
+ console.error('音频播放失败:', e);
165
+ this.showToast('音频播放失败', 'error');
166
+ });
167
+ }
168
+
169
+ renderCurrentPage() {
170
+ if (!this.bookData || this.currentPageIndex >= this.bookData.length) {
171
+ return;
172
+ }
173
+
174
+ const page = this.bookData[this.currentPageIndex];
175
+ const pageContainer = document.getElementById('pageContent');
176
+ const loading = document.getElementById('loading');
177
+
178
+ // 显示加载状态
179
+ loading.style.display = 'block';
180
+ pageContainer.style.display = 'none';
181
+
182
+ // 加载页面图片
183
+ const pageImage = document.getElementById('pageImage');
184
+ pageImage.onload = () => {
185
+ loading.style.display = 'none';
186
+ pageContainer.style.display = 'block';
187
+ this.renderTextPieces(page);
188
+ };
189
+
190
+ pageImage.onerror = () => {
191
+ // 如果原始图片加载失败,尝试加载加密图片
192
+ if (pageImage.src.includes('assets/images/') && page.encryptImgUrl) {
193
+ const encryptImageUrl = this.mapToLocalResource(page.encryptImgUrl, 'image');
194
+ pageImage.src = encryptImageUrl;
195
+ } else {
196
+ loading.style.display = 'none';
197
+ this.showToast('页面图片加载失败', 'error');
198
+ }
199
+ };
200
+
201
+ const imageUrl = this.mapToLocalResource(page.originImgUrl, 'image');
202
+ pageImage.src = imageUrl;
203
+ pageImage.alt = `第${page.pageNumber}页`;
204
+ }
205
+
206
+ renderTextPieces(page) {
207
+ const textOverlays = document.getElementById('textOverlays');
208
+ const pageImage = document.getElementById('pageImage');
209
+
210
+ // 清空现有的文本片段
211
+ textOverlays.innerHTML = '';
212
+
213
+ // 等待图片完全加载后渲染文本片段
214
+ const renderPieces = () => {
215
+ // 获取图片的实际位置和尺寸
216
+ const imageRect = pageImage.getBoundingClientRect();
217
+ const overlayRect = textOverlays.getBoundingClientRect();
218
+
219
+ // 计算图片相对于overlay容器的偏移
220
+ const offsetX = imageRect.left - overlayRect.left;
221
+ const offsetY = imageRect.top - overlayRect.top;
222
+
223
+ // 获取图片的实际显示尺寸
224
+ const imageWidth = imageRect.width;
225
+ const imageHeight = imageRect.height;
226
+
227
+ console.log('图片信息:', {
228
+ imageWidth,
229
+ imageHeight,
230
+ offsetX,
231
+ offsetY,
232
+ debugMode: this.debugMode
233
+ });
234
+
235
+ page.pieces.forEach((piece, index) => {
236
+ const textPiece = document.createElement('div');
237
+ textPiece.className = 'text-piece';
238
+ textPiece.dataset.pieceIndex = index;
239
+
240
+ // 在调试模式下添加特殊样式
241
+ if (this.debugMode) {
242
+ textPiece.style.border = '2px solid red';
243
+ textPiece.style.backgroundColor = 'rgba(255, 0, 0, 0.2)';
244
+ }
245
+
246
+ // 根据坐标信息设置位置和大小(coordinate是相对图片的比例坐标)
247
+ const coord = piece.coordinate;
248
+ const left = offsetX + (coord.x * imageWidth);
249
+ const top = offsetY + (coord.y * imageHeight);
250
+ const width = coord.width * imageWidth;
251
+ const height = coord.height * imageHeight;
252
+
253
+ textPiece.style.left = `${left}px`;
254
+ textPiece.style.top = `${top}px`;
255
+ textPiece.style.width = `${width}px`;
256
+ textPiece.style.height = `${height}px`;
257
+
258
+ if (this.debugMode) {
259
+ console.log(`Piece ${index + 1} (${piece.original}):`, {
260
+ coord,
261
+ left,
262
+ top,
263
+ width,
264
+ height
265
+ });
266
+ }
267
+
268
+ // 创建文本内容
269
+ const originalText = document.createElement('div');
270
+ originalText.className = 'piece-text';
271
+ originalText.textContent = piece.original;
272
+
273
+ const translationText = document.createElement('div');
274
+ translationText.className = 'piece-translation';
275
+ translationText.textContent = piece.translation;
276
+
277
+ textPiece.appendChild(originalText);
278
+ textPiece.appendChild(translationText);
279
+
280
+ // 点击事件
281
+ textPiece.addEventListener('click', () => this.playPiece(piece, textPiece));
282
+
283
+ textOverlays.appendChild(textPiece);
284
+ });
285
+
286
+ // 根据设置显示翻译
287
+ if (this.showTranslation || this.settings.autoTranslation) {
288
+ textOverlays.classList.add('show-translation');
289
+ }
290
+ };
291
+
292
+ // 如果图片已经加载完成,直接渲染
293
+ if (pageImage.complete && pageImage.naturalHeight !== 0) {
294
+ renderPieces();
295
+ } else {
296
+ // 否则等待图片加载完成
297
+ pageImage.addEventListener('load', renderPieces, { once: true });
298
+ }
299
+
300
+ // 监听窗口大小变化和图片加载事件
301
+ let resizeTimeout;
302
+ const handleResize = () => {
303
+ clearTimeout(resizeTimeout);
304
+ resizeTimeout = setTimeout(() => {
305
+ if (pageImage.complete && pageImage.naturalHeight !== 0) {
306
+ console.log('窗口大小变化,重新计算坐标');
307
+ renderPieces();
308
+ }
309
+ }, 100);
310
+ };
311
+
312
+ // 移除之前的监听器(如果存在)
313
+ if (pageImage.resizeObserver) {
314
+ pageImage.resizeObserver.disconnect();
315
+ }
316
+
317
+ // 创建新的监听器
318
+ pageImage.resizeObserver = new ResizeObserver(handleResize);
319
+ pageImage.resizeObserver.observe(pageImage);
320
+
321
+ // 同时监听窗口大小变化
322
+ window.addEventListener('resize', handleResize);
323
+ }
324
+
325
+ async playPiece(piece, element) {
326
+ try {
327
+ // 移除其他片段的激活状态
328
+ document.querySelectorAll('.text-piece').forEach(el => {
329
+ el.classList.remove('active', 'playing');
330
+ });
331
+
332
+ // 激活当前片段
333
+ element.classList.add('active');
334
+
335
+ // 显示音频播放器
336
+ const audioPlayer = document.getElementById('audioPlayer');
337
+ audioPlayer.style.display = 'block';
338
+
339
+ // 更新音频文本显示
340
+ const audioText = document.getElementById('audioText');
341
+ audioText.innerHTML = `
342
+ <div style="font-weight: 600; margin-bottom: 0.5rem;">${piece.original}</div>
343
+ <div style="color: var(--text-secondary); font-size: 0.9rem;">${piece.translation}</div>
344
+ `;
345
+
346
+ // 加载并播放音频
347
+ const audio = document.getElementById('audio');
348
+ const audioUrl = piece.originSoundUrl || piece.encryptSoundUrl;
349
+
350
+ if (!audioUrl) {
351
+ this.showToast('该片段没有可用的音频', 'warning');
352
+ return;
353
+ }
354
+
355
+ // 停止当前播放的音频
356
+ if (this.currentAudio && !this.currentAudio.paused) {
357
+ this.currentAudio.pause();
358
+ }
359
+
360
+ const localAudioUrl = this.mapToLocalResource(audioUrl, 'audio');
361
+ audio.src = localAudioUrl;
362
+ audio.playbackRate = this.settings.playbackSpeed;
363
+ this.currentAudio = audio;
364
+
365
+ // 播放音频
366
+ await audio.play();
367
+ this.isPlaying = true;
368
+ element.classList.add('playing');
369
+ this.updatePlayButton();
370
+
371
+ } catch (error) {
372
+ console.error('音频播放失败:', error);
373
+ this.showToast('音频播放失败', 'error');
374
+ element.classList.remove('active', 'playing');
375
+ }
376
+ }
377
+
378
+ async playAllPieces() {
379
+ const page = this.bookData[this.currentPageIndex];
380
+ if (!page || !page.pieces.length) return;
381
+
382
+ let currentPieceIndex = 0;
383
+
384
+ const playNext = async () => {
385
+ if (currentPieceIndex >= page.pieces.length) {
386
+ this.showToast('整页播放完成', 'success');
387
+ return;
388
+ }
389
+
390
+ const piece = page.pieces[currentPieceIndex];
391
+ const element = document.querySelector(`[data-piece-index="${currentPieceIndex}"]`);
392
+
393
+ if (element) {
394
+ await this.playPiece(piece, element);
395
+
396
+ // 等待当前音频播放完成
397
+ const audio = document.getElementById('audio');
398
+ audio.addEventListener('ended', () => {
399
+ currentPieceIndex++;
400
+ setTimeout(playNext, 500); // 稍微延迟播放下一个
401
+ }, { once: true });
402
+ } else {
403
+ currentPieceIndex++;
404
+ playNext();
405
+ }
406
+ };
407
+
408
+ playNext();
409
+ }
410
+
411
+ togglePlayPause() {
412
+ const audio = document.getElementById('audio');
413
+ if (!audio.src) return;
414
+
415
+ if (this.isPlaying) {
416
+ audio.pause();
417
+ this.isPlaying = false;
418
+ } else {
419
+ audio.play().then(() => {
420
+ this.isPlaying = true;
421
+ }).catch(error => {
422
+ console.error('播放失败:', error);
423
+ this.showToast('播放失败', 'error');
424
+ });
425
+ }
426
+
427
+ this.updatePlayButton();
428
+ }
429
+
430
+ repeatAudio() {
431
+ const audio = document.getElementById('audio');
432
+ if (audio.src) {
433
+ audio.currentTime = 0;
434
+ if (!this.isPlaying) {
435
+ this.togglePlayPause();
436
+ }
437
+ }
438
+ }
439
+
440
+ seekAudio(event) {
441
+ const audio = document.getElementById('audio');
442
+ if (!audio.src || !audio.duration) return;
443
+
444
+ const timeline = document.getElementById('audioTimeline');
445
+ const rect = timeline.getBoundingClientRect();
446
+ const percentage = (event.clientX - rect.left) / rect.width;
447
+ const newTime = percentage * audio.duration;
448
+
449
+ audio.currentTime = newTime;
450
+ }
451
+
452
+ updatePlayButton() {
453
+ const playPauseBtn = document.getElementById('playPauseBtn');
454
+ const icon = playPauseBtn.querySelector('i');
455
+
456
+ if (this.isPlaying) {
457
+ icon.className = 'fas fa-pause';
458
+ } else {
459
+ icon.className = 'fas fa-play';
460
+ }
461
+ }
462
+
463
+ updateAudioUI() {
464
+ const audio = document.getElementById('audio');
465
+ const totalTime = document.getElementById('totalTime');
466
+
467
+ if (audio.duration) {
468
+ totalTime.textContent = this.formatTime(audio.duration);
469
+ }
470
+ }
471
+
472
+ updateAudioProgress() {
473
+ const audio = document.getElementById('audio');
474
+ const progressBar = document.getElementById('audioProgressBar');
475
+ const currentTime = document.getElementById('currentTime');
476
+
477
+ if (audio.duration) {
478
+ const percentage = (audio.currentTime / audio.duration) * 100;
479
+ progressBar.style.width = `${percentage}%`;
480
+ currentTime.textContent = this.formatTime(audio.currentTime);
481
+ }
482
+ }
483
+
484
+ onAudioEnded() {
485
+ this.isPlaying = false;
486
+ this.updatePlayButton();
487
+
488
+ // 移除播放状态
489
+ document.querySelectorAll('.text-piece').forEach(el => {
490
+ el.classList.remove('playing');
491
+ });
492
+
493
+ // 如果启用自动播放下一个
494
+ if (this.settings.autoPlayNext) {
495
+ // 这里可以实现自动播放下一个片段的逻辑
496
+ }
497
+ }
498
+
499
+ toggleTranslation() {
500
+ this.showTranslation = !this.showTranslation;
501
+ const textOverlays = document.getElementById('textOverlays');
502
+ const translationBtn = document.getElementById('translationToggle');
503
+
504
+ if (this.showTranslation) {
505
+ textOverlays.classList.add('show-translation');
506
+ translationBtn.classList.add('active');
507
+ } else {
508
+ textOverlays.classList.remove('show-translation');
509
+ translationBtn.classList.remove('active');
510
+ }
511
+ }
512
+
513
+ toggleInteractiveAreas() {
514
+ this.showInteractiveAreas = !this.showInteractiveAreas;
515
+ const textOverlays = document.getElementById('textOverlays');
516
+ const interactiveBtn = document.getElementById('interactiveToggle');
517
+
518
+ if (this.showInteractiveAreas) {
519
+ textOverlays.classList.add('show-interactive-areas');
520
+ interactiveBtn.classList.add('active');
521
+ interactiveBtn.querySelector('i').className = 'fas fa-eye-slash';
522
+ interactiveBtn.title = '隐藏交互区域';
523
+ } else {
524
+ textOverlays.classList.remove('show-interactive-areas');
525
+ interactiveBtn.classList.remove('active');
526
+ interactiveBtn.querySelector('i').className = 'fas fa-eye';
527
+ interactiveBtn.title = '显示交互区域';
528
+ }
529
+ }
530
+
531
+ previousPage() {
532
+ if (this.currentPageIndex > 0) {
533
+ this.currentPageIndex--;
534
+ this.renderCurrentPage();
535
+ this.updateUI();
536
+ this.stopCurrentAudio();
537
+ }
538
+ }
539
+
540
+ nextPage() {
541
+ if (this.currentPageIndex < this.bookData.length - 1) {
542
+ this.currentPageIndex++;
543
+ this.renderCurrentPage();
544
+ this.updateUI();
545
+ this.stopCurrentAudio();
546
+ }
547
+ }
548
+
549
+ stopCurrentAudio() {
550
+ if (this.currentAudio && !this.currentAudio.paused) {
551
+ this.currentAudio.pause();
552
+ this.isPlaying = false;
553
+ this.updatePlayButton();
554
+ }
555
+
556
+ // 隐藏音频播放器
557
+ document.getElementById('audioPlayer').style.display = 'none';
558
+
559
+ // 移除所有激活状态
560
+ document.querySelectorAll('.text-piece').forEach(el => {
561
+ el.classList.remove('active', 'playing');
562
+ });
563
+ }
564
+
565
+ updateUI() {
566
+ // 更新页面信息
567
+ document.getElementById('currentPage').textContent = this.currentPageIndex + 1;
568
+ document.getElementById('totalPages').textContent = this.bookData.length;
569
+
570
+ // 更新进度条
571
+ const progress = ((this.currentPageIndex + 1) / this.bookData.length) * 100;
572
+ document.getElementById('progressFill').style.width = `${progress}%`;
573
+
574
+ // 更新导航按钮状态
575
+ document.getElementById('prevBtn').disabled = this.currentPageIndex === 0;
576
+ document.getElementById('nextBtn').disabled = this.currentPageIndex === this.bookData.length - 1;
577
+
578
+ // 更新书签按钮状态
579
+ this.updateBookmarkButton();
580
+ }
581
+
582
+ showSettings() {
583
+ document.getElementById('settingsPanel').style.display = 'flex';
584
+
585
+ // 同步当前设置到UI
586
+ document.getElementById('autoTranslation').checked = this.settings.autoTranslation;
587
+ document.getElementById('playbackSpeed').value = this.settings.playbackSpeed;
588
+ document.getElementById('autoPlayNext').checked = this.settings.autoPlayNext;
589
+ }
590
+
591
+ hideSettings() {
592
+ document.getElementById('settingsPanel').style.display = 'none';
593
+ }
594
+
595
+ loadSettings() {
596
+ const saved = localStorage.getItem('learningAppSettings');
597
+ if (saved) {
598
+ try {
599
+ this.settings = { ...this.settings, ...JSON.parse(saved) };
600
+ } catch (error) {
601
+ console.error('设置加载失败:', error);
602
+ }
603
+ }
604
+ }
605
+
606
+ saveSettings() {
607
+ localStorage.setItem('learningAppSettings', JSON.stringify(this.settings));
608
+ }
609
+
610
+ handleKeyPress(event) {
611
+ // 如果设置面板打开,只处理 Escape 键
612
+ if (document.getElementById('settingsPanel').style.display === 'flex') {
613
+ if (event.key === 'Escape') {
614
+ this.hideSettings();
615
+ }
616
+ return;
617
+ }
618
+
619
+ switch (event.key) {
620
+ case 'ArrowLeft':
621
+ event.preventDefault();
622
+ this.previousPage();
623
+ break;
624
+ case 'ArrowRight':
625
+ event.preventDefault();
626
+ this.nextPage();
627
+ break;
628
+ case ' ':
629
+ event.preventDefault();
630
+ this.togglePlayPause();
631
+ break;
632
+ case 't':
633
+ case 'T':
634
+ event.preventDefault();
635
+ this.toggleTranslation();
636
+ break;
637
+ case 'i':
638
+ case 'I':
639
+ event.preventDefault();
640
+ this.toggleInteractiveAreas();
641
+ break;
642
+ case 'r':
643
+ case 'R':
644
+ event.preventDefault();
645
+ this.repeatAudio();
646
+ break;
647
+ case 'p':
648
+ case 'P':
649
+ event.preventDefault();
650
+ this.playAllPieces();
651
+ break;
652
+ case 'b':
653
+ case 'B':
654
+ event.preventDefault();
655
+ this.toggleBookmark();
656
+ break;
657
+ case 'f':
658
+ case 'F':
659
+ event.preventDefault();
660
+ this.showSearch();
661
+ break;
662
+ case 'd':
663
+ case 'D':
664
+ if (event.ctrlKey) {
665
+ event.preventDefault();
666
+ this.toggleDebugMode();
667
+ }
668
+ break;
669
+ }
670
+ }
671
+
672
+ showToast(message, type = 'info') {
673
+ const toast = document.getElementById('toast');
674
+ toast.textContent = message;
675
+ toast.className = `toast ${type} show`;
676
+
677
+ setTimeout(() => {
678
+ toast.classList.remove('show');
679
+ }, 3000);
680
+ }
681
+
682
+ formatTime(seconds) {
683
+ const mins = Math.floor(seconds / 60);
684
+ const secs = Math.floor(seconds % 60);
685
+ return `${mins}:${secs.toString().padStart(2, '0')}`;
686
+ }
687
+
688
+ // 搜索功能
689
+ showSearch() {
690
+ document.getElementById('searchPanel').style.display = 'flex';
691
+ document.getElementById('searchInput').focus();
692
+ }
693
+
694
+ hideSearch() {
695
+ document.getElementById('searchPanel').style.display = 'none';
696
+ document.getElementById('searchInput').value = '';
697
+ this.clearSearchResults();
698
+ }
699
+
700
+ performSearch() {
701
+ const query = document.getElementById('searchInput').value.trim();
702
+ if (!query) {
703
+ this.showToast('请输入搜索关键词', 'warning');
704
+ return;
705
+ }
706
+
707
+ const results = [];
708
+ const regex = new RegExp(query, 'gi');
709
+
710
+ this.bookData.forEach((page, pageIndex) => {
711
+ page.pieces.forEach((piece, pieceIndex) => {
712
+ if (regex.test(piece.original) || regex.test(piece.translation)) {
713
+ results.push({
714
+ pageIndex,
715
+ pieceIndex,
716
+ page: page.pageNumber,
717
+ original: piece.original,
718
+ translation: piece.translation,
719
+ piece
720
+ });
721
+ }
722
+ });
723
+ });
724
+
725
+ this.searchResults = results;
726
+ this.displaySearchResults(query);
727
+ }
728
+
729
+ displaySearchResults(query) {
730
+ const resultsContainer = document.getElementById('searchResults');
731
+
732
+ if (this.searchResults.length === 0) {
733
+ resultsContainer.innerHTML = '<div class="search-empty">未找到匹配的内容</div>';
734
+ return;
735
+ }
736
+
737
+ const regex = new RegExp(`(${query})`, 'gi');
738
+ let html = '';
739
+
740
+ this.searchResults.forEach((result, index) => {
741
+ const highlightedOriginal = result.original.replace(regex, '<span class="search-highlight">$1</span>');
742
+ const highlightedTranslation = result.translation.replace(regex, '<span class="search-highlight">$1</span>');
743
+
744
+ html += `
745
+ <div class="search-result-item" data-result-index="${index}">
746
+ <div class="search-result-page">第 ${result.page} 页</div>
747
+ <div class="search-result-text">${highlightedOriginal}</div>
748
+ <div class="search-result-translation">${highlightedTranslation}</div>
749
+ </div>
750
+ `;
751
+ });
752
+
753
+ resultsContainer.innerHTML = html;
754
+
755
+ // 添加点击事件
756
+ resultsContainer.querySelectorAll('.search-result-item').forEach(item => {
757
+ item.addEventListener('click', (e) => {
758
+ const index = parseInt(e.currentTarget.dataset.resultIndex);
759
+ this.goToSearchResult(index);
760
+ });
761
+ });
762
+ }
763
+
764
+ goToSearchResult(index) {
765
+ const result = this.searchResults[index];
766
+ if (!result) return;
767
+
768
+ this.currentPageIndex = result.pageIndex;
769
+ this.renderCurrentPage();
770
+ this.updateUI();
771
+ this.hideSearch();
772
+
773
+ // 稍微延迟后高亮显示并播放对应片段
774
+ setTimeout(() => {
775
+ const element = document.querySelector(`[data-piece-index="${result.pieceIndex}"]`);
776
+ if (element) {
777
+ element.scrollIntoView({ behavior: 'smooth', block: 'center' });
778
+ this.playPiece(result.piece, element);
779
+ }
780
+ }, 500);
781
+
782
+ this.showToast(`跳转到第 ${result.page} 页`, 'success');
783
+ }
784
+
785
+ clearSearchResults() {
786
+ this.searchResults = [];
787
+ document.getElementById('searchResults').innerHTML = '<div class="search-empty">输入关键词开始搜索</div>';
788
+ }
789
+
790
+ // 书签功能
791
+ toggleBookmark() {
792
+ const isBookmarked = this.bookmarks.some(b => b.pageIndex === this.currentPageIndex);
793
+
794
+ if (isBookmarked) {
795
+ const bookmark = this.bookmarks.find(b => b.pageIndex === this.currentPageIndex);
796
+ this.removeBookmark(bookmark.id);
797
+ } else {
798
+ this.addBookmark();
799
+ }
800
+ }
801
+
802
+ showBookmarks() {
803
+ document.getElementById('bookmarkPanel').style.display = 'flex';
804
+ this.renderBookmarks();
805
+ }
806
+
807
+ hideBookmarks() {
808
+ document.getElementById('bookmarkPanel').style.display = 'none';
809
+ }
810
+
811
+ addBookmark() {
812
+ const currentPage = this.bookData[this.currentPageIndex];
813
+ if (!currentPage) return;
814
+
815
+ const bookmark = {
816
+ id: Date.now(),
817
+ pageIndex: this.currentPageIndex,
818
+ pageNumber: currentPage.pageNumber,
819
+ title: `第 ${currentPage.pageNumber} 页`,
820
+ timestamp: new Date().toISOString()
821
+ };
822
+
823
+ // 检查是否已存在
824
+ const exists = this.bookmarks.some(b => b.pageIndex === this.currentPageIndex);
825
+ if (exists) {
826
+ this.showToast('当前页面已在书签中', 'warning');
827
+ return;
828
+ }
829
+
830
+ this.bookmarks.push(bookmark);
831
+ this.saveBookmarks();
832
+ this.updateBookmarkButton();
833
+ this.showToast('书签添加成功', 'success');
834
+ }
835
+
836
+ removeBookmark(bookmarkId) {
837
+ this.bookmarks = this.bookmarks.filter(b => b.id !== bookmarkId);
838
+ this.saveBookmarks();
839
+ this.updateBookmarkButton();
840
+ this.renderBookmarks();
841
+ this.showToast('书签删除成功', 'success');
842
+ }
843
+
844
+ renderBookmarks() {
845
+ const container = document.getElementById('bookmarkList');
846
+
847
+ if (this.bookmarks.length === 0) {
848
+ container.innerHTML = '<div class="bookmark-empty">还没有添加任何书签</div>';
849
+ return;
850
+ }
851
+
852
+ let html = '';
853
+ this.bookmarks.forEach(bookmark => {
854
+ const date = new Date(bookmark.timestamp);
855
+ const timeString = date.toLocaleDateString() + ' ' + date.toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'});
856
+
857
+ html += `
858
+ <div class="bookmark-item" data-page-index="${bookmark.pageIndex}">
859
+ <div class="bookmark-info">
860
+ <div class="bookmark-page">${bookmark.title}</div>
861
+ <div class="bookmark-time">${timeString}</div>
862
+ </div>
863
+ <div class="bookmark-actions">
864
+ <button class="bookmark-delete" data-bookmark-id="${bookmark.id}">
865
+ <i class="fas fa-times"></i>
866
+ </button>
867
+ </div>
868
+ </div>
869
+ `;
870
+ });
871
+
872
+ container.innerHTML = html;
873
+
874
+ // 添加点击事件
875
+ container.querySelectorAll('.bookmark-item').forEach(item => {
876
+ item.addEventListener('click', (e) => {
877
+ if (e.target.closest('.bookmark-delete')) return;
878
+ const pageIndex = parseInt(item.dataset.pageIndex);
879
+ this.goToBookmark(pageIndex);
880
+ });
881
+ });
882
+
883
+ container.querySelectorAll('.bookmark-delete').forEach(btn => {
884
+ btn.addEventListener('click', (e) => {
885
+ e.stopPropagation();
886
+ const bookmarkId = parseInt(btn.dataset.bookmarkId);
887
+ this.removeBookmark(bookmarkId);
888
+ });
889
+ });
890
+ }
891
+
892
+ goToBookmark(pageIndex) {
893
+ this.currentPageIndex = pageIndex;
894
+ this.renderCurrentPage();
895
+ this.updateUI();
896
+ this.hideBookmarks();
897
+ this.showToast(`跳转到第 ${this.bookData[pageIndex].pageNumber} 页`, 'success');
898
+ }
899
+
900
+ updateBookmarkButton() {
901
+ const btn = document.getElementById('bookmarkBtn');
902
+ const isBookmarked = this.bookmarks.some(b => b.pageIndex === this.currentPageIndex);
903
+
904
+ if (isBookmarked) {
905
+ btn.classList.add('active');
906
+ btn.title = '取消书签';
907
+ } else {
908
+ btn.classList.remove('active');
909
+ btn.title = '添加书签';
910
+ }
911
+ }
912
+
913
+ loadBookmarks() {
914
+ const saved = localStorage.getItem('learningAppBookmarks');
915
+ if (saved) {
916
+ try {
917
+ this.bookmarks = JSON.parse(saved);
918
+ } catch (error) {
919
+ console.error('书签加载失败:', error);
920
+ this.bookmarks = [];
921
+ }
922
+ }
923
+ }
924
+
925
+ saveBookmarks() {
926
+ localStorage.setItem('learningAppBookmarks', JSON.stringify(this.bookmarks));
927
+ }
928
+
929
+ // 调试模式
930
+ toggleDebugMode() {
931
+ this.debugMode = !this.debugMode;
932
+ this.showToast(`调试模式: ${this.debugMode ? '开启' : '关闭'}`, 'info');
933
+
934
+ // 重新渲染当前页面以应用调试样式
935
+ this.renderCurrentPage();
936
+ }
937
+ }
938
+
939
+ // 页面加载完成后初始化应用
940
+ document.addEventListener('DOMContentLoaded', () => {
941
+ new InteractiveLearningApp();
942
+ });
943
+
944
+ // 处理页面可见性变化,暂停音频播放
945
+ document.addEventListener('visibilitychange', () => {
946
+ if (document.hidden) {
947
+ const audio = document.getElementById('audio');
948
+ if (audio && !audio.paused) {
949
+ audio.pause();
950
+ }
951
+ }
952
+ });