diff --git a/frontend/src/components/ui/Toast.tsx b/frontend/src/components/ui/Toast.tsx new file mode 100644 index 0000000..f5641b5 --- /dev/null +++ b/frontend/src/components/ui/Toast.tsx @@ -0,0 +1,99 @@ +import { + createContext, + useContext, + useState, + useCallback, + type ReactNode, +} from "react"; +import { X, CheckCircle, AlertCircle, Info } from "lucide-react"; + +// ============================================================ +// 类型 +// ============================================================ + +type ToastType = "success" | "error" | "info"; + +interface ToastItem { + id: number; + message: string; + type: ToastType; +} + +interface ToastContextValue { + toast: (message: string, type?: ToastType) => void; +} + +// ============================================================ +// Context +// ============================================================ + +const ToastContext = createContext(null); + +export function useToast() { + const ctx = useContext(ToastContext); + if (!ctx) throw new Error("useToast 必须在 ToastProvider 内使用"); + return ctx; +} + +// ============================================================ +// Provider +// ============================================================ + +let _nextId = 1; + +export function ToastProvider({ children }: { children: ReactNode }) { + const [items, setItems] = useState([]); + + const toast = useCallback((message: string, type: ToastType = "info") => { + const id = _nextId++; + setItems((prev) => [...prev, { id, message, type }]); + setTimeout(() => { + setItems((prev) => prev.filter((t) => t.id !== id)); + }, 3500); + }, []); + + return ( + + {children} + + {/* Toast 渲染区 */} +
+ {items.map((item) => { + const icon = + item.type === "success" ? ( + + ) : item.type === "error" ? ( + + ) : ( + + ); + + const borderColor = + item.type === "success" + ? "border-green-200 bg-green-50" + : item.type === "error" + ? "border-red-200 bg-red-50" + : "border-blue-200 bg-blue-50"; + + return ( +
+ {icon} + {item.message} + +
+ ); + })} +
+
+ ); +} diff --git a/frontend/src/index.css b/frontend/src/index.css index 5e7534b..60c53b9 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -34,3 +34,22 @@ body { width: 0; height: 0; } + +/* ============================================================ + Toast 动画 + ============================================================ */ + +@keyframes slide-in { + from { + opacity: 0; + transform: translateX(100%); + } + to { + opacity: 1; + transform: translateX(0); + } +} + +.animate-slide-in { + animation: slide-in 0.3s ease-out; +}