auto-sync: 2026-05-20 23:23:19

This commit is contained in:
cfdaily
2026-05-20 23:23:19 +08:00
parent 3a1abb026f
commit 64015d814e
@@ -1,7 +1,8 @@
/**
* 通知中心 — 烽火台
* 数据来源:未读邮件(store.mails+ SSE 事件(store.sseEvents
* 单一 SSE 连接由 store.ts 管理,这里只读数据
* 数据来源:SSE 实时事件(store.sseEvents
* 按 topic7-9 设计:按级别分组、操作按钮、任务链接、全部已读
* 邮件数据不在此展示(飞鸽传书 Tab 独立展示)
*/
import { useEffect } from 'react';
@@ -27,66 +28,25 @@ const TYPE_STYLES: Record<string, { icon: string; color: string; label: string }
info: { icon: '️', color: '#6a9eff', label: '🔵 通知' },
};
function mailToNotif(m: any): NotifItem {
const typeMap: Record<string, NotifItem['type']> = {
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 mails = useStore(s => s.mails);
const sseEvents = useStore(s => s.sseEvents) as NotifItem[];
const loadMails = useStore(s => s.loadMails);
useEffect(() => { loadMails(); }, []);
// 合并 + 分组
const mailNotifs = (mails || []).slice(0, 20).map(mailToNotif);
const allNotifs: NotifItem[] = [...(sseEvents || []), ...mailNotifs];
const unread = allNotifs.filter(n => !n.read).length;
const sseEvents = (useStore(s => s.sseEvents) || []) as NotifItem[];
const unread = sseEvents.filter(n => !n.read).length;
const grouped = LEVEL_ORDER.map(type => ({
type,
label: TYPE_STYLES[type].label,
items: allNotifs.filter(n => n.type === type),
items: sseEvents.filter(n => n.type === type),
})).filter(g => g.items.length > 0);
const handleMarkRead = async (notif: NotifItem) => {
if (notif.source === 'mail' && !notif.read) {
await fetch(`/api/mail/${notif.id}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ is_read: true }),
});
loadMails();
}
if (notif.source === 'event') {
useStore.setState({
sseEvents: (useStore.getState().sseEvents as NotifItem[]).map(
n => n.id === notif.id ? { ...n, read: true } : n
),
});
}
const handleMarkRead = (notif: NotifItem) => {
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',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ is_read: true }),
});
}
loadMails();
const handleMarkAllRead = () => {
useStore.setState({
sseEvents: (useStore.getState().sseEvents as NotifItem[]).map(n => ({ ...n, read: true })),
});
@@ -116,7 +76,7 @@ export default function NotificationCenter({ onClose }: { onClose: () => void })
</div>
</div>
{allNotifs.length === 0 ? (
{sseEvents.length === 0 ? (
<div style={{ textAlign: 'center', padding: '20px 0' }}>
<div style={{ fontSize: 28, marginBottom: 8 }}>🔕</div>
<div style={{ fontSize: 12, color: 'var(--muted)' }}></div>
@@ -141,9 +101,6 @@ export default function NotificationCenter({ onClose }: { onClose: () => void })
<div style={{ display: 'flex', gap: 6, alignItems: 'center' }}>
<span style={{ fontSize: 12 }}>{s.icon}</span>
<span style={{ fontSize: 11, fontWeight: 600, flex: 1 }}>{n.title}</span>
<span style={{ fontSize: 9, background: 'var(--panel)', padding: '1px 4px', borderRadius: 3, color: 'var(--muted)' }}>
{n.source === 'mail' ? '✉️' : '📡'}
</span>
{!n.read && <span style={{ width: 6, height: 6, borderRadius: 3, background: s.color, display: 'inline-block' }} />}
</div>
<div style={{ fontSize: 10, color: 'var(--muted)', marginTop: 2 }}>{n.message}</div>