From 6ae06fd982181867102e9d99a6da59a37d8df0aa Mon Sep 17 00:00:00 2001 From: cfdaily Date: Wed, 20 May 2026 22:57:26 +0800 Subject: [PATCH] auto-sync: 2026-05-20 22:57:26 --- .../src/components/NotificationCenter.tsx | 191 +++++++++++------- 1 file changed, 115 insertions(+), 76 deletions(-) diff --git a/src/frontend/src/components/NotificationCenter.tsx b/src/frontend/src/components/NotificationCenter.tsx index 115f45f..46aaa09 100644 --- a/src/frontend/src/components/NotificationCenter.tsx +++ b/src/frontend/src/components/NotificationCenter.tsx @@ -1,6 +1,7 @@ /** * 通知中心 — 烽火台 * 数据来源:未读邮件 + SSE 实时事件 + * 按 topic7-9 设计:按级别分组、操作按钮、任务链接、全部已读 */ import { useState, useEffect } from 'react'; @@ -14,23 +15,22 @@ interface NotifItem { time: string; read: boolean; source: 'mail' | 'event'; + taskId?: string; + projectId?: string; actionUrl?: string; } -const TYPE_STYLES: Record = { - info: { icon: 'ℹ️', color: '#6a9eff' }, - warning: { icon: '⚠️', color: '#f5c842' }, - success: { icon: '✅', color: '#2ecc8a' }, - error: { icon: '🚨', color: '#ff5270' }, +const LEVEL_ORDER: NotifItem['type'][] = ['error', 'warning', 'success', 'info']; +const TYPE_STYLES: Record = { + error: { icon: '🚨', color: '#ff5270', label: '🔴 紧急' }, + warning: { icon: '⚠️', color: '#f5c842', label: '🟡 警告' }, + success: { icon: '✅', color: '#2ecc8a', label: '🟢 完成' }, + info: { icon: 'ℹ️', color: '#6a9eff', label: '🔵 通知' }, }; function mailToNotif(m: any): NotifItem { const typeMap: Record = { - inform: 'info', - task: 'info', - alert: 'warning', - error: 'error', - done: 'success', + inform: 'info', task: 'info', alert: 'warning', error: 'error', done: 'success', }; return { id: m.id, @@ -45,75 +45,101 @@ function mailToNotif(m: any): NotifItem { export default function NotificationCenter({ onClose }: { onClose: () => void }) { const mails = useStore(s => s.mails); - const mailUnread = useStore(s => s.mailUnread); const loadMails = useStore(s => s.loadMails); const [sseEvents, setSseEvents] = useState([]); + const [expanded, setExpanded] = useState(null); - // 加载邮件 - useEffect(() => { - loadMails(); - }, []); + useEffect(() => { loadMails(); }, []); - // SSE 实时事件 useEffect(() => { const es = new EventSource('/api/events'); - es.onmessage = (e) => { + const importantTypes = ['task_completed', 'task_failed', 'review_result', 'agent_completed', 'task_updated']; + const handler = (e: MessageEvent) => { 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 => { - const item: NotifItem = { - 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 || 'Event', - message: data.detail || JSON.stringify(data).slice(0, 100), - time: data.timestamp || new Date().toISOString(), - read: false, - source: 'event', - }; - return [item, ...prev].slice(0, 20); - }); - } - } catch { /* ignore parse errors */ } + 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(); }; }, []); - // 合并:邮件 + SSE 事件 + // 合并 + 分组 const mailNotifs = (mails || []).slice(0, 20).map(mailToNotif); const allNotifs = [...sseEvents, ...mailNotifs]; const unread = allNotifs.filter(n => !n.read).length; + const grouped = LEVEL_ORDER.map(type => ({ + type, + label: TYPE_STYLES[type].label, + items: allNotifs.filter(n => n.type === type), + })).filter(g => g.items.length > 0); + 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 */ } + await fetch(`/api/mail/${notif.id}`, { + method: 'PATCH', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ is_read: true }), + }); + loadMails(); + } + if (notif.source === 'event') { + setSseEvents(prev => prev.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', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ is_read: true }), + }); + } + loadMails(); + setSseEvents(prev => prev.map(n => ({ ...n, read: true }))); + }; + + const handleViewTask = (notif: NotifItem) => { + if (notif.taskId) { + useStore.getState().setModalTaskId(notif.taskId); + if (notif.projectId) useStore.getState().setSelectedProjectId(notif.projectId); + onClose(); } }; return (
-
e.stopPropagation()}> +
e.stopPropagation()}>
🔔 烽火台 -
+
{unread > 0 && ( - - {unread} - + <> + {unread} + + )} - +
@@ -123,31 +149,44 @@ export default function NotificationCenter({ onClose }: { onClose: () => void })
暂无烽火
) : ( -
- {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.source === 'mail' ? '✉️' : '📡'} - -
-
{n.message}
+
+ {grouped.map(g => ( +
+
{g.label} ({g.items.length})
+
+ {g.items.map(n => { + const s = TYPE_STYLES[n.type]; + const isExpanded = expanded === n.id; + return ( +
+
setExpanded(isExpanded ? null : n.id)}> + {s.icon} + {n.title} + + {n.source === 'mail' ? '✉️' : '📡'} + +
+
{n.message}
+ {isExpanded && ( +
+ {n.taskId && ( + + )} + {!n.read && ( + + )} +
+ )} +
+ ); + })}
- ); - })} +
+ ))}
)}