import React, { useState, useEffect, useCallback, useRef } from 'react' import { useNavigate } from 'react-router-dom' import { useTranslation } from '../i18n' import { getInAppNotifications, getInAppUnreadCount, markInAppAsRead, markAllInAppRead, type InAppNotificationData, } from '../api' /** 格式化相对时间 */ function formatRelativeTime(timestamp: string, locale: string): string { if (!timestamp) return '' const diff = Date.now() - new Date(timestamp).getTime() const seconds = Math.floor(diff / 1000) const minutes = Math.floor(seconds / 60) const hours = Math.floor(minutes / 60) if (seconds < 60) return locale === 'en' ? 'Just now' : '刚刚' if (minutes < 60) return locale === 'en' ? `${minutes}m ago` : `${minutes}分钟前` if (hours < 24) return locale === 'en' ? `${hours}h ago` : `${hours}小时前` const days = Math.floor(hours / 24) return locale === 'en' ? `${days}d ago` : `${days}天前` } const TYPE_ICONS: Record = { appointment: '\u{1F4C5}', patient: '\u{1F43E}', vaccine: '\u{1F489}', bill: '\u{1F4B0}', system: '\u{1F514}', } export default function NotificationBell() { const { t, locale } = useTranslation() const navigate = useNavigate() const [unreadCount, setUnreadCount] = useState(0) const [open, setOpen] = useState(false) const [notifications, setNotifications] = useState([]) const [loading, setLoading] = useState(false) const dropdownRef = useRef(null) // 加载未读数 const loadUnreadCount = useCallback(async () => { try { const data = await getInAppUnreadCount() setUnreadCount(data.count) } catch { // 静默失败 } }, []) // 加载通知列表 const loadNotifications = useCallback(async () => { setLoading(true) try { const res = await getInAppNotifications({ page: 1, page_size: 10 }) setNotifications(res.items) } catch { // 静默失败 } finally { setLoading(false) } }, []) // 打开面板时加载 useEffect(() => { if (open) { loadNotifications() } }, [open, loadNotifications]) // 定时轮询未读数 useEffect(() => { loadUnreadCount() const interval = setInterval(loadUnreadCount, 30_000) return () => clearInterval(interval) }, [loadUnreadCount]) // 点击外部关闭 useEffect(() => { const handler = (e: MouseEvent) => { if (dropdownRef.current && !dropdownRef.current.contains(e.target as Node)) { setOpen(false) } } if (open) { document.addEventListener('mousedown', handler) } return () => document.removeEventListener('mousedown', handler) }, [open]) // 点击通知 const handleClickNotification = async (notif: InAppNotificationData) => { if (!notif.is_read) { try { await markInAppAsRead(notif.id) setUnreadCount((c) => Math.max(0, c - 1)) setNotifications((prev) => prev.map((n) => (n.id === notif.id ? { ...n, is_read: true } : n)) ) } catch { // 静默失败 } } if (notif.link) { navigate(notif.link) } setOpen(false) } // 全部已读 const handleMarkAllRead = async () => { try { await markAllInAppRead() setUnreadCount(0) setNotifications((prev) => prev.map((n) => ({ ...n, is_read: true }))) } catch { // 静默失败 } } return (
{/* 铃铛按钮 */} {/* 下拉面板 */} {open && (
{/* 头部 */}

{t('notifications.panelTitle') || '通知'}

{unreadCount > 0 && ( )}
{/* 列表 */}
{loading ? (
) : notifications.length === 0 ? (

{t('notifications.noInApp') || '暂无通知'}

) : ( notifications.map((n) => ( )) )}
{/* 底部 */}
)}
) }