| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215 |
- 'use client'
- import { useState } from 'react'
- import { useTranslation } from 'react-i18next'
- import { RiCheckDoubleLine, RiCheckLine, RiTimeLine } from '@remixicon/react'
- import { BellIcon } from '@heroicons/react/24/outline'
- import useSWR from 'swr'
- import { useRouter } from 'next/navigation'
- import classNames from '@/utils/classnames'
- import { formatDistanceToNow } from '@/utils/time'
- import { fetchNotifications, markNotificationsAsRead } from '@/service/notifications'
- import type { Notification } from '@/service/notifications'
- type NotificationPanelProps = {
- onClose: () => void
- onNotificationRead: () => void
- }
- const NotificationPanel = ({ onClose, onNotificationRead }: NotificationPanelProps) => {
- const { t } = useTranslation()
- const router = useRouter()
- const [filter, setFilter] = useState<'all' | 'unread'>('all')
- // 构建 SWR key,确保 filter 变化时触发重新请求
- const swrKey = filter === 'unread'
- ? '/datasets/notifications?is_read=false'
- : '/datasets/notifications'
- // 获取通知列表
- const { data, mutate } = useSWR(
- swrKey,
- fetchNotifications,
- {
- dedupingInterval: 0,
- revalidateOnFocus: false,
- onSuccess: (data) => {
- console.log('[NOTIFY-PANEL] Notifications data:', data)
- console.log('[NOTIFY-PANEL] Number of notifications:', data?.notifications?.length || 0)
- console.log('[NOTIFY-PANEL] Current filter:', filter)
- },
- onError: (error) => {
- console.error('[NOTIFY-PANEL] Failed to fetch notifications:', error)
- },
- },
- )
- const notifications = data?.notifications || []
- const unreadCount = data?.unread_count || 0
- // 标记单个通知为已读
- const markAsRead = async (notificationId: string) => {
- try {
- await markNotificationsAsRead({
- notification_ids: [notificationId],
- })
- mutate()
- onNotificationRead()
- }
- catch (error) {
- console.error('Failed to mark notification as read:', error)
- }
- }
- // 标记全部为已读
- const markAllAsRead = async () => {
- try {
- await markNotificationsAsRead({
- mark_all_as_read: true,
- })
- mutate()
- onNotificationRead()
- }
- catch (error) {
- console.error('Failed to mark all notifications as read:', error)
- }
- }
- // 点击通知
- const handleNotificationClick = async (notification: Notification) => {
- // 如果未读,先标记为已读
- if (!notification.is_read)
- await markAsRead(notification.id)
- // 跳转到对应页面
- if (notification.link_url) {
- router.push(notification.link_url)
- onClose()
- }
- }
- return (
- <div className="w-[420px] rounded-xl border border-components-panel-border bg-components-panel-bg shadow-xl">
- {/* 头部 */}
- <div className="flex items-center justify-between border-b border-divider-subtle px-4 py-3">
- <div className="flex items-center gap-2">
- <h3 className="system-md-semibold text-text-primary">
- {t('common.notifications.title')}
- </h3>
- {unreadCount > 0 && (
- <span className="rounded-full bg-util-colors-blue-blue-500 px-2 py-0.5 text-xs font-semibold text-white">
- {unreadCount}
- </span>
- )}
- </div>
- {/* 全部已读按钮 */}
- {unreadCount > 0 && (
- <button
- onClick={markAllAsRead}
- className="system-xs-medium flex items-center gap-1 text-text-tertiary hover:text-text-secondary"
- >
- <RiCheckDoubleLine className="h-3.5 w-3.5" />
- {t('common.notifications.markAllAsRead')}
- </button>
- )}
- </div>
- {/* 过滤标签 */}
- <div className="flex gap-2 border-b border-divider-subtle px-4 py-2">
- <button
- onClick={() => setFilter('all')}
- className={classNames(
- 'system-xs-medium rounded-md px-3 py-1',
- filter === 'all'
- ? 'bg-state-base-active text-text-primary'
- : 'text-text-tertiary hover:bg-state-base-hover hover:text-text-secondary',
- )}
- >
- {t('common.notifications.all')}
- </button>
- <button
- onClick={() => setFilter('unread')}
- className={classNames(
- 'system-xs-medium rounded-md px-3 py-1',
- filter === 'unread'
- ? 'bg-state-base-active text-text-primary'
- : 'text-text-tertiary hover:bg-state-base-hover hover:text-text-secondary',
- )}
- >
- {t('common.notifications.unread')} {unreadCount > 0 && `(${unreadCount})`}
- </button>
- </div>
- {/* 通知列表 */}
- <div className="max-h-[500px] overflow-y-auto">
- {notifications.length === 0
- ? (
- <div className="flex flex-col items-center justify-center py-12">
- <BellIcon className="mb-2 h-12 w-12 text-text-quaternary" />
- <p className="system-sm-regular text-text-tertiary">
- {filter === 'unread'
- ? t('common.notifications.noUnread')
- : t('common.notifications.noNotifications')}
- </p>
- </div>
- )
- : (
- notifications.map((notification: Notification) => (
- <div
- key={notification.id}
- onClick={() => handleNotificationClick(notification)}
- className={classNames(
- 'cursor-pointer border-b border-divider-subtle px-4 py-3 transition-colors hover:bg-state-base-hover',
- !notification.is_read && 'bg-state-base-active',
- )}
- >
- <div className="flex items-start gap-3">
- {/* 已读/未读指示器 */}
- <div className="mt-1.5 shrink-0">
- {notification.is_read
- ? <RiCheckLine className="h-4 w-4 text-text-quaternary" />
- : <div className="h-2 w-2 rounded-full bg-util-colors-blue-blue-600" />}
- </div>
- <div className="flex-1 overflow-hidden">
- {/* 标题 */}
- <div className="system-sm-semibold mb-1 text-text-primary">
- {notification.title}
- </div>
- {/* 内容预览 */}
- {notification.content && (
- <div className="system-xs-regular mb-1 line-clamp-2 text-text-secondary">
- {notification.content}
- </div>
- )}
- {/* 片段预览 */}
- {notification.segment_preview && (
- <div className="system-xs-regular mb-2 line-clamp-1 rounded bg-background-section-burn px-2 py-1 text-text-tertiary">
- "{notification.segment_preview}"
- </div>
- )}
- {/* 底部信息 */}
- <div className="flex items-center gap-2 text-xs text-text-quaternary">
- <span className="font-medium text-text-tertiary">
- {notification.dataset_name}
- </span>
- <span>·</span>
- <div className="flex items-center gap-1">
- <RiTimeLine className="h-3 w-3" />
- <span>{formatDistanceToNow(new Date(notification.created_at))}</span>
- </div>
- </div>
- </div>
- </div>
- </div>
- ))
- )}
- </div>
- </div>
- )
- }
- export default NotificationPanel
|