/** * 视图区容器 —— **净化渲染,无脚本执行权**(编码规范 §9.2;语义出处 AM 07)。 * * 「视图区容器渲染活动推送的 HTML:沙箱 iframe(srcdoc + sandbox)或净化后注入, * 二选一由 `ui-kit` 的 `ViewContainer` 统一实现,**feature 不得自行 dangerouslySetInnerHTML`。」 * * 这里选沙箱 iframe:结构上就没有脚本执行权,不依赖净化算法的完备性。 * * **高度随内容长**:HTML 报告既是独立文件,也是交互视图的组成部分 —— * 它得像页面里的一段正文那样整篇铺开、跟着外层一起滚,而不是缩在一个定高小窗里自己滚。 * 定高会把长报告拦腰截断,人得在两层滚动条之间来回找。 */ import { useCallback, useEffect, useRef, useState } from 'react'; export type ViewContainerProps = { readonly html: string; readonly title?: string; }; /** 量不到高度时的兜底,避免塌成 0 高 */ const FALLBACK_HEIGHT = 420; export function ViewContainer({ html, title = '活动视图' }: ViewContainerProps) { const ref = useRef(null); const [height, setHeight] = useState(FALLBACK_HEIGHT); const measure = useCallback(() => { const doc = ref.current?.contentDocument; if (doc?.documentElement == null) return; // 取 body 与 documentElement 的较大者:两者谁撑开高度随文档结构而异 const next = Math.max(doc.documentElement.scrollHeight, doc.body?.scrollHeight ?? 0); if (next > 0) setHeight(next); }, []); useEffect(() => { const frame = ref.current; const doc = frame?.contentDocument; if (doc?.body == null) return undefined; // 图片late-load、字体回流都会改高度 —— 盯着 body 尺寸变化重量一次 const observer = new ResizeObserver(measure); observer.observe(doc.body); return () => observer.disconnect(); }, [measure, html]); return (