"use client"; import { useState, useEffect, useRef } from "react"; import { useRouter } from "next/navigation"; import Link from "next/link"; import { Bell, CheckCheck, Heart, MessageCircle, CheckCircle2, XCircle, AtSign, ClipboardList, Award } from "lucide-react"; import { apiFetchNotifications, apiUnreadCount, apiMarkRead, apiMarkAllRead, type NotificationItem, } from "@/lib/api"; import { realtime, RT_NOTIFICATION_NEW } from "@/lib/realtime"; import { onDocumentVisible } from "@/lib/visibilityReconcile"; export default function NotificationBell({ initialUnread = 0 }: { initialUnread?: number }) { const router = useRouter(); const [open, setOpen] = useState(false); // SSR 直出未读数,红点不再挂载后才弹出;WS/回前台校准 const [unread, setUnread] = useState(initialUnread); const [notifications, setNotifications] = useState([]); const [loading, setLoading] = useState(false); const dropdownRef = useRef(null); const openRef = useRef(open); openRef.current = open; const loadNotificationsRef = useRef<() => void>(() => {}); // 未读数:首屏用 SSR;WS 推送 / 回前台校准(无定时轮询;open 变化不重订) useEffect(() => { let cancelled = false; let debounceTimer: ReturnType | null = null; const fetchCount = () => { apiUnreadCount() .then((res) => { if (!cancelled) setUnread(res.count || 0); }) .catch(() => {}); }; const fetchCountDebounced = () => { if (debounceTimer) clearTimeout(debounceTimer); debounceTimer = setTimeout(fetchCount, 200); }; const off = realtime.on(RT_NOTIFICATION_NEW, () => { fetchCountDebounced(); if (openRef.current) loadNotificationsRef.current(); }); const offVisible = onDocumentVisible(fetchCount); return () => { cancelled = true; if (debounceTimer) clearTimeout(debounceTimer); off(); offVisible(); }; }, []); // 点击外部关闭下拉 useEffect(() => { const handleClick = (e: MouseEvent) => { if (dropdownRef.current && !dropdownRef.current.contains(e.target as Node)) { setOpen(false); } }; document.addEventListener("mousedown", handleClick); return () => document.removeEventListener("mousedown", handleClick); }, []); const loadNotifications = async () => { setLoading(true); try { const res = await apiFetchNotifications(1); setNotifications(res.notifications || []); } catch { setNotifications([]); } setLoading(false); }; loadNotificationsRef.current = () => { void loadNotifications(); }; const handleToggle = () => { const next = !open; setOpen(next); if (next) { loadNotifications(); } }; const handleClickNotification = async (n: NotificationItem) => { if (!n.is_read) { apiMarkRead(n.id).catch(() => {}); setUnread((c) => Math.max(0, c - 1)); setNotifications((list) => list.map((item) => (item.id === n.id ? { ...item, is_read: true } : item)) ); } setOpen(false); // @提及通知跳到群聊房间;徽章通知无关联帖子(仅标已读);其余跳到帖子/评论 if (n.type === "mention" && n.room_id) { router.push(`/chat/${n.room_id}`); } else if (n.type === "pending_review") { router.push(n.post_id ? `/post/${n.post_id}` : "/admin/content?status=pending"); } else if (n.type === "badge") { return; } else { router.push(`/post/${n.post_id}`); } }; const handleMarkAllRead = async () => { await apiMarkAllRead(); setUnread(0); setNotifications((list) => list.map((item) => ({ ...item, is_read: true }))); }; const formatTime = (dateStr: string) => { const diff = Date.now() - new Date(dateStr).getTime(); const min = Math.floor(diff / 60000); if (min < 1) return "刚刚"; if (min < 60) return `${min} 分钟前`; const hour = Math.floor(min / 60); if (hour < 24) return `${hour} 小时前`; const day = Math.floor(hour / 24); return `${day} 天前`; }; const actionText = (n: NotificationItem) => { if (n.type === "comment") return "评论了你的帖子"; if (n.type === "reply") return "回复了你的评论"; if (n.type === "approved") return "审核通过"; if (n.type === "rejected") return "未通过审核"; if (n.type === "mention") return "在消息中提到了你"; if (n.type === "pending_review") return "有新内容待审核"; if (n.type === "deleted") return "删除了你的帖子"; if (n.type === "badge") return "向你颁布了徽章"; return "点赞了你的帖子"; }; // 审核结果类通知:无互动对象语义,使用专用图标与语义色 const isApproval = (n: NotificationItem) => n.type === "approved" || n.type === "rejected" || n.type === "deleted"; const iconStyle = (n: NotificationItem): { color: string; bg: string; node: React.ReactNode } => { if (n.type === "approved") return { color: "var(--ok)", bg: "var(--ok-soft)", node: }; if (n.type === "rejected" || n.type === "deleted") return { color: "var(--danger)", bg: "var(--danger-soft)", node: }; if (n.type === "like") return { color: "var(--clay)", bg: "var(--clay-soft)", node: }; if (n.type === "mention") return { color: "var(--accent)", bg: "var(--accent-soft)", node: }; if (n.type === "pending_review") return { color: "var(--accent)", bg: "var(--accent-soft)", node: }; if (n.type === "badge") return { color: "var(--gold)", bg: "var(--gold-soft)", node: }; return { color: "var(--accent)", bg: "var(--accent-soft)", node: }; }; return (
{open && (
通知 {unread > 0 && ( )}
{loading ? (

加载中...

) : notifications.length === 0 ? (

还没有通知

) : ( notifications.map((n) => { const ic = iconStyle(n); const approval = isApproval(n); return ( ); }) )}
setOpen(false)} className="meta text-xs hover:text-[var(--accent)] transition-colors" > 查看全部通知 →
)}
); }