"use client"; import { useCallback, useEffect, useState } from "react"; import { useSession } from "next-auth/react"; import { Bell, Check, AlertTriangle, TrendingDown, TrendingUp, Users, Globe } from "lucide-react"; import { Button } from "@/components/ui/button"; import { Badge } from "@/components/ui/badge"; import { Popover, PopoverContent, PopoverTrigger, } from "@/components/ui/popover"; import { api } from "@/lib/api"; // ============================================================ // 类型定义 // ============================================================ interface AlertItem { id: string; brand_id: string; user_id: string; alert_type: string; severity: string; title: string; message: string; data: Record | null; is_read: boolean; created_at: string; } interface AlertListResponse { items: AlertItem[]; total: number; } interface UnreadCountResponse { unread_count: number; } // ============================================================ // 告警类型配置 // ============================================================ const ALERT_TYPE_CONFIG: Record = { score_drop: { label: "评分下降", icon: TrendingDown, color: "text-red-500" }, score_rise: { label: "评分上升", icon: TrendingUp, color: "text-emerald-500" }, negative_sentiment: { label: "负面情感", icon: AlertTriangle, color: "text-orange-500" }, competitor_overtake: { label: "竞品超越", icon: Users, color: "text-amber-500" }, new_platform_mention: { label: "新平台提及", icon: Globe, color: "text-blue-500" }, }; const SEVERITY_CONFIG: Record = { critical: { label: "严重", className: "bg-red-100 text-red-700 border-red-200" }, warning: { label: "警告", className: "bg-orange-100 text-orange-700 border-orange-200" }, info: { label: "信息", className: "bg-blue-100 text-blue-700 border-blue-200" }, }; // ============================================================ // 工具函数 // ============================================================ function formatTimeAgo(dateStr: string): string { const date = new Date(dateStr); const now = new Date(); const diffMs = now.getTime() - date.getTime(); const diffMinutes = Math.floor(diffMs / 60000); const diffHours = Math.floor(diffMs / 3600000); const diffDays = Math.floor(diffMs / 86400000); if (diffMinutes < 1) return "刚刚"; if (diffMinutes < 60) return `${diffMinutes}分钟前`; if (diffHours < 24) return `${diffHours}小时前`; if (diffDays < 7) return `${diffDays}天前`; return date.toLocaleDateString("zh-CN"); } // ============================================================ // 通知铃铛组件 // ============================================================ export function AlertBell() { const { data: session } = useSession(); const [unreadCount, setUnreadCount] = useState(0); const [alerts, setAlerts] = useState([]); const [isOpen, setIsOpen] = useState(false); const [loading, setLoading] = useState(false); const token = session?.accessToken; // 获取未读数量 const fetchUnreadCount = useCallback(async () => { if (!token) return; try { const data = (await api.alerts.getUnreadCount(token)) as UnreadCountResponse; setUnreadCount(data.unread_count); } catch { // 静默失败 } }, [token]); // 获取告警列表 const fetchAlerts = useCallback(async () => { if (!token) return; setLoading(true); try { const data = (await api.alerts.getAlerts(token, { limit: 20, })) as AlertListResponse; setAlerts(data.items); } catch { // 静默失败 } finally { setLoading(false); } }, [token]); // 标记单条已读 const handleMarkRead = async (alertId: string) => { if (!token) return; try { await api.alerts.markRead(token, alertId); setAlerts((prev) => prev.map((a) => (a.id === alertId ? { ...a, is_read: true } : a)), ); setUnreadCount((prev) => Math.max(0, prev - 1)); } catch { // 静默失败 } }; // 全部标记已读 const handleMarkAllRead = async () => { if (!token) return; try { await api.alerts.markAllRead(token); setAlerts((prev) => prev.map((a) => ({ ...a, is_read: true }))); setUnreadCount(0); } catch { // 静默失败 } }; // 打开面板时加载告警列表 const handleOpenChange = (open: boolean) => { setIsOpen(open); if (open) { fetchAlerts(); } }; // 轮询检查新告警(每60秒) useEffect(() => { fetchUnreadCount(); const interval = setInterval(fetchUnreadCount, 60000); return () => clearInterval(interval); }, [fetchUnreadCount]); return ( {/* 头部 */}

通知

{unreadCount > 0 && ( )}
{/* 告警列表 */}
{loading ? (

加载中...

) : alerts.length === 0 ? (

暂无告警通知

) : (
{alerts.map((alert) => { const typeConfig = ALERT_TYPE_CONFIG[alert.alert_type] || { label: alert.alert_type, icon: Bell, color: "text-gray-500", }; const severityConfig = SEVERITY_CONFIG[alert.severity] || { label: alert.severity, className: "bg-gray-100 text-gray-700 border-gray-200", }; const IconComponent = typeConfig.icon; return (
{ if (!alert.is_read) { handleMarkRead(alert.id); } }} > {/* 图标 */}
{/* 内容 */}

{alert.title}

{severityConfig.label}

{alert.message}

{formatTimeAgo(alert.created_at)}

{/* 未读标记 */} {!alert.is_read && (
)}
); })}
)}
{/* 底部 */} {alerts.length > 0 && (
)} ); }