| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101 |
- import React, { useEffect, useState } from 'react';
- import { CheckCircle2, AlertCircle, X } from 'lucide-react';
- export type ToastType = 'success' | 'error';
- interface ToastProps {
- message: string;
- type: ToastType;
- isVisible: boolean;
- onClose: () => void;
- duration?: number;
- }
- const Toast: React.FC<ToastProps> = ({
- message,
- type,
- isVisible,
- onClose,
- duration = 2500
- }) => {
- const [shouldRender, setShouldRender] = useState(false);
- const [isAnimating, setIsAnimating] = useState(false);
- useEffect(() => {
- if (isVisible) {
- setShouldRender(true);
- // 触发动画
- const animateTimer = setTimeout(() => setIsAnimating(true), 10);
-
- const autoCloseTimer = setTimeout(() => {
- setIsAnimating(false);
- // 等待动画完成后再移除DOM
- setTimeout(() => {
- setShouldRender(false);
- onClose();
- }, 300);
- }, duration);
-
- return () => {
- clearTimeout(animateTimer);
- clearTimeout(autoCloseTimer);
- };
- } else {
- // 当isVisible变为false时,先触发淡出动画
- setIsAnimating(false);
- const hideTimer = setTimeout(() => {
- setShouldRender(false);
- }, 300);
- return () => clearTimeout(hideTimer);
- }
- }, [isVisible, duration, onClose]);
- if (!shouldRender) return null;
- const icon = type === 'success' ? (
- <CheckCircle2 className="w-5 h-5 text-green-600" />
- ) : (
- <AlertCircle className="w-5 h-5 text-red-600" />
- );
- const bgColor = type === 'success'
- ? 'bg-green-50 border-green-200'
- : 'bg-red-50 border-red-200';
-
- const textColor = type === 'success'
- ? 'text-green-700'
- : 'text-red-700';
- return (
- <div
- className={`fixed top-20 left-1/2 transform -translate-x-1/2 z-[9999] transition-all duration-300 ${
- isAnimating
- ? 'opacity-100 translate-y-0'
- : 'opacity-0 -translate-y-2 pointer-events-none'
- }`}
- >
- <div className={`flex items-center gap-3 px-4 py-3 rounded-lg border shadow-lg ${bgColor} min-w-[280px] max-w-[90vw]`}>
- {icon}
- <span className={`text-sm font-medium flex-1 ${textColor}`}>
- {message}
- </span>
- <button
- onClick={() => {
- setIsAnimating(false);
- setTimeout(() => {
- setShouldRender(false);
- onClose();
- }, 300);
- }}
- className={`${textColor} hover:opacity-70 transition-opacity`}
- aria-label="关闭"
- >
- <X className="w-4 h-4" />
- </button>
- </div>
- </div>
- );
- };
- export default Toast;
|