返回 JoyAI-Echo
ToastHost.tsx
1 import classNames from "classnames";
2 import { useCallback, useEffect, useState, type TransitionEvent } from "react";
3 import {
4 registerToastListener,
5 type ToastState,
6 type ToastType,
7 } from "./toast";
8 import styles from "./Toast.module.scss";
9
10 function ToastIcon({ type }: { type: ToastType }) {
11 if (type === "success") {
12 return (
13 <div className={styles.toastIconSuccess} aria-hidden="true">
14 <svg
15 viewBox="64 64 896 896"
16 focusable="false"
17 width="1em"
18 height="1em"
19 fill="currentColor"
20 >
21 <path d="M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm193.5 301.7l-210.6 292a31.8 31.8 0 01-51.7 0L318.5 484.9c-3.8-5.3 0-12.7 6.5-12.7h46.9c10.2 0 19.9 4.9 25.9 13.3l71.2 98.8 157.2-218c6-8.3 15.6-13.3 25.9-13.3H699c6.5 0 10.3 7.4 6.5 12.7z" />
22 </svg>
23 </div>
24 );
25 }
26
27 return <div className={styles.toastIcon}>!</div>;
28 }
29
30 /** 全局 Toast 挂载点,配合 toast.error / toast.success 使用 */
31 export function ToastHost() {
32 const [toastState, setToastState] = useState<ToastState | null>(null);
33
34 useEffect(() => registerToastListener(setToastState), []);
35
36 const handleTransitionEnd = useCallback((event: TransitionEvent<HTMLDivElement>) => {
37 if (event.target !== event.currentTarget) return;
38 if (event.propertyName !== "opacity") return;
39
40 setToastState((prev) => {
41 if (prev && !prev.visible) return null;
42 return prev;
43 });
44 }, []);
45
46 return (
47 <div
48 className={classNames(
49 styles.toast,
50 toastState?.visible && styles.visible,
51 )}
52 role="status"
53 aria-live="polite"
54 onTransitionEnd={handleTransitionEnd}
55 >
56 {toastState && (
57 <>
58 <ToastIcon type={toastState.type} />
59 <span className={styles.toastText}>{toastState.message}</span>
60 </>
61 )}
62 </div>
63 );
64 }
65
65 lines Plain Text