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

663 lines
16 KiB
JavaScript

/**
* 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();