feat: add chat page export actions
This commit is contained in:
@@ -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>,
|
||||
|
||||
Reference in New Issue
Block a user