'use client'; import { useEffect, useState, useCallback, useRef } from 'react'; import { useRouter } from 'next/navigation'; import { useAuth } from '@/components/admin/auth-context'; import { adminApi } from '@/lib/admin-api'; import { toast } from '@/components/ui/sonner'; import { Bell, CheckCheck, Mail, MailOpen, Clock, AlertCircle, CheckCircle, XCircle, Archive, ExternalLink, RefreshCw, } from 'lucide-react'; interface NotificationItem { id: string; type: string; title: string; message: string; read: boolean; relatedItemId: string; relatedModelCode: string; createdAt: string; } const NOTIFICATION_TYPE_LABELS: Record = { review_pending: '待审核', review_approved: '审核通过', review_rejected: '审核驳回', item_archived: '已归档', }; const NOTIFICATION_TYPE_ICONS: Record = { review_pending: , review_approved: , review_rejected: , item_archived: , }; const NOTIFICATION_TYPE_COLORS: Record = { review_pending: 'bg-warning-bg border-border-secondary', review_approved: 'bg-success-bg border-border-secondary', review_rejected: 'bg-error-bg border-border-secondary', item_archived: 'bg-bg-secondary border-border-primary', }; export default function NotificationsPage() { const { user, loading: authLoading } = useAuth(); const router = useRouter(); const [notifications, setNotifications] = useState([]); const [total, setTotal] = useState(0); const [page, setPage] = useState(1); const [loading, setLoading] = useState(true); const [error, setError] = useState(''); const [unreadOnly, setUnreadOnly] = useState(false); const [unreadCount, setUnreadCount] = useState(0); const pageSize = 20; // 单调递增的请求序号:只有最新一次请求的响应可以写进 state const fetchSeqRef = useRef(0); const fetchNotifications = useCallback(async () => { const seq = ++fetchSeqRef.current; setLoading(true); setError(''); try { const query = new URLSearchParams(); query.set('page', String(page)); query.set('pageSize', String(pageSize)); if (unreadOnly) query.set('unreadOnly', 'true'); const data = await adminApi.request<{ items: NotificationItem[]; total: number; page: number; pageSize: number; totalPages: number; }>(`/api/admin/notifications?${query.toString()}`); if (seq !== fetchSeqRef.current) return; setNotifications(data.items || []); setTotal(data.total || 0); } catch (err) { if (seq !== fetchSeqRef.current) return; setError(err instanceof Error ? err.message : '加载失败'); } finally { if (seq === fetchSeqRef.current) setLoading(false); } }, [page, unreadOnly]); const fetchUnreadCount = useCallback(async () => { try { const data = await adminApi.request<{ count: number }>('/api/admin/notifications/unread-count'); setUnreadCount(data.count || 0); } catch { // Non-critical } }, []); useEffect(() => { if (!authLoading && !user) { router.push('/admin/login'); return; } if (user) { fetchNotifications(); fetchUnreadCount(); } }, [user, authLoading, router, fetchNotifications, fetchUnreadCount]); const handleMarkAsRead = async (id: string) => { try { await adminApi.request(`/api/admin/notifications/${id}/read`, { method: 'POST' }); setNotifications((prev) => prev.map((n) => (n.id === id ? { ...n, read: true } : n)) ); setUnreadCount((prev) => Math.max(0, prev - 1)); } catch { toast.error('标记已读失败'); } }; const handleMarkAllAsRead = async () => { try { await adminApi.request('/api/admin/notifications/read-all', { method: 'POST' }); setNotifications((prev) => prev.map((n) => ({ ...n, read: true }))); setUnreadCount(0); toast.success('全部标记为已读'); } catch { toast.error('操作失败'); } }; const handleViewRelated = (item: NotificationItem) => { if (item.relatedItemId && item.relatedModelCode) { router.push(`/admin/content/${item.relatedModelCode}/${item.relatedItemId}`); } }; const totalPages = Math.ceil(total / pageSize); if (authLoading || (!user && typeof window !== 'undefined')) { return (
); } return (

通知中心

共 {total} 条通知 {unreadCount > 0 && ( {unreadCount} 条未读 )}

{unreadCount > 0 && ( )}
{error && (
{error}
)} {/* Notifications list */}
{loading ? (
加载中...
) : notifications.length === 0 ? (

暂无通知

{unreadOnly && (

没有未读通知,试试查看全部

)}
) : ( notifications.map((item) => (
{/* Unread indicator */} {!item.read && (
)} {/* Type icon */}
{NOTIFICATION_TYPE_ICONS[item.type] || }
{/* Content */}
{NOTIFICATION_TYPE_LABELS[item.type] || item.type} {new Date(item.createdAt).toLocaleString('zh-CN', { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', })}

{item.title}

{item.message}

{/* Actions */}
{!item.read && ( )} {item.relatedItemId && item.relatedModelCode && ( )}
)) )}
{/* Pagination */} {totalPages > 1 && (
共 {total} 条,第 {page}/{totalPages} 页
)}
); }