/**
* 交互式英语学习应用 - 阅读页面逻辑
* 支持点读、翻译显示和音频播放功能
* 适配新的 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();
}
}
});