/** * YCat Toast Notification Component * 全局 Toast 提示组件,替代 alert() * 支持 success/error/warning/info 四种类型 */ const Toast = { container: null, toasts: [], maxToasts: 5, init() { if (this.container) return; this.container = document.createElement('div'); this.container.id = 'toastContainer'; this.container.style.cssText = ` position: fixed; top: 20px; right: 20px; z-index: 100000; display: flex; flex-direction: column; gap: 10px; pointer-events: none; max-width: 400px; width: calc(100% - 40px); `; document.body.appendChild(this.container); // Mobile positioning if (window.innerWidth <= 768) { this.container.style.top = 'auto'; this.container.style.bottom = '20px'; this.container.style.right = '10px'; this.container.style.left = '10px'; this.container.style.maxWidth = '100%'; this.container.style.width = 'auto'; } }, /** * Show a toast notification * @param {string} message - 消息内容 * @param {string} type - success/error/warning/info * @param {number} duration - 显示时长(ms),默认3000 */ show(message, type = 'info', duration = 3000) { this.init(); const toast = document.createElement('div'); toast.className = `ycat-toast ycat-toast-${type}`; const icons = { success: '✅', error: '❌', warning: '⚠️', info: 'ℹ️' }; const bgColors = { success: '#10b981', error: '#ef4444', warning: '#f59e0b', info: '#6366f1' }; toast.innerHTML = `
${icons[type] || icons.info}
${this._escapeHtml(message)}
`; toast.style.cssText = ` display: flex; align-items: center; gap: 10px; padding: 14px 18px; background: white; border-radius: 12px; box-shadow: 0 8px 30px rgba(0,0,0,0.12); border-left: 4px solid ${bgColors[type] || bgColors.info}; pointer-events: auto; animation: toastSlideIn 0.35s cubic-bezier(0.21, 1.02, 0.73, 1); position: relative; overflow: hidden; font-size: 14px; line-height: 1.5; `; // Progress bar const progressBar = document.createElement('div'); progressBar.style.cssText = ` position: absolute; bottom: 0; left: 0; height: 3px; background: ${bgColors[type] || bgColors.info}; opacity: 0.6; border-radius: 0 0 0 12px; animation: toastProgress ${duration}ms linear forwards; `; toast.appendChild(progressBar); // Add styles if not exists if (!document.getElementById('toast-styles')) { const style = document.createElement('style'); style.id = 'toast-styles'; style.textContent = ` @keyframes toastSlideIn { from { transform: translateX(100%); opacity: 0; } to { transform: translateX(0); opacity: 1; } } @keyframes toastSlideOut { from { transform: translateX(0); opacity: 1; } to { transform: translateX(100%); opacity: 0; } } @keyframes toastProgress { from { width: 100%; } to { width: 0%; } } .ycat-toast-icon { flex-shrink: 0; font-size: 18px; } .ycat-toast-message { flex: 1; color: #1f2937; word-break: break-word; } .ycat-toast-close { flex-shrink: 0; background: none; border: none; color: #9ca3af; cursor: pointer; font-size: 16px; padding: 0 2px; line-height: 1; transition: color 0.2s; } .ycat-toast-close:hover { color: #374151; } .ycat-toast-success .ycat-toast-icon { color: #10b981; } .ycat-toast-error .ycat-toast-icon { color: #ef4444; } .ycat-toast-warning .ycat-toast-icon { color: #f59e0b; } .ycat-toast-info .ycat-toast-icon { color: #6366f1; } `; document.head.appendChild(style); } // Limit toast count if (this.toasts.length >= this.maxToasts) { this.dismiss(this.toasts[0]); } this.container.appendChild(toast); this.toasts.push(toast); // Auto dismiss if (duration > 0) { toast._timer = setTimeout(() => this.dismiss(toast), duration); } return toast; }, /** * Dismiss a toast */ dismiss(toast) { if (!toast || !toast.parentNode) return; clearTimeout(toast._timer); toast.style.animation = 'toastSlideOut 0.3s ease forwards'; setTimeout(() => { if (toast.parentNode) toast.parentNode.removeChild(toast); this.toasts = this.toasts.filter(t => t !== toast); }, 300); }, /** * Dismiss all toasts */ dismissAll() { [...this.toasts].forEach(t => this.dismiss(t)); }, // Shorthand methods success(msg, duration) { return this.show(msg, 'success', duration); }, error(msg, duration) { return this.show(msg, 'error', duration || 5000); }, warning(msg, duration) { return this.show(msg, 'warning', duration || 4000); }, info(msg, duration) { return this.show(msg, 'info', duration); }, /** * Confirm dialog (替代 confirm()) * @returns {Promise} */ confirm(message, { title = '确认操作', confirmText = '确认', cancelText = '取消', type = 'warning' } = {}) { return new Promise((resolve) => { this.init(); const colors = { success: '#10b981', error: '#ef4444', warning: '#f59e0b', info: '#6366f1' }; const icons = { success: '✅', error: '❌', warning: '⚠️', info: 'ℹ️' }; const overlay = document.createElement('div'); overlay.style.cssText = ` position: fixed; top:0; left:0; right:0; bottom:0; background: rgba(0,0,0,0.5); z-index: 100001; display: flex; align-items: center; justify-content: center; animation: fadeIn 0.2s ease; `; const dialog = document.createElement('div'); dialog.style.cssText = ` background: white; border-radius: 16px; padding: 28px; max-width: 420px; width: 90%; box-shadow: 0 20px 60px rgba(0,0,0,0.2); animation: toastSlideIn 0.3s cubic-bezier(0.21,1.02,0.73,1); `; dialog.innerHTML = `
${icons[type]}

${title}

${this._escapeHtml(message)}

`; overlay.appendChild(dialog); document.body.appendChild(overlay); const cleanup = (result) => { overlay.style.animation = 'fadeIn 0.15s ease reverse'; setTimeout(() => overlay.remove(), 150); resolve(result); }; dialog.querySelector('#toastConfirm').onclick = () => cleanup(true); dialog.querySelector('#toastCancel').onclick = () => cleanup(false); overlay.onclick = (e) => { if (e.target === overlay) cleanup(false); }; }); }, _escapeHtml(str) { const div = document.createElement('div'); div.textContent = str; return div.innerHTML; } }; window.Toast = Toast;