Files
memind/src/components/ChatSharePreviewModal.tsx
T
2026-07-02 19:52:26 +08:00

195 lines
7.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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<SharePhase>({ kind: 'idle' });
const [exportPhase, setExportPhase] = useState<ExportPhase>({ kind: 'idle' });
const [copied, setCopied] = useState(false);
const copyTimer = useRef<ReturnType<typeof setTimeout> | 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(
<div className="chat-share-preview-overlay" role="dialog" aria-modal="true" aria-label="页面预览">
<div className="chat-share-preview-header">
<span className="chat-share-preview-title"></span>
<button type="button" className="chat-share-preview-close" onClick={onClose} aria-label="关闭"></button>
</div>
<div className="chat-share-preview-body">
<iframe src={previewUrl} title="页面预览" sandbox="allow-same-origin allow-scripts allow-popups allow-downloads" className="chat-share-preview-frame" />
<div className="chat-share-side-actions">
{phase.kind === 'done' ? (
<div className="chat-share-result-card">
<p className="chat-share-result-label"> </p>
<button type="button" className="chat-share-result-copy" onClick={() => void copy(phase.publicUrl)}>
{copied ? '已复制 ✓' : '复制链接'}
</button>
<a href={phase.publicUrl} target="_blank" rel="noreferrer" className="chat-share-result-open">
</a>
</div>
) : (
<button
type="button"
className={`chat-share-side-btn chat-share-side-btn-primary${phase.kind === 'loading' ? ' is-loading' : ''}`}
disabled={phase.kind === 'loading'}
onClick={() => void share()}
title="将页面转为公开链接,私有图片自动转为公开地址"
>
{phase.kind === 'loading' ? '处理中…' : '公开分享'}
</button>
)}
{phase.kind === 'error' && <p className="chat-share-side-error">{phase.message}</p>}
{onSave && (
<button type="button" className="chat-share-side-btn" onClick={handleSave}>
</button>
)}
<button
type="button"
className="chat-share-side-btn"
disabled={exportPhase.kind === 'image-loading' || exportPhase.kind === 'docx-loading'}
onClick={() => void downloadImage()}
>
{exportPhase.kind === 'image-loading' ? '生成图片…' : '下载图片'}
</button>
<button
type="button"
className="chat-share-side-btn"
disabled={exportPhase.kind === 'image-loading' || exportPhase.kind === 'docx-loading'}
onClick={() => void downloadDocx()}
>
{exportPhase.kind === 'docx-loading' ? '生成文档…' : '保存文档'}
</button>
{exportPhase.kind === 'error' && <p className="chat-share-side-error">{exportPhase.message}</p>}
</div>
</div>
</div>,
document.body,
);
}