85 lines
2.9 KiB
TypeScript
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>
|
|
)
|
|
}
|