import { useCallback, useContext, useEffect, useId, useMemo, useRef, useState, type ReactNode } from "react"; import { Dialog } from "tdesign-react"; import { useBlocker } from "react-router-dom"; import { UnsavedContext } from "./UnsavedChangesContext"; // Keep navigation protection state local to the mounted route tree. // The context identity itself is kept in the dedicated registry module. export function UnsavedChangesProvider({ children }: { children: ReactNode }) { const [warning, setWarning] = useState(null); const [pendingAction, setPendingAction] = useState<(() => void) | null>(null); const warnings = useRef(new Map()); const warningRef = useRef(null); const blocker = useBlocker(Boolean(warning)); useEffect(() => { if (!warning) return; const preventUnload = (event: BeforeUnloadEvent) => { event.preventDefault(); event.returnValue = ""; }; window.addEventListener("beforeunload", preventUnload); return () => window.removeEventListener("beforeunload", preventUnload); }, [warning]); const registerWarning = useCallback((id: string, message: string | null) => { if (message) warnings.current.set(id, message); else warnings.current.delete(id); const nextWarning = warnings.current.values().next().value ?? null; warningRef.current = nextWarning; if (!nextWarning) setPendingAction(null); setWarning(nextWarning); }, []); const clearWarnings = useCallback(() => { warnings.current.clear(); warningRef.current = null; setWarning(null); }, []); const requestDiscard = useCallback((action: () => void) => { if (!warningRef.current) { action(); return; } setPendingAction(() => action); }, []); const value = useMemo(() => ({ registerWarning, requestDiscard }), [registerWarning, requestDiscard]); const visible = blocker.state === "blocked" || Boolean(pendingAction); useEffect(() => { // A dirty form can disappear for reasons unrelated to navigation (for // example session expiry). Do not leave its stale close callback attached // to the confirmation dialog that belongs to the new screen. if (!warning && pendingAction) setPendingAction(null); }, [pendingAction, warning]); const confirm = () => { const action = pendingAction; setPendingAction(null); if (blocker.state === "blocked") { blocker.proceed(); } else { clearWarnings(); action?.(); } }; const cancel = () => { setPendingAction(null); if (blocker.state === "blocked") blocker.reset(); }; return {children} {warning || "当前页面有未保存的内容,离开后将无法恢复。"} ; } export function useUnsavedChanges(active: boolean, message = "当前表单有未保存的内容,离开后这些内容会丢失。") { const context = useContext(UnsavedContext); if (!context) throw new Error("useUnsavedChanges must be used inside UnsavedChangesProvider"); const id = useId(); useEffect(() => { context.registerWarning(id, active ? message : null); return () => context.registerWarning(id, null); }, [active, context, id, message]); } export function useUnsavedActions() { const context = useContext(UnsavedContext); if (!context) throw new Error("useUnsavedActions must be used inside UnsavedChangesProvider"); return context; }