From 6d0a6a8e7f67f2f3cd2c45fd6c42eab693f631f3 Mon Sep 17 00:00:00 2001 From: cfdaily Date: Mon, 18 May 2026 12:57:33 +0800 Subject: [PATCH] auto-sync: 2026-05-18 12:57:32 --- src/frontend/src/components/MailPanel.tsx | 213 ++++++++++++++++++++++ 1 file changed, 213 insertions(+) create mode 100644 src/frontend/src/components/MailPanel.tsx diff --git a/src/frontend/src/components/MailPanel.tsx b/src/frontend/src/components/MailPanel.tsx new file mode 100644 index 0000000..9c94352 --- /dev/null +++ b/src/frontend/src/components/MailPanel.tsx @@ -0,0 +1,213 @@ +/** + * MailPanel — 飞鸽传书(v2.7) + * Mail = _mail project 里的两点 Task(from → to) + * List 展示:时间 | From | To | Title | 状态 + */ +import { useEffect, useState } from 'react'; +import { useStore } from '../store'; + +const AGENT_EMOJI: Record = { + 'pangtong-fujunshi': '🐦', 'simayi-challenger': '🦅', 'jiangwei-infra': '🔧', + 'guanyu-dev': '⚔️', 'zhangfei-dev': '💪', 'zhaoyun-data': '📊', +}; + +const TYPE_ICON: Record = { + inform: '📢', request: '📋', 'task-assign': '📝', reply: '↩️', text: '💬', +}; + +function fmtTime(iso: string): string { + try { + const d = new Date(iso.includes('T') ? iso : iso.replace(' ', 'T') + 'Z'); + const now = Date.now(); + const diff = now - d.getTime(); + const mins = Math.floor(diff / 60000); + if (mins < 1) return '刚刚'; + if (mins < 60) return `${mins}分钟前`; + const hrs = Math.floor(mins / 60); + if (hrs < 24) return `${hrs}小时前`; + return `${d.getMonth() + 1}/${d.getDate()} ${d.getHours()}:${String(d.getMinutes()).padStart(2, '0')}`; + } catch { return iso; } +} + +export default function MailPanel() { + const mails = useStore(s => s.mails); + const loadMails = useStore(s => s.loadMails); + const [selectedId, setSelectedId] = useState(null); + const [detail, setDetail] = useState(null); + const [filterFrom, setFilterFrom] = useState(''); + const [filterUnread, setFilterUnread] = useState(false); + + useEffect(() => { loadMails(); }, []); + + // 加载详情 + useEffect(() => { + if (!selectedId) { setDetail(null); return; } + (async () => { + try { + const res = await fetch(`/api/mail/${selectedId}`); + if (res.ok) { + const data = await res.json(); + setDetail(data); + // 标记已读 + if (!data.is_read) { + await fetch(`/api/mail/${selectedId}`, { + method: 'PATCH', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ is_read: true }), + }); + loadMails(); + } + } + } catch { /* */ } + })(); + }, [selectedId]); + + const filtered = mails.filter((m: any) => { + if (filterFrom && m.from !== filterFrom) return false; + if (filterUnread && m.is_read) return false; + return true; + }); + + return ( +
+ {/* 左侧列表 */} +
+ {/* 筛选栏 */} +
+ + + {filtered.length} 封 +
+ + {/* 邮件列表 */} +
+ {filtered.length === 0 && ( +
暂无邮件
+ )} + {filtered.map((m: any) => ( +
setSelectedId(m.id)} style={{ + padding: '10px 14px', borderBottom: '1px solid var(--line)', + cursor: 'pointer', transition: 'background .15s', + background: selectedId === m.id ? 'var(--panel2)' : m.is_read ? 'transparent' : '#0a1530', + }} + onMouseEnter={e => e.currentTarget.style.background = 'var(--panel2)'} + onMouseLeave={e => e.currentTarget.style.background = selectedId === m.id ? 'var(--panel2)' : m.is_read ? 'transparent' : '#0a1530'} + > +
+
+ {!m.is_read && } + {m.from || '—'} + + {m.to || '—'} +
+ {fmtTime(m.created_at)} +
+
+ {TYPE_ICON[m.type] || '💬'} {m.title} +
+
+ ))} +
+
+ + {/* 右侧详情 */} +
+ {!detail ? ( +
+
✉️
+
选择一封邮件查看详情
+
+ ) : ( + <> + {/* 头部 */} +
+
+ + {detail.type || 'text'} + + + {detail.is_read ? '已读' : '未读'} + +
+
{detail.title}
+
+ {AGENT_EMOJI[detail.from] || '👤'} {detail.from} + + {AGENT_EMOJI[detail.to] || '👤'} {detail.to} + · + {fmtTime(detail.created_at)} +
+
+ + {/* 正文 */} +
+ {detail.description || '(无正文)'} +
+ + {/* 评论线程 */} + {detail.comments && detail.comments.length > 0 && ( +
+
💬 回复 ({detail.comments.length})
+ {detail.comments.map((c: any, i: number) => ( +
+
+ + {AGENT_EMOJI[c.author] || '👤'} {c.author} + + {fmtTime(c.created_at)} +
+
{c.body}
+
+ ))} +
+ )} + + {/* 操作 */} +
+ {!detail.is_read && ( + + )} + +
+ + )} +
+
+ ); +}