/** * Drama Ling - 主應用程序 * 原生JavaScript模組化架構 */ import { AppState } from './utils.js'; import { ApiClient } from './api.js'; import { AuthManager } from './auth.js'; import { Sidebar } from './components/sidebar.js'; import { Navbar } from './components/navbar.js'; import { Modal } from './components/modal.js'; import { Toast } from './components/toast.js'; class DramaLingApp { constructor() { this.state = new AppState(); this.api = new ApiClient(this.getApiBaseUrl()); this.auth = new AuthManager(this.api, this.state); // UI 組件 this.sidebar = null; this.navbar = null; this.modal = new Modal(); this.toast = new Toast(); // 當前頁面 this.currentPage = 'home'; this.init(); } /** * 初始化應用程序 */ async init() { try { // 顯示載入畫面 this.showLoading(); // 檢查認證狀態 const isAuthenticated = await this.auth.checkAuthStatus(); // 初始化 UI 組件 this.initializeComponents(); // 設定事件監聽器 this.setupEventListeners(); // 根據認證狀態決定初始頁面 if (isAuthenticated) { await this.loadUserData(); this.showMainApp(); } else { this.showAuthPage(); } // 隱藏載入畫面 this.hideLoading(); } catch (error) { console.error('應用程序初始化失敗:', error); this.toast.show('錯誤', '應用程序載入失敗,請刷新頁面重試', 'error'); this.hideLoading(); } } /** * 獲取 API 基礎 URL */ getApiBaseUrl() { return import.meta.env?.VITE_API_BASE_URL || 'http://localhost:3000/api'; } /** * 初始化 UI 組件 */ initializeComponents() { this.navbar = new Navbar(this); this.sidebar = new Sidebar(this); // 綁定組件到應用狀態 this.state.addListener('userUpdated', (user) => { this.navbar.updateUserInfo(user); this.sidebar.updateUserInfo(user); }); this.state.addListener('statsUpdated', (stats) => { this.navbar.updateStats(stats); }); } /** * 設定事件監聽器 */ setupEventListeners() { // 全域鍵盤快捷鍵 document.addEventListener('keydown', this.handleKeyboardShortcuts.bind(this)); // 路由變化監聽 window.addEventListener('popstate', this.handlePopState.bind(this)); // 網路狀態監聽 window.addEventListener('online', this.handleOnline.bind(this)); window.addEventListener('offline', this.handleOffline.bind(this)); // 頁面可見性變化 document.addEventListener('visibilitychange', this.handleVisibilityChange.bind(this)); // 模組卡片點擊 document.querySelectorAll('.module-card').forEach(card => { card.addEventListener('click', this.handleModuleClick.bind(this)); }); // 選單項目點擊 document.addEventListener('click', (e) => { const menuItem = e.target.closest('.menu-item'); if (menuItem && menuItem.dataset.page) { e.preventDefault(); this.navigateTo(menuItem.dataset.page); } }); } /** * 鍵盤快捷鍵處理 */ handleKeyboardShortcuts(e) { // Ctrl/Cmd + K: 開啟搜尋 if ((e.ctrlKey || e.metaKey) && e.key === 'k') { e.preventDefault(); this.openSearch(); } // Ctrl/Cmd + /: 開啟快捷鍵說明 if ((e.ctrlKey || e.metaKey) && e.key === '/') { e.preventDefault(); this.showShortcutsHelp(); } // Escape: 關閉彈窗和選單 if (e.key === 'Escape') { this.sidebar.close(); this.modal.close(); } // Alt + 數字鍵: 快速導航 if (e.altKey && /^[1-5]$/.test(e.key)) { e.preventDefault(); const pages = ['home', 'vocabulary', 'dialogue', 'profile', 'settings']; const pageIndex = parseInt(e.key) - 1; if (pages[pageIndex]) { this.navigateTo(pages[pageIndex]); } } } /** * 瀏覽器歷史記錄變化處理 */ handlePopState(e) { const page = e.state?.page || 'home'; this.navigateTo(page, false); // false = 不更新歷史記錄 } /** * 網路連線恢復 */ handleOnline() { this.toast.show('網路已連線', '已恢復網路連接', 'success'); this.syncPendingData(); } /** * 網路連線中斷 */ handleOffline() { this.toast.show('網路已中斷', '請檢查網路連接,離線模式已啟用', 'warning'); } /** * 頁面可見性變化 */ handleVisibilityChange() { if (document.hidden) { // 頁面被隱藏時,保存當前狀態 this.saveCurrentState(); } else { // 頁面重新可見時,檢查是否需要更新數據 this.checkForUpdates(); } } /** * 模組卡片點擊處理 */ handleModuleClick(e) { const moduleCard = e.currentTarget; const moduleType = moduleCard.dataset.module; switch (moduleType) { case 'vocabulary': this.navigateTo('vocabulary'); break; case 'dialogue': this.navigateTo('dialogue'); break; default: console.warn(`未知的模組類型: ${moduleType}`); } } /** * 頁面導航 */ navigateTo(page, updateHistory = true) { if (this.currentPage === page) return; // 隱藏當前頁面 this.hidePage(this.currentPage); // 顯示新頁面 this.showPage(page); // 更新當前頁面 this.currentPage = page; // 更新選單狀態 this.updateMenuState(page); // 更新瀏覽器歷史記錄 if (updateHistory) { const url = page === 'home' ? '/' : `/${page}`; history.pushState({ page }, document.title, url); } // 關閉側邊欄(手機版) this.sidebar.close(); // 載入頁面數據 this.loadPageData(page); } /** * 隱藏頁面 */ hidePage(page) { const pageElement = document.getElementById(`${page}-view`); if (pageElement) { pageElement.classList.remove('active'); } } /** * 顯示頁面 */ showPage(page) { const pageElement = document.getElementById(`${page}-view`); if (pageElement) { pageElement.classList.add('active'); } else { // 頁面不存在,需要動態載入 this.loadPageComponent(page); } } /** * 更新選單狀態 */ updateMenuState(activePage) { document.querySelectorAll('.menu-item').forEach(item => { if (item.dataset.page === activePage) { item.classList.add('active'); } else { item.classList.remove('active'); } }); } /** * 載入頁面數據 */ async loadPageData(page) { try { switch (page) { case 'home': await this.loadDashboardData(); break; case 'vocabulary': await this.loadVocabularyData(); break; case 'dialogue': await this.loadDialogueData(); break; case 'profile': await this.loadProfileData(); break; default: console.log(`頁面 ${page} 不需要載入額外數據`); } } catch (error) { console.error(`載入頁面 ${page} 數據失敗:`, error); this.toast.show('載入失敗', '頁面數據載入失敗', 'error'); } } /** * 載入儀表板數據 */ async loadDashboardData() { const stats = await this.api.getUserStats(); this.state.updateStats(stats); this.updateDashboardStats(stats); } /** * 更新儀表板統計 */ updateDashboardStats(stats) { const elements = { totalWords: document.getElementById('total-words'), studyTime: document.getElementById('study-time'), achievements: document.getElementById('achievements') }; if (elements.totalWords) elements.totalWords.textContent = stats.totalWords || 0; if (elements.studyTime) elements.studyTime.textContent = stats.studyTime || 0; if (elements.achievements) elements.achievements.textContent = stats.achievements || 0; } /** * 載入詞彙數據 */ async loadVocabularyData() { // 這將在詞彙模組中實現 console.log('載入詞彙數據...'); } /** * 載入對話數據 */ async loadDialogueData() { // 這將在對話模組中實現 console.log('載入對話數據...'); } /** * 載入個人檔案數據 */ async loadProfileData() { // 這將在個人檔案模組中實現 console.log('載入個人檔案數據...'); } /** * 動態載入頁面組件 */ async loadPageComponent(page) { try { // 動態導入頁面模組 const module = await import(`./pages/${page}.js`); const PageClass = module.default; // 創建頁面實例 const pageInstance = new PageClass(this); await pageInstance.render(); } catch (error) { console.error(`載入頁面組件 ${page} 失敗:`, error); this.show404Page(); } } /** * 顯示 404 頁面 */ show404Page() { const mainContent = document.getElementById('main-content'); if (mainContent) { mainContent.innerHTML = `

404

找不到頁面

`; } } /** * 載入用戶數據 */ async loadUserData() { try { const user = await this.api.getCurrentUser(); this.state.setUser(user); } catch (error) { console.error('載入用戶數據失敗:', error); } } /** * 顯示主應用 */ showMainApp() { const app = document.getElementById('app'); if (app) { app.style.display = 'block'; } } /** * 顯示認證頁面 */ showAuthPage() { this.navigateTo('login'); } /** * 顯示載入畫面 */ showLoading() { const loadingScreen = document.getElementById('app-loading'); if (loadingScreen) { loadingScreen.classList.remove('hidden'); } } /** * 隱藏載入畫面 */ hideLoading() { const loadingScreen = document.getElementById('app-loading'); if (loadingScreen) { setTimeout(() => { loadingScreen.classList.add('hidden'); }, 500); } } /** * 開啟搜尋功能 */ openSearch() { // TODO: 實現搜尋功能 this.toast.show('搜尋', '搜尋功能開發中...', 'info'); } /** * 顯示快捷鍵說明 */ showShortcutsHelp() { const helpContent = `

鍵盤快捷鍵

Ctrl + K 開啟搜尋
Ctrl + / 顯示快捷鍵說明
Alt + 1-5 快速導航
Esc 關閉彈窗
`; this.modal.show('快捷鍵說明', helpContent); } /** * 同步待處理數據 */ async syncPendingData() { // TODO: 實現離線數據同步 console.log('同步待處理數據...'); } /** * 保存當前狀態 */ saveCurrentState() { const state = { page: this.currentPage, timestamp: Date.now() }; localStorage.setItem('app-state', JSON.stringify(state)); } /** * 檢查更新 */ async checkForUpdates() { // TODO: 檢查數據是否需要更新 console.log('檢查更新...'); } /** * 登出 */ async logout() { try { await this.auth.logout(); this.showAuthPage(); this.toast.show('已登出', '您已成功登出', 'success'); } catch (error) { console.error('登出失敗:', error); this.toast.show('登出失敗', '請重試', 'error'); } } } // 等待 DOM 載入完成後初始化應用 document.addEventListener('DOMContentLoaded', () => { window.app = new DramaLingApp(); }); // 導出應用類別供其他模組使用 export default DramaLingApp;