diff --git a/src/frontend/src/components/NotificationCenter.tsx b/src/frontend/src/components/NotificationCenter.tsx index ec1356b..1350668 100644 --- a/src/frontend/src/components/NotificationCenter.tsx +++ b/src/frontend/src/components/NotificationCenter.tsx @@ -1,10 +1,10 @@ /** * 通知中心 — 烽火台 - * 数据来源:未读邮件 + SSE 实时事件 - * 按 topic7-9 设计:按级别分组、操作按钮、任务链接、全部已读 + * 数据来源:未读邮件(store.mails)+ SSE 事件(store.sseEvents) + * 单一 SSE 连接由 store.ts 管理,这里只读数据 */ -import { useState, useEffect } from 'react'; +import { useEffect } from 'react'; import { useStore } from '../store'; interface NotifItem { @@ -17,7 +17,6 @@ interface NotifItem { source: 'mail' | 'event'; taskId?: string; projectId?: string; - actionUrl?: string; } const LEVEL_ORDER: NotifItem['type'][] = ['error', 'warning', 'success', 'info']; @@ -45,45 +44,14 @@ function mailToNotif(m: any): NotifItem { export default function NotificationCenter({ onClose }: { onClose: () => void }) { const mails = useStore(s => s.mails); + const sseEvents = useStore(s => s.sseEvents) as NotifItem[]; const loadMails = useStore(s => s.loadMails); - const [sseEvents, setSseEvents] = useState([]); - const [expanded, setExpanded] = useState(null); useEffect(() => { loadMails(); }, []); - useEffect(() => { - const es = new EventSource('/api/events'); - const importantTypes = ['task_completed', 'task_failed', 'review_result', 'agent_completed', 'task_updated']; - const handler = (e: MessageEvent) => { - try { - const data = JSON.parse(e.data); - const etype = (e as any).type || data.event_type || ''; - if (!importantTypes.includes(etype)) return; - const ntype = etype === 'task_failed' ? 'error' : etype === 'task_completed' ? 'success' : etype === 'review_result' ? 'warning' : 'info'; - setSseEvents(prev => { - const item: NotifItem = { - id: data.id || `sse-${Date.now()}`, - type: ntype, - title: data.task_id ? `任务 ${data.task_id?.slice(0, 8)}...` : (data.event_type || etype), - message: `${data.old_status || ''} → ${data.new_status || data.event_type || ''}`, - time: data.timestamp || new Date().toISOString(), - read: false, - source: 'event', - taskId: data.task_id, - projectId: data.project_id, - }; - return [item, ...prev].slice(0, 30); - }); - } catch { /* ignore */ } - }; - importantTypes.forEach(t => es.addEventListener(t, handler)); - es.onerror = () => { es.close(); }; - return () => { es.close(); }; - }, []); - // 合并 + 分组 const mailNotifs = (mails || []).slice(0, 20).map(mailToNotif); - const allNotifs = [...sseEvents, ...mailNotifs]; + const allNotifs: NotifItem[] = [...(sseEvents || []), ...mailNotifs]; const unread = allNotifs.filter(n => !n.read).length; const grouped = LEVEL_ORDER.map(type => ({ @@ -102,12 +70,15 @@ export default function NotificationCenter({ onClose }: { onClose: () => void }) loadMails(); } if (notif.source === 'event') { - setSseEvents(prev => prev.map(n => n.id === notif.id ? { ...n, read: true } : n)); + useStore.setState({ + sseEvents: (useStore.getState().sseEvents as NotifItem[]).map( + n => n.id === notif.id ? { ...n, read: true } : n + ), + }); } }; const handleMarkAllRead = async () => { - // 标记所有邮件已读 for (const n of allNotifs.filter(n => !n.read && n.source === 'mail')) { await fetch(`/api/mail/${n.id}`, { method: 'PATCH', @@ -116,7 +87,9 @@ export default function NotificationCenter({ onClose }: { onClose: () => void }) }); } loadMails(); - setSseEvents(prev => prev.map(n => ({ ...n, read: true }))); + useStore.setState({ + sseEvents: (useStore.getState().sseEvents as NotifItem[]).map(n => ({ ...n, read: true })), + }); }; const handleViewTask = (notif: NotifItem) => { @@ -158,15 +131,12 @@ export default function NotificationCenter({ onClose }: { onClose: () => void }) const s = TYPE_STYLES[n.type]; return (
{ - // 直接操作:有任务链接就跳转,否则标记已读 if (n.taskId) { handleViewTask(n); } else if (!n.read) { handleMarkRead(n); } }} style={{ padding: '8px 10px', borderRadius: 8, background: 'var(--panel2)', border: `1px solid ${n.read ? 'var(--line)' : s.color + '44'}`, - opacity: n.read ? 0.5 : 1, - cursor: 'pointer', - transition: 'opacity 0.15s', + opacity: n.read ? 0.5 : 1, cursor: 'pointer', }}>
{s.icon} @@ -174,7 +144,7 @@ export default function NotificationCenter({ onClose }: { onClose: () => void }) {n.source === 'mail' ? '✉️' : '📡'} - {!n.read && } + {!n.read && }
{n.message}