"use client"; import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState, type ReactNode, } from "react"; import { buttonClass } from "@/lib/ui/variants"; type ToastKind = "info" | "error" | "success"; interface ToastAction { label: string; onClick: () => void; } interface ToastOptions { action?: ToastAction; durationMs?: number; } interface ToastItem { id: number; message: string; kind: ToastKind; action?: ToastAction; } type ShowToast = ( message: string, kind?: ToastKind, options?: ToastOptions, ) => void; const ToastContext = createContext(null); // 普通提示 4s 自动消失;错误更需被看到,默认延长到 7s。 const TOAST_TTL_MS = 4000; const ERROR_TOAST_TTL_MS = 7000; // 屏读前缀:让无视觉的用户也能区分提示性质。 const SR_PREFIX: Record = { error: "错误:", success: "成功:", info: "", }; function toastShellClass(kind: ToastKind): string { if (kind === "error") return "border-conflict bg-panel text-conflict"; if (kind === "success") return "border-pass bg-panel text-pass"; return "border-line bg-panel text-ink"; } export function ToastProvider({ children }: { children: ReactNode }) { const [items, setItems] = useState([]); // 单调计数器作 id(不再用 Date.now()+random,避免极小概率碰撞且稳定可测)。 const nextIdRef = useRef(0); // 在途定时器集合:卸载时统一清理,避免 setState-after-unmount 泄漏。 const timersRef = useRef>>(new Set()); const dismiss = useCallback((id: number) => { setItems((prev) => prev.filter((t) => t.id !== id)); }, []); const show = useCallback((message, kind = "info", options) => { const id = nextIdRef.current++; setItems((prev) => [...prev, { id, message, kind, action: options?.action }]); const ttl = options?.durationMs ?? (kind === "error" ? ERROR_TOAST_TTL_MS : TOAST_TTL_MS); const timer = setTimeout(() => { timersRef.current.delete(timer); setItems((prev) => prev.filter((t) => t.id !== id)); }, ttl); timersRef.current.add(timer); }, []); // 卸载清理所有在途定时器。 useEffect(() => { const timers = timersRef.current; return () => { timers.forEach((t) => clearTimeout(t)); timers.clear(); }; }, []); const value = useMemo(() => show, [show]); const errorItems = items.filter((t) => t.kind === "error"); const politeItems = items.filter((t) => t.kind !== "error"); const renderItem = (t: ToastItem) => (
{SR_PREFIX[t.kind]} {t.message} {t.action ? ( ) : null}
); return ( {children} {/* error 用 assertive 立即播报;其余 info/success 用 polite 礼貌播报。两区共用同一锚点容器避免重叠。 */}
{errorItems.map(renderItem)}
{politeItems.map(renderItem)}
); } export function useToast(): ShowToast { const ctx = useContext(ToastContext); if (!ctx) { // 容错:未挂 Provider 时退化为 no-op(不应在生产路径发生)。 return () => undefined; } return ctx; }