Files
memind/src/components/MindSpacePageDraftPreviewFrame.tsx
john ea6c49f046 feat(mindspace): 全部页面管理、级联删除与页面同步去重
支持全部页面分页/多选/删除/分享,删除时可选移除广场帖并清理工作区附件;修复并发同步重复创建页面,并补齐预览下载链接重写与 iframe 下载权限。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-01 18:08:06 +08:00

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' }}
/>
</>
);
}