feat: add chat page export actions

This commit is contained in:
john
2026-07-02 19:52:26 +08:00
parent 2d777ef394
commit ddfa60607a
7 changed files with 544 additions and 17 deletions
+86 -3
View File
@@ -1,6 +1,6 @@
import { useEffect, useRef, useState } from 'react';
import { createPortal } from 'react-dom';
import { buildChatSavePreviewFrameUrl, quickShareFromChat } from '../api/client';
import { buildChatSavePreviewFrameUrl, downloadChatMessageDocx, quickShareFromChat } from '../api/client';
type SharePhase =
| { kind: 'idle' }
@@ -8,6 +8,38 @@ type SharePhase =
| { 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,
@@ -22,6 +54,7 @@ export function ChatSharePreviewModal({
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);
@@ -52,6 +85,39 @@ export function ChatSharePreviewModal({
}
};
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);
@@ -94,15 +160,32 @@ export function ChatSharePreviewModal({
onClick={() => void share()}
title="将页面转为公开链接,私有图片自动转为公开地址"
>
{phase.kind === 'loading' ? '处理中…' : '🌐 公开分享'}
{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>,