183 lines
5.6 KiB
TypeScript
183 lines
5.6 KiB
TypeScript
"use client"
|
|
|
|
import { useState, createContext, useContext, type ReactNode } from "react"
|
|
import { Card, CardContent } from "@/app/components/ui/card"
|
|
import { Button } from "@/app/components/ui/button"
|
|
import { X, CheckCircle, AlertCircle, Info, AlertTriangle } from "lucide-react"
|
|
import { cn } from "@/app/lib/utils"
|
|
|
|
export type NotificationType = "success" | "error" | "warning" | "info"
|
|
|
|
export interface Notification {
|
|
id: string
|
|
type: NotificationType
|
|
title: string
|
|
message?: string
|
|
duration?: number
|
|
persistent?: boolean
|
|
actions?: Array<{
|
|
label: string
|
|
onClick: () => void
|
|
variant?: "default" | "outline"
|
|
}>
|
|
}
|
|
|
|
interface NotificationContextType {
|
|
notifications: Notification[]
|
|
addNotification: (notification: Omit<Notification, "id">) => void
|
|
removeNotification: (id: string) => void
|
|
clearAll: () => void
|
|
}
|
|
|
|
const NotificationContext = createContext<NotificationContextType | undefined>(undefined)
|
|
|
|
export function useNotifications() {
|
|
const context = useContext(NotificationContext)
|
|
if (!context) {
|
|
throw new Error("useNotifications must be used within a NotificationProvider")
|
|
}
|
|
return context
|
|
}
|
|
|
|
export function NotificationProvider({ children }: { children: ReactNode }) {
|
|
const [notifications, setNotifications] = useState<Notification[]>([])
|
|
|
|
const addNotification = (notification: Omit<Notification, "id">) => {
|
|
const id = Math.random().toString(36).substr(2, 9)
|
|
const newNotification: Notification = {
|
|
...notification,
|
|
id,
|
|
duration: notification.duration ?? 5000,
|
|
}
|
|
|
|
setNotifications((prev) => [newNotification, ...prev])
|
|
|
|
// 自动移除非持久化通知
|
|
if (!notification.persistent && newNotification.duration > 0) {
|
|
setTimeout(() => {
|
|
removeNotification(id)
|
|
}, newNotification.duration)
|
|
}
|
|
}
|
|
|
|
const removeNotification = (id: string) => {
|
|
setNotifications((prev) => prev.filter((notification) => notification.id !== id))
|
|
}
|
|
|
|
const clearAll = () => {
|
|
setNotifications([])
|
|
}
|
|
|
|
return (
|
|
<NotificationContext.Provider value={{ notifications, addNotification, removeNotification, clearAll }}>
|
|
{children}
|
|
<NotificationContainer />
|
|
</NotificationContext.Provider>
|
|
)
|
|
}
|
|
|
|
function NotificationContainer() {
|
|
const { notifications, removeNotification } = useNotifications()
|
|
|
|
if (notifications.length === 0) return null
|
|
|
|
return (
|
|
<div className="fixed top-4 right-4 z-50 space-y-2 max-w-sm w-full">
|
|
{notifications.map((notification) => (
|
|
<NotificationItem key={notification.id} notification={notification} onRemove={removeNotification} />
|
|
))}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
function NotificationItem({
|
|
notification,
|
|
onRemove,
|
|
}: {
|
|
notification: Notification
|
|
onRemove: (id: string) => void
|
|
}) {
|
|
const getIcon = () => {
|
|
switch (notification.type) {
|
|
case "success":
|
|
return <CheckCircle className="h-5 w-5 text-green-500" />
|
|
case "error":
|
|
return <AlertCircle className="h-5 w-5 text-red-500" />
|
|
case "warning":
|
|
return <AlertTriangle className="h-5 w-5 text-yellow-500" />
|
|
case "info":
|
|
return <Info className="h-5 w-5 text-blue-500" />
|
|
}
|
|
}
|
|
|
|
const getBorderColor = () => {
|
|
switch (notification.type) {
|
|
case "success":
|
|
return "border-l-green-500"
|
|
case "error":
|
|
return "border-l-red-500"
|
|
case "warning":
|
|
return "border-l-yellow-500"
|
|
case "info":
|
|
return "border-l-blue-500"
|
|
}
|
|
}
|
|
|
|
return (
|
|
<Card className={cn("border-l-4 shadow-lg animate-in slide-in-from-right", getBorderColor())}>
|
|
<CardContent className="p-4">
|
|
<div className="flex items-start space-x-3">
|
|
{getIcon()}
|
|
<div className="flex-1 min-w-0">
|
|
<div className="flex items-center justify-between">
|
|
<h4 className="font-medium text-sm">{notification.title}</h4>
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
className="h-6 w-6 text-gray-400 hover:text-gray-600"
|
|
onClick={() => onRemove(notification.id)}
|
|
>
|
|
<X className="h-4 w-4" />
|
|
</Button>
|
|
</div>
|
|
{notification.message && <p className="text-sm text-gray-600 mt-1">{notification.message}</p>}
|
|
{notification.actions && notification.actions.length > 0 && (
|
|
<div className="flex space-x-2 mt-3">
|
|
{notification.actions.map((action, index) => (
|
|
<Button
|
|
key={index}
|
|
variant={action.variant || "outline"}
|
|
size="sm"
|
|
onClick={() => {
|
|
action.onClick()
|
|
onRemove(notification.id)
|
|
}}
|
|
>
|
|
{action.label}
|
|
</Button>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
)
|
|
}
|
|
|
|
// 便捷的通知钩子
|
|
export function useNotify() {
|
|
const { addNotification } = useNotifications()
|
|
|
|
return {
|
|
success: (title: string, message?: string, options?: Partial<Notification>) =>
|
|
addNotification({ type: "success", title, message, ...options }),
|
|
error: (title: string, message?: string, options?: Partial<Notification>) =>
|
|
addNotification({ type: "error", title, message, ...options }),
|
|
warning: (title: string, message?: string, options?: Partial<Notification>) =>
|
|
addNotification({ type: "warning", title, message, ...options }),
|
|
info: (title: string, message?: string, options?: Partial<Notification>) =>
|
|
addNotification({ type: "info", title, message, ...options }),
|
|
}
|
|
}
|