Initial ecommerce standalone package
This commit is contained in:
@@ -0,0 +1,37 @@
|
||||
import { useToastState, type ToastItem } from "./toastStore";
|
||||
|
||||
const iconMap: Record<string, string> = {
|
||||
success: "✓",
|
||||
error: "✕",
|
||||
info: "ℹ",
|
||||
};
|
||||
|
||||
function ToastItemView({ item, onDismiss }: { item: ToastItem; onDismiss: (id: number) => void }) {
|
||||
return (
|
||||
<div className={`app-toast app-toast--${item.type}`} role="alert">
|
||||
<span className="app-toast__icon">{iconMap[item.type]}</span>
|
||||
<span className="app-toast__msg">{item.message}</span>
|
||||
{item.onRetry && (
|
||||
<button type="button" className="app-toast__retry" onClick={item.onRetry}>
|
||||
重试
|
||||
</button>
|
||||
)}
|
||||
<button type="button" className="app-toast__close" onClick={() => onDismiss(item.id)} aria-label="关闭">
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function ToastContainer() {
|
||||
const { items, dismiss } = useToastState();
|
||||
if (!items.length) return null;
|
||||
|
||||
return (
|
||||
<div className="app-toast-container" aria-live="polite">
|
||||
{items.map((item) => (
|
||||
<ToastItemView key={item.id} item={item} onDismiss={dismiss} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
|
||||
export type ToastType = "success" | "error" | "info";
|
||||
|
||||
export interface ToastItem {
|
||||
id: number;
|
||||
type: ToastType;
|
||||
message: string;
|
||||
onRetry?: () => void;
|
||||
}
|
||||
|
||||
let toastId = 0;
|
||||
const listeners = new Set<(items: ToastItem[]) => void>();
|
||||
let queue: ToastItem[] = [];
|
||||
|
||||
function notify() {
|
||||
listeners.forEach((fn) => fn([...queue]));
|
||||
}
|
||||
|
||||
function dismiss(id: number) {
|
||||
queue = queue.filter((t) => t.id !== id);
|
||||
notify();
|
||||
}
|
||||
|
||||
export function toast(message: string, type: ToastType = "info", onRetry?: () => void) {
|
||||
const id = ++toastId;
|
||||
queue = [...queue.slice(-4), { id, type, message, onRetry }];
|
||||
notify();
|
||||
setTimeout(() => dismiss(id), type === "error" ? 5000 : 3000);
|
||||
return id;
|
||||
}
|
||||
|
||||
toast.success = (msg: string) => toast(msg, "success");
|
||||
toast.error = (msg: string, onRetry?: () => void) => toast(msg, "error", onRetry);
|
||||
toast.info = (msg: string) => toast(msg, "info");
|
||||
|
||||
export function useToastState() {
|
||||
const [items, setItems] = useState<ToastItem[]>([]);
|
||||
useEffect(() => {
|
||||
listeners.add(setItems);
|
||||
return () => { listeners.delete(setItems); };
|
||||
}, []);
|
||||
return { items, dismiss };
|
||||
}
|
||||
Reference in New Issue
Block a user