112 lines
4.0 KiB
TypeScript
112 lines
4.0 KiB
TypeScript
import { useEffect, useRef, useState } from 'react';
|
||
import { createPortal } from 'react-dom';
|
||
import { buildChatSavePreviewFrameUrl, quickShareFromChat } from '../api/client';
|
||
|
||
type SharePhase =
|
||
| { kind: 'idle' }
|
||
| { kind: 'loading' }
|
||
| { kind: 'done'; publicUrl: string }
|
||
| { kind: 'error'; message: string };
|
||
|
||
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 [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 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" 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>
|
||
)}
|
||
</div>
|
||
</div>
|
||
</div>,
|
||
document.body,
|
||
);
|
||
}
|