import { useEffect, useRef, useState } from 'react'; import { rewriteMindSpacePageDownloadLinks } from '../api/client'; import { bindIframeDocumentHeightSync, measureDocumentHeight } from '../utils/iframeContentHeight'; import { buildEditablePreviewDocument, MINDSPACE_PAGE_CONTENT_MESSAGE, } from '../utils/mindspaceVisualEditor'; export function MindSpacePageEditablePreviewFrame({ pageId, title, content, reloadKey, minHeight = 680, maxHeight = 12000, className, compact = false, fillViewport = false, showHint = true, onFrameReady, onContentChange, }: { pageId: string; title: string; content: string; reloadKey: number; minHeight?: number; maxHeight?: number; className?: string; compact?: boolean; fillViewport?: boolean; showHint?: boolean; onFrameReady?: (iframe: HTMLIFrameElement | null) => void; onContentChange: (html: string) => void; }) { const scrollInsideFrame = compact || fillViewport; const frameClassName = className ?? (compact ? 'page-save-mini-page-frame' : 'mindspace-page-preview-frame'); const [previewFailed, setPreviewFailed] = useState(false); const [displayContent, setDisplayContent] = useState(content); const [srcdoc, setSrcdoc] = useState(() => buildEditablePreviewDocument(content)); const [height, setHeight] = useState(minHeight); const iframeContentRef = useRef(content); const iframeRef = useRef(null); const syncHeightRef = useRef<(() => void) | null>(null); useEffect(() => { let cancelled = false; const timer = window.setTimeout(() => { void rewriteMindSpacePageDownloadLinks(pageId, content) .then((html) => { if (cancelled) return; setDisplayContent(html); }) .catch(() => { if (cancelled) return; setDisplayContent(content); }); }, 120); return () => { cancelled = true; window.clearTimeout(timer); }; }, [pageId, content, reloadKey]); useEffect(() => { iframeContentRef.current = displayContent; setSrcdoc(buildEditablePreviewDocument(displayContent)); setPreviewFailed(false); setHeight(minHeight); }, [displayContent, reloadKey, minHeight]); useEffect(() => { const onMessage = (event: MessageEvent) => { if (event.source !== iframeRef.current?.contentWindow) return; if (event.data?.type !== MINDSPACE_PAGE_CONTENT_MESSAGE) return; const html = String(event.data.html ?? ''); if (!html.trim()) return; iframeContentRef.current = html; onContentChange(html); syncHeightRef.current?.(); }; window.addEventListener('message', onMessage); return () => window.removeEventListener('message', onMessage); }, [onContentChange]); useEffect(() => { if (scrollInsideFrame) { syncHeightRef.current = null; return; } const iframe = iframeRef.current; if (!iframe || !srcdoc) return; const applyHeight = (nextHeight: number) => setHeight(nextHeight); const unbind = bindIframeDocumentHeightSync(iframe, { minHeight, maxHeight, onHeight: applyHeight, }); syncHeightRef.current = () => { try { const doc = iframe.contentDocument; if (!doc) return; applyHeight(Math.min(measureDocumentHeight(doc, minHeight), maxHeight)); } catch { applyHeight(minHeight); } }; return () => { syncHeightRef.current = null; unbind(); }; }, [maxHeight, minHeight, scrollInsideFrame, srcdoc]); useEffect(() => { onFrameReady?.(iframeRef.current); return () => { onFrameReady?.(null); }; }, [onFrameReady, reloadKey, srcdoc, minHeight, maxHeight, scrollInsideFrame, compact, fillViewport]); if (previewFailed) { return
页面预览加载失败
; } return ( <> {showHint ? (

文字 图片/媒体 链接 背景 — 预览内左上角有完整图例,虚线框即可直接修改

) : null}