/** * 交互式英语学习应用 - 阅读页面逻辑 * 支持点读、翻译显示和音频播放功能 * 适配新的 data 数据结构 */ class InteractiveLearningApp { constructor() { this.bookId = null; this.bookInfo = null; this.pages = []; this.catalog = []; this.currentPageIndex = 0; this.currentAudio = null; this.isPlaying = false; this.showTranslation = false; this.showInteractiveAreas = false; this.bookmarks = []; this.searchResults = []; this.debugMode = false; this.settings = { autoTranslation: false, playbackSpeed: 1, autoPlayNext: false }; this.init(); } async init() { try { // 从URL获取书籍ID const urlParams = new URLSearchParams(window.location.search); this.bookId = urlParams.get('book_id'); if (!this.bookId) { throw new Error('未指定书籍ID'); } await this.loadBookInfo(); await this.loadBookPages(); await this.loadBookCatalog(); this.setupEventListeners(); this.loadSettings(); this.loadBookmarks(); await this.renderCurrentPage(); this.updateUI(); } catch (error) { console.error('应用初始化失败:', error); this.showToast('应用初始化失败: ' + error.message, 'error'); } } /** * 加载书籍信息 */ async loadBookInfo() { try { const response = await fetch(`/api/v2/books/${this.bookId}`); if (!response.ok) { throw new Error('获取书籍信息失败'); } const result = await response.json(); if (!result.success) { throw new Error(result.error || '获取书籍信息失败'); } this.bookInfo = result.book; // 更新页面标题 document.getElementById('bookTitle').innerHTML = ` ${this.bookInfo.market_book_name} `; document.title = `${this.bookInfo.market_book_name} - 交互式英语学习`; console.log('书籍信息:', this.bookInfo); } catch (error) { console.error('加载书籍信息失败:', error); throw error; } } /** * 加载书籍页面列表 */ async loadBookPages() { try { const response = await fetch(`/api/v2/books/${this.bookId}/pages`); if (!response.ok) { throw new Error('获取页面列表失败'); } const result = await response.json(); if (!result.success) { throw new Error(result.error || '获取页面列表失败'); } this.pages = result.pages || []; console.log(`加载了 ${this.pages.length} 页`); } catch (error) { console.error('加载页面列表失败:', error); throw error; } } /** * 加载书籍目录 */ async loadBookCatalog() { try { const response = await fetch(`/api/v2/books/${this.bookId}/catalog`); if (!response.ok) { console.warn('获取目录失败'); return; } const result = await response.json(); if (result.success) { this.catalog = result.catalog || []; console.log(`加载了 ${this.catalog.length} 个目录项`); } } catch (error) { console.warn('加载目录失败:', error); } } /** * 获取资源URL(支持本地data目录) */ getResourceUrl(relativePath) { if (!relativePath) return ''; // 资源路径格式: "168_一年级上册/images/page_001.jpg" return `data/${relativePath}`; } setupEventListeners() { // 返回按钮 document.getElementById('backBtn').addEventListener('click', () => { window.location.href = '/'; }); // 目录按钮 document.getElementById('catalogBtn').addEventListener('click', () => this.showCatalog()); // 页面导航 document.getElementById('prevBtn').addEventListener('click', () => this.previousPage()); document.getElementById('nextBtn').addEventListener('click', () => this.nextPage()); // 控制按钮 document.getElementById('translationToggle').addEventListener('click', () => this.toggleTranslation()); document.getElementById('interactiveToggle').addEventListener('click', () => this.toggleInteractiveAreas()); document.getElementById('playAllBtn').addEventListener('click', () => this.playAllPieces()); document.getElementById('bookmarkBtn').addEventListener('click', () => this.toggleBookmark()); document.getElementById('searchBtn').addEventListener('click', () => this.showSearch()); document.getElementById('settingsBtn').addEventListener('click', () => this.showSettings()); // 音频控制 document.getElementById('playPauseBtn').addEventListener('click', () => this.togglePlayPause()); document.getElementById('repeatBtn').addEventListener('click', () => this.repeatAudio()); document.getElementById('audioTimeline').addEventListener('click', (e) => this.seekAudio(e)); // 关闭按钮 document.getElementById('closeCatalog')?.addEventListener('click', () => this.hideCatalog()); document.getElementById('closeSettings').addEventListener('click', () => this.hideSettings()); document.getElementById('closeSearch').addEventListener('click', () => this.hideSearch()); document.getElementById('closeBookmark')?.addEventListener('click', () => this.hideBookmarks()); // 搜索 document.getElementById('doSearch').addEventListener('click', () => this.performSearch()); document.getElementById('searchInput').addEventListener('keypress', (e) => { if (e.key === 'Enter') this.performSearch(); }); // 设置 document.getElementById('autoTranslation').addEventListener('change', (e) => { this.settings.autoTranslation = e.target.checked; this.saveSettings(); if (e.target.checked) { document.getElementById('textOverlays').classList.add('show-translation'); } }); document.getElementById('playbackSpeed').addEventListener('change', (e) => { this.settings.playbackSpeed = parseFloat(e.target.value); this.saveSettings(); if (this.currentAudio) { this.currentAudio.playbackRate = this.settings.playbackSpeed; } }); document.getElementById('autoPlayNext').addEventListener('change', (e) => { this.settings.autoPlayNext = e.target.checked; this.saveSettings(); }); // 键盘快捷键 document.addEventListener('keydown', (e) => this.handleKeyPress(e)); // 音频事件 const audio = document.getElementById('audio'); audio.addEventListener('loadedmetadata', () => this.updateAudioUI()); audio.addEventListener('timeupdate', () => this.updateAudioProgress()); audio.addEventListener('ended', () => this.onAudioEnded()); audio.addEventListener('error', (e) => { console.error('音频播放失败:', e); this.showToast('音频播放失败', 'error'); }); } /** * 渲染当前页面 */ async renderCurrentPage() { if (!this.pages || this.currentPageIndex >= this.pages.length) { return; } const pageContainer = document.getElementById('pageContent'); const loading = document.getElementById('loading'); loading.style.display = 'block'; pageContainer.style.display = 'none'; try { // 获取页面信息 const pageInfo = this.pages[this.currentPageIndex]; const pageNumber = pageInfo.page_number; // 加载页面详细内容 const response = await fetch(`/api/v2/books/${this.bookId}/pages/${pageNumber}`); if (!response.ok) { throw new Error(`加载第 ${pageNumber} 页失败`); } const result = await response.json(); if (!result.success) { throw new Error(result.error || '加载页面内容失败'); } const page = result.page; // 加载页面图片 const pageImage = document.getElementById('pageImage'); pageImage.onload = () => { loading.style.display = 'none'; pageContainer.style.display = 'block'; this.renderTextPieces(page); }; pageImage.onerror = () => { loading.style.display = 'none'; this.showToast('页面图片加载失败', 'error'); }; const imageUrl = this.getResourceUrl(page.origin_img_url); pageImage.src = imageUrl; pageImage.alt = `第${pageNumber}页`; } catch (error) { loading.style.display = 'none'; console.error('渲染页面失败:', error); this.showToast('页面加载失败: ' + error.message, 'error'); } } /** * 渲染文本片段 */ renderTextPieces(page) { const textOverlays = document.getElementById('textOverlays'); const pageImage = document.getElementById('pageImage'); textOverlays.innerHTML = ''; const renderPieces = () => { const imageRect = pageImage.getBoundingClientRect(); const overlayRect = textOverlays.getBoundingClientRect(); const offsetX = imageRect.left - overlayRect.left; const offsetY = imageRect.top - overlayRect.top; const imageWidth = imageRect.width; const imageHeight = imageRect.height; if (!page.pieces || page.pieces.length === 0) { console.warn('页面没有内容片段'); return; } page.pieces.forEach((piece, index) => { const textPiece = document.createElement('div'); textPiece.className = 'text-piece'; textPiece.dataset.pieceIndex = index; if (this.debugMode) { textPiece.style.border = '2px solid red'; textPiece.style.backgroundColor = 'rgba(255, 0, 0, 0.2)'; } // 设置位置和大小 const coord = piece.coordinate; const left = offsetX + (coord.x * imageWidth); const top = offsetY + (coord.y * imageHeight); const width = coord.width * imageWidth; const height = coord.height * imageHeight; textPiece.style.left = `${left}px`; textPiece.style.top = `${top}px`; textPiece.style.width = `${width}px`; textPiece.style.height = `${height}px`; // 创建文本内容 const originalText = document.createElement('div'); originalText.className = 'piece-text'; originalText.textContent = piece.original; const translationText = document.createElement('div'); translationText.className = 'piece-translation'; translationText.textContent = piece.translation; textPiece.appendChild(originalText); textPiece.appendChild(translationText); // 点击事件 textPiece.addEventListener('click', () => this.playPiece(piece, textPiece)); textOverlays.appendChild(textPiece); }); // 根据设置显示翻译 if (this.showTranslation || this.settings.autoTranslation) { textOverlays.classList.add('show-translation'); } }; if (pageImage.complete && pageImage.naturalHeight !== 0) { renderPieces(); } else { pageImage.addEventListener('load', renderPieces, { once: true }); } // 监听窗口大小变化 let resizeTimeout; const handleResize = () => { clearTimeout(resizeTimeout); resizeTimeout = setTimeout(() => { if (pageImage.complete && pageImage.naturalHeight !== 0) { renderPieces(); } }, 100); }; if (pageImage.resizeObserver) { pageImage.resizeObserver.disconnect(); } pageImage.resizeObserver = new ResizeObserver(handleResize); pageImage.resizeObserver.observe(pageImage); window.addEventListener('resize', handleResize); } /** * 播放片段音频 */ async playPiece(piece, element) { try { document.querySelectorAll('.text-piece').forEach(el => { el.classList.remove('active', 'playing'); }); element.classList.add('active'); const audioPlayer = document.getElementById('audioPlayer'); audioPlayer.style.display = 'block'; const audioText = document.getElementById('audioText'); audioText.innerHTML = `
${piece.original}
${piece.translation}
`; const audio = document.getElementById('audio'); const audioUrl = piece.origin_sound_url || piece.encrypt_sound_url; if (!audioUrl) { this.showToast('该片段没有可用的音频', 'warning'); return; } if (this.currentAudio && !this.currentAudio.paused) { this.currentAudio.pause(); } const fullAudioUrl = this.getResourceUrl(audioUrl); audio.src = fullAudioUrl; audio.playbackRate = this.settings.playbackSpeed; this.currentAudio = audio; await audio.play(); this.isPlaying = true; element.classList.add('playing'); this.updatePlayButton(); } catch (error) { console.error('音频播放失败:', error); this.showToast('音频播放失败', 'error'); element.classList.remove('active', 'playing'); } } /** * 播放整页音频 */ async playAllPieces() { this.showToast('整页播放功能开发中...', 'info'); } /** * 目录相关 */ showCatalog() { const panel = document.getElementById('catalogPanel'); panel.style.display = 'flex'; this.renderCatalog(); } hideCatalog() { document.getElementById('catalogPanel').style.display = 'none'; } renderCatalog() { const catalogList = document.getElementById('catalogList'); if (!this.catalog || this.catalog.length === 0) { // 空目录时,显示页码列表 catalogList.innerHTML = `

📖 此书暂无目录信息

共 ${this.pages.length} 页,可通过页面导航按钮浏览

`; return; } catalogList.innerHTML = ''; // 如果只有一个目录项,也显示提示 if (this.catalog.length === 1) { const notice = document.createElement('div'); notice.style.cssText = 'padding: 0.75rem; background: #fef3c7; border-radius: 8px; margin-bottom: 1rem; font-size: 0.9rem;'; notice.innerHTML = ` 此书仅有 1 个目录项,建议使用页面导航浏览 `; catalogList.appendChild(notice); } this.catalog.forEach(item => { const catalogItem = document.createElement('div'); catalogItem.className = 'catalog-item'; catalogItem.innerHTML = `
${item.catalog_name || ''}
${item.catalog_name_cn || ''}
P${item.start_page}${item.start_page !== item.end_page ? '-' + item.end_page : ''}
`; catalogItem.addEventListener('click', () => { this.goToPage(item.start_page); this.hideCatalog(); }); catalogList.appendChild(catalogItem); }); } goToPage(pageNumber) { // 找到对应的页面索引 const index = this.pages.findIndex(p => p.page_number === pageNumber); if (index >= 0) { this.currentPageIndex = index; this.renderCurrentPage(); this.updateUI(); this.stopCurrentAudio(); } } /** * 页面导航 */ previousPage() { if (this.currentPageIndex > 0) { this.currentPageIndex--; this.renderCurrentPage(); this.updateUI(); this.stopCurrentAudio(); } } nextPage() { if (this.currentPageIndex < this.pages.length - 1) { this.currentPageIndex++; this.renderCurrentPage(); this.updateUI(); this.stopCurrentAudio(); } } /** * UI更新 */ updateUI() { const currentPage = this.pages[this.currentPageIndex]; document.getElementById('currentPage').textContent = currentPage ? currentPage.page_number : 0; document.getElementById('totalPages').textContent = this.pages.length || 0; const progress = this.pages.length ? ((this.currentPageIndex + 1) / this.pages.length) * 100 : 0; document.getElementById('progressFill').style.width = `${progress}%`; document.getElementById('prevBtn').disabled = this.currentPageIndex === 0; document.getElementById('nextBtn').disabled = this.currentPageIndex === this.pages.length - 1; this.updateBookmarkButton(); } /** * 翻译和交互区域切换 */ toggleTranslation() { this.showTranslation = !this.showTranslation; const textOverlays = document.getElementById('textOverlays'); const translationBtn = document.getElementById('translationToggle'); if (this.showTranslation) { textOverlays.classList.add('show-translation'); translationBtn.classList.add('active'); } else { textOverlays.classList.remove('show-translation'); translationBtn.classList.remove('active'); } } toggleInteractiveAreas() { this.showInteractiveAreas = !this.showInteractiveAreas; const textOverlays = document.getElementById('textOverlays'); const interactiveBtn = document.getElementById('interactiveToggle'); if (this.showInteractiveAreas) { textOverlays.classList.add('show-interactive-areas'); interactiveBtn.classList.add('active'); } else { textOverlays.classList.remove('show-interactive-areas'); interactiveBtn.classList.remove('active'); } } /** * 音频控制 */ togglePlayPause() { const audio = document.getElementById('audio'); if (!audio.src) return; if (this.isPlaying) { audio.pause(); this.isPlaying = false; } else { audio.play().then(() => { this.isPlaying = true; }).catch(error => { console.error('播放失败:', error); this.showToast('播放失败', 'error'); }); } this.updatePlayButton(); } repeatAudio() { const audio = document.getElementById('audio'); if (audio.src) { audio.currentTime = 0; if (!this.isPlaying) { this.togglePlayPause(); } } } seekAudio(event) { const audio = document.getElementById('audio'); if (!audio.src || !audio.duration) return; const timeline = document.getElementById('audioTimeline'); const rect = timeline.getBoundingClientRect(); const percentage = (event.clientX - rect.left) / rect.width; const newTime = percentage * audio.duration; audio.currentTime = newTime; } updatePlayButton() { const playPauseBtn = document.getElementById('playPauseBtn'); const icon = playPauseBtn.querySelector('i'); if (this.isPlaying) { icon.className = 'fas fa-pause'; } else { icon.className = 'fas fa-play'; } } updateAudioUI() { const audio = document.getElementById('audio'); const totalTime = document.getElementById('totalTime'); if (audio.duration) { totalTime.textContent = this.formatTime(audio.duration); } } updateAudioProgress() { const audio = document.getElementById('audio'); const progressBar = document.getElementById('audioProgressBar'); const currentTime = document.getElementById('currentTime'); if (audio.duration) { const percentage = (audio.currentTime / audio.duration) * 100; progressBar.style.width = `${percentage}%`; currentTime.textContent = this.formatTime(audio.currentTime); } } onAudioEnded() { this.isPlaying = false; this.updatePlayButton(); document.querySelectorAll('.text-piece').forEach(el => { el.classList.remove('playing'); }); } stopCurrentAudio() { if (this.currentAudio && !this.currentAudio.paused) { this.currentAudio.pause(); this.isPlaying = false; this.updatePlayButton(); } document.getElementById('audioPlayer').style.display = 'none'; document.querySelectorAll('.text-piece').forEach(el => { el.classList.remove('active', 'playing'); }); } /** * 搜索功能 */ showSearch() { document.getElementById('searchPanel').style.display = 'flex'; document.getElementById('searchInput').focus(); } hideSearch() { document.getElementById('searchPanel').style.display = 'none'; document.getElementById('searchInput').value = ''; } async performSearch() { const keyword = document.getElementById('searchInput').value.trim(); if (!keyword) { this.showToast('请输入搜索关键词', 'warning'); return; } try { const response = await fetch(`/api/v2/books/${this.bookId}/search?keyword=${encodeURIComponent(keyword)}&limit=20`); if (!response.ok) { throw new Error('搜索失败'); } const result = await response.json(); if (!result.success) { throw new Error(result.error || '搜索失败'); } this.searchResults = result.results || []; this.displaySearchResults(keyword); } catch (error) { console.error('搜索失败:', error); this.showToast('搜索失败: ' + error.message, 'error'); } } displaySearchResults(keyword) { const resultsContainer = document.getElementById('searchResults'); if (this.searchResults.length === 0) { resultsContainer.innerHTML = '
未找到匹配的内容
'; return; } resultsContainer.innerHTML = ''; this.searchResults.forEach((result, index) => { const resultItem = document.createElement('div'); resultItem.className = 'search-result-item'; resultItem.innerHTML = `
第 ${result.page_number} 页
${this.highlightKeyword(result.original, keyword)}
${this.highlightKeyword(result.translation, keyword)}
`; resultItem.addEventListener('click', () => { this.goToPage(result.page_number); this.hideSearch(); }); resultsContainer.appendChild(resultItem); }); } highlightKeyword(text, keyword) { if (!text || !keyword) return text; const regex = new RegExp(`(${keyword})`, 'gi'); return text.replace(regex, '$1'); } /** * 设置相关 */ showSettings() { document.getElementById('settingsPanel').style.display = 'flex'; document.getElementById('autoTranslation').checked = this.settings.autoTranslation; document.getElementById('playbackSpeed').value = this.settings.playbackSpeed; document.getElementById('autoPlayNext').checked = this.settings.autoPlayNext; } hideSettings() { document.getElementById('settingsPanel').style.display = 'none'; } loadSettings() { const saved = localStorage.getItem('learningAppSettings'); if (saved) { try { this.settings = { ...this.settings, ...JSON.parse(saved) }; } catch (error) { console.error('设置加载失败:', error); } } } saveSettings() { localStorage.setItem('learningAppSettings', JSON.stringify(this.settings)); } /** * 书签功能 */ toggleBookmark() { this.showToast('书签功能开发中...', 'info'); } updateBookmarkButton() { // 实现书签按钮更新逻辑 } loadBookmarks() { // 实现书签加载逻辑 } hideBookmarks() { document.getElementById('bookmarkPanel').style.display = 'none'; } /** * 键盘快捷键 */ handleKeyPress(event) { if (document.getElementById('settingsPanel').style.display === 'flex' || document.getElementById('searchPanel').style.display === 'flex') { if (event.key === 'Escape') { this.hideSettings(); this.hideSearch(); } return; } switch (event.key) { case 'ArrowLeft': event.preventDefault(); this.previousPage(); break; case 'ArrowRight': event.preventDefault(); this.nextPage(); break; case ' ': event.preventDefault(); this.togglePlayPause(); break; case 't': case 'T': event.preventDefault(); this.toggleTranslation(); break; case 'i': case 'I': event.preventDefault(); this.toggleInteractiveAreas(); break; } } /** * 工具方法 */ formatTime(seconds) { const mins = Math.floor(seconds / 60); const secs = Math.floor(seconds % 60); return `${mins}:${secs.toString().padStart(2, '0')}`; } showToast(message, type = 'info') { const toast = document.getElementById('toast'); toast.textContent = message; toast.className = `toast ${type} show`; setTimeout(() => { toast.classList.remove('show'); }, 3000); } } // 页面加载完成后初始化应用 document.addEventListener('DOMContentLoaded', () => { new InteractiveLearningApp(); }); // 处理页面可见性变化 document.addEventListener('visibilitychange', () => { if (document.hidden) { const audio = document.getElementById('audio'); if (audio && !audio.paused) { audio.pause(); } } });