ea6c49f046
支持全部页面分页/多选/删除/分享,删除时可选移除广场帖并清理工作区附件;修复并发同步重复创建页面,并补齐预览下载链接重写与 iframe 下载权限。 Co-authored-by: Cursor <cursoragent@cursor.com>
139 lines
4.1 KiB
TypeScript
139 lines
4.1 KiB
TypeScript
import { useEffect, useRef, useState } from 'react';
|
|
import { fetchMindSpacePageDraftPreview } from '../api/client';
|
|
import { bindIframeDocumentHeightSync } from '../utils/iframeContentHeight';
|
|
import { applyPageChangeHighlight } from '../utils/mindspacePageChangeHighlight';
|
|
|
|
export type MindSpacePreviewChangeHighlight = {
|
|
revision: number;
|
|
previousTitle: string;
|
|
previousContent: string;
|
|
};
|
|
|
|
export function MindSpacePageDraftPreviewFrame({
|
|
pageId,
|
|
title,
|
|
summary,
|
|
content,
|
|
templateId,
|
|
reloadKey = 0,
|
|
liveUpdate = true,
|
|
minHeight = 680,
|
|
maxHeight = 12000,
|
|
className = 'mindspace-page-preview-frame',
|
|
changeHighlight = null,
|
|
onFrameReady,
|
|
}: {
|
|
pageId: string;
|
|
title: string;
|
|
summary: string;
|
|
content: string;
|
|
templateId: string;
|
|
reloadKey?: number;
|
|
liveUpdate?: boolean;
|
|
minHeight?: number;
|
|
maxHeight?: number;
|
|
className?: string;
|
|
changeHighlight?: MindSpacePreviewChangeHighlight | null;
|
|
onFrameReady?: (iframe: HTMLIFrameElement | null) => void;
|
|
}) {
|
|
const iframeRef = useRef<HTMLIFrameElement>(null);
|
|
const [srcdoc, setSrcdoc] = useState('');
|
|
const [height, setHeight] = useState(minHeight);
|
|
const [initialLoading, setInitialLoading] = useState(true);
|
|
const [error, setError] = useState<string | null>(null);
|
|
const requestRef = useRef(0);
|
|
const hasRenderedRef = useRef(false);
|
|
const appliedHighlightRevisionRef = useRef<number | null>(null);
|
|
|
|
useEffect(() => {
|
|
const requestId = requestRef.current + 1;
|
|
requestRef.current = requestId;
|
|
if (!hasRenderedRef.current) setInitialLoading(true);
|
|
setError(null);
|
|
|
|
const timer = window.setTimeout(() => {
|
|
void fetchMindSpacePageDraftPreview(pageId, { title, summary, content, templateId })
|
|
.then((html) => {
|
|
if (requestRef.current !== requestId) return;
|
|
setSrcdoc(html);
|
|
hasRenderedRef.current = true;
|
|
setInitialLoading(false);
|
|
})
|
|
.catch((err) => {
|
|
if (requestRef.current !== requestId) return;
|
|
setError(err instanceof Error ? err.message : '预览加载失败');
|
|
setInitialLoading(false);
|
|
});
|
|
}, hasRenderedRef.current ? 450 : 120);
|
|
|
|
return () => window.clearTimeout(timer);
|
|
}, liveUpdate
|
|
? [pageId, title, summary, content, templateId, reloadKey]
|
|
: [pageId, reloadKey, templateId]);
|
|
|
|
useEffect(() => {
|
|
setHeight(minHeight);
|
|
}, [srcdoc, reloadKey, minHeight]);
|
|
|
|
useEffect(() => {
|
|
onFrameReady?.(iframeRef.current);
|
|
return () => {
|
|
onFrameReady?.(null);
|
|
};
|
|
}, [onFrameReady, srcdoc, reloadKey]);
|
|
|
|
useEffect(() => {
|
|
const iframe = iframeRef.current;
|
|
if (!iframe || !srcdoc) return;
|
|
|
|
const applyHeight = (nextHeight: number) => {
|
|
setHeight(nextHeight);
|
|
|
|
if (
|
|
changeHighlight &&
|
|
appliedHighlightRevisionRef.current !== changeHighlight.revision
|
|
) {
|
|
try {
|
|
const doc = iframe.contentDocument;
|
|
if (!doc) return;
|
|
appliedHighlightRevisionRef.current = changeHighlight.revision;
|
|
applyPageChangeHighlight(doc, {
|
|
previousTitle: changeHighlight.previousTitle,
|
|
nextTitle: title,
|
|
previousContent: changeHighlight.previousContent,
|
|
nextContent: content,
|
|
});
|
|
} catch {
|
|
// ignore highlight errors in sandboxed preview
|
|
}
|
|
}
|
|
};
|
|
|
|
return bindIframeDocumentHeightSync(iframe, {
|
|
minHeight,
|
|
maxHeight,
|
|
onHeight: applyHeight,
|
|
});
|
|
}, [changeHighlight, content, maxHeight, minHeight, srcdoc, title]);
|
|
|
|
if (error) {
|
|
return <div className="mindspace-state mindspace-error">{error}</div>;
|
|
}
|
|
|
|
return (
|
|
<>
|
|
{initialLoading && !srcdoc ? (
|
|
<div className="mindspace-state mindspace-page-preview-loading">正在加载预览…</div>
|
|
) : null}
|
|
<iframe
|
|
ref={iframeRef}
|
|
title={`${title || '页面'} 预览`}
|
|
srcDoc={srcdoc}
|
|
sandbox="allow-same-origin allow-downloads"
|
|
className={className}
|
|
style={{ height: `${height}px`, visibility: srcdoc ? 'visible' : 'hidden' }}
|
|
/>
|
|
</>
|
|
);
|
|
}
|