zhangl
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.
c7e7ea8 | /** | |
| * 交互式英语学习应用 - 主要逻辑 | |
| * 支持点读、翻译显示和音频播放功能 | |
| */ | |
| class InteractiveLearningApp { | |
| constructor() { | |
| this.bookData = null; | |
| 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 { | |
| await this.loadBookData(); | |
| this.setupEventListeners(); | |
| this.loadSettings(); | |
| this.loadBookmarks(); | |
| this.renderCurrentPage(); | |
| this.updateUI(); | |
| } catch (error) { | |
| console.error('应用初始化失败:', error); | |
| this.showToast('应用初始化失败,请刷新页面重试', 'error'); | |
| } | |
| } | |
| async loadBookData() { | |
| try { | |
| const response = await fetch('./book_10242.json'); | |
| if (!response.ok) { | |
| throw new Error('数据文件加载失败'); | |
| } | |
| const jsonData = await response.json(); | |
| if (!jsonData.Data) { | |
| throw new Error('数据格式不正确'); | |
| } | |
| this.bookData = JSON.parse(jsonData.Data); | |
| console.log('书籍数据加载成功:', this.bookData.length, '页'); | |
| } catch (error) { | |
| console.error('数据加载失败:', error); | |
| throw error; | |
| } | |
| } | |
| setupEventListeners() { | |
| // 页面导航 | |
| 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('closeSettings').addEventListener('click', () => this.hideSettings()); | |
| // 搜索面板 | |
| document.getElementById('closeSearch').addEventListener('click', () => this.hideSearch()); | |
| document.getElementById('doSearch').addEventListener('click', () => this.performSearch()); | |
| document.getElementById('searchInput').addEventListener('keypress', (e) => { | |
| if (e.key === 'Enter') this.performSearch(); | |
| }); | |
| // 书签面板 | |
| document.getElementById('closeBookmark').addEventListener('click', () => this.hideBookmarks()); | |
| document.getElementById('autoTranslation').addEventListener('change', (e) => { | |
| this.settings.autoTranslation = e.target.checked; | |
| this.saveSettings(); | |
| }); | |
| 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)); | |
| // 点击设置面板外部关闭 | |
| document.getElementById('settingsPanel').addEventListener('click', (e) => { | |
| if (e.target.id === 'settingsPanel') { | |
| this.hideSettings(); | |
| } | |
| }); | |
| // 音频事件监听 | |
| 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'); | |
| }); | |
| } | |
| renderCurrentPage() { | |
| if (!this.bookData || this.currentPageIndex >= this.bookData.length) { | |
| return; | |
| } | |
| const page = this.bookData[this.currentPageIndex]; | |
| const pageContainer = document.getElementById('pageContent'); | |
| const loading = document.getElementById('loading'); | |
| // 显示加载状态 | |
| loading.style.display = 'block'; | |
| pageContainer.style.display = 'none'; | |
| // 加载页面图片 | |
| const pageImage = document.getElementById('pageImage'); | |
| pageImage.onload = () => { | |
| loading.style.display = 'none'; | |
| pageContainer.style.display = 'block'; | |
| this.renderTextPieces(page); | |
| }; | |
| pageImage.onerror = () => { | |
| // 如果原始图片加载失败,尝试加载加密图片 | |
| if (pageImage.src === page.originImgUrl && page.encryptImgUrl) { | |
| pageImage.src = page.encryptImgUrl; | |
| } else { | |
| loading.style.display = 'none'; | |
| this.showToast('页面图片加载失败', 'error'); | |
| } | |
| }; | |
| pageImage.src = page.originImgUrl; | |
| pageImage.alt = `第${page.pageNumber}页`; | |
| } | |
| renderTextPieces(page) { | |
| const textOverlays = document.getElementById('textOverlays'); | |
| const pageImage = document.getElementById('pageImage'); | |
| // 清空现有的文本片段 | |
| textOverlays.innerHTML = ''; | |
| // 等待图片完全加载后渲染文本片段 | |
| const renderPieces = () => { | |
| // 获取图片的实际位置和尺寸 | |
| const imageRect = pageImage.getBoundingClientRect(); | |
| const overlayRect = textOverlays.getBoundingClientRect(); | |
| // 计算图片相对于overlay容器的偏移 | |
| const offsetX = imageRect.left - overlayRect.left; | |
| const offsetY = imageRect.top - overlayRect.top; | |
| // 获取图片的实际显示尺寸 | |
| const imageWidth = imageRect.width; | |
| const imageHeight = imageRect.height; | |
| console.log('图片信息:', { | |
| imageWidth, | |
| imageHeight, | |
| offsetX, | |
| offsetY, | |
| debugMode: this.debugMode | |
| }); | |
| 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)'; | |
| } | |
| // 根据坐标信息设置位置和大小(coordinate是相对图片的比例坐标) | |
| 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`; | |
| if (this.debugMode) { | |
| console.log(`Piece ${index + 1} (${piece.original}):`, { | |
| coord, | |
| left, | |
| top, | |
| width, | |
| height | |
| }); | |
| } | |
| // 创建文本内容 | |
| 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) { | |
| console.log('窗口大小变化,重新计算坐标'); | |
| 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 = ` | |
| <div style="font-weight: 600; margin-bottom: 0.5rem;">${piece.original}</div> | |
| <div style="color: var(--text-secondary); font-size: 0.9rem;">${piece.translation}</div> | |
| `; | |
| // 加载并播放音频 | |
| const audio = document.getElementById('audio'); | |
| const audioUrl = piece.originSoundUrl || piece.encryptSoundUrl; | |
| if (!audioUrl) { | |
| this.showToast('该片段没有可用的音频', 'warning'); | |
| return; | |
| } | |
| // 停止当前播放的音频 | |
| if (this.currentAudio && !this.currentAudio.paused) { | |
| this.currentAudio.pause(); | |
| } | |
| audio.src = audioUrl; | |
| 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() { | |
| const page = this.bookData[this.currentPageIndex]; | |
| if (!page || !page.pieces.length) return; | |
| let currentPieceIndex = 0; | |
| const playNext = async () => { | |
| if (currentPieceIndex >= page.pieces.length) { | |
| this.showToast('整页播放完成', 'success'); | |
| return; | |
| } | |
| const piece = page.pieces[currentPieceIndex]; | |
| const element = document.querySelector(`[data-piece-index="${currentPieceIndex}"]`); | |
| if (element) { | |
| await this.playPiece(piece, element); | |
| // 等待当前音频播放完成 | |
| const audio = document.getElementById('audio'); | |
| audio.addEventListener('ended', () => { | |
| currentPieceIndex++; | |
| setTimeout(playNext, 500); // 稍微延迟播放下一个 | |
| }, { once: true }); | |
| } else { | |
| currentPieceIndex++; | |
| playNext(); | |
| } | |
| }; | |
| playNext(); | |
| } | |
| 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'); | |
| }); | |
| // 如果启用自动播放下一个 | |
| if (this.settings.autoPlayNext) { | |
| // 这里可以实现自动播放下一个片段的逻辑 | |
| } | |
| } | |
| 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'); | |
| interactiveBtn.querySelector('i').className = 'fas fa-eye-slash'; | |
| interactiveBtn.title = '隐藏交互区域'; | |
| } else { | |
| textOverlays.classList.remove('show-interactive-areas'); | |
| interactiveBtn.classList.remove('active'); | |
| interactiveBtn.querySelector('i').className = 'fas fa-eye'; | |
| interactiveBtn.title = '显示交互区域'; | |
| } | |
| } | |
| previousPage() { | |
| if (this.currentPageIndex > 0) { | |
| this.currentPageIndex--; | |
| this.renderCurrentPage(); | |
| this.updateUI(); | |
| this.stopCurrentAudio(); | |
| } | |
| } | |
| nextPage() { | |
| if (this.currentPageIndex < this.bookData.length - 1) { | |
| this.currentPageIndex++; | |
| this.renderCurrentPage(); | |
| this.updateUI(); | |
| this.stopCurrentAudio(); | |
| } | |
| } | |
| 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'); | |
| }); | |
| } | |
| updateUI() { | |
| // 更新页面信息 | |
| document.getElementById('currentPage').textContent = this.currentPageIndex + 1; | |
| document.getElementById('totalPages').textContent = this.bookData.length; | |
| // 更新进度条 | |
| const progress = ((this.currentPageIndex + 1) / this.bookData.length) * 100; | |
| document.getElementById('progressFill').style.width = `${progress}%`; | |
| // 更新导航按钮状态 | |
| document.getElementById('prevBtn').disabled = this.currentPageIndex === 0; | |
| document.getElementById('nextBtn').disabled = this.currentPageIndex === this.bookData.length - 1; | |
| // 更新书签按钮状态 | |
| this.updateBookmarkButton(); | |
| } | |
| showSettings() { | |
| document.getElementById('settingsPanel').style.display = 'flex'; | |
| // 同步当前设置到UI | |
| 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)); | |
| } | |
| handleKeyPress(event) { | |
| // 如果设置面板打开,只处理 Escape 键 | |
| if (document.getElementById('settingsPanel').style.display === 'flex') { | |
| if (event.key === 'Escape') { | |
| this.hideSettings(); | |
| } | |
| 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; | |
| case 'r': | |
| case 'R': | |
| event.preventDefault(); | |
| this.repeatAudio(); | |
| break; | |
| case 'p': | |
| case 'P': | |
| event.preventDefault(); | |
| this.playAllPieces(); | |
| break; | |
| case 'b': | |
| case 'B': | |
| event.preventDefault(); | |
| this.toggleBookmark(); | |
| break; | |
| case 'f': | |
| case 'F': | |
| event.preventDefault(); | |
| this.showSearch(); | |
| break; | |
| case 'd': | |
| case 'D': | |
| if (event.ctrlKey) { | |
| event.preventDefault(); | |
| this.toggleDebugMode(); | |
| } | |
| break; | |
| } | |
| } | |
| showToast(message, type = 'info') { | |
| const toast = document.getElementById('toast'); | |
| toast.textContent = message; | |
| toast.className = `toast ${type} show`; | |
| setTimeout(() => { | |
| toast.classList.remove('show'); | |
| }, 3000); | |
| } | |
| formatTime(seconds) { | |
| const mins = Math.floor(seconds / 60); | |
| const secs = Math.floor(seconds % 60); | |
| return `${mins}:${secs.toString().padStart(2, '0')}`; | |
| } | |
| // 搜索功能 | |
| showSearch() { | |
| document.getElementById('searchPanel').style.display = 'flex'; | |
| document.getElementById('searchInput').focus(); | |
| } | |
| hideSearch() { | |
| document.getElementById('searchPanel').style.display = 'none'; | |
| document.getElementById('searchInput').value = ''; | |
| this.clearSearchResults(); | |
| } | |
| performSearch() { | |
| const query = document.getElementById('searchInput').value.trim(); | |
| if (!query) { | |
| this.showToast('请输入搜索关键词', 'warning'); | |
| return; | |
| } | |
| const results = []; | |
| const regex = new RegExp(query, 'gi'); | |
| this.bookData.forEach((page, pageIndex) => { | |
| page.pieces.forEach((piece, pieceIndex) => { | |
| if (regex.test(piece.original) || regex.test(piece.translation)) { | |
| results.push({ | |
| pageIndex, | |
| pieceIndex, | |
| page: page.pageNumber, | |
| original: piece.original, | |
| translation: piece.translation, | |
| piece | |
| }); | |
| } | |
| }); | |
| }); | |
| this.searchResults = results; | |
| this.displaySearchResults(query); | |
| } | |
| displaySearchResults(query) { | |
| const resultsContainer = document.getElementById('searchResults'); | |
| if (this.searchResults.length === 0) { | |
| resultsContainer.innerHTML = '<div class="search-empty">未找到匹配的内容</div>'; | |
| return; | |
| } | |
| const regex = new RegExp(`(${query})`, 'gi'); | |
| let html = ''; | |
| this.searchResults.forEach((result, index) => { | |
| const highlightedOriginal = result.original.replace(regex, '<span class="search-highlight">$1</span>'); | |
| const highlightedTranslation = result.translation.replace(regex, '<span class="search-highlight">$1</span>'); | |
| html += ` | |
| <div class="search-result-item" data-result-index="${index}"> | |
| <div class="search-result-page">第 ${result.page} 页</div> | |
| <div class="search-result-text">${highlightedOriginal}</div> | |
| <div class="search-result-translation">${highlightedTranslation}</div> | |
| </div> | |
| `; | |
| }); | |
| resultsContainer.innerHTML = html; | |
| // 添加点击事件 | |
| resultsContainer.querySelectorAll('.search-result-item').forEach(item => { | |
| item.addEventListener('click', (e) => { | |
| const index = parseInt(e.currentTarget.dataset.resultIndex); | |
| this.goToSearchResult(index); | |
| }); | |
| }); | |
| } | |
| goToSearchResult(index) { | |
| const result = this.searchResults[index]; | |
| if (!result) return; | |
| this.currentPageIndex = result.pageIndex; | |
| this.renderCurrentPage(); | |
| this.updateUI(); | |
| this.hideSearch(); | |
| // 稍微延迟后高亮显示并播放对应片段 | |
| setTimeout(() => { | |
| const element = document.querySelector(`[data-piece-index="${result.pieceIndex}"]`); | |
| if (element) { | |
| element.scrollIntoView({ behavior: 'smooth', block: 'center' }); | |
| this.playPiece(result.piece, element); | |
| } | |
| }, 500); | |
| this.showToast(`跳转到第 ${result.page} 页`, 'success'); | |
| } | |
| clearSearchResults() { | |
| this.searchResults = []; | |
| document.getElementById('searchResults').innerHTML = '<div class="search-empty">输入关键词开始搜索</div>'; | |
| } | |
| // 书签功能 | |
| toggleBookmark() { | |
| const isBookmarked = this.bookmarks.some(b => b.pageIndex === this.currentPageIndex); | |
| if (isBookmarked) { | |
| const bookmark = this.bookmarks.find(b => b.pageIndex === this.currentPageIndex); | |
| this.removeBookmark(bookmark.id); | |
| } else { | |
| this.addBookmark(); | |
| } | |
| } | |
| showBookmarks() { | |
| document.getElementById('bookmarkPanel').style.display = 'flex'; | |
| this.renderBookmarks(); | |
| } | |
| hideBookmarks() { | |
| document.getElementById('bookmarkPanel').style.display = 'none'; | |
| } | |
| addBookmark() { | |
| const currentPage = this.bookData[this.currentPageIndex]; | |
| if (!currentPage) return; | |
| const bookmark = { | |
| id: Date.now(), | |
| pageIndex: this.currentPageIndex, | |
| pageNumber: currentPage.pageNumber, | |
| title: `第 ${currentPage.pageNumber} 页`, | |
| timestamp: new Date().toISOString() | |
| }; | |
| // 检查是否已存在 | |
| const exists = this.bookmarks.some(b => b.pageIndex === this.currentPageIndex); | |
| if (exists) { | |
| this.showToast('当前页面已在书签中', 'warning'); | |
| return; | |
| } | |
| this.bookmarks.push(bookmark); | |
| this.saveBookmarks(); | |
| this.updateBookmarkButton(); | |
| this.showToast('书签添加成功', 'success'); | |
| } | |
| removeBookmark(bookmarkId) { | |
| this.bookmarks = this.bookmarks.filter(b => b.id !== bookmarkId); | |
| this.saveBookmarks(); | |
| this.updateBookmarkButton(); | |
| this.renderBookmarks(); | |
| this.showToast('书签删除成功', 'success'); | |
| } | |
| renderBookmarks() { | |
| const container = document.getElementById('bookmarkList'); | |
| if (this.bookmarks.length === 0) { | |
| container.innerHTML = '<div class="bookmark-empty">还没有添加任何书签</div>'; | |
| return; | |
| } | |
| let html = ''; | |
| this.bookmarks.forEach(bookmark => { | |
| const date = new Date(bookmark.timestamp); | |
| const timeString = date.toLocaleDateString() + ' ' + date.toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'}); | |
| html += ` | |
| <div class="bookmark-item" data-page-index="${bookmark.pageIndex}"> | |
| <div class="bookmark-info"> | |
| <div class="bookmark-page">${bookmark.title}</div> | |
| <div class="bookmark-time">${timeString}</div> | |
| </div> | |
| <div class="bookmark-actions"> | |
| <button class="bookmark-delete" data-bookmark-id="${bookmark.id}"> | |
| <i class="fas fa-times"></i> | |
| </button> | |
| </div> | |
| </div> | |
| `; | |
| }); | |
| container.innerHTML = html; | |
| // 添加点击事件 | |
| container.querySelectorAll('.bookmark-item').forEach(item => { | |
| item.addEventListener('click', (e) => { | |
| if (e.target.closest('.bookmark-delete')) return; | |
| const pageIndex = parseInt(item.dataset.pageIndex); | |
| this.goToBookmark(pageIndex); | |
| }); | |
| }); | |
| container.querySelectorAll('.bookmark-delete').forEach(btn => { | |
| btn.addEventListener('click', (e) => { | |
| e.stopPropagation(); | |
| const bookmarkId = parseInt(btn.dataset.bookmarkId); | |
| this.removeBookmark(bookmarkId); | |
| }); | |
| }); | |
| } | |
| goToBookmark(pageIndex) { | |
| this.currentPageIndex = pageIndex; | |
| this.renderCurrentPage(); | |
| this.updateUI(); | |
| this.hideBookmarks(); | |
| this.showToast(`跳转到第 ${this.bookData[pageIndex].pageNumber} 页`, 'success'); | |
| } | |
| updateBookmarkButton() { | |
| const btn = document.getElementById('bookmarkBtn'); | |
| const isBookmarked = this.bookmarks.some(b => b.pageIndex === this.currentPageIndex); | |
| if (isBookmarked) { | |
| btn.classList.add('active'); | |
| btn.title = '取消书签'; | |
| } else { | |
| btn.classList.remove('active'); | |
| btn.title = '添加书签'; | |
| } | |
| } | |
| loadBookmarks() { | |
| const saved = localStorage.getItem('learningAppBookmarks'); | |
| if (saved) { | |
| try { | |
| this.bookmarks = JSON.parse(saved); | |
| } catch (error) { | |
| console.error('书签加载失败:', error); | |
| this.bookmarks = []; | |
| } | |
| } | |
| } | |
| saveBookmarks() { | |
| localStorage.setItem('learningAppBookmarks', JSON.stringify(this.bookmarks)); | |
| } | |
| // 调试模式 | |
| toggleDebugMode() { | |
| this.debugMode = !this.debugMode; | |
| this.showToast(`调试模式: ${this.debugMode ? '开启' : '关闭'}`, 'info'); | |
| // 重新渲染当前页面以应用调试样式 | |
| this.renderCurrentPage(); | |
| } | |
| } | |
| // 页面加载完成后初始化应用 | |
| document.addEventListener('DOMContentLoaded', () => { | |
| new InteractiveLearningApp(); | |
| }); | |
| // 处理页面可见性变化,暂停音频播放 | |
| document.addEventListener('visibilitychange', () => { | |
| if (document.hidden) { | |
| const audio = document.getElementById('audio'); | |
| if (audio && !audio.paused) { | |
| audio.pause(); | |
| } | |
| } | |
| }); | |