ibook / script.js
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
Raw
History Blame Contribute Delete
32.5 kB
/**
* 交互式英语学习应用 - 主要逻辑
* 支持点读、翻译显示和音频播放功能
*/
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();
}
}
});