geo/frontend/components/ui/notification-container.tsx

90 lines
3.4 KiB
TypeScript

/**
* 全局 Toast/通知 UI 组件
*
* 从 notification-store 读取通知队列,渲染为浮动通知列表。
* 放置在 layout 层级,自动显示/消失。
*/
"use client";
import { X, CheckCircle, AlertTriangle, Info, AlertCircle } from "lucide-react";
import { useNotificationStore } from "@/lib/stores/notification-store";
import type { Notification, NotificationType } from "@/lib/stores/notification-store";
import { cn } from "@/lib/utils";
// ── 图标映射 ────────────────────────────────────────────────────────────
const ICON_BY_TYPE: Record<NotificationType, React.ReactNode> = {
success: <CheckCircle className="h-5 w-5 text-emerald-500" />,
error: <AlertCircle className="h-5 w-5 text-red-500" />,
warning: <AlertTriangle className="h-5 w-5 text-amber-500" />,
info: <Info className="h-5 w-5 text-blue-500" />,
};
// ── 样式映射 ────────────────────────────────────────────────────────────
const STYLE_BY_TYPE: Record<NotificationType, string> = {
success: "border-emerald-200 bg-emerald-50 text-emerald-800",
error: "border-red-200 bg-red-50 text-red-800",
warning: "border-amber-200 bg-amber-50 text-amber-800",
info: "border-blue-200 bg-blue-50 text-blue-800",
};
// ── 单条通知 ────────────────────────────────────────────────────────────
function NotificationItem({
notification,
onRemove,
}: {
notification: Notification;
onRemove: (id: string) => void;
}) {
return (
<div
className={cn(
"flex items-start gap-3 rounded-lg border px-4 py-3 shadow-sm",
"animate-in slide-in-from-right-5 fade-in-0 duration-200",
STYLE_BY_TYPE[notification.type]
)}
>
<div className="shrink-0 mt-0.5">{ICON_BY_TYPE[notification.type]}</div>
<div className="flex-1 min-w-0">
{notification.title && (
<p className="text-sm font-semibold leading-tight">
{notification.title}
</p>
)}
<p className="text-sm leading-tight">{notification.message}</p>
</div>
<button
type="button"
onClick={() => onRemove(notification.id)}
className="shrink-0 text-current opacity-50 hover:opacity-100 transition-opacity"
>
<X className="h-4 w-4" />
</button>
</div>
);
}
// ── 通知容器 ────────────────────────────────────────────────────────────
export function NotificationContainer() {
const notifications = useNotificationStore((s) => s.notifications);
const removeNotification = useNotificationStore((s) => s.removeNotification);
if (notifications.length === 0) return null;
return (
<div className="fixed top-4 right-4 z-[100] flex flex-col gap-2 max-w-sm w-full pointer-events-none">
{notifications.map((notification) => (
<div key={notification.id} className="pointer-events-auto">
<NotificationItem
notification={notification}
onRemove={removeNotification}
/>
</div>
))}
</div>
);
}