import { useEffect, useRef, useState } from 'react'; import { createPortal } from 'react-dom'; import { buildChatSavePreviewFrameUrl, downloadChatMessageDocx, quickShareFromChat } from '../api/client'; type SharePhase = | { kind: 'idle' } | { kind: 'loading' } | { kind: 'done'; publicUrl: string } | { kind: 'error'; message: string }; type ExportPhase = | { kind: 'idle' } | { kind: 'image-loading' } | { kind: 'docx-loading' } | { kind: 'error'; message: string }; function appendLongImageDownloadParam(url: string) { const next = new URL(url, window.location.href); next.searchParams.set('download', 'long-image'); return next.toString(); } function triggerUrlDownload(url: string) { const link = document.createElement('a'); link.href = url; link.download = ''; document.body.appendChild(link); link.click(); link.remove(); } function downloadBlob(blob: Blob, filename: string) { const url = URL.createObjectURL(blob); const link = document.createElement('a'); link.href = url; link.download = filename; document.body.appendChild(link); link.click(); link.remove(); window.setTimeout(() => URL.revokeObjectURL(url), 60_000); } export function ChatSharePreviewModal({ sessionId, messageId, selectedLinkIndex = 0, onClose, onSave, }: { sessionId: string; messageId: string; selectedLinkIndex?: number; onClose: () => void; onSave?: () => void; }) { const [phase, setPhase] = useState({ kind: 'idle' }); const [exportPhase, setExportPhase] = useState({ kind: 'idle' }); const [copied, setCopied] = useState(false); const copyTimer = useRef | null>(null); const previewUrl = buildChatSavePreviewFrameUrl({ sessionId, messageId, selectedLinkIndex }); useEffect(() => { const prev = document.body.style.overflow; document.body.style.overflow = 'hidden'; return () => { document.body.style.overflow = prev; }; }, []); useEffect(() => { const onKey = (e: KeyboardEvent) => { if (e.key === 'Escape') onClose(); }; window.addEventListener('keydown', onKey); return () => window.removeEventListener('keydown', onKey); }, [onClose]); useEffect(() => () => { if (copyTimer.current) clearTimeout(copyTimer.current); }, []); const share = async () => { if (phase.kind === 'loading') return; setPhase({ kind: 'loading' }); try { const result = await quickShareFromChat({ sessionId, messageId, selectedLinkIndex }); setPhase({ kind: 'done', publicUrl: result.publicUrl }); } catch (err) { setPhase({ kind: 'error', message: err instanceof Error ? err.message : '公开分享失败,请重试' }); } }; const ensurePublicUrl = async () => { if (phase.kind === 'done') return phase.publicUrl; setPhase({ kind: 'loading' }); const result = await quickShareFromChat({ sessionId, messageId, selectedLinkIndex }); setPhase({ kind: 'done', publicUrl: result.publicUrl }); return result.publicUrl; }; const downloadImage = async () => { if (exportPhase.kind === 'image-loading' || exportPhase.kind === 'docx-loading') return; setExportPhase({ kind: 'image-loading' }); try { const publicUrl = await ensurePublicUrl(); triggerUrlDownload(appendLongImageDownloadParam(publicUrl)); setExportPhase({ kind: 'idle' }); } catch (err) { setExportPhase({ kind: 'error', message: err instanceof Error ? err.message : '长图下载失败,请重试' }); setPhase((current) => (current.kind === 'loading' ? { kind: 'idle' } : current)); } }; const downloadDocx = async () => { if (exportPhase.kind === 'image-loading' || exportPhase.kind === 'docx-loading') return; setExportPhase({ kind: 'docx-loading' }); try { const result = await downloadChatMessageDocx({ sessionId, messageId, selectedLinkIndex }); downloadBlob(result.blob, result.filename); setExportPhase({ kind: 'idle' }); } catch (err) { setExportPhase({ kind: 'error', message: err instanceof Error ? err.message : '文档下载失败,请重试' }); } }; const copy = async (url: string) => { try { await navigator.clipboard.writeText(url); setCopied(true); if (copyTimer.current) clearTimeout(copyTimer.current); copyTimer.current = setTimeout(() => setCopied(false), 2000); } catch { /* ignore */ } }; const handleSave = () => { onClose(); onSave?.(); }; return createPortal(
页面预览