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