/** * Toast 通知组件 * * 包含单个 Toast 条目渲染 + ToastProvider 全局容器 * 右上角堆叠显示,支持入场/退场动画 */ import { useEffect, useState, useCallback } from 'react' import { useTranslation } from 'react-i18next' import { CheckCircle, XCircle, AlertTriangle, Info, X } from 'lucide-react' import { useToastStore, type ToastItem, type ToastType } from '../../stores/toastStore' /** 类型 → 图标映射 */ const iconMap: Record = { success: CheckCircle, error: XCircle, warning: AlertTriangle, info: Info, } /** 类型 → 颜色样式映射(theme tokens,浅色/深色都可读) */ const colorMap: Record = { success: { bg: 'bg-feedback-success-bg', border: 'border-feedback-success/35', icon: 'text-feedback-success', text: 'text-content-primary', }, error: { bg: 'bg-feedback-error-bg', border: 'border-feedback-error/35', icon: 'text-feedback-error', text: 'text-content-primary', }, warning: { bg: 'bg-feedback-warning-bg', border: 'border-feedback-warning/35', icon: 'text-feedback-warning', text: 'text-content-primary', }, info: { bg: 'bg-feedback-info-bg', border: 'border-feedback-info/35', icon: 'text-feedback-info', text: 'text-content-primary', }, } interface ToastItemProps { toast: ToastItem onClose: (id: string) => void } /** * 单个 Toast 条目 * * 支持自动消失和手动关闭,带入场/退场动画 */ function ToastItemView({ toast, onClose }: ToastItemProps) { const { t } = useTranslation('common') const [visible, setVisible] = useState(false) const Icon = iconMap[toast.type] const colors = colorMap[toast.type] // 入场动画:挂载后下一帧设为可见 useEffect(() => { const raf = requestAnimationFrame(() => setVisible(true)) return () => cancelAnimationFrame(raf) }, []) // 自动关闭 const handleClose = useCallback(() => { setVisible(false) // 等待退场动画结束后再移除 setTimeout(() => onClose(toast.id), 200) }, [onClose, toast.id]) useEffect(() => { if (toast.duration <= 0) return const timer = setTimeout(() => { handleClose() }, toast.duration) return () => clearTimeout(timer) }, [toast.duration, handleClose]) return (
{/* 图标 */} {/* 消息 */}

{toast.message}

{/* 关闭按钮 */}
) } /** * Toast 通知容器(Provider) * * 在应用根节点中使用,渲染右上角通知堆叠 * * @example * // 在 App.tsx 中 * * * */ export function ToastProvider({ children }: { children: React.ReactNode }) { const toasts = useToastStore((s) => s.toasts) const removeToast = useToastStore((s) => s.removeToast) return ( <> {children}
{toasts.map((toast) => (
))}
) } export default ToastProvider