'use client' import { useState, useEffect } from 'react' import { createPortal } from 'react-dom' export interface ToastProps { message: string type: 'success' | 'error' | 'warning' | 'info' duration?: number onClose: () => void } const TOAST_ICONS = { success: '✅', error: '❌', warning: '⚠️', info: 'ℹ️' } as const const TOAST_STYLES = { success: 'bg-green-50 border-green-200 text-green-800', error: 'bg-red-50 border-red-200 text-red-800', warning: 'bg-yellow-50 border-yellow-200 text-yellow-800', info: 'bg-blue-50 border-blue-200 text-blue-800' } as const export function Toast({ message, type, duration = 3000, onClose, position, isLatest }: ToastProps & { position: number, isLatest: boolean }) { const [isVisible, setIsVisible] = useState(!isLatest) // 舊通知直接顯示,新通知需要動畫 const [mounted, setMounted] = useState(false) const [hasShownEntrance, setHasShownEntrance] = useState(!isLatest) // 追蹤是否已經顯示過入場動畫 useEffect(() => { setMounted(true) // 只有最新的通知且尚未顯示過入場動畫才需要滑入動畫 if (isLatest && !hasShownEntrance) { const showTimer = setTimeout(() => { setIsVisible(true) setHasShownEntrance(true) }, 50) return () => clearTimeout(showTimer) } }, [isLatest, hasShownEntrance]) useEffect(() => { // 自動消失計時器 const hideTimer = setTimeout(() => { setIsVisible(false) // 等待動畫完成後關閉 setTimeout(onClose, 300) }, duration) return () => clearTimeout(hideTimer) }, [duration, onClose]) if (!mounted) return null // 計算垂直位置:第一個在 top-4,後續每個往下偏移 80px const topPosition = 16 + (position * 80) // 16px (top-4) + 80px * position return createPortal(
{TOAST_ICONS[type]}

{message}

, document.body ) } // Toast 管理 Hook export function useToast() { const [toasts, setToasts] = useState }>>([]) const showToast = (props: Omit) => { const id = Math.random().toString(36).substring(2, 11) setToasts(prev => { const newToasts = [...prev, { id, props }] // 限制最多顯示 5 個通知,移除最舊的 return newToasts.length > 5 ? newToasts.slice(-5) : newToasts }) } const hideToast = (id: string) => { setToasts(prev => prev.filter(toast => toast.id !== id)) } const ToastContainer = () => ( <> {toasts.map(({ id, props }, index) => ( hideToast(id)} /> ))} ) return { showToast, ToastContainer, // 便捷方法 success: (message: string, duration?: number) => showToast({ message, type: 'success', duration }), error: (message: string, duration?: number) => showToast({ message, type: 'error', duration }), warning: (message: string, duration?: number) => showToast({ message, type: 'warning', duration }), info: (message: string, duration?: number) => showToast({ message, type: 'info', duration }) } }