/** * Drama Ling - 工具類別和狀態管理 * 提供應用程序所需的基礎工具函數和狀態管理 */ /** * 應用狀態管理類別 * 簡潔的響應式狀態管理,不依賴外部框架 */ export class AppState { constructor() { this.data = { user: null, isAuthenticated: false, vocabulary: [], currentSession: null, stats: { totalWords: 0, studyTime: 0, achievements: 0, streak: 0, diamonds: 0 }, settings: { theme: 'auto', language: 'zh-TW', soundEnabled: true, notificationsEnabled: true }, offline: false }; this.listeners = new Map(); // 從本地存儲恢復狀態 this.loadFromStorage(); } /** * 添加狀態變化監聽器 */ addListener(event, callback) { if (!this.listeners.has(event)) { this.listeners.set(event, []); } this.listeners.get(event).push(callback); } /** * 移除狀態變化監聽器 */ removeListener(event, callback) { if (this.listeners.has(event)) { const callbacks = this.listeners.get(event); const index = callbacks.indexOf(callback); if (index > -1) { callbacks.splice(index, 1); } } } /** * 觸發狀態變化事件 */ emit(event, data) { if (this.listeners.has(event)) { this.listeners.get(event).forEach(callback => { try { callback(data); } catch (error) { console.error(`狀態事件監聽器錯誤 (${event}):`, error); } }); } } /** * 設定用戶資料 */ setUser(user) { this.data.user = user; this.data.isAuthenticated = !!user; this.saveToStorage(); this.emit('userUpdated', user); this.emit('authStatusChanged', !!user); } /** * 獲取用戶資料 */ getUser() { return this.data.user; } /** * 檢查是否已認證 */ isAuthenticated() { return this.data.isAuthenticated; } /** * 更新統計資料 */ updateStats(newStats) { this.data.stats = { ...this.data.stats, ...newStats }; this.saveToStorage(); this.emit('statsUpdated', this.data.stats); } /** * 獲取統計資料 */ getStats() { return this.data.stats; } /** * 更新設定 */ updateSettings(newSettings) { this.data.settings = { ...this.data.settings, ...newSettings }; this.saveToStorage(); this.emit('settingsUpdated', this.data.settings); } /** * 獲取設定 */ getSettings() { return this.data.settings; } /** * 設定離線狀態 */ setOffline(offline) { this.data.offline = offline; this.emit('offlineStatusChanged', offline); } /** * 檢查是否離線 */ isOffline() { return this.data.offline; } /** * 保存狀態到本地存儲 */ saveToStorage() { try { const stateToSave = { user: this.data.user, settings: this.data.settings, stats: this.data.stats }; localStorage.setItem('drama-ling-state', JSON.stringify(stateToSave)); } catch (error) { console.error('保存狀態失敗:', error); } } /** * 從本地存儲載入狀態 */ loadFromStorage() { try { const savedState = localStorage.getItem('drama-ling-state'); if (savedState) { const parsedState = JSON.parse(savedState); this.data.user = parsedState.user; this.data.isAuthenticated = !!parsedState.user; this.data.settings = { ...this.data.settings, ...parsedState.settings }; this.data.stats = { ...this.data.stats, ...parsedState.stats }; } } catch (error) { console.error('載入狀態失敗:', error); } } /** * 清除所有狀態 */ clear() { this.data = { user: null, isAuthenticated: false, vocabulary: [], currentSession: null, stats: { totalWords: 0, studyTime: 0, achievements: 0, streak: 0, diamonds: 0 }, settings: { theme: 'auto', language: 'zh-TW', soundEnabled: true, notificationsEnabled: true }, offline: false }; localStorage.removeItem('drama-ling-state'); this.emit('stateCleared'); } } /** * 工具函數集合 */ export class Utils { /** * 防抖函數 */ static debounce(func, wait, immediate = false) { let timeout; return function executedFunction(...args) { const later = () => { timeout = null; if (!immediate) func.apply(this, args); }; const callNow = immediate && !timeout; clearTimeout(timeout); timeout = setTimeout(later, wait); if (callNow) func.apply(this, args); }; } /** * 節流函數 */ static throttle(func, limit) { let inThrottle; return function executedFunction(...args) { if (!inThrottle) { func.apply(this, args); inThrottle = true; setTimeout(() => inThrottle = false, limit); } }; } /** * 深度複製對象 */ static deepClone(obj) { if (obj === null || typeof obj !== 'object') { return obj; } if (obj instanceof Date) { return new Date(obj.getTime()); } if (obj instanceof Array) { return obj.map(item => Utils.deepClone(item)); } if (typeof obj === 'object') { const clonedObj = {}; for (const key in obj) { if (obj.hasOwnProperty(key)) { clonedObj[key] = Utils.deepClone(obj[key]); } } return clonedObj; } } /** * 格式化時間 */ static formatTime(seconds) { const hours = Math.floor(seconds / 3600); const minutes = Math.floor((seconds % 3600) / 60); const remainingSeconds = seconds % 60; if (hours > 0) { return `${hours}:${minutes.toString().padStart(2, '0')}:${remainingSeconds.toString().padStart(2, '0')}`; } else { return `${minutes}:${remainingSeconds.toString().padStart(2, '0')}`; } } /** * 格式化數字 */ static formatNumber(num) { if (num >= 1000000) { return (num / 1000000).toFixed(1) + 'M'; } else if (num >= 1000) { return (num / 1000).toFixed(1) + 'K'; } return num.toString(); } /** * 格式化日期 */ static formatDate(date, locale = 'zh-TW') { if (!(date instanceof Date)) { date = new Date(date); } return date.toLocaleDateString(locale, { year: 'numeric', month: 'long', day: 'numeric' }); } /** * 獲取相對時間 */ static getRelativeTime(date, locale = 'zh-TW') { const now = new Date(); const targetDate = new Date(date); const diffInSeconds = Math.floor((now - targetDate) / 1000); const intervals = { year: 31536000, month: 2592000, week: 604800, day: 86400, hour: 3600, minute: 60 }; for (const [unit, seconds] of Object.entries(intervals)) { const interval = Math.floor(diffInSeconds / seconds); if (interval >= 1) { const rtf = new Intl.RelativeTimeFormat(locale, { numeric: 'auto' }); return rtf.format(-interval, unit); } } return '剛剛'; } /** * 產生唯一 ID */ static generateId() { return Date.now().toString(36) + Math.random().toString(36).substr(2); } /** * 驗證電子郵件格式 */ static validateEmail(email) { const re = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; return re.test(email); } /** * 驗證密碼強度 */ static validatePassword(password) { const minLength = 8; const hasUpperCase = /[A-Z]/.test(password); const hasLowerCase = /[a-z]/.test(password); const hasNumbers = /\d/.test(password); const hasNonalphas = /\W/.test(password); return { isValid: password.length >= minLength && hasUpperCase && hasLowerCase && hasNumbers, strength: [hasUpperCase, hasLowerCase, hasNumbers, hasNonalphas].filter(Boolean).length, minLength: password.length >= minLength, hasUpperCase, hasLowerCase, hasNumbers, hasSpecialChars: hasNonalphas }; } /** * 本地存儲輔助函數 */ static storage = { set(key, value) { try { localStorage.setItem(key, JSON.stringify(value)); return true; } catch (error) { console.error('本地存儲設置失敗:', error); return false; } }, get(key, defaultValue = null) { try { const item = localStorage.getItem(key); return item ? JSON.parse(item) : defaultValue; } catch (error) { console.error('本地存儲讀取失敗:', error); return defaultValue; } }, remove(key) { try { localStorage.removeItem(key); return true; } catch (error) { console.error('本地存儲刪除失敗:', error); return false; } }, clear() { try { localStorage.clear(); return true; } catch (error) { console.error('本地存儲清除失敗:', error); return false; } } }; /** * URL 參數處理 */ static url = { getParams() { return new URLSearchParams(window.location.search); }, getParam(key, defaultValue = null) { const params = new URLSearchParams(window.location.search); return params.get(key) || defaultValue; }, setParam(key, value) { const url = new URL(window.location); url.searchParams.set(key, value); window.history.pushState({}, '', url); }, removeParam(key) { const url = new URL(window.location); url.searchParams.delete(key); window.history.pushState({}, '', url); } }; /** * DOM 輔助函數 */ static dom = { /** * 創建元素 */ create(tagName, attributes = {}, textContent = '') { const element = document.createElement(tagName); for (const [key, value] of Object.entries(attributes)) { if (key === 'className') { element.className = value; } else if (key.startsWith('data-')) { element.setAttribute(key, value); } else { element[key] = value; } } if (textContent) { element.textContent = textContent; } return element; }, /** * 查詢元素 */ $(selector, context = document) { return context.querySelector(selector); }, /** * 查詢所有元素 */ $$(selector, context = document) { return Array.from(context.querySelectorAll(selector)); }, /** * 添加事件監聽器 */ on(element, event, handler, options = false) { element.addEventListener(event, handler, options); }, /** * 移除事件監聽器 */ off(element, event, handler, options = false) { element.removeEventListener(event, handler, options); }, /** * 添加類別 */ addClass(element, className) { element.classList.add(className); }, /** * 移除類別 */ removeClass(element, className) { element.classList.remove(className); }, /** * 切換類別 */ toggleClass(element, className) { element.classList.toggle(className); }, /** * 檢查是否有類別 */ hasClass(element, className) { return element.classList.contains(className); } }; /** * 數學輔助函數 */ static math = { /** * 限制數值範圍 */ clamp(value, min, max) { return Math.min(Math.max(value, min), max); }, /** * 線性插值 */ lerp(start, end, factor) { return start + (end - start) * factor; }, /** * 隨機整數 */ randomInt(min, max) { return Math.floor(Math.random() * (max - min + 1)) + min; }, /** * 隨機浮點數 */ randomFloat(min, max) { return Math.random() * (max - min) + min; }, /** * 百分比計算 */ percentage(value, total) { return total > 0 ? (value / total) * 100 : 0; } }; /** * 顏色輔助函數 */ static color = { /** * 十六進制轉 RGB */ hexToRgb(hex) { const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex); return result ? { r: parseInt(result[1], 16), g: parseInt(result[2], 16), b: parseInt(result[3], 16) } : null; }, /** * RGB 轉十六進制 */ rgbToHex(r, g, b) { return "#" + ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1); }, /** * 生成隨機顏色 */ random() { return '#' + Math.floor(Math.random() * 16777215).toString(16); } }; } /** * 事件匯流排 * 用於組件間通信 */ export class EventBus { constructor() { this.events = {}; } /** * 監聽事件 */ on(event, callback) { if (!this.events[event]) { this.events[event] = []; } this.events[event].push(callback); } /** * 觸發事件 */ emit(event, data) { if (this.events[event]) { this.events[event].forEach(callback => { try { callback(data); } catch (error) { console.error(`事件處理器錯誤 (${event}):`, error); } }); } } /** * 移除事件監聽器 */ off(event, callback) { if (this.events[event]) { const index = this.events[event].indexOf(callback); if (index > -1) { this.events[event].splice(index, 1); } } } /** * 清除所有事件監聽器 */ clear() { this.events = {}; } } // 創建全域事件匯流排實例 export const eventBus = new EventBus();