返回 DeepSeek-Reasonix
toast.tsx
根目录 / desktop / frontend / src / lib / toast.tsx
1 import { createContext, useCallback, useContext, useEffect, useRef, useState, type ReactNode } from "react";
2 import { ErrorMessage } from "../components/ErrorMessage";
3 import { useT } from "./i18n";
4
5 export interface Toast {
6 id: number;
7 text: string;
8 level: "info" | "warn" | "error";
9 actionLabel?: string;
10 onAction?: () => void;
11 }
12
13 export interface ToastOptions {
14 actionLabel?: string;
15 onAction?: () => void;
16 durationMs?: number;
17 }
18
19 export interface ToastContextValue {
20 toasts: Toast[];
21 showToast: (text: string, level?: Toast["level"], options?: ToastOptions) => void;
22 }
23
24 const ToastContext = createContext<ToastContextValue>({ toasts: [], showToast: () => {} });
25
26 export function useToast() {
27 return useContext(ToastContext);
28 }
29
30 let nextId = 1;
31
32 export function ToastProvider({ children }: { children: ReactNode }) {
33 const translate = useT();
34 const [toasts, setToasts] = useState<Toast[]>([]);
35 const timers = useRef(new Map<number, ReturnType<typeof setTimeout>>());
36 useEffect(() => {
37 const activeTimers = timers.current;
38 return () => { for (const timer of activeTimers.values()) clearTimeout(timer); activeTimers.clear(); };
39 }, []);
40
41 const showToast = useCallback((text: string, level: Toast["level"] = "info", options: ToastOptions = {}) => {
42 const id = nextId++;
43 setToasts((prev) => [...prev, { id, text, level, actionLabel: options.actionLabel, onAction: options.onAction }]);
44 const timer = setTimeout(() => {
45 setToasts((prev) => prev.filter((t) => t.id !== id));
46 timers.current.delete(id);
47 }, options.durationMs ?? (options.actionLabel || level !== "info" ? 8000 : 2500));
48 timers.current.set(id, timer);
49 }, []);
50
51 const dismissToast = useCallback((id: number) => {
52 const timer = timers.current.get(id);
53 if (timer) clearTimeout(timer);
54 timers.current.delete(id);
55 setToasts((prev) => prev.filter((t) => t.id !== id));
56 }, []);
57
58 return (
59 <ToastContext.Provider value={{ toasts, showToast }}>
60 {children}
61 <div className="toast-container" role="status" aria-live="polite">
62 {toasts.map((t) => (
63 <div key={t.id} className={`toast toast--${t.level}`} onClick={() => dismissToast(t.id)}>
64 {t.level === "warn" && <span className="toast__icon">⚠️</span>}
65 {t.level === "error" && <span className="toast__icon">❌</span>}
66 <span className="toast__text">{t.level === "info" ? t.text : <ErrorMessage error={t.text} onInspect={() => {
67 clearTimeout(timers.current.get(t.id));
68 timers.current.delete(t.id);
69 }} />}</span>
70 {t.actionLabel && t.onAction && (
71 <button
72 type="button"
73 className="toast__action"
74 onClick={(event) => {
75 event.stopPropagation();
76 dismissToast(t.id);
77 t.onAction?.();
78 }}
79 >
80 {t.actionLabel}
81 </button>
82 )}
83 <button type="button" className="toast__action" aria-label={translate("error.dismiss")}
84 onClick={(event) => { event.stopPropagation(); dismissToast(t.id); }}>×</button>
85 </div>
86 ))}
87 </div>
88 </ToastContext.Provider>
89 );
90 }
91
91 lines Plain Text