dramaling-app/apps/web-native/assets/js/app.js

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;