Toast.tsx 2.5 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586
  1. import React, { useEffect, useState } from 'react';
  2. import { CheckCircle, AlertCircle, XCircle, Info, X } from 'lucide-react';
  3. export type ToastType = 'success' | 'error' | 'warning' | 'info';
  4. export interface ToastProps {
  5. type: ToastType;
  6. title?: string;
  7. message: string;
  8. duration?: number;
  9. onClose: () => void;
  10. }
  11. const Toast: React.FC<ToastProps> = ({ type, title, message, duration = 5000, onClose }) => {
  12. const [isVisible, setIsVisible] = useState(true);
  13. useEffect(() => {
  14. const timer = setTimeout(() => {
  15. setIsVisible(false);
  16. setTimeout(onClose, 300); // 等待动画完成
  17. }, duration);
  18. return () => clearTimeout(timer);
  19. }, [duration, onClose]);
  20. const getIcon = () => {
  21. switch (type) {
  22. case 'success':
  23. return <CheckCircle className="w-5 h-5 text-green-500" />;
  24. case 'error':
  25. return <XCircle className="w-5 h-5 text-red-500" />;
  26. case 'warning':
  27. return <AlertCircle className="w-5 h-5 text-yellow-500" />;
  28. case 'info':
  29. return <Info className="w-5 h-5 text-blue-500" />;
  30. }
  31. };
  32. const getStyles = () => {
  33. switch (type) {
  34. case 'success':
  35. return 'bg-green-50 border-green-200 text-green-800';
  36. case 'error':
  37. return 'bg-red-50 border-red-200 text-red-800';
  38. case 'warning':
  39. return 'bg-yellow-50 border-yellow-200 text-yellow-800';
  40. case 'info':
  41. return 'bg-blue-50 border-blue-200 text-blue-800';
  42. }
  43. };
  44. return (
  45. <div
  46. className={`relative w-full transition-all duration-300 ease-in-out ${isVisible ? 'translate-x-0 opacity-100 max-h-40 mb-3' : 'translate-x-full opacity-0 max-h-0 mb-0 overflow-hidden'
  47. }`}
  48. role="alert"
  49. aria-live="polite"
  50. >
  51. <div className={`rounded-lg border shadow-lg p-4 ${getStyles()}`}>
  52. <div className="flex items-start gap-3">
  53. <div className="flex-shrink-0 mt-0.5">
  54. {getIcon()}
  55. </div>
  56. <div className="flex-1 min-w-0">
  57. {title && (
  58. <p className="text-sm font-semibold mb-1">{title}</p>
  59. )}
  60. <p className="text-sm break-words">{message}</p>
  61. </div>
  62. <button
  63. onClick={() => {
  64. setIsVisible(false);
  65. setTimeout(onClose, 300);
  66. }}
  67. className="flex-shrink-0 ml-2 text-gray-400 hover:text-gray-600 transition-colors"
  68. aria-label="Close"
  69. >
  70. <X className="w-4 h-4" />
  71. </button>
  72. </div>
  73. </div>
  74. </div>
  75. );
  76. };
  77. export default Toast;