zhangl commited on
Commit
c7e7ea8
·
1 Parent(s): 33e1e1c

Refactor index.html, README.md, and style.css for interactive English learning app. Updated HTML structure, added navigation and control features, enhanced styling, and improved documentation in README.

Browse files
Files changed (9) hide show
  1. Dockerfile +19 -0
  2. README.md +78 -6
  3. app.py +88 -0
  4. book_10242.json +0 -0
  5. book_104.json +0 -0
  6. index.html +188 -18
  7. requirements.txt +2 -0
  8. script.js +907 -0
  9. style.css +900 -18
Dockerfile ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.9-slim
2
+
3
+ # 设置工作目录
4
+ WORKDIR /app
5
+
6
+ # 复制requirements文件
7
+ COPY requirements.txt .
8
+
9
+ # 安装Python依赖
10
+ RUN pip install --no-cache-dir -r requirements.txt
11
+
12
+ # 复制应用文件
13
+ COPY . .
14
+
15
+ # 暴露7860端口(Hugging Face Spaces要求)
16
+ EXPOSE 7860
17
+
18
+ # 启动应用
19
+ CMD ["python", "app.py"]
README.md CHANGED
@@ -1,10 +1,82 @@
1
  ---
2
- title: Ibook
3
- emoji: 🌖
4
- colorFrom: pink
5
- colorTo: red
6
- sdk: static
 
7
  pinned: false
 
8
  ---
9
 
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ title: 交互式英语学习应用
3
+ emoji: 📚
4
+ colorFrom: blue
5
+ colorTo: green
6
+ sdk: docker
7
+ app_port: 7860
8
  pinned: false
9
+ license: mit
10
  ---
11
 
12
+ # 交互式英语学习应用 📚
13
+
14
+ 一个现代化的交互式英语学习应用,支持点读、翻译显示和音频播放功能。
15
+
16
+ ## ✨ 主要功能
17
+
18
+ ### 📖 核心学习功能
19
+ - **点读功能**: 点击任意文本片段即可播放对应音频
20
+ - **双语显示**: 支持英文原文和中文翻译切换显示
21
+ - **音频播放**: 高质量音频播放,支持播放控制和进度调节
22
+ - **页面导航**: 流畅的页面切换,支持进度跟踪
23
+
24
+ ### 🔍 高级功能
25
+ - **全文搜索**: 快速搜索英文原文和中文翻译内容
26
+ - **智能书签**: 添加/删除书签,快速跳转到重要页面
27
+ - **播放控制**: 支持播放速度调节、重复播放等
28
+ - **键盘快捷键**: 完整的键盘操作支持
29
+
30
+ ### 🎨 用户体验
31
+ - **响应式设计**: 完美适配桌面和移动设备
32
+ - **现代化界面**: 美观的材质设计风格
33
+ - **流畅动画**: 丰富的交互动画效果
34
+ - **本地存储**: 自动保存用户设置和书签
35
+
36
+ ## ⌨️ 键盘快捷键
37
+
38
+ | 快捷键 | 功能 |
39
+ |--------|------|
40
+ | `←` / `→` | 上一页 / 下一页 |
41
+ | `空格` | 播放/暂停当前音频 |
42
+ | `T` | 切换翻译显示 |
43
+ | `I` | 显示/隐藏交互区域 |
44
+ | `P` | 播放整页内容 |
45
+ | `R` | 重复播放当前音频 |
46
+ | `B` | 添加/删除书签 |
47
+ | `F` | 打开搜索功能 |
48
+
49
+ ## 🎮 使用指南
50
+
51
+ ### 基本操作
52
+ 1. **页面切换**: 使用导航按钮或键盘箭头键
53
+ 2. **点读学习**: 点击页面上的任意文本片段
54
+ 3. **翻译显示**: 点击翻译按钮或按T键切换
55
+ 4. **音频控制**: 使用底部音频播放控制器
56
+
57
+ ### 搜索功能
58
+ 1. 点击搜索图标或按F键
59
+ 2. 输入要搜索的英文或中文内容
60
+ 3. 点击搜索结果直接跳转到对应页面
61
+
62
+ ### 书签管理
63
+ 1. 在想要标记的页面点击书签按钮或按B键
64
+ 2. 点击书签图标查看所有书签
65
+ 3. 点击书签项目快速跳转
66
+ 4. 使用删除按钮移除不需要的书签
67
+
68
+ ## 🔧 技术栈
69
+
70
+ - **前端**: HTML5, CSS3, Vanilla JavaScript
71
+ - **字体**: FZKT_GBK.woff (方正楷体)
72
+ - **数据**: JSON格式的学习内容
73
+ - **音频**: Web Audio API
74
+ - **存储**: localStorage
75
+
76
+ ## 📄 许可证
77
+
78
+ MIT License
79
+
80
+ ---
81
+
82
+ **享受学习英语的乐趣!** 🎉
app.py ADDED
@@ -0,0 +1,88 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """
4
+ 交互式英语学习应用 - Hugging Face Spaces Docker部署版本
5
+ 在7860端口提供HTTP服务
6
+ """
7
+
8
+ import http.server
9
+ import socketserver
10
+ import os
11
+ import sys
12
+ import threading
13
+ import time
14
+
15
+ class CORSHTTPRequestHandler(http.server.SimpleHTTPRequestHandler):
16
+ """支持CORS的HTTP请求处理器"""
17
+
18
+ def end_headers(self):
19
+ # 添加CORS头部,允许跨域访问
20
+ self.send_header('Access-Control-Allow-Origin', '*')
21
+ self.send_header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS')
22
+ self.send_header('Access-Control-Allow-Headers', 'Content-Type')
23
+ self.send_header('Cache-Control', 'no-cache, no-store, must-revalidate')
24
+ self.send_header('Pragma', 'no-cache')
25
+ self.send_header('Expires', '0')
26
+ super().end_headers()
27
+
28
+ def do_OPTIONS(self):
29
+ # 处理预检请求
30
+ self.send_response(200)
31
+ self.end_headers()
32
+
33
+ def log_message(self, format, *args):
34
+ # 自定义日志格式
35
+ timestamp = time.strftime('%Y-%m-%d %H:%M:%S')
36
+ client_ip = self.address_string()
37
+ print(f"[{timestamp}] {client_ip} - {format % args}")
38
+
39
+ def check_required_files():
40
+ """检查必要文件是否存在"""
41
+ required_files = ['index.html', 'style.css', 'script.js', 'book_104.json', 'FZKT_GBK.woff']
42
+ missing_files = []
43
+
44
+ for file in required_files:
45
+ if not os.path.exists(file):
46
+ missing_files.append(file)
47
+
48
+ if missing_files:
49
+ print(f"❌ 缺少必要文件: {', '.join(missing_files)}")
50
+ return False
51
+
52
+ print("✅ 所有必要文件检查完成")
53
+ return True
54
+
55
+ def main():
56
+ """主函数 - 在7860端口启动HTTP服务器"""
57
+ print("🚀 交互式英语学习应用 - Hugging Face Spaces")
58
+ print("=" * 60)
59
+
60
+ # 检查必要文件
61
+ if not check_required_files():
62
+ return 1
63
+
64
+ # Hugging Face Spaces要求监听7860端口
65
+ port = int(os.environ.get('PORT', 7860))
66
+
67
+ try:
68
+ # 创建服务器
69
+ with socketserver.TCPServer(("0.0.0.0", port), CORSHTTPRequestHandler) as httpd:
70
+ print(f"📱 服务器启动成功")
71
+ print(f"🌐 监听端口: {port}")
72
+ print(f"📁 工作目录: {os.getcwd()}")
73
+ print("=" * 60)
74
+ print("🎉 应用已准备就绪!")
75
+ print("=" * 60)
76
+
77
+ # 启动服务器
78
+ httpd.serve_forever()
79
+
80
+ except KeyboardInterrupt:
81
+ print("\n\n🛑 服务器已停止")
82
+ return 0
83
+ except Exception as e:
84
+ print(f"❌ 服务器启动失败: {e}")
85
+ return 1
86
+
87
+ if __name__ == "__main__":
88
+ sys.exit(main())
book_10242.json ADDED
The diff for this file is too large to render. See raw diff
 
book_104.json ADDED
The diff for this file is too large to render. See raw diff
 
index.html CHANGED
@@ -1,19 +1,189 @@
1
- <!doctype html>
2
- <html>
3
- <head>
4
- <meta charset="utf-8" />
5
- <meta name="viewport" content="width=device-width" />
6
- <title>My static Space</title>
7
- <link rel="stylesheet" href="style.css" />
8
- </head>
9
- <body>
10
- <div class="card">
11
- <h1>Welcome to your static Space!</h1>
12
- <p>You can modify this app directly by editing <i>index.html</i> in the Files and versions tab.</p>
13
- <p>
14
- Also don't forget to check the
15
- <a href="https://huggingface.co/docs/hub/spaces" target="_blank">Spaces documentation</a>.
16
- </p>
17
- </div>
18
- </body>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
19
  </html>
 
1
+ <!DOCTYPE html>
2
+ <html lang="zh-CN">
3
+ <head>
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">
15
+ <!-- 顶部导航栏 -->
16
+ <header class="header">
17
+ <div class="header-content">
18
+ <h1 class="app-title">
19
+ <i class="fas fa-book-open"></i>
20
+ 交互式英语学习
21
+ </h1>
22
+ <div class="controls">
23
+ <button class="control-btn" id="translationToggle" title="切换翻译显示">
24
+ <i class="fas fa-language"></i>
25
+ <span>翻译</span>
26
+ </button>
27
+ <button class="control-btn" id="interactiveToggle" title="显示/隐藏交互区域">
28
+ <i class="fas fa-eye"></i>
29
+ <span>交互</span>
30
+ </button>
31
+ <button class="control-btn" id="playAllBtn" title="播放整页">
32
+ <i class="fas fa-play-circle"></i>
33
+ <span>播放</span>
34
+ </button>
35
+ <button class="control-btn" id="bookmarkBtn" title="书签">
36
+ <i class="fas fa-bookmark"></i>
37
+ </button>
38
+ <button class="control-btn" id="searchBtn" title="搜索">
39
+ <i class="fas fa-search"></i>
40
+ </button>
41
+ <button class="control-btn" id="settingsBtn" title="设置">
42
+ <i class="fas fa-cog"></i>
43
+ </button>
44
+ </div>
45
+ </div>
46
+ </header>
47
+
48
+ <!-- 主要内容区域 -->
49
+ <main class="main-content">
50
+ <!-- 页面导航 -->
51
+ <nav class="page-navigation">
52
+ <button class="nav-btn" id="prevBtn" title="上一页">
53
+ <i class="fas fa-chevron-left"></i>
54
+ </button>
55
+ <div class="page-info">
56
+ <span id="currentPage">1</span> / <span id="totalPages">63</span>
57
+ </div>
58
+ <button class="nav-btn" id="nextBtn" title="下一页">
59
+ <i class="fas fa-chevron-right"></i>
60
+ </button>
61
+ <div class="progress-bar">
62
+ <div class="progress-fill" id="progressFill"></div>
63
+ </div>
64
+ </nav>
65
+
66
+ <!-- 页面内容显示区域 -->
67
+ <div class="page-container" id="pageContainer">
68
+ <div class="loading" id="loading">
69
+ <i class="fas fa-spinner fa-spin"></i>
70
+ <p>加载中...</p>
71
+ </div>
72
+
73
+ <!-- 页面内容将动态插入这里 -->
74
+ <div class="page-content" id="pageContent" style="display: none;">
75
+ <div class="page-image-container">
76
+ <img id="pageImage" class="page-image" alt="页面图片">
77
+ <div class="text-overlays" id="textOverlays">
78
+ <!-- 文本片段覆盖层将在这里动态生成 -->
79
+ </div>
80
+ </div>
81
+ </div>
82
+ </div>
83
+
84
+ <!-- 底部控制面板 -->
85
+ <div class="bottom-panel">
86
+ <div class="audio-player" id="audioPlayer" style="display: none;">
87
+ <audio id="audio" preload="none"></audio>
88
+ <div class="audio-info">
89
+ <div class="audio-text" id="audioText">点击文本片段开始学习</div>
90
+ <div class="audio-controls">
91
+ <button class="audio-btn" id="playPauseBtn">
92
+ <i class="fas fa-play"></i>
93
+ </button>
94
+ <div class="audio-progress">
95
+ <div class="audio-timeline" id="audioTimeline">
96
+ <div class="audio-progress-bar" id="audioProgressBar"></div>
97
+ </div>
98
+ <div class="audio-time">
99
+ <span id="currentTime">0:00</span> / <span id="totalTime">0:00</span>
100
+ </div>
101
+ </div>
102
+ <button class="audio-btn" id="repeatBtn" title="重复播放">
103
+ <i class="fas fa-redo"></i>
104
+ </button>
105
+ </div>
106
+ </div>
107
+ </div>
108
+ </div>
109
+ </main>
110
+
111
+ <!-- 搜索面板 -->
112
+ <div class="search-panel" id="searchPanel" style="display: none;">
113
+ <div class="search-content">
114
+ <div class="search-header">
115
+ <h3>搜索内容</h3>
116
+ <button class="close-btn" id="closeSearch">
117
+ <i class="fas fa-times"></i>
118
+ </button>
119
+ </div>
120
+ <div class="search-body">
121
+ <div class="search-input-container">
122
+ <input type="text" id="searchInput" placeholder="搜索文本内容..." autocomplete="off">
123
+ <button class="search-btn" id="doSearch">
124
+ <i class="fas fa-search"></i>
125
+ </button>
126
+ </div>
127
+ <div class="search-results" id="searchResults">
128
+ <div class="search-empty">输入关键词开始搜索</div>
129
+ </div>
130
+ </div>
131
+ </div>
132
+ </div>
133
+
134
+ <!-- 书签面板 -->
135
+ <div class="bookmark-panel" id="bookmarkPanel" style="display: none;">
136
+ <div class="bookmark-content">
137
+ <div class="bookmark-header">
138
+ <h3>我的书签</h3>
139
+ <button class="close-btn" id="closeBookmark">
140
+ <i class="fas fa-times"></i>
141
+ </button>
142
+ </div>
143
+ <div class="bookmark-body">
144
+ <div class="bookmark-list" id="bookmarkList">
145
+ <div class="bookmark-empty">还没有添加任何书签</div>
146
+ </div>
147
+ </div>
148
+ </div>
149
+ </div>
150
+
151
+ <!-- 设置面板 -->
152
+ <div class="settings-panel" id="settingsPanel" style="display: none;">
153
+ <div class="settings-content">
154
+ <div class="settings-header">
155
+ <h3>设置</h3>
156
+ <button class="close-btn" id="closeSettings">
157
+ <i class="fas fa-times"></i>
158
+ </button>
159
+ </div>
160
+ <div class="settings-body">
161
+ <div class="setting-item">
162
+ <label>自动显示翻译</label>
163
+ <input type="checkbox" id="autoTranslation">
164
+ </div>
165
+ <div class="setting-item">
166
+ <label>播放速度</label>
167
+ <select id="playbackSpeed">
168
+ <option value="0.5">0.5x</option>
169
+ <option value="0.75">0.75x</option>
170
+ <option value="1" selected>1x</option>
171
+ <option value="1.25">1.25x</option>
172
+ <option value="1.5">1.5x</option>
173
+ </select>
174
+ </div>
175
+ <div class="setting-item">
176
+ <label>自动播放下一个</label>
177
+ <input type="checkbox" id="autoPlayNext">
178
+ </div>
179
+ </div>
180
+ </div>
181
+ </div>
182
+
183
+ <!-- 消息提示 -->
184
+ <div class="toast" id="toast"></div>
185
+ </div>
186
+
187
+ <script src="script.js"></script>
188
+ </body>
189
  </html>
requirements.txt ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ # 交互式英语学习应用 - Hugging Face Spaces Docker部署
2
+ # Web服务器依赖
script.js ADDED
@@ -0,0 +1,907 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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.settings = {
18
+ autoTranslation: false,
19
+ playbackSpeed: 1,
20
+ autoPlayNext: false
21
+ };
22
+
23
+ this.init();
24
+ }
25
+
26
+ async init() {
27
+ try {
28
+ await this.loadBookData();
29
+ this.setupEventListeners();
30
+ this.loadSettings();
31
+ this.loadBookmarks();
32
+ this.renderCurrentPage();
33
+ this.updateUI();
34
+ } catch (error) {
35
+ console.error('应用初始化失败:', error);
36
+ this.showToast('应用初始化失败,请刷新页面重试', 'error');
37
+ }
38
+ }
39
+
40
+ async loadBookData() {
41
+ try {
42
+ const response = await fetch('./book_10242.json');
43
+ if (!response.ok) {
44
+ throw new Error('数据文件加载失败');
45
+ }
46
+ const jsonData = await response.json();
47
+
48
+ if (!jsonData.Data) {
49
+ throw new Error('数据格式不正确');
50
+ }
51
+
52
+ this.bookData = JSON.parse(jsonData.Data);
53
+ console.log('书籍数据加载成功:', this.bookData.length, '页');
54
+ } catch (error) {
55
+ console.error('数据加载失败:', error);
56
+ throw error;
57
+ }
58
+ }
59
+
60
+ setupEventListeners() {
61
+ // 页面导航
62
+ document.getElementById('prevBtn').addEventListener('click', () => this.previousPage());
63
+ document.getElementById('nextBtn').addEventListener('click', () => this.nextPage());
64
+
65
+ // 控制按钮
66
+ document.getElementById('translationToggle').addEventListener('click', () => this.toggleTranslation());
67
+ document.getElementById('interactiveToggle').addEventListener('click', () => this.toggleInteractiveAreas());
68
+ document.getElementById('playAllBtn').addEventListener('click', () => this.playAllPieces());
69
+ document.getElementById('bookmarkBtn').addEventListener('click', () => this.toggleBookmark());
70
+ document.getElementById('searchBtn').addEventListener('click', () => this.showSearch());
71
+ document.getElementById('settingsBtn').addEventListener('click', () => this.showSettings());
72
+
73
+ // 音频控制
74
+ document.getElementById('playPauseBtn').addEventListener('click', () => this.togglePlayPause());
75
+ document.getElementById('repeatBtn').addEventListener('click', () => this.repeatAudio());
76
+ document.getElementById('audioTimeline').addEventListener('click', (e) => this.seekAudio(e));
77
+
78
+ // 设置面板
79
+ document.getElementById('closeSettings').addEventListener('click', () => this.hideSettings());
80
+
81
+ // 搜索面板
82
+ document.getElementById('closeSearch').addEventListener('click', () => this.hideSearch());
83
+ document.getElementById('doSearch').addEventListener('click', () => this.performSearch());
84
+ document.getElementById('searchInput').addEventListener('keypress', (e) => {
85
+ if (e.key === 'Enter') this.performSearch();
86
+ });
87
+
88
+ // 书签面板
89
+ document.getElementById('closeBookmark').addEventListener('click', () => this.hideBookmarks());
90
+ document.getElementById('autoTranslation').addEventListener('change', (e) => {
91
+ this.settings.autoTranslation = e.target.checked;
92
+ this.saveSettings();
93
+ });
94
+ document.getElementById('playbackSpeed').addEventListener('change', (e) => {
95
+ this.settings.playbackSpeed = parseFloat(e.target.value);
96
+ this.saveSettings();
97
+ if (this.currentAudio) {
98
+ this.currentAudio.playbackRate = this.settings.playbackSpeed;
99
+ }
100
+ });
101
+ document.getElementById('autoPlayNext').addEventListener('change', (e) => {
102
+ this.settings.autoPlayNext = e.target.checked;
103
+ this.saveSettings();
104
+ });
105
+
106
+ // 键盘快捷键
107
+ document.addEventListener('keydown', (e) => this.handleKeyPress(e));
108
+
109
+ // 点击设置面板外部关闭
110
+ document.getElementById('settingsPanel').addEventListener('click', (e) => {
111
+ if (e.target.id === 'settingsPanel') {
112
+ this.hideSettings();
113
+ }
114
+ });
115
+
116
+ // 音频事件监听
117
+ const audio = document.getElementById('audio');
118
+ audio.addEventListener('loadedmetadata', () => this.updateAudioUI());
119
+ audio.addEventListener('timeupdate', () => this.updateAudioProgress());
120
+ audio.addEventListener('ended', () => this.onAudioEnded());
121
+ audio.addEventListener('error', (e) => {
122
+ console.error('音频播放失败:', e);
123
+ this.showToast('音频播放失败', 'error');
124
+ });
125
+ }
126
+
127
+ renderCurrentPage() {
128
+ if (!this.bookData || this.currentPageIndex >= this.bookData.length) {
129
+ return;
130
+ }
131
+
132
+ const page = this.bookData[this.currentPageIndex];
133
+ const pageContainer = document.getElementById('pageContent');
134
+ const loading = document.getElementById('loading');
135
+
136
+ // 显示加载状态
137
+ loading.style.display = 'block';
138
+ pageContainer.style.display = 'none';
139
+
140
+ // 加载页面图片
141
+ const pageImage = document.getElementById('pageImage');
142
+ pageImage.onload = () => {
143
+ loading.style.display = 'none';
144
+ pageContainer.style.display = 'block';
145
+ this.renderTextPieces(page);
146
+ };
147
+
148
+ pageImage.onerror = () => {
149
+ // 如果原始图片加载失败,尝试加载加密图片
150
+ if (pageImage.src === page.originImgUrl && page.encryptImgUrl) {
151
+ pageImage.src = page.encryptImgUrl;
152
+ } else {
153
+ loading.style.display = 'none';
154
+ this.showToast('页面图片加载失败', 'error');
155
+ }
156
+ };
157
+
158
+ pageImage.src = page.originImgUrl;
159
+ pageImage.alt = `第${page.pageNumber}页`;
160
+ }
161
+
162
+ renderTextPieces(page) {
163
+ const textOverlays = document.getElementById('textOverlays');
164
+ const pageImage = document.getElementById('pageImage');
165
+
166
+ // 清空现有的文本片段
167
+ textOverlays.innerHTML = '';
168
+
169
+ // 等待图片完全加载后渲染文本片段
170
+ const renderPieces = () => {
171
+ // 获取图片的实际位置和尺寸
172
+ const imageRect = pageImage.getBoundingClientRect();
173
+ const overlayRect = textOverlays.getBoundingClientRect();
174
+
175
+ // 计算图片相对于overlay容器的偏移
176
+ const offsetX = imageRect.left - overlayRect.left;
177
+ const offsetY = imageRect.top - overlayRect.top;
178
+
179
+ // 获取图片的实际显示尺寸
180
+ const imageWidth = imageRect.width;
181
+ const imageHeight = imageRect.height;
182
+
183
+ console.log('图片信息:', {
184
+ imageWidth,
185
+ imageHeight,
186
+ offsetX,
187
+ offsetY,
188
+ debugMode: this.debugMode
189
+ });
190
+
191
+ page.pieces.forEach((piece, index) => {
192
+ const textPiece = document.createElement('div');
193
+ textPiece.className = 'text-piece';
194
+ textPiece.dataset.pieceIndex = index;
195
+
196
+ // 在调试模式下添加特殊样式
197
+ if (this.debugMode) {
198
+ textPiece.style.border = '2px solid red';
199
+ textPiece.style.backgroundColor = 'rgba(255, 0, 0, 0.2)';
200
+ }
201
+
202
+ // 根据坐标信息设置位置和大小(coordinate是相对图片的比例坐标)
203
+ const coord = piece.coordinate;
204
+ const left = offsetX + (coord.x * imageWidth);
205
+ const top = offsetY + (coord.y * imageHeight);
206
+ const width = coord.width * imageWidth;
207
+ const height = coord.height * imageHeight;
208
+
209
+ textPiece.style.left = `${left}px`;
210
+ textPiece.style.top = `${top}px`;
211
+ textPiece.style.width = `${width}px`;
212
+ textPiece.style.height = `${height}px`;
213
+
214
+ if (this.debugMode) {
215
+ console.log(`Piece ${index + 1} (${piece.original}):`, {
216
+ coord,
217
+ left,
218
+ top,
219
+ width,
220
+ height
221
+ });
222
+ }
223
+
224
+ // 创建文本内容
225
+ const originalText = document.createElement('div');
226
+ originalText.className = 'piece-text';
227
+ originalText.textContent = piece.original;
228
+
229
+ const translationText = document.createElement('div');
230
+ translationText.className = 'piece-translation';
231
+ translationText.textContent = piece.translation;
232
+
233
+ textPiece.appendChild(originalText);
234
+ textPiece.appendChild(translationText);
235
+
236
+ // 点击事件
237
+ textPiece.addEventListener('click', () => this.playPiece(piece, textPiece));
238
+
239
+ textOverlays.appendChild(textPiece);
240
+ });
241
+
242
+ // 根据设置显示翻译
243
+ if (this.showTranslation || this.settings.autoTranslation) {
244
+ textOverlays.classList.add('show-translation');
245
+ }
246
+ };
247
+
248
+ // 如果图片已经加载完成,直接渲染
249
+ if (pageImage.complete && pageImage.naturalHeight !== 0) {
250
+ renderPieces();
251
+ } else {
252
+ // 否则等待图片加载完成
253
+ pageImage.addEventListener('load', renderPieces, { once: true });
254
+ }
255
+
256
+ // 监听窗口大小变化和图片加载事件
257
+ let resizeTimeout;
258
+ const handleResize = () => {
259
+ clearTimeout(resizeTimeout);
260
+ resizeTimeout = setTimeout(() => {
261
+ if (pageImage.complete && pageImage.naturalHeight !== 0) {
262
+ console.log('窗口大小变化,重新计算坐标');
263
+ renderPieces();
264
+ }
265
+ }, 100);
266
+ };
267
+
268
+ // 移除之前的监听器(如果存在)
269
+ if (pageImage.resizeObserver) {
270
+ pageImage.resizeObserver.disconnect();
271
+ }
272
+
273
+ // 创建新的监听器
274
+ pageImage.resizeObserver = new ResizeObserver(handleResize);
275
+ pageImage.resizeObserver.observe(pageImage);
276
+
277
+ // 同时监听窗口大小变化
278
+ window.addEventListener('resize', handleResize);
279
+ }
280
+
281
+ async playPiece(piece, element) {
282
+ try {
283
+ // 移除其他片段的激活状态
284
+ document.querySelectorAll('.text-piece').forEach(el => {
285
+ el.classList.remove('active', 'playing');
286
+ });
287
+
288
+ // 激活当前片段
289
+ element.classList.add('active');
290
+
291
+ // 显示音频播放器
292
+ const audioPlayer = document.getElementById('audioPlayer');
293
+ audioPlayer.style.display = 'block';
294
+
295
+ // 更新音频文本显示
296
+ const audioText = document.getElementById('audioText');
297
+ audioText.innerHTML = `
298
+ <div style="font-weight: 600; margin-bottom: 0.5rem;">${piece.original}</div>
299
+ <div style="color: var(--text-secondary); font-size: 0.9rem;">${piece.translation}</div>
300
+ `;
301
+
302
+ // 加载并播放音频
303
+ const audio = document.getElementById('audio');
304
+ const audioUrl = piece.originSoundUrl || piece.encryptSoundUrl;
305
+
306
+ if (!audioUrl) {
307
+ this.showToast('该片段没有可用的音频', 'warning');
308
+ return;
309
+ }
310
+
311
+ // 停止当前播放的音频
312
+ if (this.currentAudio && !this.currentAudio.paused) {
313
+ this.currentAudio.pause();
314
+ }
315
+
316
+ audio.src = audioUrl;
317
+ audio.playbackRate = this.settings.playbackSpeed;
318
+ this.currentAudio = audio;
319
+
320
+ // 播放音频
321
+ await audio.play();
322
+ this.isPlaying = true;
323
+ element.classList.add('playing');
324
+ this.updatePlayButton();
325
+
326
+ } catch (error) {
327
+ console.error('音频播放失败:', error);
328
+ this.showToast('音频播放失败', 'error');
329
+ element.classList.remove('active', 'playing');
330
+ }
331
+ }
332
+
333
+ async playAllPieces() {
334
+ const page = this.bookData[this.currentPageIndex];
335
+ if (!page || !page.pieces.length) return;
336
+
337
+ let currentPieceIndex = 0;
338
+
339
+ const playNext = async () => {
340
+ if (currentPieceIndex >= page.pieces.length) {
341
+ this.showToast('整页播放完成', 'success');
342
+ return;
343
+ }
344
+
345
+ const piece = page.pieces[currentPieceIndex];
346
+ const element = document.querySelector(`[data-piece-index="${currentPieceIndex}"]`);
347
+
348
+ if (element) {
349
+ await this.playPiece(piece, element);
350
+
351
+ // 等待当前音频播放完成
352
+ const audio = document.getElementById('audio');
353
+ audio.addEventListener('ended', () => {
354
+ currentPieceIndex++;
355
+ setTimeout(playNext, 500); // 稍微延迟播放下一个
356
+ }, { once: true });
357
+ } else {
358
+ currentPieceIndex++;
359
+ playNext();
360
+ }
361
+ };
362
+
363
+ playNext();
364
+ }
365
+
366
+ togglePlayPause() {
367
+ const audio = document.getElementById('audio');
368
+ if (!audio.src) return;
369
+
370
+ if (this.isPlaying) {
371
+ audio.pause();
372
+ this.isPlaying = false;
373
+ } else {
374
+ audio.play().then(() => {
375
+ this.isPlaying = true;
376
+ }).catch(error => {
377
+ console.error('播放失败:', error);
378
+ this.showToast('播放失败', 'error');
379
+ });
380
+ }
381
+
382
+ this.updatePlayButton();
383
+ }
384
+
385
+ repeatAudio() {
386
+ const audio = document.getElementById('audio');
387
+ if (audio.src) {
388
+ audio.currentTime = 0;
389
+ if (!this.isPlaying) {
390
+ this.togglePlayPause();
391
+ }
392
+ }
393
+ }
394
+
395
+ seekAudio(event) {
396
+ const audio = document.getElementById('audio');
397
+ if (!audio.src || !audio.duration) return;
398
+
399
+ const timeline = document.getElementById('audioTimeline');
400
+ const rect = timeline.getBoundingClientRect();
401
+ const percentage = (event.clientX - rect.left) / rect.width;
402
+ const newTime = percentage * audio.duration;
403
+
404
+ audio.currentTime = newTime;
405
+ }
406
+
407
+ updatePlayButton() {
408
+ const playPauseBtn = document.getElementById('playPauseBtn');
409
+ const icon = playPauseBtn.querySelector('i');
410
+
411
+ if (this.isPlaying) {
412
+ icon.className = 'fas fa-pause';
413
+ } else {
414
+ icon.className = 'fas fa-play';
415
+ }
416
+ }
417
+
418
+ updateAudioUI() {
419
+ const audio = document.getElementById('audio');
420
+ const totalTime = document.getElementById('totalTime');
421
+
422
+ if (audio.duration) {
423
+ totalTime.textContent = this.formatTime(audio.duration);
424
+ }
425
+ }
426
+
427
+ updateAudioProgress() {
428
+ const audio = document.getElementById('audio');
429
+ const progressBar = document.getElementById('audioProgressBar');
430
+ const currentTime = document.getElementById('currentTime');
431
+
432
+ if (audio.duration) {
433
+ const percentage = (audio.currentTime / audio.duration) * 100;
434
+ progressBar.style.width = `${percentage}%`;
435
+ currentTime.textContent = this.formatTime(audio.currentTime);
436
+ }
437
+ }
438
+
439
+ onAudioEnded() {
440
+ this.isPlaying = false;
441
+ this.updatePlayButton();
442
+
443
+ // 移除播放状态
444
+ document.querySelectorAll('.text-piece').forEach(el => {
445
+ el.classList.remove('playing');
446
+ });
447
+
448
+ // 如果启用自动播放下一个
449
+ if (this.settings.autoPlayNext) {
450
+ // 这里可以实现自动播放下一个片段的逻辑
451
+ }
452
+ }
453
+
454
+ toggleTranslation() {
455
+ this.showTranslation = !this.showTranslation;
456
+ const textOverlays = document.getElementById('textOverlays');
457
+ const translationBtn = document.getElementById('translationToggle');
458
+
459
+ if (this.showTranslation) {
460
+ textOverlays.classList.add('show-translation');
461
+ translationBtn.classList.add('active');
462
+ } else {
463
+ textOverlays.classList.remove('show-translation');
464
+ translationBtn.classList.remove('active');
465
+ }
466
+ }
467
+
468
+ toggleInteractiveAreas() {
469
+ this.showInteractiveAreas = !this.showInteractiveAreas;
470
+ const textOverlays = document.getElementById('textOverlays');
471
+ const interactiveBtn = document.getElementById('interactiveToggle');
472
+
473
+ if (this.showInteractiveAreas) {
474
+ textOverlays.classList.add('show-interactive-areas');
475
+ interactiveBtn.classList.add('active');
476
+ interactiveBtn.querySelector('i').className = 'fas fa-eye-slash';
477
+ interactiveBtn.title = '隐藏交互区域';
478
+ } else {
479
+ textOverlays.classList.remove('show-interactive-areas');
480
+ interactiveBtn.classList.remove('active');
481
+ interactiveBtn.querySelector('i').className = 'fas fa-eye';
482
+ interactiveBtn.title = '显示交互区域';
483
+ }
484
+ }
485
+
486
+ previousPage() {
487
+ if (this.currentPageIndex > 0) {
488
+ this.currentPageIndex--;
489
+ this.renderCurrentPage();
490
+ this.updateUI();
491
+ this.stopCurrentAudio();
492
+ }
493
+ }
494
+
495
+ nextPage() {
496
+ if (this.currentPageIndex < this.bookData.length - 1) {
497
+ this.currentPageIndex++;
498
+ this.renderCurrentPage();
499
+ this.updateUI();
500
+ this.stopCurrentAudio();
501
+ }
502
+ }
503
+
504
+ stopCurrentAudio() {
505
+ if (this.currentAudio && !this.currentAudio.paused) {
506
+ this.currentAudio.pause();
507
+ this.isPlaying = false;
508
+ this.updatePlayButton();
509
+ }
510
+
511
+ // 隐藏音频播放器
512
+ document.getElementById('audioPlayer').style.display = 'none';
513
+
514
+ // 移除所有激活状态
515
+ document.querySelectorAll('.text-piece').forEach(el => {
516
+ el.classList.remove('active', 'playing');
517
+ });
518
+ }
519
+
520
+ updateUI() {
521
+ // 更新页面信息
522
+ document.getElementById('currentPage').textContent = this.currentPageIndex + 1;
523
+ document.getElementById('totalPages').textContent = this.bookData.length;
524
+
525
+ // 更新进度条
526
+ const progress = ((this.currentPageIndex + 1) / this.bookData.length) * 100;
527
+ document.getElementById('progressFill').style.width = `${progress}%`;
528
+
529
+ // 更新导航按钮状态
530
+ document.getElementById('prevBtn').disabled = this.currentPageIndex === 0;
531
+ document.getElementById('nextBtn').disabled = this.currentPageIndex === this.bookData.length - 1;
532
+
533
+ // 更新书签按钮状态
534
+ this.updateBookmarkButton();
535
+ }
536
+
537
+ showSettings() {
538
+ document.getElementById('settingsPanel').style.display = 'flex';
539
+
540
+ // 同步当前设置到UI
541
+ document.getElementById('autoTranslation').checked = this.settings.autoTranslation;
542
+ document.getElementById('playbackSpeed').value = this.settings.playbackSpeed;
543
+ document.getElementById('autoPlayNext').checked = this.settings.autoPlayNext;
544
+ }
545
+
546
+ hideSettings() {
547
+ document.getElementById('settingsPanel').style.display = 'none';
548
+ }
549
+
550
+ loadSettings() {
551
+ const saved = localStorage.getItem('learningAppSettings');
552
+ if (saved) {
553
+ try {
554
+ this.settings = { ...this.settings, ...JSON.parse(saved) };
555
+ } catch (error) {
556
+ console.error('设置加载失败:', error);
557
+ }
558
+ }
559
+ }
560
+
561
+ saveSettings() {
562
+ localStorage.setItem('learningAppSettings', JSON.stringify(this.settings));
563
+ }
564
+
565
+ handleKeyPress(event) {
566
+ // 如果设置面板打开,只处理 Escape 键
567
+ if (document.getElementById('settingsPanel').style.display === 'flex') {
568
+ if (event.key === 'Escape') {
569
+ this.hideSettings();
570
+ }
571
+ return;
572
+ }
573
+
574
+ switch (event.key) {
575
+ case 'ArrowLeft':
576
+ event.preventDefault();
577
+ this.previousPage();
578
+ break;
579
+ case 'ArrowRight':
580
+ event.preventDefault();
581
+ this.nextPage();
582
+ break;
583
+ case ' ':
584
+ event.preventDefault();
585
+ this.togglePlayPause();
586
+ break;
587
+ case 't':
588
+ case 'T':
589
+ event.preventDefault();
590
+ this.toggleTranslation();
591
+ break;
592
+ case 'i':
593
+ case 'I':
594
+ event.preventDefault();
595
+ this.toggleInteractiveAreas();
596
+ break;
597
+ case 'r':
598
+ case 'R':
599
+ event.preventDefault();
600
+ this.repeatAudio();
601
+ break;
602
+ case 'p':
603
+ case 'P':
604
+ event.preventDefault();
605
+ this.playAllPieces();
606
+ break;
607
+ case 'b':
608
+ case 'B':
609
+ event.preventDefault();
610
+ this.toggleBookmark();
611
+ break;
612
+ case 'f':
613
+ case 'F':
614
+ event.preventDefault();
615
+ this.showSearch();
616
+ break;
617
+ case 'd':
618
+ case 'D':
619
+ if (event.ctrlKey) {
620
+ event.preventDefault();
621
+ this.toggleDebugMode();
622
+ }
623
+ break;
624
+ }
625
+ }
626
+
627
+ showToast(message, type = 'info') {
628
+ const toast = document.getElementById('toast');
629
+ toast.textContent = message;
630
+ toast.className = `toast ${type} show`;
631
+
632
+ setTimeout(() => {
633
+ toast.classList.remove('show');
634
+ }, 3000);
635
+ }
636
+
637
+ formatTime(seconds) {
638
+ const mins = Math.floor(seconds / 60);
639
+ const secs = Math.floor(seconds % 60);
640
+ return `${mins}:${secs.toString().padStart(2, '0')}`;
641
+ }
642
+
643
+ // 搜索功能
644
+ showSearch() {
645
+ document.getElementById('searchPanel').style.display = 'flex';
646
+ document.getElementById('searchInput').focus();
647
+ }
648
+
649
+ hideSearch() {
650
+ document.getElementById('searchPanel').style.display = 'none';
651
+ document.getElementById('searchInput').value = '';
652
+ this.clearSearchResults();
653
+ }
654
+
655
+ performSearch() {
656
+ const query = document.getElementById('searchInput').value.trim();
657
+ if (!query) {
658
+ this.showToast('请输入搜索关键词', 'warning');
659
+ return;
660
+ }
661
+
662
+ const results = [];
663
+ const regex = new RegExp(query, 'gi');
664
+
665
+ this.bookData.forEach((page, pageIndex) => {
666
+ page.pieces.forEach((piece, pieceIndex) => {
667
+ if (regex.test(piece.original) || regex.test(piece.translation)) {
668
+ results.push({
669
+ pageIndex,
670
+ pieceIndex,
671
+ page: page.pageNumber,
672
+ original: piece.original,
673
+ translation: piece.translation,
674
+ piece
675
+ });
676
+ }
677
+ });
678
+ });
679
+
680
+ this.searchResults = results;
681
+ this.displaySearchResults(query);
682
+ }
683
+
684
+ displaySearchResults(query) {
685
+ const resultsContainer = document.getElementById('searchResults');
686
+
687
+ if (this.searchResults.length === 0) {
688
+ resultsContainer.innerHTML = '<div class="search-empty">未找到匹配的内容</div>';
689
+ return;
690
+ }
691
+
692
+ const regex = new RegExp(`(${query})`, 'gi');
693
+ let html = '';
694
+
695
+ this.searchResults.forEach((result, index) => {
696
+ const highlightedOriginal = result.original.replace(regex, '<span class="search-highlight">$1</span>');
697
+ const highlightedTranslation = result.translation.replace(regex, '<span class="search-highlight">$1</span>');
698
+
699
+ html += `
700
+ <div class="search-result-item" data-result-index="${index}">
701
+ <div class="search-result-page">第 ${result.page} 页</div>
702
+ <div class="search-result-text">${highlightedOriginal}</div>
703
+ <div class="search-result-translation">${highlightedTranslation}</div>
704
+ </div>
705
+ `;
706
+ });
707
+
708
+ resultsContainer.innerHTML = html;
709
+
710
+ // 添加点击事件
711
+ resultsContainer.querySelectorAll('.search-result-item').forEach(item => {
712
+ item.addEventListener('click', (e) => {
713
+ const index = parseInt(e.currentTarget.dataset.resultIndex);
714
+ this.goToSearchResult(index);
715
+ });
716
+ });
717
+ }
718
+
719
+ goToSearchResult(index) {
720
+ const result = this.searchResults[index];
721
+ if (!result) return;
722
+
723
+ this.currentPageIndex = result.pageIndex;
724
+ this.renderCurrentPage();
725
+ this.updateUI();
726
+ this.hideSearch();
727
+
728
+ // 稍微延迟后高亮显示并播放对应片段
729
+ setTimeout(() => {
730
+ const element = document.querySelector(`[data-piece-index="${result.pieceIndex}"]`);
731
+ if (element) {
732
+ element.scrollIntoView({ behavior: 'smooth', block: 'center' });
733
+ this.playPiece(result.piece, element);
734
+ }
735
+ }, 500);
736
+
737
+ this.showToast(`跳转到第 ${result.page} 页`, 'success');
738
+ }
739
+
740
+ clearSearchResults() {
741
+ this.searchResults = [];
742
+ document.getElementById('searchResults').innerHTML = '<div class="search-empty">输入关键词开始搜索</div>';
743
+ }
744
+
745
+ // 书签功能
746
+ toggleBookmark() {
747
+ const isBookmarked = this.bookmarks.some(b => b.pageIndex === this.currentPageIndex);
748
+
749
+ if (isBookmarked) {
750
+ const bookmark = this.bookmarks.find(b => b.pageIndex === this.currentPageIndex);
751
+ this.removeBookmark(bookmark.id);
752
+ } else {
753
+ this.addBookmark();
754
+ }
755
+ }
756
+
757
+ showBookmarks() {
758
+ document.getElementById('bookmarkPanel').style.display = 'flex';
759
+ this.renderBookmarks();
760
+ }
761
+
762
+ hideBookmarks() {
763
+ document.getElementById('bookmarkPanel').style.display = 'none';
764
+ }
765
+
766
+ addBookmark() {
767
+ const currentPage = this.bookData[this.currentPageIndex];
768
+ if (!currentPage) return;
769
+
770
+ const bookmark = {
771
+ id: Date.now(),
772
+ pageIndex: this.currentPageIndex,
773
+ pageNumber: currentPage.pageNumber,
774
+ title: `第 ${currentPage.pageNumber} 页`,
775
+ timestamp: new Date().toISOString()
776
+ };
777
+
778
+ // 检查是否已存在
779
+ const exists = this.bookmarks.some(b => b.pageIndex === this.currentPageIndex);
780
+ if (exists) {
781
+ this.showToast('当前页面已在书签中', 'warning');
782
+ return;
783
+ }
784
+
785
+ this.bookmarks.push(bookmark);
786
+ this.saveBookmarks();
787
+ this.updateBookmarkButton();
788
+ this.showToast('书签添加成功', 'success');
789
+ }
790
+
791
+ removeBookmark(bookmarkId) {
792
+ this.bookmarks = this.bookmarks.filter(b => b.id !== bookmarkId);
793
+ this.saveBookmarks();
794
+ this.updateBookmarkButton();
795
+ this.renderBookmarks();
796
+ this.showToast('书签删除成功', 'success');
797
+ }
798
+
799
+ renderBookmarks() {
800
+ const container = document.getElementById('bookmarkList');
801
+
802
+ if (this.bookmarks.length === 0) {
803
+ container.innerHTML = '<div class="bookmark-empty">还没有添加任何书签</div>';
804
+ return;
805
+ }
806
+
807
+ let html = '';
808
+ this.bookmarks.forEach(bookmark => {
809
+ const date = new Date(bookmark.timestamp);
810
+ const timeString = date.toLocaleDateString() + ' ' + date.toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'});
811
+
812
+ html += `
813
+ <div class="bookmark-item" data-page-index="${bookmark.pageIndex}">
814
+ <div class="bookmark-info">
815
+ <div class="bookmark-page">${bookmark.title}</div>
816
+ <div class="bookmark-time">${timeString}</div>
817
+ </div>
818
+ <div class="bookmark-actions">
819
+ <button class="bookmark-delete" data-bookmark-id="${bookmark.id}">
820
+ <i class="fas fa-times"></i>
821
+ </button>
822
+ </div>
823
+ </div>
824
+ `;
825
+ });
826
+
827
+ container.innerHTML = html;
828
+
829
+ // 添加点击事件
830
+ container.querySelectorAll('.bookmark-item').forEach(item => {
831
+ item.addEventListener('click', (e) => {
832
+ if (e.target.closest('.bookmark-delete')) return;
833
+ const pageIndex = parseInt(item.dataset.pageIndex);
834
+ this.goToBookmark(pageIndex);
835
+ });
836
+ });
837
+
838
+ container.querySelectorAll('.bookmark-delete').forEach(btn => {
839
+ btn.addEventListener('click', (e) => {
840
+ e.stopPropagation();
841
+ const bookmarkId = parseInt(btn.dataset.bookmarkId);
842
+ this.removeBookmark(bookmarkId);
843
+ });
844
+ });
845
+ }
846
+
847
+ goToBookmark(pageIndex) {
848
+ this.currentPageIndex = pageIndex;
849
+ this.renderCurrentPage();
850
+ this.updateUI();
851
+ this.hideBookmarks();
852
+ this.showToast(`跳转到第 ${this.bookData[pageIndex].pageNumber} 页`, 'success');
853
+ }
854
+
855
+ updateBookmarkButton() {
856
+ const btn = document.getElementById('bookmarkBtn');
857
+ const isBookmarked = this.bookmarks.some(b => b.pageIndex === this.currentPageIndex);
858
+
859
+ if (isBookmarked) {
860
+ btn.classList.add('active');
861
+ btn.title = '取消书签';
862
+ } else {
863
+ btn.classList.remove('active');
864
+ btn.title = '添加书签';
865
+ }
866
+ }
867
+
868
+ loadBookmarks() {
869
+ const saved = localStorage.getItem('learningAppBookmarks');
870
+ if (saved) {
871
+ try {
872
+ this.bookmarks = JSON.parse(saved);
873
+ } catch (error) {
874
+ console.error('书签加载失败:', error);
875
+ this.bookmarks = [];
876
+ }
877
+ }
878
+ }
879
+
880
+ saveBookmarks() {
881
+ localStorage.setItem('learningAppBookmarks', JSON.stringify(this.bookmarks));
882
+ }
883
+
884
+ // 调试模式
885
+ toggleDebugMode() {
886
+ this.debugMode = !this.debugMode;
887
+ this.showToast(`调试模式: ${this.debugMode ? '开启' : '关闭'}`, 'info');
888
+
889
+ // 重新渲染当前页面以应用调试样式
890
+ this.renderCurrentPage();
891
+ }
892
+ }
893
+
894
+ // 页面加载完成后初始化应用
895
+ document.addEventListener('DOMContentLoaded', () => {
896
+ new InteractiveLearningApp();
897
+ });
898
+
899
+ // 处理页面可见性变化,暂停音频播放
900
+ document.addEventListener('visibilitychange', () => {
901
+ if (document.hidden) {
902
+ const audio = document.getElementById('audio');
903
+ if (audio && !audio.paused) {
904
+ audio.pause();
905
+ }
906
+ }
907
+ });
style.css CHANGED
@@ -1,28 +1,910 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  body {
2
- padding: 2rem;
3
- font-family: -apple-system, BlinkMacSystemFont, "Arial", sans-serif;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4
  }
5
 
6
- h1 {
7
- font-size: 16px;
8
- margin-top: 0;
 
9
  }
10
 
11
- p {
12
- color: rgb(107, 114, 128);
13
- font-size: 15px;
14
- margin-bottom: 10px;
15
- margin-top: 5px;
16
  }
17
 
18
- .card {
19
- max-width: 620px;
20
- margin: 0 auto;
21
- padding: 16px;
22
- border: 1px solid lightgray;
23
- border-radius: 16px;
24
  }
25
 
26
- .card p:last-child {
27
- margin-bottom: 0;
28
  }
 
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
  }