auto-sync: 2026-05-18 12:57:32

This commit is contained in:
cfdaily
2026-05-18 12:57:33 +08:00
parent 5cedb0440d
commit 6d0a6a8e7f
+213
View File
@@ -0,0 +1,213 @@
/**
* MailPanel — 飞鸽传书(v2.7
* Mail = _mail project 里的两点 Taskfrom → to
* List 展示:时间 | From | To | Title | 状态
*/
import { useEffect, useState } from 'react';
import { useStore } from '../store';
const AGENT_EMOJI: Record<string, string> = {
'pangtong-fujunshi': '🐦', 'simayi-challenger': '🦅', 'jiangwei-infra': '🔧',
'guanyu-dev': '⚔️', 'zhangfei-dev': '💪', 'zhaoyun-data': '📊',
};
const TYPE_ICON: Record<string, string> = {
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<string | null>(null);
const [detail, setDetail] = useState<any>(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 (
<div style={{ display: 'flex', gap: 0, height: '100%', minHeight: 500 }}>
{/* 左侧列表 */}
<div style={{ width: 380, borderRight: '1px solid var(--line)', display: 'flex', flexDirection: 'column', flexShrink: 0 }}>
{/* 筛选栏 */}
<div style={{ padding: '10px 14px', borderBottom: '1px solid var(--line)', display: 'flex', gap: 6, alignItems: 'center' }}>
<button onClick={() => setFilterUnread(!filterUnread)} style={{
padding: '3px 8px', borderRadius: 4, fontSize: 10,
border: `1px solid ${filterUnread ? 'var(--acc)' : '#2a3550'}`,
background: filterUnread ? 'var(--acc)22' : '#161b2e',
color: filterUnread ? 'var(--acc)' : '#8899aa', cursor: 'pointer',
}}>📭 </button>
<button onClick={() => loadMails()} style={{
padding: '3px 8px', borderRadius: 4, fontSize: 10,
border: '1px solid #2a3550', background: '#161b2e', color: '#8899aa', cursor: 'pointer',
}}>🔄 </button>
<span style={{ fontSize: 10, color: 'var(--muted)', marginLeft: 'auto' }}>{filtered.length} </span>
</div>
{/* 邮件列表 */}
<div style={{ flex: 1, overflowY: 'auto' }}>
{filtered.length === 0 && (
<div style={{ textAlign: 'center', padding: 40, color: 'var(--muted)', fontSize: 12 }}></div>
)}
{filtered.map((m: any) => (
<div key={m.id} onClick={() => 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'}
>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 4 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
{!m.is_read && <span style={{ width: 6, height: 6, borderRadius: 3, background: '#6a9eff', display: 'inline-block' }} />}
<span style={{ fontSize: 11, color: 'var(--acc)' }}>{m.from || '—'}</span>
<span style={{ fontSize: 10, color: 'var(--muted)' }}></span>
<span style={{ fontSize: 11, color: '#dde4f8' }}>{m.to || '—'}</span>
</div>
<span style={{ fontSize: 9, color: 'var(--muted)' }}>{fmtTime(m.created_at)}</span>
</div>
<div style={{
fontSize: 12, fontWeight: m.is_read ? 400 : 600, color: m.is_read ? '#a0aec0' : '#dde4f8',
overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap',
}}>
{TYPE_ICON[m.type] || '💬'} {m.title}
</div>
</div>
))}
</div>
</div>
{/* 右侧详情 */}
<div style={{ flex: 1, padding: '16px 20px', overflowY: 'auto' }}>
{!detail ? (
<div style={{ textAlign: 'center', padding: 60, color: 'var(--muted)' }}>
<div style={{ fontSize: 36, marginBottom: 12 }}></div>
<div style={{ fontSize: 13 }}></div>
</div>
) : (
<>
{/* 头部 */}
<div style={{ marginBottom: 16 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 6 }}>
<span style={{ fontSize: 10, padding: '2px 6px', borderRadius: 4, background: '#6a9eff22', color: '#6a9eff' }}>
{detail.type || 'text'}
</span>
<span style={{ fontSize: 10, padding: '2px 6px', borderRadius: 4, background: detail.is_read ? '#2a3550' : '#f59e0b22', color: detail.is_read ? '#6b7280' : '#f59e0b' }}>
{detail.is_read ? '已读' : '未读'}
</span>
</div>
<div style={{ fontSize: 18, fontWeight: 700, lineHeight: 1.3 }}>{detail.title}</div>
<div style={{ fontSize: 12, color: 'var(--muted)', marginTop: 6 }}>
{AGENT_EMOJI[detail.from] || '👤'} {detail.from}
<span style={{ margin: '0 6px' }}></span>
{AGENT_EMOJI[detail.to] || '👤'} {detail.to}
<span style={{ margin: '0 6px' }}>·</span>
{fmtTime(detail.created_at)}
</div>
</div>
{/* 正文 */}
<div style={{
padding: '14px 16px', background: 'var(--panel2)', borderRadius: 10,
fontSize: 13, color: '#a0aec0', lineHeight: 1.7, whiteSpace: 'pre-wrap',
}}>
{detail.description || '(无正文)'}
</div>
{/* 评论线程 */}
{detail.comments && detail.comments.length > 0 && (
<div style={{ marginTop: 16 }}>
<div style={{ fontSize: 11, color: 'var(--muted)', marginBottom: 8, fontWeight: 600 }}>💬 ({detail.comments.length})</div>
{detail.comments.map((c: any, i: number) => (
<div key={c.id || i} style={{
padding: '8px 12px', background: 'var(--panel2)', borderRadius: 6, marginBottom: 6,
}}>
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 4 }}>
<span style={{ fontSize: 10, color: 'var(--acc)', fontWeight: 600 }}>
{AGENT_EMOJI[c.author] || '👤'} {c.author}
</span>
<span style={{ fontSize: 9, color: 'var(--muted)' }}>{fmtTime(c.created_at)}</span>
</div>
<div style={{ fontSize: 12, color: '#a0aec0', lineHeight: 1.5 }}>{c.body}</div>
</div>
))}
</div>
)}
{/* 操作 */}
<div style={{ marginTop: 16, display: 'flex', gap: 8 }}>
{!detail.is_read && (
<button onClick={async () => {
await fetch(`/api/mail/${detail.id}`, {
method: 'PATCH', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ is_read: true }),
});
loadMails();
setDetail({ ...detail, is_read: true });
}} style={{
padding: '6px 14px', borderRadius: 6, fontSize: 11,
border: '1px solid var(--acc)', background: 'var(--acc)22', color: 'var(--acc)', cursor: 'pointer',
}}> </button>
)}
<button onClick={async () => {
await fetch(`/api/mail/${detail.id}`, {
method: 'PATCH', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ mark_executed: true }),
});
loadMails();
setDetail({ ...detail, is_read: true, status: 'done' });
}} style={{
padding: '6px 14px', borderRadius: 6, fontSize: 11,
border: '1px solid #2ecc8a', background: '#2ecc8a22', color: '#2ecc8a', cursor: 'pointer',
}}> </button>
</div>
</>
)}
</div>
</div>
);
}