index.tsx 3.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125
  1. 'use client'
  2. import { useEffect, useRef, useState } from 'react'
  3. import { BellIcon as BellIconOutline } from '@heroicons/react/24/outline'
  4. import { BellIcon as BellIconSolid } from '@heroicons/react/24/solid'
  5. import useSWR from 'swr'
  6. import classNames from '@/utils/classnames'
  7. import { useAppContext } from '@/context/app-context'
  8. import { fetchUnreadCount } from '@/service/notifications'
  9. import NotificationPanel from './notification-panel'
  10. type NotificationsNavProps = {
  11. className?: string
  12. }
  13. const NotificationsNav = ({ className }: NotificationsNavProps) => {
  14. const [isOpen, setIsOpen] = useState(false)
  15. const panelRef = useRef<HTMLDivElement>(null)
  16. const buttonRef = useRef<HTMLDivElement>(null)
  17. const { userProfile } = useAppContext()
  18. // 调试:输出当前用户信息
  19. useEffect(() => {
  20. if (userProfile?.id) {
  21. console.log('[NOTIFY] Current user:', {
  22. id: userProfile.id,
  23. name: userProfile.name,
  24. email: userProfile.email,
  25. })
  26. }
  27. }, [userProfile])
  28. // 获取未读通知数量
  29. const { data: unreadData, mutate: mutateUnreadCount } = useSWR(
  30. '/datasets/notifications/unread-count',
  31. fetchUnreadCount,
  32. {
  33. refreshInterval: 30000, // 每30秒刷新一次
  34. onSuccess: (data) => {
  35. console.log('[NOTIFY] Unread count data:', data)
  36. },
  37. onError: (error) => {
  38. console.error('[NOTIFY] Failed to fetch unread count:', error)
  39. },
  40. },
  41. )
  42. const unreadCount = unreadData?.unread_count || 0
  43. // 点击外部关闭面板
  44. useEffect(() => {
  45. const handleClickOutside = (event: MouseEvent) => {
  46. if (
  47. panelRef.current
  48. && !panelRef.current.contains(event.target as Node)
  49. && buttonRef.current
  50. && !buttonRef.current.contains(event.target as Node)
  51. )
  52. setIsOpen(false)
  53. }
  54. if (isOpen) {
  55. document.addEventListener('mousedown', handleClickOutside)
  56. return () => {
  57. document.removeEventListener('mousedown', handleClickOutside)
  58. }
  59. }
  60. }, [isOpen])
  61. const handleToggle = () => {
  62. setIsOpen(!isOpen)
  63. }
  64. const handleNotificationRead = () => {
  65. // 重新获取未读数量
  66. mutateUnreadCount()
  67. }
  68. return (
  69. <div className="relative">
  70. <div
  71. ref={buttonRef}
  72. onClick={handleToggle}
  73. className={classNames(
  74. className,
  75. 'group cursor-pointer',
  76. )}
  77. >
  78. <div
  79. className={classNames(
  80. 'system-sm-medium relative flex h-8 w-8 flex-row items-center justify-center rounded-xl border border-transparent p-1.5',
  81. isOpen && 'border-components-main-nav-nav-button-border bg-components-main-nav-nav-button-bg-active text-components-main-nav-nav-button-text shadow-md',
  82. !isOpen && 'text-text-tertiary hover:bg-state-base-hover hover:text-text-secondary',
  83. )}
  84. >
  85. {/* 未读数量徽章 */}
  86. {unreadCount > 0 && (
  87. <div className="absolute right-[-4px] top-[-4px] flex h-5 min-w-[20px] items-center justify-center rounded-full bg-util-colors-red-red-600 px-1 text-[11px] font-semibold text-white">
  88. {unreadCount > 99 ? '99+' : unreadCount}
  89. </div>
  90. )}
  91. {/* 铃铛图标 */}
  92. {isOpen
  93. ? <BellIconSolid className="h-4 w-4" />
  94. : <BellIconOutline className="h-4 w-4" />}
  95. </div>
  96. </div>
  97. {/* 通知面板 */}
  98. {isOpen && (
  99. <div
  100. ref={panelRef}
  101. className="absolute right-0 top-[calc(100%+8px)] z-[1200]"
  102. >
  103. <NotificationPanel
  104. onClose={() => setIsOpen(false)}
  105. onNotificationRead={handleNotificationRead}
  106. />
  107. </div>
  108. )}
  109. </div>
  110. )
  111. }
  112. export default NotificationsNav