528 lines
14 KiB
JavaScript
528 lines
14 KiB
JavaScript
/**
|
|
* 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 = `
|
|
<div class="error-page">
|
|
<h1>404</h1>
|
|
<p>找不到頁面</p>
|
|
<button class="btn btn-primary" onclick="app.navigateTo('home')">返回首頁</button>
|
|
</div>
|
|
`;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 載入用戶數據
|
|
*/
|
|
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 = `
|
|
<div class="shortcuts-help">
|
|
<h3>鍵盤快捷鍵</h3>
|
|
<div class="shortcut-list">
|
|
<div class="shortcut-item">
|
|
<kbd>Ctrl</kbd> + <kbd>K</kbd>
|
|
<span>開啟搜尋</span>
|
|
</div>
|
|
<div class="shortcut-item">
|
|
<kbd>Ctrl</kbd> + <kbd>/</kbd>
|
|
<span>顯示快捷鍵說明</span>
|
|
</div>
|
|
<div class="shortcut-item">
|
|
<kbd>Alt</kbd> + <kbd>1-5</kbd>
|
|
<span>快速導航</span>
|
|
</div>
|
|
<div class="shortcut-item">
|
|
<kbd>Esc</kbd>
|
|
<span>關閉彈窗</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
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; |