dramaling-app/apps/web/src/modules/VocabularyApp.js

585 lines
21 KiB
JavaScript

// Vocabulary Learning Application Main Module
import { AudioManager } from '../utils/AudioManager.js';
export class VocabularyApp {
constructor(state) {
this.state = state;
this.currentMode = 'flashcard';
this.isCardFlipped = false;
this.isMobileMenuOpen = false;
this.unsubscribe = null;
this.audioManager = new AudioManager();
}
async init() {
// Subscribe to state changes
this.unsubscribe = this.state.subscribe((event, data) => {
this.onStateChange(event, data);
});
// Set initial current word if none selected
if (!this.state.getCurrentWord()) {
const reviewQueue = this.state.getReviewQueue();
const newWords = this.state.getNewWords(1);
const nextWord = reviewQueue[0] || newWords[0];
if (nextWord) {
this.state.setCurrentWord(nextWord);
}
}
console.log('📚 VocabularyApp initialized');
}
render() {
const progress = this.state.getProgress();
const currentWord = this.state.getCurrentWord();
return `
<div class="vocabulary-layout">
${this.renderSidebar()}
${this.renderMainContent(progress, currentWord)}
</div>
`;
}
renderSidebar() {
return `
<!-- 手機版選單按鈕 -->
<button class="mobile-menu-btn" id="mobileMenuBtn">☰</button>
<!-- 側邊欄 -->
<aside class="sidebar ${this.isMobileMenuOpen ? 'open' : ''}" id="sidebar">
<div class="sidebar-header">
<a href="#" class="logo">
🎭 Drama Ling
</a>
</div>
<nav class="sidebar-nav">
<div class="nav-section">
<div class="nav-section-title">主要功能</div>
<a href="#" class="nav-item">
<span class="nav-icon">📊</span>
學習儀表板
</a>
<a href="#" class="nav-item active">
<span class="nav-icon">📚</span>
詞彙學習
</a>
<a href="#" class="nav-item">
<span class="nav-icon">💬</span>
對話練習
</a>
<a href="#" class="nav-item">
<span class="nav-icon">🎭</span>
角色扮演
</a>
<a href="#" class="nav-item">
<span class="nav-icon">🎵</span>
發音練習
</a>
</div>
<div class="nav-section">
<div class="nav-section-title">個人管理</div>
<a href="#" class="nav-item">
<span class="nav-icon">👤</span>
個人檔案
</a>
<a href="#" class="nav-item">
<span class="nav-icon">📈</span>
學習進度
</a>
<a href="#" class="nav-item">
<span class="nav-icon">⚙️</span>
設定
</a>
</div>
<div class="nav-section">
<div class="nav-section-title">訂閱服務</div>
<a href="#" class="nav-item">
<span class="nav-icon">💎</span>
訂閱管理
</a>
</div>
</nav>
<div class="sidebar-footer">
<div class="user-profile">
<div class="user-avatar">張</div>
<div class="user-info">
<div class="user-name">張小明</div>
<div class="user-level">Level 12</div>
</div>
</div>
</div>
</aside>
`;
}
renderMainContent(progress, currentWord) {
return `
<!-- 主內容區 -->
<main class="main-content">
${this.renderPageHeader(progress)}
${this.renderModeSelector()}
${this.renderFlashcardSection(currentWord)}
${this.renderVocabularyList()}
</main>
`;
}
renderPageHeader(progress) {
return `
<div class="page-header">
<div class="header-section">
<div class="header-text">
<h1>詞彙學習</h1>
<p>透過間隔重複和情境學習,有效掌握新詞彙</p>
</div>
<div class="header-stats">
<div class="stat-item">
<span class="stat-value">${progress.learned || 0}</span>
<span class="stat-label">已學詞彙</span>
</div>
<div class="stat-item">
<span class="stat-value">${progress.todayNew || 0}</span>
<span class="stat-label">今日新增</span>
</div>
<div class="stat-item">
<span class="stat-value">${progress.masteryRate || 0}%</span>
<span class="stat-label">掌握程度</span>
</div>
</div>
</div>
</div>
`;
}
renderModeSelector() {
const reviewQueue = this.state.getReviewQueue();
const newWords = this.state.getNewWords();
return `
<!-- 學習模式選擇 -->
<div class="mode-selector">
<div class="mode-card ${this.currentMode === 'flashcard' ? 'active' : ''}" data-mode="flashcard">
<div class="mode-icon">🃏</div>
<h3 class="mode-title">記憶卡片</h3>
<p class="mode-description">透過卡片翻轉快速記憶新詞彙</p>
<div class="mode-progress">
<span>待複習: ${reviewQueue.length}</span>
<span>新詞彙: ${newWords.length}</span>
</div>
</div>
<div class="mode-card ${this.currentMode === 'quiz' ? 'active' : ''}" data-mode="quiz">
<div class="mode-icon">🎯</div>
<h3 class="mode-title">詞彙測驗</h3>
<p class="mode-description">選擇題和填空題測試詞彙掌握</p>
<div class="mode-progress">
<span>正確率: 92%</span>
<span>完成: 45/50</span>
</div>
</div>
<div class="mode-card ${this.currentMode === 'context' ? 'active' : ''}" data-mode="context">
<div class="mode-icon">📖</div>
<h3 class="mode-title">情境學習</h3>
<p class="mode-description">在真實情境中學習詞彙運用</p>
<div class="mode-progress">
<span>場景: 咖啡廳</span>
<span>進度: 3/5</span>
</div>
</div>
</div>
`;
}
renderFlashcardSection(currentWord) {
if (!currentWord) {
return `
<div class="vocabulary-section" id="flashcardSection">
<div class="vocabulary-card">
<div class="no-words-message">
<h3>🎉 太棒了!</h3>
<p>目前沒有需要複習的詞彙</p>
<button class="control-btn primary" onclick="location.reload()">
重新開始學習
</button>
</div>
</div>
</div>
`;
}
return `
<!-- 詞彙卡片學習區 -->
<div class="vocabulary-section ${this.currentMode === 'flashcard' ? '' : 'hidden'}" id="flashcardSection">
<div class="vocabulary-card">
<div class="vocabulary-word">${currentWord.word}</div>
<div class="vocabulary-phonetic">${currentWord.phonetic}</div>
<div class="vocabulary-definition ${this.isCardFlipped ? '' : 'hidden'}">${currentWord.definition}</div>
<div class="vocabulary-example ${this.isCardFlipped ? '' : 'hidden'}">
"${currentWord.example}"<br>
${currentWord.translation}
</div>
<div class="vocabulary-controls">
<button class="control-btn" id="playAudioBtn">
🔊 發音
</button>
<button class="control-btn" id="flipCardBtn">
🔄 ${this.isCardFlipped ? '翻回' : '翻轉'}
</button>
<button class="control-btn primary" id="nextCardBtn">
下一個 →
</button>
</div>
<div class="difficulty-buttons ${this.isCardFlipped ? '' : 'hidden'}">
<button class="difficulty-btn easy" data-difficulty="easy">簡單 (3天後)</button>
<button class="difficulty-btn" data-difficulty="normal">普通 (1天後)</button>
<button class="difficulty-btn hard" data-difficulty="hard">困難 (10分鐘後)</button>
</div>
</div>
</div>
`;
}
renderVocabularyList() {
const allWords = this.state.getAllWords();
return `
<!-- 詞彙清單 -->
<div class="vocabulary-list">
<div class="list-header">
<h2 class="list-title">我的詞彙庫</h2>
<div class="filter-tabs">
<button class="filter-tab active" data-filter="all">全部</button>
<button class="filter-tab" data-filter="learning">學習中</button>
<button class="filter-tab" data-filter="learned">已掌握</button>
<button class="filter-tab" data-filter="new">需複習</button>
</div>
</div>
<div class="vocabulary-items">
${allWords.map(word => this.renderVocabularyItem(word)).join('')}
</div>
</div>
`;
}
renderVocabularyItem(word) {
const statusClass = word.status === 'learned' ? 'learned' :
word.status === 'learning' ? 'learning' : '';
return `
<div class="vocabulary-item" data-word-id="${word.id}">
<div class="word-info">
<div class="mastery-indicator ${statusClass}"></div>
<div class="word-text">
<span class="word-main">${word.word}</span>
<span class="word-definition">${word.definition}</span>
</div>
</div>
<div class="word-status">
<button class="play-btn" data-word="${word.word}">▶</button>
</div>
</div>
`;
}
bindEvents() {
// 手機版選單切換
const mobileMenuBtn = document.getElementById('mobileMenuBtn');
const sidebar = document.getElementById('sidebar');
if (mobileMenuBtn) {
mobileMenuBtn.addEventListener('click', () => {
this.isMobileMenuOpen = !this.isMobileMenuOpen;
sidebar?.classList.toggle('open');
});
}
// 點擊外部關閉側邊欄
document.addEventListener('click', (e) => {
if (window.innerWidth <= 1024 &&
sidebar && !sidebar.contains(e.target) &&
mobileMenuBtn && !mobileMenuBtn.contains(e.target)) {
this.isMobileMenuOpen = false;
sidebar.classList.remove('open');
}
});
// 學習模式切換
document.querySelectorAll('.mode-card').forEach(card => {
card.addEventListener('click', () => {
const mode = card.dataset.mode;
this.switchMode(mode);
});
});
// 詞彙卡片控制
this.bindFlashcardEvents();
// 詞彙清單互動
this.bindVocabularyListEvents();
// 響應式處理
window.addEventListener('resize', () => {
if (window.innerWidth > 1024) {
this.isMobileMenuOpen = false;
sidebar?.classList.remove('open');
}
});
}
bindFlashcardEvents() {
const flipCardBtn = document.getElementById('flipCardBtn');
const nextCardBtn = document.getElementById('nextCardBtn');
const playAudioBtn = document.getElementById('playAudioBtn');
if (flipCardBtn) {
flipCardBtn.addEventListener('click', () => {
this.flipCard();
});
}
if (nextCardBtn) {
nextCardBtn.addEventListener('click', () => {
this.nextCard();
});
}
if (playAudioBtn) {
playAudioBtn.addEventListener('click', () => {
this.playAudio();
});
}
// 難度按鈕
document.querySelectorAll('.difficulty-btn').forEach(btn => {
btn.addEventListener('click', () => {
const difficulty = btn.dataset.difficulty;
this.selectDifficulty(difficulty);
});
});
}
bindVocabularyListEvents() {
// 篩選標籤
document.querySelectorAll('.filter-tab').forEach(tab => {
tab.addEventListener('click', () => {
document.querySelectorAll('.filter-tab').forEach(t => t.classList.remove('active'));
tab.classList.add('active');
const filter = tab.dataset.filter;
this.filterVocabulary(filter);
});
});
// 詞彙項目點擊
document.querySelectorAll('.vocabulary-item').forEach(item => {
item.addEventListener('click', (e) => {
if (!e.target.classList.contains('play-btn')) {
const wordId = item.dataset.wordId;
this.selectWord(wordId);
}
});
});
// 播放按鈕
document.querySelectorAll('.play-btn').forEach(btn => {
btn.addEventListener('click', (e) => {
e.stopPropagation();
const word = btn.dataset.word;
this.playWordAudio(word);
});
});
}
// Event Handlers
switchMode(mode) {
this.currentMode = mode;
// 更新UI
document.querySelectorAll('.mode-card').forEach(card => {
card.classList.toggle('active', card.dataset.mode === mode);
});
// 顯示對應的學習區域
const flashcardSection = document.getElementById('flashcardSection');
if (mode === 'flashcard') {
flashcardSection?.classList.remove('hidden');
} else {
flashcardSection?.classList.add('hidden');
if (mode === 'quiz') {
alert('詞彙測驗模式即將推出!');
} else if (mode === 'context') {
alert('情境學習模式即將推出!');
}
}
}
flipCard() {
this.isCardFlipped = !this.isCardFlipped;
// 更新UI
const definition = document.querySelector('.vocabulary-definition');
const example = document.querySelector('.vocabulary-example');
const difficultyButtons = document.querySelector('.difficulty-buttons');
const flipBtn = document.getElementById('flipCardBtn');
if (this.isCardFlipped) {
definition?.classList.remove('hidden');
example?.classList.remove('hidden');
difficultyButtons?.classList.remove('hidden');
if (flipBtn) flipBtn.textContent = '🔄 翻回';
} else {
definition?.classList.add('hidden');
example?.classList.add('hidden');
difficultyButtons?.classList.add('hidden');
if (flipBtn) flipBtn.textContent = '🔄 翻轉';
}
}
nextCard() {
// 重置翻轉狀態
this.isCardFlipped = false;
// 獲取下一個詞彙
const reviewQueue = this.state.getReviewQueue();
const newWords = this.state.getNewWords(1);
const nextWord = reviewQueue[0] || newWords[0];
if (nextWord) {
this.state.setCurrentWord(nextWord);
} else {
// 沒有更多詞彙時重新渲染
this.updateFlashcardSection();
}
}
selectDifficulty(difficulty) {
const currentWord = this.state.getCurrentWord();
if (currentWord) {
this.state.markWordReview(currentWord.id, difficulty);
this.nextCard();
}
}
selectWord(wordId) {
const words = this.state.getAllWords();
const word = words.find(w => w.id === wordId);
if (word) {
this.state.setCurrentWord(word);
this.switchMode('flashcard');
}
}
filterVocabulary(filter) {
const items = document.querySelectorAll('.vocabulary-item');
items.forEach(item => {
const wordId = item.dataset.wordId;
const words = this.state.getAllWords();
const word = words.find(w => w.id === wordId);
if (!word) return;
let show = true;
switch (filter) {
case 'learning':
show = word.status === 'learning';
break;
case 'learned':
show = word.status === 'learned';
break;
case 'new':
show = word.status === 'new';
break;
case 'all':
default:
show = true;
}
item.style.display = show ? 'flex' : 'none';
});
}
playAudio() {
const currentWord = this.state.getCurrentWord();
if (currentWord) {
this.playWordAudio(currentWord.word);
}
}
async playWordAudio(word) {
try {
console.log(`🔊 Playing pronunciation for: ${word}`);
await this.audioManager.speakWord(word);
} catch (error) {
console.error('Failed to play audio:', error);
// Fallback to alert for now
alert(`🔊 播放 "${word}" 的發音 (${error.message})`);
}
}
// State change handlers
onStateChange(event, data) {
switch (event) {
case 'currentWordChanged':
this.updateFlashcardSection();
break;
case 'progressUpdated':
this.updateProgressStats();
break;
case 'wordUpdated':
case 'wordAdded':
this.updateVocabularyList();
this.updateProgressStats();
break;
}
}
updateFlashcardSection() {
const flashcardSection = document.getElementById('flashcardSection');
if (flashcardSection) {
const currentWord = this.state.getCurrentWord();
flashcardSection.outerHTML = this.renderFlashcardSection(currentWord);
this.bindFlashcardEvents();
}
}
updateProgressStats() {
const progress = this.state.getProgress();
const statValues = document.querySelectorAll('.stat-value');
if (statValues.length >= 3) {
statValues[0].textContent = progress.learned || 0;
statValues[1].textContent = progress.todayNew || 0;
statValues[2].textContent = `${progress.masteryRate || 0}%`;
}
}
updateVocabularyList() {
const vocabularyItems = document.querySelector('.vocabulary-items');
if (vocabularyItems) {
const allWords = this.state.getAllWords();
vocabularyItems.innerHTML = allWords.map(word => this.renderVocabularyItem(word)).join('');
this.bindVocabularyListEvents();
}
}
destroy() {
if (this.unsubscribe) {
this.unsubscribe();
}
}
}