722b18326f
含 MindSpace 三列布局与统计修复、聊天加载态与连接降级、平台页脚标记与 og:site_name 微信卡片、勾选资料删除 Agent 接口及内部话术过滤。 Co-authored-by: Cursor <cursoragent@cursor.com>
157 lines
4.9 KiB
TypeScript
157 lines
4.9 KiB
TypeScript
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<HTMLIFrameElement>(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 <div className="mindspace-state mindspace-error">页面预览加载失败</div>;
|
||
}
|
||
|
||
return (
|
||
<>
|
||
{showHint ? (
|
||
<p className="mindspace-page-editable-hint">
|
||
<span className="mindspace-page-editable-legend-swatch mindspace-page-editable-legend-swatch-text" />
|
||
文字
|
||
<span className="mindspace-page-editable-legend-swatch mindspace-page-editable-legend-swatch-media" />
|
||
图片/媒体
|
||
<span className="mindspace-page-editable-legend-swatch mindspace-page-editable-legend-swatch-link" />
|
||
链接
|
||
<span className="mindspace-page-editable-legend-swatch mindspace-page-editable-legend-swatch-bg" />
|
||
背景 — 预览内左上角有完整图例,虚线框即可直接修改
|
||
</p>
|
||
) : null}
|
||
<iframe
|
||
ref={iframeRef}
|
||
title={`${title || '页面'} 预览`}
|
||
srcDoc={srcdoc}
|
||
sandbox="allow-scripts allow-downloads"
|
||
scrolling={scrollInsideFrame ? 'yes' : 'no'}
|
||
className={frameClassName}
|
||
style={scrollInsideFrame ? { height: '100%' } : { height: `${height}px` }}
|
||
onError={() => setPreviewFailed(true)}
|
||
/>
|
||
</>
|
||
);
|
||
}
|