import { useRef, useState } from 'react'; import * as api from '../api/client'; import type { Attachment } from '../types'; import { PaperclipIcon, DownloadIcon, FileIcon, CloseIcon, SpinnerIcon } from './icons'; /** 已发出邮件的附件清单(只读,点击下载)。 */ export function AttachmentList({ items }: { items: Attachment[] }) { if (!items || items.length === 0) return null; return (
附件 {items.length}
); } /** 待发送的附件:已上传到服务器、等着随邮件发出。 */ export interface PendingAttachment { id: string; filename: string; size: number; } /** * 写信时的附件选择器。 * * 上传是独立一步:选中即上传,拿到 attachment_id 后暂存,发信时一并提交。 * 之所以不等到点「发送」再传:大文件上传要时间,让用户在写正文时就完成上传体验更好, * 而且上传失败能立刻反馈而不是卡在发送那一刻。 */ export function AttachmentPicker({ items, onChange, disabled }: { items: PendingAttachment[]; onChange: (next: PendingAttachment[]) => void; disabled?: boolean; }) { const inputRef = useRef(null); const [uploading, setUploading] = useState<{ name: string; pct: number } | null>(null); const [error, setError] = useState(null); const pick = () => inputRef.current?.click(); const handleFiles = async (files: FileList | null) => { if (!files || files.length === 0) return; setError(null); // 逐个上传而非并发:并发时进度条只能显示其中一个,且大文件同时传更容易触发体积限制 const added: PendingAttachment[] = []; for (const file of Array.from(files)) { setUploading({ name: file.name, pct: 0 }); try { const r = await api.uploadAttachment(file, pct => setUploading({ name: file.name, pct })); added.push({ id: r.attachment.attachment_id, filename: r.attachment.filename, size: r.attachment.size_bytes }); } catch (err) { setError(`${file.name}:${err instanceof Error ? err.message : String(err)}`); break; // 一个失败就停下,避免连续弹同类错误 } } setUploading(null); if (added.length > 0) onChange([...items, ...added]); // 清空 input,否则重复选同一个文件不会触发 change if (inputRef.current) inputRef.current.value = ''; }; const remove = async (a: PendingAttachment) => { // 从服务器删掉未挂载的附件,不然它会占着磁盘等 24 小时 GC try { await api.deleteAttachment(a.id); } catch { /* 删不掉也只是留给 GC,不该阻塞用户移除操作 */ } onChange(items.filter(x => x.id !== a.id)); }; return (
handleFiles(e.target.files)} />
{uploading && ( {uploading.name} {uploading.pct}% )} {items.length > 0 && !uploading && ( {items.length} 个附件 ·{' '} {api.formatSize(items.reduce((sum, a) => sum + a.size, 0))} )}
{error &&
{error}
} {items.length > 0 && (
    {items.map(a => (
  • {a.filename} {api.formatSize(a.size)}
  • ))}
)}
); }