auto-sync: 2026-05-20 23:23:19
This commit is contained in:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user