585 lines
21 KiB
JavaScript
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();
|
|
}
|
|
}
|
|
} |