auto-sync: 2026-05-18 12:57:32
This commit is contained in:
@@ -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<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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user