Toast.tsx 2.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101
  1. import React, { useEffect, useState } from 'react';
  2. import { CheckCircle2, AlertCircle, X } from 'lucide-react';
  3. export type ToastType = 'success' | 'error';
  4. interface ToastProps {
  5. message: string;
  6. type: ToastType;
  7. isVisible: boolean;
  8. onClose: () => void;
  9. duration?: number;
  10. }
  11. const Toast: React.FC<ToastProps> = ({
  12. message,
  13. type,
  14. isVisible,
  15. onClose,
  16. duration = 2500
  17. }) => {
  18. const [shouldRender, setShouldRender] = useState(false);
  19. const [isAnimating, setIsAnimating] = useState(false);
  20. useEffect(() => {
  21. if (isVisible) {
  22. setShouldRender(true);
  23. // 触发动画
  24. const animateTimer = setTimeout(() => setIsAnimating(true), 10);
  25. const autoCloseTimer = setTimeout(() => {
  26. setIsAnimating(false);
  27. // 等待动画完成后再移除DOM
  28. setTimeout(() => {
  29. setShouldRender(false);
  30. onClose();
  31. }, 300);
  32. }, duration);
  33. return () => {
  34. clearTimeout(animateTimer);
  35. clearTimeout(autoCloseTimer);
  36. };
  37. } else {
  38. // 当isVisible变为false时,先触发淡出动画
  39. setIsAnimating(false);
  40. const hideTimer = setTimeout(() => {
  41. setShouldRender(false);
  42. }, 300);
  43. return () => clearTimeout(hideTimer);
  44. }
  45. }, [isVisible, duration, onClose]);
  46. if (!shouldRender) return null;
  47. const icon = type === 'success' ? (
  48. <CheckCircle2 className="w-5 h-5 text-green-600" />
  49. ) : (
  50. <AlertCircle className="w-5 h-5 text-red-600" />
  51. );
  52. const bgColor = type === 'success'
  53. ? 'bg-green-50 border-green-200'
  54. : 'bg-red-50 border-red-200';
  55. const textColor = type === 'success'
  56. ? 'text-green-700'
  57. : 'text-red-700';
  58. return (
  59. <div
  60. className={`fixed top-20 left-1/2 transform -translate-x-1/2 z-[9999] transition-all duration-300 ${
  61. isAnimating
  62. ? 'opacity-100 translate-y-0'
  63. : 'opacity-0 -translate-y-2 pointer-events-none'
  64. }`}
  65. >
  66. <div className={`flex items-center gap-3 px-4 py-3 rounded-lg border shadow-lg ${bgColor} min-w-[280px] max-w-[90vw]`}>
  67. {icon}
  68. <span className={`text-sm font-medium flex-1 ${textColor}`}>
  69. {message}
  70. </span>
  71. <button
  72. onClick={() => {
  73. setIsAnimating(false);
  74. setTimeout(() => {
  75. setShouldRender(false);
  76. onClose();
  77. }, 300);
  78. }}
  79. className={`${textColor} hover:opacity-70 transition-opacity`}
  80. aria-label="关闭"
  81. >
  82. <X className="w-4 h-4" />
  83. </button>
  84. </div>
  85. </div>
  86. );
  87. };
  88. export default Toast;