/** * 通用工具库 * 包含 Toast 通知、主题切换、工具函数等 */ // ============================================ // Toast 通知系统 // ============================================ class ToastManager { constructor() { this.container = null; this.init(); } init() { this.container = document.createElement('div'); this.container.className = 'toast-container'; document.body.appendChild(this.container); } show(message, type = 'info', duration = 4000) { const toast = document.createElement('div'); toast.className = `toast ${type}`; const icon = this.getIcon(type); toast.innerHTML = ` `; this.container.appendChild(toast); // 自动移除 setTimeout(() => { toast.style.animation = 'slideOut 0.3s ease forwards'; setTimeout(() => toast.remove(), 300); }, duration); return toast; } getIcon(type) { const icons = { success: '✓', error: '✕', warning: '⚠', info: 'ℹ' }; return icons[type] || icons.info; } escapeHtml(text) { const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } success(message, duration) { return this.show(message, 'success', duration); } error(message, duration) { return this.show(message, 'error', duration); } warning(message, duration) { return this.show(message, 'warning', duration); } info(message, duration) { return this.show(message, 'info', duration); } } // 全局 Toast 实例 const toast = new ToastManager(); // ============================================ // 主题管理 // ============================================ class ThemeManager { constructor() { this.theme = this.loadTheme(); this.applyTheme(); } loadTheme() { return localStorage.getItem('theme') || 'light'; } saveTheme(theme) { localStorage.setItem('theme', theme); } applyTheme() { document.documentElement.setAttribute('data-theme', this.theme); this.updateToggleButtons(); } toggle() { this.theme = this.theme === 'light' ? 'dark' : 'light'; this.saveTheme(this.theme); this.applyTheme(); } setTheme(theme) { this.theme = theme; this.saveTheme(theme); this.applyTheme(); } updateToggleButtons() { const buttons = document.querySelectorAll('.theme-toggle'); buttons.forEach(btn => { btn.innerHTML = this.theme === 'light' ? '🌙' : '☀️'; btn.title = this.theme === 'light' ? '切换到暗色模式' : '切换到亮色模式'; }); } } // 全局主题实例 const theme = new ThemeManager(); // ============================================ // 加载状态管理 // ============================================ class LoadingManager { constructor() { this.activeLoaders = new Set(); } show(element, text = '加载中...') { if (typeof element === 'string') { element = document.getElementById(element); } if (!element) return; element.classList.add('loading'); element.dataset.originalText = element.innerHTML; element.innerHTML = ` ${text}`; element.disabled = true; this.activeLoaders.add(element); } hide(element) { if (typeof element === 'string') { element = document.getElementById(element); } if (!element) return; element.classList.remove('loading'); if (element.dataset.originalText) { element.innerHTML = element.dataset.originalText; delete element.dataset.originalText; } element.disabled = false; this.activeLoaders.delete(element); } hideAll() { this.activeLoaders.forEach(element => this.hide(element)); } } const loading = new LoadingManager(); // ============================================ // API 请求封装 // ============================================ class ApiClient { constructor(baseUrl = '/api') { this.baseUrl = baseUrl; } async request(path, options = {}) { const url = `${this.baseUrl}${path}`; const defaultOptions = { headers: { 'Content-Type': 'application/json', }, }; const finalOptions = { ...defaultOptions, ...options }; const timeoutMs = Number(finalOptions.timeoutMs || 0); delete finalOptions.timeoutMs; if (finalOptions.body && typeof finalOptions.body === 'object') { finalOptions.body = JSON.stringify(finalOptions.body); } let timeoutId = null; try { if (timeoutMs > 0) { const controller = new AbortController(); finalOptions.signal = controller.signal; timeoutId = setTimeout(() => controller.abort(), timeoutMs); } const response = await fetch(url, finalOptions); const data = await response.json().catch(() => ({})); if (!response.ok) { const error = new Error(data.detail || `HTTP ${response.status}`); error.response = response; error.data = data; throw error; } return data; } catch (error) { if (error.name === 'AbortError') { const timeoutError = new Error('请求超时,请稍后重试'); throw timeoutError; } // 网络错误处理 if (!error.response) { toast.error('网络连接失败,请检查网络'); } throw error; } finally { if (timeoutId) { clearTimeout(timeoutId); } } } get(path, options = {}) { return this.request(path, { ...options, method: 'GET' }); } post(path, body, options = {}) { return this.request(path, { ...options, method: 'POST', body }); } put(path, body, options = {}) { return this.request(path, { ...options, method: 'PUT', body }); } patch(path, body, options = {}) { return this.request(path, { ...options, method: 'PATCH', body }); } delete(path, options = {}) { return this.request(path, { ...options, method: 'DELETE' }); } } const api = new ApiClient(); // ============================================ // 事件委托助手 // ============================================ function delegate(element, eventType, selector, handler) { element.addEventListener(eventType, (e) => { const target = e.target.closest(selector); if (target && element.contains(target)) { handler.call(target, e, target); } }); } // ============================================ // 防抖和节流 // ============================================ function debounce(func, wait) { let timeout; return function executedFunction(...args) { const later = () => { clearTimeout(timeout); func(...args); }; clearTimeout(timeout); timeout = setTimeout(later, wait); }; } function throttle(func, limit) { let inThrottle; return function executedFunction(...args) { if (!inThrottle) { func(...args); inThrottle = true; setTimeout(() => inThrottle = false, limit); } }; } // ============================================ // 格式化工具 // ============================================ const format = { date(dateStr) { if (!dateStr) return '-'; const date = new Date(dateStr); return date.toLocaleString('zh-CN', { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' }); }, dateShort(dateStr) { if (!dateStr) return '-'; const date = new Date(dateStr); return date.toLocaleDateString('zh-CN'); }, relativeTime(dateStr) { if (!dateStr) return '-'; const date = new Date(dateStr); const now = new Date(); const diff = now - date; const seconds = Math.floor(diff / 1000); const minutes = Math.floor(seconds / 60); const hours = Math.floor(minutes / 60); const days = Math.floor(hours / 24); if (seconds < 60) return '刚刚'; if (minutes < 60) return `${minutes} 分钟前`; if (hours < 24) return `${hours} 小时前`; if (days < 7) return `${days} 天前`; return this.dateShort(dateStr); }, bytes(bytes) { if (bytes === 0) return '0 B'; const k = 1024; const sizes = ['B', 'KB', 'MB', 'GB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; }, number(num) { if (num === null || num === undefined) return '-'; return num.toLocaleString('zh-CN'); } }; // ============================================ // 状态映射 // ============================================ const statusMap = { account: { active: { text: '活跃', class: 'active' }, expired: { text: '过期', class: 'expired' }, banned: { text: '封禁', class: 'banned' }, failed: { text: '失败', class: 'failed' } }, task: { pending: { text: '等待中', class: 'pending' }, running: { text: '运行中', class: 'running' }, completed: { text: '已完成', class: 'completed' }, failed: { text: '失败', class: 'failed' }, cancelled: { text: '已取消', class: 'disabled' } }, service: { tempmail: 'Tempmail.lol', outlook: 'Outlook', moe_mail: 'MoeMail', temp_mail: 'Temp-Mail(自部署)', duck_mail: 'DuckMail', freemail: 'Freemail', imap_mail: 'IMAP 邮箱' } }; function getStatusText(type, status) { return statusMap[type]?.[status]?.text || status; } function getStatusClass(type, status) { return statusMap[type]?.[status]?.class || ''; } function getServiceTypeText(type) { return statusMap.service[type] || type; } const accountStatusIconMap = { active: { icon: '🟢', title: '活跃' }, expired: { icon: '🟡', title: '过期' }, banned: { icon: '🔴', title: '封禁' }, failed: { icon: '❌', title: '失败' }, }; function getStatusIcon(status) { const s = accountStatusIconMap[status]; if (!s) return `⚪`; return `${s.icon}`; } // ============================================ // 确认对话框 // ============================================ function confirm(message, title = '确认操作') { return new Promise((resolve) => { const modal = document.createElement('div'); modal.className = 'modal active'; modal.innerHTML = `
`; document.body.appendChild(modal); const cancelBtn = modal.querySelector('#confirm-cancel'); const okBtn = modal.querySelector('#confirm-ok'); cancelBtn.onclick = () => { modal.remove(); resolve(false); }; okBtn.onclick = () => { modal.remove(); resolve(true); }; modal.onclick = (e) => { if (e.target === modal) { modal.remove(); resolve(false); } }; }); } // ============================================ // 复制到剪贴板 // ============================================ async function copyToClipboard(text) { if (navigator.clipboard && navigator.clipboard.writeText) { try { await navigator.clipboard.writeText(text); toast.success('已复制到剪贴板'); return true; } catch (err) { // 降级到 execCommand } } try { const ta = document.createElement('textarea'); ta.value = text; ta.style.cssText = 'position:fixed;top:0;left:0;opacity:0;pointer-events:none;'; document.body.appendChild(ta); ta.focus(); ta.select(); const ok = document.execCommand('copy'); document.body.removeChild(ta); if (ok) { toast.success('已复制到剪贴板'); return true; } throw new Error('execCommand failed'); } catch (err) { toast.error('复制失败'); return false; } } // ============================================ // 本地存储助手 // ============================================ const storage = { get(key, defaultValue = null) { try { const value = localStorage.getItem(key); return value ? JSON.parse(value) : defaultValue; } catch { return defaultValue; } }, set(key, value) { try { localStorage.setItem(key, JSON.stringify(value)); return true; } catch { return false; } }, remove(key) { localStorage.removeItem(key); } }; // ============================================ // 页面初始化 // ============================================ document.addEventListener('DOMContentLoaded', () => { // 初始化主题 theme.applyTheme(); // 全局键盘快捷键 document.addEventListener('keydown', (e) => { // Ctrl/Cmd + K: 聚焦搜索 if ((e.ctrlKey || e.metaKey) && e.key === 'k') { e.preventDefault(); const searchInput = document.querySelector('#search-input, [type="search"]'); if (searchInput) searchInput.focus(); } // Escape: 关闭模态框 if (e.key === 'Escape') { const activeModal = document.querySelector('.modal.active'); if (activeModal) activeModal.classList.remove('active'); } }); }); // 导出全局对象 window.toast = toast; window.theme = theme; window.loading = loading; window.api = api; window.format = format; window.confirm = confirm; window.copyToClipboard = copyToClipboard; window.storage = storage; window.delegate = delegate; window.debounce = debounce; window.throttle = throttle; window.getStatusText = getStatusText; window.getStatusClass = getStatusClass; window.getServiceTypeText = getServiceTypeText; window.getStatusIcon = getStatusIcon;