import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState, } from "react"; import type { ToastContextValue, ToastItem, ToastOptions, ToastProviderProps, ToastType, } from "./Toast.types"; const DEFAULT_DURATION = 2400; const ToastContext = createContext(null); function createToastId() { return `toast-${Date.now()}-${Math.random().toString(36).slice(2)}`; } function getDefaultTitle(type: ToastType) { const titleMap: Record = { info: "提示", success: "成功", warning: "注意", error: "错误", }; return titleMap[type]; } export function ToastProvider({ children }: ToastProviderProps) { const [toasts, setToasts] = useState([]); const timersRef = useRef>(new Map()); const remove = useCallback((id: string) => { const timer = timersRef.current.get(id); if (timer) { window.clearTimeout(timer); timersRef.current.delete(id); } setToasts((current) => current.filter((toast) => toast.id !== id)); }, []); const clear = useCallback(() => { timersRef.current.forEach((timer) => { window.clearTimeout(timer); }); timersRef.current.clear(); setToasts([]); }, []); useEffect(() => { return () => { timersRef.current.forEach((timer) => { window.clearTimeout(timer); }); timersRef.current.clear(); }; }, []); const show = useCallback( (options: ToastOptions | string) => { const normalizedOptions: ToastOptions = typeof options === "string" ? { message: options } : options; const type = normalizedOptions.type ?? "info"; const id = createToastId(); const toast: ToastItem = { id, type, title: normalizedOptions.title ?? getDefaultTitle(type), message: normalizedOptions.message, duration: normalizedOptions.duration ?? DEFAULT_DURATION, className: normalizedOptions.className, data: normalizedOptions.data, }; setToasts((current) => [...current, toast]); if (toast.duration > 0) { const timer = window.setTimeout(() => remove(id), toast.duration); timersRef.current.set(id, timer); } return id; }, [remove], ); const contextValue = useMemo( () => ({ toasts, show, success: (message, options) => show({ ...options, message, type: "success" }), error: (message, options) => show({ ...options, message, type: "error" }), warning: (message, options) => show({ ...options, message, type: "warning" }), info: (message, options) => show({ ...options, message, type: "info" }), remove, clear, }), [clear, remove, show, toasts], ); return ( {children} ); } export function useToast() { const context = useContext(ToastContext); if (!context) { throw new Error("useToast must be used within ToastProvider"); } return context; }