'use client'; import { useState, useEffect } from 'react'; import { motion, AnimatePresence } from 'framer-motion'; import { XMarkIcon, ExclamationTriangleIcon, CheckCircleIcon, InformationCircleIcon } from '@heroicons/react/24/outline'; export type ErrorType = 'error' | 'warning' | 'success' | 'info'; interface ErrorNotificationProps { message: string; type?: ErrorType; duration?: number; onClose?: () => void; } export function ErrorNotification({ message, type = 'error', duration = 5000, onClose }: ErrorNotificationProps) { const [isVisible, setIsVisible] = useState(true); const [isHovered, setIsHovered] = useState(false); const [progress, setProgress] = useState(100); useEffect(() => { if (duration > 0 && !isHovered) { const startTime = Date.now(); const timer = setInterval(() => { const elapsed = Date.now() - startTime; const remaining = Math.max(0, duration - elapsed); setProgress((remaining / duration) * 100); if (remaining === 0) { setIsVisible(false); onClose?.(); } }, 50); return () => clearInterval(timer); } }, [duration, onClose, isHovered]); const handleClose = () => { setIsVisible(false); onClose?.(); }; const getIcon = () => { switch (type) { case 'error': return ; case 'warning': return ; case 'success': return ; case 'info': return ; } }; const getStyles = () => { switch (type) { case 'error': return { bg: 'bg-red-50 dark:bg-red-900/20', border: 'border-red-200 dark:border-red-800', text: 'text-red-800 dark:text-red-200', icon: 'text-red-500', close: 'text-red-400 hover:text-red-600 dark:text-red-300 dark:hover:text-red-100', progress: 'bg-red-500' }; case 'warning': return { bg: 'bg-yellow-50 dark:bg-yellow-900/20', border: 'border-yellow-200 dark:border-yellow-800', text: 'text-yellow-800 dark:text-yellow-200', icon: 'text-yellow-500', close: 'text-yellow-400 hover:text-yellow-600 dark:text-yellow-300 dark:hover:text-yellow-100', progress: 'bg-yellow-500' }; case 'success': return { bg: 'bg-green-50 dark:bg-green-900/20', border: 'border-green-200 dark:border-green-800', text: 'text-green-800 dark:text-green-200', icon: 'text-green-500', close: 'text-green-400 hover:text-green-600 dark:text-green-300 dark:hover:text-green-100', progress: 'bg-green-500' }; case 'info': return { bg: 'bg-blue-50 dark:bg-blue-900/20', border: 'border-blue-200 dark:border-blue-800', text: 'text-blue-800 dark:text-blue-200', icon: 'text-blue-500', close: 'text-blue-400 hover:text-blue-600 dark:text-blue-300 dark:hover:text-blue-100', progress: 'bg-blue-500' }; } }; const styles = getStyles(); const getAnimationVariants = () => { switch (type) { case 'error': return { initial: { opacity: 0, x: 100, scale: 0.8, rotate: -5 }, animate: { opacity: 1, x: 0, scale: 1, rotate: 0 }, exit: { opacity: 0, x: 100, scale: 0.8, rotate: 5 }, }; case 'warning': return { initial: { opacity: 0, y: -20, scale: 0.9 }, animate: { opacity: 1, y: 0, scale: 1 }, exit: { opacity: 0, y: -20, scale: 0.9 }, }; case 'success': return { initial: { opacity: 0, scale: 0.5 }, animate: { opacity: 1, scale: 1 }, exit: { opacity: 0, scale: 0.5 }, }; case 'info': return { initial: { opacity: 0, x: -20, scale: 0.9 }, animate: { opacity: 1, x: 0, scale: 1 }, exit: { opacity: 0, x: -20, scale: 0.9 }, }; } }; return ( {isVisible && ( setIsHovered(true)} onMouseLeave={() => setIsHovered(false)} whileHover={{ scale: 1.02, y: -2 }} > {/* Progress bar */}
{getIcon()}

{message}

)}
); } // 增强的全局错误管理器 class ErrorManager { private static instance: ErrorManager; private listeners: Array<(notification: ErrorNotificationProps & { id: string }) => void> = []; private soundEnabled: boolean = true; static getInstance(): ErrorManager { if (!ErrorManager.instance) { ErrorManager.instance = new ErrorManager(); } return ErrorManager.instance; } private playSound(type: ErrorType) { if (!this.soundEnabled) return; // 创建音频反馈 const audioContext = new (window.AudioContext || (window as any).webkitAudioContext)(); const oscillator = audioContext.createOscillator(); const gainNode = audioContext.createGain(); oscillator.connect(gainNode); gainNode.connect(audioContext.destination); const frequencies = { error: 300, warning: 400, success: 600, info: 500 }; oscillator.frequency.setValueAtTime(frequencies[type], audioContext.currentTime); oscillator.type = type === 'error' ? 'sawtooth' : 'sine'; gainNode.gain.setValueAtTime(0.1, audioContext.currentTime); gainNode.gain.exponentialRampToValueAtTime(0.01, audioContext.currentTime + 0.3); oscillator.start(audioContext.currentTime); oscillator.stop(audioContext.currentTime + 0.3); } showError(message: string, duration?: number) { this.playSound('error'); this.showNotification(message, 'error', duration); } showWarning(message: string, duration?: number) { this.playSound('warning'); this.showNotification(message, 'warning', duration); } showSuccess(message: string, duration?: number) { this.playSound('success'); this.showNotification(message, 'success', duration); } showInfo(message: string, duration?: number) { this.playSound('info'); this.showNotification(message, 'info', duration); } private showNotification(message: string, type: ErrorType, duration?: number) { const notification = { id: Math.random().toString(36).substr(2, 9), message, type, duration: duration ?? (type === 'error' ? 5000 : 3000) }; this.listeners.forEach(listener => listener(notification)); } subscribe(listener: (notification: ErrorNotificationProps & { id: string }) => void) { this.listeners.push(listener); return () => { this.listeners = this.listeners.filter(l => l !== listener); }; } setSoundEnabled(enabled: boolean) { this.soundEnabled = enabled; } } export const errorManager = ErrorManager.getInstance(); // 增强的错误提示容器组件 export function ErrorNotificationContainer() { const [notifications, setNotifications] = useState>([]); const [soundEnabled, setSoundEnabled] = useState(true); useEffect(() => { const unsubscribe = errorManager.subscribe((notification) => { setNotifications(prev => [...prev, notification]); }); return unsubscribe; }, []); useEffect(() => { errorManager.setSoundEnabled(soundEnabled); }, [soundEnabled]); const removeNotification = (id: string) => { setNotifications(prev => prev.filter(n => n.id !== id)); }; return ( <> {notifications.map((notification, index) => ( removeNotification(notification.id)} /> ))} ); }