Files
memind/src/components/ChatSharePreviewModal.tsx
T

112 lines
4.0 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, 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,
);
}