book-management-frontend/components/notification-provider.tsx
2025-05-22 20:42:10 +08:00

85 lines
2.9 KiB
TypeScript

"use client"
import { useEffect, useState } from "react"
import { AnimatePresence, motion } from "framer-motion"
import { X } from "lucide-react"
import { eventBus, EVENT_TYPES, type Notification } from "@/lib/event-bus"
import { cn } from "@/lib/utils"
export function NotificationProvider() {
const [notifications, setNotifications] = useState<(Notification & { id: number })[]>([])
let lastId = 0
useEffect(() => {
const handleNotification = (notification: Notification) => {
const id = ++lastId
setNotifications((prev) => [...prev, { ...notification, id }])
// 自动移除通知
if (notification.duration !== 0) {
const duration = notification.duration || 5000
setTimeout(() => {
removeNotification(id)
}, duration)
}
}
// 订阅通知事件
eventBus.on(EVENT_TYPES.NOTIFICATION, handleNotification)
// 订阅API错误事件
eventBus.on(EVENT_TYPES.API_ERROR, (error) => {
handleNotification({
title: "请求失败",
message: error.message || "服务器返回错误",
type: "error",
duration: 5000,
})
})
return () => {
eventBus.off(EVENT_TYPES.NOTIFICATION)
eventBus.off(EVENT_TYPES.API_ERROR)
}
}, [])
const removeNotification = (id: number) => {
setNotifications((prev) => prev.filter((notification) => notification.id !== id))
}
return (
<div className="fixed bottom-4 right-4 z-50 flex flex-col gap-2 max-w-md">
<AnimatePresence>
{notifications.map((notification) => (
<motion.div
key={notification.id}
initial={{ opacity: 0, y: 50, scale: 0.3 }}
animate={{ opacity: 1, y: 0, scale: 1 }}
exit={{ opacity: 0, scale: 0.5, transition: { duration: 0.2 } }}
className={cn(
"rounded-lg shadow-lg p-4 backdrop-blur-md border",
notification.type === "error" && "bg-destructive/90 border-destructive text-destructive-foreground",
notification.type === "success" && "bg-green-500/90 border-green-600 text-white",
notification.type === "warning" && "bg-amber-500/90 border-amber-600 text-white",
notification.type === "info" && "bg-blue-500/90 border-blue-600 text-white",
)}
>
<div className="flex justify-between items-start">
<div className="flex-1">
<h4 className="font-medium text-sm">{notification.title}</h4>
<p className="text-sm mt-1 opacity-90">{notification.message}</p>
</div>
<button
onClick={() => removeNotification(notification.id)}
className="ml-4 p-1 rounded-full hover:bg-white/20 transition-colors"
>
<X className="h-4 w-4" />
</button>
</div>
</motion.div>
))}
</AnimatePresence>
</div>
)
}