'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';