From 806da277f127f368ba04c3d57c10ba42f943b197 Mon Sep 17 00:00:00 2001 From: cfdaily Date: Wed, 20 May 2026 22:38:29 +0800 Subject: [PATCH] auto-sync: 2026-05-20 22:38:29 --- .../src/components/NotificationCenter.tsx | 112 +++++++++++++++--- 1 file changed, 96 insertions(+), 16 deletions(-) diff --git a/src/frontend/src/components/NotificationCenter.tsx b/src/frontend/src/components/NotificationCenter.tsx index eff6c9c..8e43458 100644 --- a/src/frontend/src/components/NotificationCenter.tsx +++ b/src/frontend/src/components/NotificationCenter.tsx @@ -1,18 +1,20 @@ /** * 通知中心 — 烽火台 - * 显示系统通知、审批请求、告警信息 + * 数据来源:未读邮件 + SSE 实时事件 */ -import { useState } from 'react'; +import { useState, useEffect } from 'react'; import { useStore } from '../store'; -export interface Notification { +interface NotifItem { id: string; type: 'info' | 'warning' | 'success' | 'error'; title: string; message: string; time: string; read: boolean; + source: 'mail' | 'event'; + actionUrl?: string; } const TYPE_STYLES: Record = { @@ -22,44 +24,122 @@ const TYPE_STYLES: Record = { error: { icon: '🚨', color: '#ff5270' }, }; +function mailToNotif(m: any): NotifItem { + const typeMap: Record = { + inform: 'info', + task: 'info', + alert: 'warning', + error: 'error', + done: 'success', + }; + return { + id: m.id, + type: typeMap[m.type] || 'info', + title: m.title || '(无标题)', + message: `${m.from || '?'} → ${m.to || '?'}`, + time: m.created_at || '', + read: !!m.is_read, + source: 'mail', + }; +} + export default function NotificationCenter({ onClose }: { onClose: () => void }) { - const [notifications] = useState([]); - const unread = notifications.filter((n) => !n.read).length; + const mails = useStore(s => s.mails); + const mailUnread = useStore(s => s.mailUnread); + const loadMails = useStore(s => s.loadMails); + const [sseEvents, setSseEvents] = useState([]); + + // 加载邮件 + useEffect(() => { + loadMails(); + }, []); + + // SSE 实时事件 + useEffect(() => { + const es = new EventSource('/api/events'); + es.onmessage = (e) => { + try { + const data = JSON.parse(e.data); + // 只关注关键事件 + const importantTypes = [ + 'task_completed', 'task_failed', 'review_result', + 'agent_completed', 'agent_spawned', + ]; + if (importantTypes.includes(e.type || data.event_type)) { + setSseEvents(prev => [{ + id: data.id || `sse-${Date.now()}`, + type: (e.type === 'task_failed' ? 'error' : e.type === 'task_completed' ? 'success' : 'info'), + title: data.title || data.event_type || e.type, + message: data.detail || JSON.stringify(data).slice(0, 100), + time: data.timestamp || new Date().toISOString(), + read: false, + source: 'event', + }, ...prev].slice(0, 20)]); + } + } catch { /* ignore parse errors */ } + }; + return () => { es.close(); }; + }, []); + + // 合并:邮件 + SSE 事件 + const mailNotifs = (mails || []).slice(0, 20).map(mailToNotif); + const allNotifs = [...sseEvents, ...mailNotifs]; + const unread = allNotifs.filter(n => !n.read).length; + + const handleMarkRead = async (notif: NotifItem) => { + if (notif.source === 'mail' && !notif.read) { + try { + await fetch(`/api/mail/${notif.id}`, { + method: 'PATCH', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ is_read: true }), + }); + loadMails(); + } catch { /* ignore */ } + } + }; return (
-
e.stopPropagation()}> +
e.stopPropagation()}>
🔔 烽火台 - {unread > 0 && ( - - {unread} - - )} +
+ {unread > 0 && ( + + {unread} + + )} + +
- {notifications.length === 0 ? ( + {allNotifs.length === 0 ? (
🔕
-
暂无烽火(通知功能待后端支持)
+
暂无烽火
) : ( -
- {notifications.map((n) => { +
+ {allNotifs.map((n) => { const style = TYPE_STYLES[n.type] || TYPE_STYLES.info; return (
handleMarkRead(n)} style={{ padding: '10px 12px', borderRadius: 8, background: 'var(--panel2)', border: `1px solid ${n.read ? 'var(--line)' : style.color + '44'}`, opacity: n.read ? 0.6 : 1, + cursor: n.source === 'mail' && !n.read ? 'pointer' : 'default', }} >
{style.icon} {n.title} - {n.time} + + {n.source === 'mail' ? '✉️' : '📡'} +
{n.message}