90 lines
3.4 KiB
TypeScript
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>
|
|
);
|
|
} |