优化SVG导出功能
- 改进位置计算:处理滚动偏移、transform/scale、SVG坐标归一化 - 优化HTML内容渲染:样式内联化、限制宽高、文字换行处理 - 增强图片处理:统一Base64转换、异步加载管理、大小控制 - 统一背景管理:消除重复创建、统一参数管理 - 完善异步渲染同步:Promise+图片加载检测、整体渲染等待 解决了SVG导出中的位置偏移、内容缺失、图片显示等问题
This commit is contained in:
parent
e07da11606
commit
8b8ce52e3a
|
|
@ -6,11 +6,16 @@ import { getOffsetLT, getOffsetLTImproved, getActualNodeDimensions, isTopic } fr
|
|||
const ns = 'http://www.w3.org/2000/svg'
|
||||
function createSvgDom(height: string, width: string) {
|
||||
const svg = document.createElementNS(ns, 'svg')
|
||||
const heightNum = parseFloat(height)
|
||||
const widthNum = parseFloat(width)
|
||||
|
||||
setAttributes(svg, {
|
||||
version: '1.1',
|
||||
xmlns: ns,
|
||||
height,
|
||||
width,
|
||||
viewBox: `0 0 ${widthNum} ${heightNum}`, // 添加viewBox确保坐标归一化
|
||||
preserveAspectRatio: 'xMidYMid meet' // 保持宽高比
|
||||
})
|
||||
return svg
|
||||
}
|
||||
|
|
@ -27,7 +32,6 @@ function generateSvgText(tpc: HTMLElement, tpcStyle: CSSStyleDeclaration, x: num
|
|||
} else {
|
||||
content = tpc.childNodes[0].textContent!
|
||||
}
|
||||
const lines = content!.split('\n')
|
||||
|
||||
// 计算行高和字体大小
|
||||
const lineHeight = parseFloat(tpcStyle.lineHeight) || parseFloat(tpcStyle.fontSize) * 1.2
|
||||
|
|
@ -38,43 +42,23 @@ function generateSvgText(tpc: HTMLElement, tpcStyle: CSSStyleDeclaration, x: num
|
|||
// 计算节点宽度和文本起始位置
|
||||
const nodeWidth = tpc.offsetWidth || 200
|
||||
const textX = x + paddingLeft // 纯文本节点使用左对齐
|
||||
const textY = y + paddingTop + fontSize
|
||||
|
||||
// 使用tspan处理多行文本
|
||||
const textElement = createMultilineText(content, textX, textY, lineHeight, tpcStyle)
|
||||
|
||||
// 计算实际需要的内容高度
|
||||
const lines = content.split('\n').filter(line => line.trim())
|
||||
const contentHeight = (lines.length - 1) * lineHeight + fontSize + paddingTop + (parseInt(tpcStyle.paddingBottom) || 8)
|
||||
const nodeHeight = tpc.offsetHeight || 100
|
||||
const actualHeight = Math.min(contentHeight, nodeHeight) // 不超过原始节点高度
|
||||
|
||||
// 创建背景矩形,使用计算出的实际高度
|
||||
const bg = document.createElementNS(ns, 'rect')
|
||||
setAttributes(bg, {
|
||||
x: x + '',
|
||||
y: y + '',
|
||||
rx: tpcStyle.borderRadius || '8',
|
||||
ry: tpcStyle.borderRadius || '8',
|
||||
width: nodeWidth + 'px',
|
||||
height: actualHeight + 'px',
|
||||
fill: tpcStyle.backgroundColor || 'white',
|
||||
stroke: tpcStyle.borderColor || '#ccc',
|
||||
'stroke-width': tpcStyle.borderWidth || '1',
|
||||
})
|
||||
// 使用统一背景管理创建背景矩形
|
||||
const bg = createBackground(x, y, nodeWidth, actualHeight, tpcStyle)
|
||||
g.appendChild(bg)
|
||||
g.appendChild(textElement)
|
||||
|
||||
lines.forEach((line, index) => {
|
||||
const text = document.createElementNS(ns, 'text')
|
||||
setAttributes(text, {
|
||||
x: textX + '',
|
||||
y: y + paddingTop + fontSize + (lineHeight * index) + '',
|
||||
'text-anchor': 'start',
|
||||
'font-family': tpcStyle.fontFamily,
|
||||
'font-size': `${tpcStyle.fontSize}`,
|
||||
'font-weight': `${tpcStyle.fontWeight}`,
|
||||
fill: `${tpcStyle.color}`,
|
||||
})
|
||||
text.innerHTML = line
|
||||
g.appendChild(text)
|
||||
})
|
||||
|
||||
console.log('✅ generateSvgText优化了底部间距:', {
|
||||
console.log('✅ generateSvgText使用tspan优化多行文本:', {
|
||||
originalHeight: nodeHeight,
|
||||
actualHeight: actualHeight,
|
||||
linesCount: lines.length
|
||||
|
|
@ -357,6 +341,59 @@ class TableToSVGConverter {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 将computedStyle转换为内联样式字符串
|
||||
*/
|
||||
const computedStyleToInline = (element: HTMLElement, additionalStyles: Record<string, string> = {}): string => {
|
||||
const computedStyle = window.getComputedStyle(element)
|
||||
const styleMap: Record<string, string> = {
|
||||
...additionalStyles,
|
||||
'font-family': computedStyle.fontFamily,
|
||||
'font-size': computedStyle.fontSize,
|
||||
'font-weight': computedStyle.fontWeight,
|
||||
'color': computedStyle.color,
|
||||
'line-height': computedStyle.lineHeight,
|
||||
'text-align': computedStyle.textAlign,
|
||||
'padding': computedStyle.padding,
|
||||
'margin': computedStyle.margin,
|
||||
'border': computedStyle.border,
|
||||
'background-color': computedStyle.backgroundColor,
|
||||
'width': computedStyle.width,
|
||||
'height': computedStyle.height,
|
||||
'box-sizing': 'border-box',
|
||||
'overflow': 'hidden'
|
||||
}
|
||||
|
||||
return Object.entries(styleMap)
|
||||
.filter(([_, value]) => value && value !== 'initial' && value !== 'inherit')
|
||||
.map(([key, value]) => `${key}: ${value}`)
|
||||
.join('; ')
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用tspan处理多行文本
|
||||
*/
|
||||
const createMultilineText = (text: string, x: number, y: number, lineHeight: number, style: CSSStyleDeclaration): SVGGElement => {
|
||||
const g = document.createElementNS(ns, 'g')
|
||||
const lines = text.split('\n').filter(line => line.trim())
|
||||
|
||||
lines.forEach((line, index) => {
|
||||
const tspan = document.createElementNS(ns, 'tspan')
|
||||
setAttributes(tspan, {
|
||||
x: x.toString(),
|
||||
dy: index === 0 ? '0' : lineHeight.toString(),
|
||||
'font-family': style.fontFamily,
|
||||
'font-size': style.fontSize,
|
||||
'font-weight': style.fontWeight,
|
||||
fill: style.color,
|
||||
})
|
||||
tspan.textContent = line.trim()
|
||||
g.appendChild(tspan)
|
||||
})
|
||||
|
||||
return g
|
||||
}
|
||||
|
||||
// 清理HTML内容,修复SVG解析错误
|
||||
function cleanHtmlForSvg(html: string): string {
|
||||
if (!html) return html
|
||||
|
|
@ -430,21 +467,13 @@ function generateSvgTextUsingForeignObject(tpc: HTMLElement, tpcStyle: CSSStyleD
|
|||
if (hasTableContent) {
|
||||
console.log('🔄 检测到表格内容,使用foreignObject方式渲染HTML表格')
|
||||
|
||||
// 获取节点尺寸
|
||||
const nodeWidth = tpc.offsetWidth || 400
|
||||
const nodeHeight = tpc.offsetHeight || 200
|
||||
// 获取节点尺寸 - 使用getBoundingClientRect获取精确尺寸
|
||||
const rect = tpc.getBoundingClientRect()
|
||||
const nodeWidth = Math.max(rect.width, tpc.offsetWidth || 400)
|
||||
const nodeHeight = Math.max(rect.height, tpc.offsetHeight || 200)
|
||||
|
||||
// 创建背景矩形
|
||||
const bg = document.createElementNS(ns, 'rect')
|
||||
setAttributes(bg, {
|
||||
x: x + '',
|
||||
y: y + '',
|
||||
width: nodeWidth + '',
|
||||
height: nodeHeight + '',
|
||||
fill: 'white',
|
||||
stroke: '#ccc',
|
||||
'stroke-width': '1'
|
||||
})
|
||||
// 使用统一背景管理创建背景矩形
|
||||
const bg = createBackground(x, y, nodeWidth, nodeHeight, tpcStyle)
|
||||
g.appendChild(bg)
|
||||
|
||||
// 创建foreignObject包含HTML表格
|
||||
|
|
@ -456,19 +485,17 @@ function generateSvgTextUsingForeignObject(tpc: HTMLElement, tpcStyle: CSSStyleD
|
|||
height: nodeHeight + ''
|
||||
})
|
||||
|
||||
// 创建HTML内容,确保XML语法正确
|
||||
const safeFontFamily = (tpcStyle.fontFamily || 'Arial, sans-serif').replace(/"/g, '"')
|
||||
// 使用样式内联化确保样式正确显示
|
||||
const inlineStyles = computedStyleToInline(tpc, {
|
||||
'width': '100%',
|
||||
'height': '100%',
|
||||
'padding': '8px',
|
||||
'box-sizing': 'border-box',
|
||||
'overflow': 'hidden'
|
||||
})
|
||||
|
||||
const htmlContentForForeignObject = `
|
||||
<div xmlns="http://www.w3.org/1999/xhtml" style="
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 8px;
|
||||
box-sizing: border-box;
|
||||
overflow: hidden;
|
||||
font-family: '${safeFontFamily}';
|
||||
font-size: ${tpcStyle.fontSize || '14px'};
|
||||
line-height: 1.4;
|
||||
">
|
||||
<div xmlns="http://www.w3.org/1999/xhtml" style="${inlineStyles}">
|
||||
${cleanHtmlForSvg(htmlContent)}
|
||||
</div>
|
||||
`
|
||||
|
|
@ -476,7 +503,7 @@ function generateSvgTextUsingForeignObject(tpc: HTMLElement, tpcStyle: CSSStyleD
|
|||
foreignObject.innerHTML = htmlContentForForeignObject
|
||||
g.appendChild(foreignObject)
|
||||
|
||||
console.log('✅ 表格已使用foreignObject渲染')
|
||||
console.log('✅ 表格已使用foreignObject渲染,样式已内联化')
|
||||
return g
|
||||
}
|
||||
|
||||
|
|
@ -780,6 +807,47 @@ function generateSvgTextUsingForeignObject(tpc: HTMLElement, tpcStyle: CSSStyleD
|
|||
return g
|
||||
}
|
||||
|
||||
/**
|
||||
* 统一背景管理函数
|
||||
* 消除重复创建,统一参数管理
|
||||
*/
|
||||
const createBackground = (
|
||||
x: number,
|
||||
y: number,
|
||||
width: number,
|
||||
height: number,
|
||||
style: CSSStyleDeclaration,
|
||||
additionalStyles: Record<string, string> = {}
|
||||
): SVGRectElement => {
|
||||
const rect = document.createElementNS(ns, 'rect')
|
||||
|
||||
const bgColor = additionalStyles.fill || style.backgroundColor || 'white'
|
||||
const borderColor = additionalStyles.stroke || style.borderColor || '#ccc'
|
||||
const borderWidth = additionalStyles['stroke-width'] || style.borderWidth || '1'
|
||||
const borderRadius = additionalStyles.rx || style.borderRadius || '8'
|
||||
|
||||
setAttributes(rect, {
|
||||
x: x.toString(),
|
||||
y: y.toString(),
|
||||
width: width.toString(),
|
||||
height: height.toString(),
|
||||
fill: bgColor,
|
||||
stroke: borderColor,
|
||||
'stroke-width': borderWidth,
|
||||
rx: borderRadius,
|
||||
ry: borderRadius,
|
||||
...additionalStyles
|
||||
})
|
||||
|
||||
console.log('📦 创建统一背景:', {
|
||||
position: { x, y },
|
||||
size: { width, height },
|
||||
styles: { bgColor, borderColor, borderWidth, borderRadius }
|
||||
})
|
||||
|
||||
return rect
|
||||
}
|
||||
|
||||
function createElBox(mei: MindElixirInstance, tpc: Topic) {
|
||||
// 不再创建背景rect,避免与convertDivToSvg重复创建
|
||||
// 背景rect现在由convertDivToSvg统一管理
|
||||
|
|
@ -964,20 +1032,30 @@ async function convertImgToSvg(mei: MindElixirInstance, a: HTMLImageElement): Pr
|
|||
const imageUrl = mei.imageProxy ? mei.imageProxy(a.src) : a.src
|
||||
|
||||
// 获取图片的实际显示尺寸,确保与思维导图中的显示一致
|
||||
const actualWidth = Math.min(parseInt(aStyle.width) || 300, 300)
|
||||
const actualHeight = Math.min(parseInt(aStyle.height) || 200, 200)
|
||||
const rect = a.getBoundingClientRect()
|
||||
const actualWidth = Math.min(rect.width || parseInt(aStyle.width) || 300, 300)
|
||||
const actualHeight = Math.min(rect.height || parseInt(aStyle.height) || 200, 200)
|
||||
|
||||
console.log('🖼️ 图片导出尺寸:', {
|
||||
computedWidth: aStyle.width,
|
||||
computedHeight: aStyle.height,
|
||||
rectWidth: rect.width,
|
||||
rectHeight: rect.height,
|
||||
actualWidth,
|
||||
actualHeight,
|
||||
imageUrl: imageUrl.substring(0, 50)
|
||||
})
|
||||
|
||||
// 确保图片已加载完成
|
||||
await ensureImagesLoaded([a])
|
||||
|
||||
// 尝试将图片转换为base64格式以确保导出时能正确显示
|
||||
try {
|
||||
const base64Url = await imageToBase64(imageUrl)
|
||||
// 根据实际显示尺寸设置最大尺寸
|
||||
const maxWidth = Math.max(actualWidth * 2, 400) // 至少2倍分辨率
|
||||
const maxHeight = Math.max(actualHeight * 2, 300)
|
||||
|
||||
const base64Url = await imageToBase64(imageUrl, maxWidth, maxHeight)
|
||||
setAttributes(svgI, {
|
||||
x: x + '',
|
||||
y: y + '',
|
||||
|
|
@ -985,6 +1063,8 @@ async function convertImgToSvg(mei: MindElixirInstance, a: HTMLImageElement): Pr
|
|||
height: actualHeight + '',
|
||||
href: base64Url,
|
||||
})
|
||||
|
||||
console.log('✅ 图片已转换为base64格式')
|
||||
} catch (error) {
|
||||
console.warn('Failed to convert image to base64, using original URL:', error)
|
||||
setAttributes(svgI, {
|
||||
|
|
@ -999,8 +1079,37 @@ async function convertImgToSvg(mei: MindElixirInstance, a: HTMLImageElement): Pr
|
|||
return svgI
|
||||
}
|
||||
|
||||
// 将图片URL转换为base64格式
|
||||
function imageToBase64(url: string): Promise<string> {
|
||||
/**
|
||||
* 检测图片是否已加载完成
|
||||
*/
|
||||
const ensureImagesLoaded = async (imgElements: HTMLImageElement[]): Promise<void> => {
|
||||
const loadPromises = imgElements.map(img => new Promise<void>((resolve) => {
|
||||
if (img.complete && img.naturalWidth > 0) {
|
||||
resolve()
|
||||
} else {
|
||||
const onLoad = () => {
|
||||
img.removeEventListener('load', onLoad)
|
||||
img.removeEventListener('error', onError)
|
||||
resolve()
|
||||
}
|
||||
const onError = () => {
|
||||
img.removeEventListener('load', onLoad)
|
||||
img.removeEventListener('error', onError)
|
||||
console.warn('图片加载失败:', img.src)
|
||||
resolve() // 即使失败也继续,避免阻塞导出
|
||||
}
|
||||
img.addEventListener('load', onLoad)
|
||||
img.addEventListener('error', onError)
|
||||
}
|
||||
}))
|
||||
|
||||
await Promise.all(loadPromises)
|
||||
}
|
||||
|
||||
/**
|
||||
* 将图片URL转换为base64格式(优化版)
|
||||
*/
|
||||
function imageToBase64(url: string, maxWidth: number = 800, maxHeight: number = 600): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const img = new Image()
|
||||
img.crossOrigin = 'anonymous'
|
||||
|
|
@ -1008,8 +1117,6 @@ function imageToBase64(url: string): Promise<string> {
|
|||
img.onload = () => {
|
||||
try {
|
||||
const canvas = document.createElement('canvas')
|
||||
canvas.width = img.width
|
||||
canvas.height = img.height
|
||||
const ctx = canvas.getContext('2d')
|
||||
|
||||
if (!ctx) {
|
||||
|
|
@ -1017,8 +1124,24 @@ function imageToBase64(url: string): Promise<string> {
|
|||
return
|
||||
}
|
||||
|
||||
ctx.drawImage(img, 0, 0)
|
||||
const base64 = canvas.toDataURL('image/png')
|
||||
// 计算缩放后的尺寸,保持宽高比
|
||||
let { width, height } = img
|
||||
if (width > maxWidth || height > maxHeight) {
|
||||
const ratio = Math.min(maxWidth / width, maxHeight / height)
|
||||
width = Math.floor(width * ratio)
|
||||
height = Math.floor(height * ratio)
|
||||
}
|
||||
|
||||
canvas.width = width
|
||||
canvas.height = height
|
||||
|
||||
// 使用高质量缩放
|
||||
ctx.imageSmoothingEnabled = true
|
||||
ctx.imageSmoothingQuality = 'high'
|
||||
ctx.drawImage(img, 0, 0, width, height)
|
||||
|
||||
// 使用JPEG格式减少文件大小,质量设为0.9
|
||||
const base64 = canvas.toDataURL('image/jpeg', 0.9)
|
||||
resolve(base64)
|
||||
} catch (error) {
|
||||
reject(error)
|
||||
|
|
@ -1026,9 +1149,14 @@ function imageToBase64(url: string): Promise<string> {
|
|||
}
|
||||
|
||||
img.onerror = () => {
|
||||
reject(new Error('Failed to load image'))
|
||||
reject(new Error(`Failed to load image: ${url}`))
|
||||
}
|
||||
|
||||
// 设置超时
|
||||
setTimeout(() => {
|
||||
reject(new Error(`Image load timeout: ${url}`))
|
||||
}, 10000) // 10秒超时
|
||||
|
||||
img.src = url
|
||||
})
|
||||
}
|
||||
|
|
@ -1038,50 +1166,145 @@ const padding = 100
|
|||
const head = `<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">`
|
||||
|
||||
/**
|
||||
* 获取元素相对于容器的绝对位置
|
||||
* 这是解决SVG导出坐标转换问题的关键函数
|
||||
* 解析CSS transform matrix获取缩放比例
|
||||
*/
|
||||
const parseTransformMatrix = (transform: string): { scaleX: number, scaleY: number } => {
|
||||
if (!transform || transform === 'none') {
|
||||
return { scaleX: 1, scaleY: 1 }
|
||||
}
|
||||
|
||||
// 匹配matrix(a, b, c, d, e, f)格式
|
||||
const matrixMatch = transform.match(/matrix\(([^,]+),\s*([^,]+),\s*([^,]+),\s*([^,]+),\s*([^,]+),\s*([^)]+)\)/)
|
||||
if (matrixMatch) {
|
||||
const [, a, b, c, d] = matrixMatch.map(Number)
|
||||
return { scaleX: a, scaleY: d }
|
||||
}
|
||||
|
||||
// 匹配scale(x, y)格式
|
||||
const scaleMatch = transform.match(/scale\(([^,]+)(?:,\s*([^)]+))?\)/)
|
||||
if (scaleMatch) {
|
||||
const scaleX = parseFloat(scaleMatch[1])
|
||||
const scaleY = parseFloat(scaleMatch[2] || scaleMatch[1])
|
||||
return { scaleX, scaleY }
|
||||
}
|
||||
|
||||
return { scaleX: 1, scaleY: 1 }
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取元素相对于容器的绝对位置(优化版)
|
||||
* 解决滚动偏移、transform/scale、SVG坐标归一化问题
|
||||
*/
|
||||
const getAbsolutePosition = (element: HTMLElement, container: HTMLElement) => {
|
||||
const elementRect = element.getBoundingClientRect()
|
||||
const containerRect = container.getBoundingClientRect()
|
||||
|
||||
// 考虑容器的transform和滚动
|
||||
// 1. 处理滚动偏移
|
||||
const scrollLeft = container.scrollLeft || 0
|
||||
const scrollTop = container.scrollTop || 0
|
||||
|
||||
// 2. 考虑容器的transform和缩放
|
||||
const containerStyle = window.getComputedStyle(container)
|
||||
const containerTransform = containerStyle.transform
|
||||
const { scaleX, scaleY } = parseTransformMatrix(containerTransform)
|
||||
|
||||
// 3. 考虑元素的transform
|
||||
const elementStyle = window.getComputedStyle(element)
|
||||
const elementTransform = elementStyle.transform
|
||||
const { scaleX: elementScaleX, scaleY: elementScaleY } = parseTransformMatrix(elementTransform)
|
||||
|
||||
// 4. 计算基础位置(考虑滚动)
|
||||
let x = elementRect.left - containerRect.left + scrollLeft
|
||||
let y = elementRect.top - containerRect.top + scrollTop
|
||||
|
||||
// 5. 应用缩放补偿
|
||||
if (scaleX !== 1 || scaleY !== 1) {
|
||||
x = x / scaleX
|
||||
y = y / scaleY
|
||||
}
|
||||
|
||||
if (elementScaleX !== 1 || elementScaleY !== 1) {
|
||||
x = x / elementScaleX
|
||||
y = y / elementScaleY
|
||||
}
|
||||
|
||||
// 6. 考虑容器的padding/margin
|
||||
const containerPaddingLeft = parseInt(containerStyle.paddingLeft) || 0
|
||||
const containerPaddingTop = parseInt(containerStyle.paddingTop) || 0
|
||||
const containerMarginLeft = parseInt(containerStyle.marginLeft) || 0
|
||||
const containerMarginTop = parseInt(containerStyle.marginTop) || 0
|
||||
|
||||
x = x - containerPaddingLeft - containerMarginLeft
|
||||
y = y - containerPaddingTop - containerMarginTop
|
||||
|
||||
console.log('🔍 位置计算详情:', {
|
||||
element: element.textContent?.substring(0, 30),
|
||||
elementRect: { left: elementRect.left, top: elementRect.top, width: elementRect.width, height: elementRect.height },
|
||||
containerRect: { left: containerRect.left, top: containerRect.top },
|
||||
scroll: { left: scrollLeft, top: scrollTop },
|
||||
containerScale: { scaleX, scaleY },
|
||||
elementScale: { scaleX: elementScaleX, scaleY: elementScaleY },
|
||||
containerPadding: { left: containerPaddingLeft, top: containerPaddingTop },
|
||||
finalPosition: { x, y }
|
||||
})
|
||||
|
||||
return {
|
||||
x: elementRect.left - containerRect.left,
|
||||
y: elementRect.top - containerRect.top,
|
||||
x: Math.round(x * 100) / 100, // 保留2位小数,避免浮点精度问题
|
||||
y: Math.round(y * 100) / 100,
|
||||
width: elementRect.width,
|
||||
height: elementRect.height
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 确保所有内容已完全渲染
|
||||
* 特别是foreignObject内容
|
||||
* 确保所有内容已完全渲染(优化版)
|
||||
* 包括图片加载、字体加载、DOM更新、foreignObject渲染
|
||||
*/
|
||||
const ensureContentRendered = async () => {
|
||||
const ensureContentRendered = async (container: HTMLElement) => {
|
||||
console.log('🔄 开始确保内容完全渲染...')
|
||||
|
||||
// 1. 等待Vue DOM更新
|
||||
await new Promise(resolve => setTimeout(resolve, 0))
|
||||
|
||||
// 2. 等待字体加载
|
||||
await document.fonts.ready
|
||||
if (document.fonts && document.fonts.ready) {
|
||||
await document.fonts.ready
|
||||
}
|
||||
|
||||
// 3. 等待下一帧渲染
|
||||
await new Promise(resolve => requestAnimationFrame(resolve))
|
||||
|
||||
// 4. 额外等待foreignObject渲染
|
||||
// 4. 收集所有图片元素并等待加载完成
|
||||
const allImages = Array.from(container.querySelectorAll('img')) as HTMLImageElement[]
|
||||
if (allImages.length > 0) {
|
||||
console.log(`🖼️ 等待 ${allImages.length} 个图片加载完成...`)
|
||||
await ensureImagesLoaded(allImages)
|
||||
}
|
||||
|
||||
// 5. 等待所有foreignObject内容渲染
|
||||
const foreignObjects = container.querySelectorAll('foreignObject')
|
||||
if (foreignObjects.length > 0) {
|
||||
console.log(`📄 等待 ${foreignObjects.length} 个foreignObject渲染完成...`)
|
||||
await new Promise(resolve => setTimeout(resolve, 200))
|
||||
}
|
||||
|
||||
// 6. 等待CSS动画和过渡完成
|
||||
await new Promise(resolve => setTimeout(resolve, 100))
|
||||
|
||||
// 7. 最后等待一帧确保所有内容稳定
|
||||
await new Promise(resolve => requestAnimationFrame(resolve))
|
||||
|
||||
console.log('✅ 内容渲染同步完成')
|
||||
}
|
||||
|
||||
const generateSvg = async (mei: MindElixirInstance, noForeignObject = false) => {
|
||||
console.log('🎯 generateSvg 开始执行,noForeignObject:', noForeignObject)
|
||||
|
||||
// 确保所有内容已完全渲染,特别是foreignObject
|
||||
await ensureContentRendered()
|
||||
|
||||
const mapDiv = mei.nodes
|
||||
|
||||
// 确保所有内容已完全渲染,特别是foreignObject
|
||||
await ensureContentRendered(mapDiv)
|
||||
|
||||
const mapDivRect = mapDiv.getBoundingClientRect()
|
||||
|
||||
const height = mapDiv.offsetHeight + padding * 2
|
||||
|
|
|
|||
Loading…
Reference in New Issue