Toast.tsx 2.5 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485
  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. role="alert"
  47. className={`relative w-full max-w-sm transition-all duration-300 transform ${isVisible ? 'translate-x-0 opacity-100' : 'translate-x-full opacity-0'
  48. }`}
  49. >
  50. <div className={`rounded-lg border shadow-lg p-4 backdrop-blur-sm ${getStyles()}`}>
  51. <div className="flex items-start gap-3">
  52. <div className="flex-shrink-0 mt-0.5">
  53. {getIcon()}
  54. </div>
  55. <div className="flex-1 min-w-0">
  56. {title && (
  57. <p className="text-sm font-semibold mb-1">{title}</p>
  58. )}
  59. <p className="text-sm font-medium leading-relaxed">{message}</p>
  60. </div>
  61. <button
  62. onClick={() => {
  63. setIsVisible(false);
  64. setTimeout(onClose, 300);
  65. }}
  66. className="flex-shrink-0 ml-2 p-1 rounded-full opacity-60 hover:opacity-100 hover:bg-black/5 transition-all"
  67. aria-label="Close"
  68. >
  69. <X className="w-4 h-4" />
  70. </button>
  71. </div>
  72. </div>
  73. </div>
  74. );
  75. };
  76. export default Toast;