'use client'; import { useState, useEffect } from 'react'; import { motion, AnimatePresence } from 'framer-motion'; import { XMarkIcon, ExclamationTriangleIcon, CheckCircleIcon, InformationCircleIcon, XCircleIcon, BellIcon } from '@heroicons/react/24/outline'; export type MessageType = 'success' | 'error' | 'warning' | 'info' | 'loading'; export interface MessageNotificationProps { message: string; type?: MessageType; duration?: number; onClose?: () => void; title?: string; position?: 'top-left' | 'top-right' | 'top-center' | 'bottom-left' | 'bottom-right' | 'bottom-center'; showProgress?: boolean; closable?: boolean; action?: { label: string; onClick: () => void; }; } interface Message extends MessageNotificationProps { id: string; createdAt: number; } export function MessageNotification({ message, type = 'info', duration = 3000, onClose, title, position = 'top-right', showProgress = true, closable = true, action }: MessageNotificationProps) { const [isVisible, setIsVisible] = useState(true); const [remainingTime, setRemainingTime] = useState(duration); useEffect(() => { if (duration > 0 && type !== 'loading') { const timer = setTimeout(() => { setIsVisible(false); onClose?.(); }, duration); // 进度条更新 const progressTimer = setInterval(() => { setRemainingTime(prev => { const newTime = prev - 100; if (newTime <= 0) { clearInterval(progressTimer); } return newTime; }); }, 100); return () => { clearTimeout(timer); clearInterval(progressTimer); }; } }, [duration, onClose, type]); const handleClose = () => { setIsVisible(false); onClose?.(); }; const getIcon = () => { const iconClass = "w-5 h-5"; switch (type) { case 'error': return ; case 'warning': return ; case 'success': return ; case 'loading': return (
); case 'info': default: 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', action: 'bg-red-500 hover:bg-red-600 text-white' }; 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', action: 'bg-yellow-500 hover:bg-yellow-600 text-white' }; 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', action: 'bg-green-500 hover:bg-green-600 text-white' }; case 'loading': 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', action: 'bg-blue-500 hover:bg-blue-600 text-white' }; case 'info': default: 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', action: 'bg-blue-500 hover:bg-blue-600 text-white' }; } }; const getPositionStyles = () => { switch (position) { case 'top-left': return 'top-4 left-4'; case 'top-center': return 'top-4 left-1/2 -translate-x-1/2'; case 'top-right': return 'top-4 right-4'; case 'bottom-left': return 'bottom-4 left-4'; case 'bottom-center': return 'bottom-4 left-1/2 -translate-x-1/2'; case 'bottom-right': return 'bottom-4 right-4'; default: return 'top-4 right-4'; } }; const styles = getStyles(); const positionStyles = getPositionStyles(); const progressPercentage = duration > 0 ? (remainingTime / duration) * 100 : 0; return ( {isVisible && (
{getIcon()}
{title && (

{title}

)}

{message}

{action && ( )}
{closable && ( )}
{showProgress && duration > 0 && type !== 'loading' && (
)}
)}
); } // 全局消息管理器 class MessageManager { private static instance: MessageManager; private listeners: Array<(message: Message) => void> = []; static getInstance(): MessageManager { if (!MessageManager.instance) { MessageManager.instance = new MessageManager(); } return MessageManager.instance; } // 基础消息方法 show(message: string, options?: Omit) { return this.addMessage(message, 'info', options); } success(message: string, options?: Omit) { return this.addMessage(message, 'success', options); } error(message: string, options?: Omit) { return this.addMessage(message, 'error', options); } warning(message: string, options?: Omit) { return this.addMessage(message, 'warning', options); } info(message: string, options?: Omit) { return this.addMessage(message, 'info', options); } loading(message: string, options?: Omit) { return this.addMessage(message, 'loading', { ...options, duration: 0 }); } // 隐藏加载消息 hideLoading(id?: string) { if (id) { this.listeners.forEach(listener => listener({ id, message: '', type: 'loading', createdAt: Date.now() } as Message)); } } private addMessage(message: string, type: MessageType, options?: Omit) { const id = Math.random().toString(36).substr(2, 9); const notification: Message = { id, message, type, createdAt: Date.now(), ...options }; this.listeners.forEach(listener => listener(notification)); return id; } subscribe(listener: (message: Message) => void) { this.listeners.push(listener); return () => { this.listeners = this.listeners.filter(l => l !== listener); }; } } export const messageManager = MessageManager.getInstance(); // 消息提示容器组件 export function MessageNotificationContainer() { const [messages, setMessages] = useState([]); useEffect(() => { const unsubscribe = messageManager.subscribe((message) => { if (message.type === 'loading' && message.message === '') { // 隐藏加载消息 setMessages(prev => prev.filter(m => m.id !== message.id)); } else { setMessages(prev => [...prev, message]); } }); return unsubscribe; }, []); const removeMessage = (id: string) => { setMessages(prev => prev.filter(m => m.id !== id)); }; // 按位置分组消息 const messagesByPosition = messages.reduce((acc, message) => { const position = message.position || 'top-right'; if (!acc[position]) { acc[position] = []; } acc[position].push(message); return acc; }, {} as Record); return ( <> {Object.entries(messagesByPosition).map(([position, positionMessages]) => (
{positionMessages.map((message) => ( removeMessage(message.id)} /> ))}
))} ); } function getPositionContainerStyles(position: string) { switch (position) { case 'top-left': return 'top-4 left-4'; case 'top-center': return 'top-4 left-1/2 -translate-x-1/2'; case 'top-right': return 'top-4 right-4'; case 'bottom-left': return 'bottom-4 left-4'; case 'bottom-center': return 'bottom-4 left-1/2 -translate-x-1/2'; case 'bottom-right': return 'bottom-4 right-4'; default: return 'top-4 right-4'; } } // 兼容层:保持与ErrorNotification的兼容性 export { errorManager } from './ErrorNotification'; export type { ErrorType } from './ErrorNotification';