"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 } from "lucide-react"; import { apiFetchNotifications, apiUnreadCount, apiMarkRead, apiMarkAllRead, type NotificationItem, } from "@/lib/api"; export default function NotificationBell({ initialUnread = 0 }: { initialUnread?: number }) { const router = useRouter(); const [open, setOpen] = useState(false); // SSR 直出未读数,红点不再挂载后才弹出;挂载后轮询自然校正 const [unread, setUnread] = useState(initialUnread); const [notifications, setNotifications] = useState([]); const [loading, setLoading] = useState(false); const dropdownRef = useRef(null); // 轮询未读数(30 秒);页面从后台切回时立即探测一次, // 使账号被封禁等服务端状态变化在用户切回标签页的瞬间即可被发现 useEffect(() => { let cancelled = false; const fetchCount = () => { apiUnreadCount() .then((res) => { if (!cancelled) setUnread(res.count || 0); }) .catch(() => {}); }; fetchCount(); const timer = setInterval(fetchCount, 30000); const onVisible = () => { if (document.visibilityState === "visible") fetchCount(); }; document.addEventListener("visibilitychange", onVisible); return () => { cancelled = true; clearInterval(timer); document.removeEventListener("visibilitychange", onVisible); }; }, []); // 点击外部关闭下拉 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); }; 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); 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 "未通过审核"; return "点赞了你的帖子"; }; // 审核结果类通知:无互动对象语义,使用专用图标与语义色 const isApproval = (n: NotificationItem) => n.type === "approved" || n.type === "rejected"; 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") return { color: "var(--danger)", bg: "var(--danger-soft)", node: }; if (n.type === "like") return { color: "var(--clay)", bg: "var(--clay-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" > 查看全部通知 →
)}
); }