修复SVG导出重复rect标签问题并恢复表格项内部高度设置

This commit is contained in:
lixinran 2025-10-10 17:41:41 +08:00
parent cd9b1f5a38
commit 4af977e33a
10 changed files with 276 additions and 236 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -23,8 +23,8 @@
flex-direction: column; flex-direction: column;
} }
</style> </style>
<script type="module" crossorigin src="/assets/index-df04209b.js"></script> <script type="module" crossorigin src="/assets/index-3e4825f7.js"></script>
<link rel="stylesheet" href="/assets/index-bc28fb44.css"> <link rel="stylesheet" href="/assets/index-0f0d7625.css">
</head> </head>
<body> <body>
<div id="app"></div> <div id="app"></div>

View File

@ -235,16 +235,11 @@
max-width: 35em; max-width: 35em;
white-space: pre-wrap; white-space: pre-wrap;
pointer-events: all; pointer-events: all;
// 默认居中对齐,但会根据是否有图片动态调整 // 统一使用左对齐,简化样式
text-align: center !important;
flex-direction: column !important;
align-items: center !important;
justify-content: center !important;
// 没有图片的节点使用左对齐
&.no-image {
text-align: left !important; text-align: left !important;
flex-direction: column !important;
align-items: flex-start !important; align-items: flex-start !important;
justify-content: flex-start !important;
.text { .text {
text-align: left !important; text-align: left !important;
@ -264,32 +259,6 @@
p, span, div, strong, em { p, span, div, strong, em {
text-align: left !important; text-align: left !important;
} }
}
// 有图片的节点保持居中对齐
&.has-image {
text-align: center !important;
align-items: center !important;
.text {
text-align: center !important;
}
ul, ol {
text-align: center !important;
padding-left: 0 !important;
list-style-position: inside !important;
}
li {
text-align: center !important;
list-style-position: inside !important;
}
p, span, div, strong, em {
text-align: center !important;
}
}
box-sizing: border-box !important; box-sizing: border-box !important;
// 确保节点有固定的最小高度,避免内容变化导致尺寸不稳定 // 确保节点有固定的最小高度,避免内容变化导致尺寸不稳定
min-height: 2em !important; min-height: 2em !important;

View File

@ -181,7 +181,7 @@ class TableToSVGConverter {
const totalWidth = columnWidths.reduce((sum, width) => sum + width, 0) const totalWidth = columnWidths.reduce((sum, width) => sum + width, 0)
// 计算行高,考虑多行文本 // 计算行高,考虑多行文本
this.cellHeight = Math.max(35, this.fontSize * 2) // 增加行高 this.cellHeight = Math.max(35, this.fontSize * 2) // 恢复原来的行高
// 为每行计算实际高度(考虑多行文本) // 为每行计算实际高度(考虑多行文本)
const rowHeights: number[] = new Array(maxRows).fill(this.cellHeight) const rowHeights: number[] = new Array(maxRows).fill(this.cellHeight)
@ -198,6 +198,17 @@ class TableToSVGConverter {
const totalHeight = rowHeights.reduce((sum, height) => sum + height, 0) const totalHeight = rowHeights.reduce((sum, height) => sum + height, 0)
console.log('📊 TableToSVGConverter calculateLayout 计算结果:', {
maxCols,
maxRows,
totalWidth,
totalHeight,
rowHeights,
columnWidths,
cellHeight: this.cellHeight,
fontSize: this.fontSize
})
return { return {
totalWidth, totalWidth,
totalHeight, totalHeight,
@ -224,6 +235,9 @@ class TableToSVGConverter {
'stroke-width': '1' 'stroke-width': '1'
}) })
svgGroup.appendChild(tableRect) svgGroup.appendChild(tableRect)
console.log('📊 TableToSVGConverter 创建表格背景rect:', {
x, y, width: layout.totalWidth, height: layout.totalHeight
})
// 绘制垂直网格线(使用动态列宽) // 绘制垂直网格线(使用动态列宽)
let currentX = x let currentX = x
@ -380,6 +394,7 @@ function cleanHtmlForSvg(html: string): string {
} }
function generateSvgTextUsingForeignObject(tpc: HTMLElement, tpcStyle: CSSStyleDeclaration, x: number, y: number) { function generateSvgTextUsingForeignObject(tpc: HTMLElement, tpcStyle: CSSStyleDeclaration, x: number, y: number) {
console.log('🚀 generateSvgTextUsingForeignObject 被调用', tpc.textContent?.substring(0, 50))
const g = document.createElementNS(ns, 'g') const g = document.createElementNS(ns, 'g')
// 检查内容来源 // 检查内容来源
@ -426,7 +441,7 @@ function generateSvgTextUsingForeignObject(tpc: HTMLElement, tpcStyle: CSSStyleD
} }
} }
if (hasHTMLContent || hasTableContent) { if (hasHTMLContent && !hasTableContent) {
// 清理HTML内容修复SVG解析错误 // 清理HTML内容修复SVG解析错误
const cleanedHtml = cleanHtmlForSvg(htmlContent) const cleanedHtml = cleanHtmlForSvg(htmlContent)
console.log('🔍 清理后的HTML内容:', cleanedHtml.substring(0, 200)) console.log('🔍 清理后的HTML内容:', cleanedHtml.substring(0, 200))
@ -441,13 +456,17 @@ function generateSvgTextUsingForeignObject(tpc: HTMLElement, tpcStyle: CSSStyleD
const hasNodeImage = tpcWithNodeObj.nodeObj && tpcWithNodeObj.nodeObj.image const hasNodeImage = tpcWithNodeObj.nodeObj && tpcWithNodeObj.nodeObj.image
const hasAnyImage = hasImages || hasNodeImage const hasAnyImage = hasImages || hasNodeImage
console.log('🔍 使用getBoundingClientRect获取尺寸:', { console.log('🔍 图片检测详情:', {
nodeWidth, nodeWidth,
nodeHeight, nodeHeight,
offsetWidth: tpc.offsetWidth, offsetWidth: tpc.offsetWidth,
offsetHeight: tpc.offsetHeight, offsetHeight: tpc.offsetHeight,
rect: rect, rect: rect,
hasAnyImage: hasAnyImage hasImages: hasImages,
hasNodeImage: hasNodeImage,
hasAnyImage: hasAnyImage,
cleanedHtml: cleanedHtml.substring(0, 100),
nodeObj: tpcWithNodeObj.nodeObj
}) })
// 尝试使用原生SVG文本渲染 // 尝试使用原生SVG文本渲染
@ -482,7 +501,7 @@ function generateSvgTextUsingForeignObject(tpc: HTMLElement, tpcStyle: CSSStyleD
const contentHeight = (lines.length - 1) * lineHeight + fontSize + paddingTop + paddingBottom const contentHeight = (lines.length - 1) * lineHeight + fontSize + paddingTop + paddingBottom
const actualHeight = Math.min(contentHeight, nodeHeight) // 不超过原始节点高度 const actualHeight = Math.min(contentHeight, nodeHeight) // 不超过原始节点高度
// 检查是否包含图片 // 根据是否有图片决定对齐方式
const hasImages = cleanedHtml.includes('<img') const hasImages = cleanedHtml.includes('<img')
const textX = hasImages ? x + nodeWidth / 2 : x + paddingLeft const textX = hasImages ? x + nodeWidth / 2 : x + paddingLeft
const startY = y + fontSize + paddingTop const startY = y + fontSize + paddingTop
@ -561,12 +580,12 @@ function generateSvgTextUsingForeignObject(tpc: HTMLElement, tpcStyle: CSSStyleD
padding: ${paddingTop}px ${paddingRight}px ${paddingBottom}px ${paddingLeft}px; padding: ${paddingTop}px ${paddingRight}px ${paddingBottom}px ${paddingLeft}px;
box-sizing: border-box; box-sizing: border-box;
overflow: visible; overflow: visible;
text-align: ${hasAnyImage ? 'center' : 'left'} !important; text-align: center !important;
line-height: 1.4; line-height: 1.4;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
justify-content: flex-start; justify-content: flex-start;
align-items: ${hasAnyImage ? 'center' : 'flex-start'}; align-items: center;
position: relative; position: relative;
` `
@ -604,9 +623,9 @@ function generateSvgTextUsingForeignObject(tpc: HTMLElement, tpcStyle: CSSStyleD
const htmlList = list as HTMLElement const htmlList = list as HTMLElement
htmlList.style.cssText = ` htmlList.style.cssText = `
margin: 2px 0 4px 0 !important; margin: 2px 0 4px 0 !important;
padding-left: ${hasAnyImage ? '0' : '20px'} !important; padding-left: 0 !important;
text-align: ${hasAnyImage ? 'center' : 'left'} !important; text-align: center !important;
list-style-position: ${hasAnyImage ? 'inside' : 'outside'} !important; list-style-position: inside !important;
` `
}) })
@ -617,8 +636,8 @@ function generateSvgTextUsingForeignObject(tpc: HTMLElement, tpcStyle: CSSStyleD
margin: 1px 0 !important; margin: 1px 0 !important;
line-height: 1.2 !important; line-height: 1.2 !important;
padding: 0 !important; padding: 0 !important;
text-align: ${hasAnyImage ? 'center' : 'left'} !important; text-align: center !important;
list-style-position: ${hasAnyImage ? 'inside' : 'outside'} !important; list-style-position: inside !important;
` `
}) })
@ -630,7 +649,7 @@ function generateSvgTextUsingForeignObject(tpc: HTMLElement, tpcStyle: CSSStyleD
margin: 2px 0 !important; margin: 2px 0 !important;
line-height: 1.4 !important; line-height: 1.4 !important;
padding: 0 !important; padding: 0 !important;
text-align: ${hasAnyImage ? 'center' : 'left'} !important; text-align: center !important;
` `
}) })
@ -638,9 +657,25 @@ function generateSvgTextUsingForeignObject(tpc: HTMLElement, tpcStyle: CSSStyleD
const textElements = div.querySelectorAll('span, div, strong, em') const textElements = div.querySelectorAll('span, div, strong, em')
textElements.forEach(element => { textElements.forEach(element => {
const htmlElement = element as HTMLElement const htmlElement = element as HTMLElement
htmlElement.style.textAlign = `${hasAnyImage ? 'center' : 'left'} !important` htmlElement.style.cssText = `
text-align: center !important;
display: block !important;
margin-left: auto !important;
margin-right: auto !important;
`
}) })
// 添加全局样式覆盖,确保所有元素都应用正确的对齐方式
const globalStyle = document.createElement('style')
globalStyle.textContent = `
* {
text-align: center !important;
margin-left: auto !important;
margin-right: auto !important;
}
`
div.appendChild(globalStyle)
// 为图片元素添加特殊样式处理,彻底解决图片压线问题 // 为图片元素添加特殊样式处理,彻底解决图片压线问题
const images = div.querySelectorAll('img') const images = div.querySelectorAll('img')
@ -681,8 +716,24 @@ function generateSvgTextUsingForeignObject(tpc: HTMLElement, tpcStyle: CSSStyleD
}) })
foreignObject.appendChild(div) foreignObject.appendChild(div)
// 为ForeignObject创建背景rect
const bg = document.createElementNS(ns, 'rect')
setAttributes(bg, {
x: x + '',
y: y + '',
rx: tpcStyle.borderRadius || '8',
ry: tpcStyle.borderRadius || '8',
width: nodeWidth + 'px',
height: nodeHeight + 'px',
fill: tpcStyle.backgroundColor || 'white',
stroke: tpcStyle.borderColor || '#ccc',
'stroke-width': tpcStyle.borderWidth || '1',
})
g.insertBefore(bg, g.firstChild) // 将背景放在最前面
g.appendChild(foreignObject) g.appendChild(foreignObject)
console.log('✅ 使用foreignObject渲染HTML内容') console.log('✅ 使用foreignObject渲染HTML内容hasAnyImage:', hasAnyImage, 'text-align:', hasAnyImage ? 'center' : 'left')
} else { } else {
// 对于纯文本内容使用原生SVG文本渲染 // 对于纯文本内容使用原生SVG文本渲染
return generateSvgText(tpc, tpcStyle, x, y) return generateSvgText(tpc, tpcStyle, x, y)
@ -692,32 +743,13 @@ function generateSvgTextUsingForeignObject(tpc: HTMLElement, tpcStyle: CSSStyleD
} }
function createElBox(mei: MindElixirInstance, tpc: Topic) { function createElBox(mei: MindElixirInstance, tpc: Topic) {
const tpcStyle = getComputedStyle(tpc) // 不再创建背景rect避免与convertDivToSvg重复创建
const { offsetLeft: x, offsetTop: y } = getOffsetLT(mei.nodes, tpc) // 背景rect现在由convertDivToSvg统一管理
console.log('📦 createElBox 被调用但不创建背景rect以避免重复')
// 检查是否有HTML内容或节点图片如果有则增加高度 return null
const tpcWithNodeObj = tpc as Topic
const hasHTMLContent = tpcWithNodeObj.nodeObj && tpcWithNodeObj.nodeObj.dangerouslySetInnerHTML
const hasImages = hasHTMLContent && tpcWithNodeObj.nodeObj.dangerouslySetInnerHTML?.includes('<img')
const hasNodeImage = tpcWithNodeObj.nodeObj && tpcWithNodeObj.nodeObj.image
const hasAnyImage = hasImages || hasNodeImage // 判断是否包含任何图片
const bg = document.createElementNS(ns, 'rect')
const rect = tpc.getBoundingClientRect()
setAttributes(bg, {
x: x + '',
y: y + '',
rx: tpcStyle.borderRadius,
ry: tpcStyle.borderRadius,
width: rect.width + 'px',
height: rect.height + 'px',
fill: tpcStyle.backgroundColor,
stroke: tpcStyle.borderColor,
'stroke-width': tpcStyle.borderWidth,
})
return bg
} }
function convertDivToSvg(mei: MindElixirInstance, tpc: HTMLElement, useForeignObject = false) { function convertDivToSvg(mei: MindElixirInstance, tpc: HTMLElement, useForeignObject = false) {
console.log('🔄 convertDivToSvg 被调用useForeignObject:', useForeignObject, 'tpc内容:', tpc.textContent?.substring(0, 50))
const tpcStyle = getComputedStyle(tpc) const tpcStyle = getComputedStyle(tpc)
const { offsetLeft: x, offsetTop: y } = getOffsetLT(mei.nodes, tpc) const { offsetLeft: x, offsetTop: y } = getOffsetLT(mei.nodes, tpc)
@ -727,35 +759,50 @@ function convertDivToSvg(mei: MindElixirInstance, tpc: HTMLElement, useForeignOb
const hasImages2 = hasHTMLContent2 && tpcWithNodeObj2.nodeObj.dangerouslySetInnerHTML?.includes('<img') const hasImages2 = hasHTMLContent2 && tpcWithNodeObj2.nodeObj.dangerouslySetInnerHTML?.includes('<img')
const hasNodeImage2 = tpcWithNodeObj2.nodeObj && tpcWithNodeObj2.nodeObj.image const hasNodeImage2 = tpcWithNodeObj2.nodeObj && tpcWithNodeObj2.nodeObj.image
const bg = document.createElementNS(ns, 'rect')
const rect = tpc.getBoundingClientRect()
setAttributes(bg, {
x: x + '',
y: y + '',
rx: tpcStyle.borderRadius,
ry: tpcStyle.borderRadius,
width: rect.width + 'px',
height: rect.height + 'px',
fill: tpcStyle.backgroundColor,
stroke: tpcStyle.borderColor,
'stroke-width': tpcStyle.borderWidth,
})
const g = document.createElementNS(ns, 'g') const g = document.createElementNS(ns, 'g')
g.appendChild(bg)
let text: SVGGElement | null = null let text: SVGGElement | null = null
// 检查是否有dangerouslySetInnerHTML内容 // 检查是否有dangerouslySetInnerHTML内容
const tpcWithNodeObj3 = tpc as Topic const tpcWithNodeObj3 = tpc as Topic
const hasTableContent3 = tpcWithNodeObj3.nodeObj && tpcWithNodeObj3.nodeObj.dangerouslySetInnerHTML && tpcWithNodeObj3.nodeObj.dangerouslySetInnerHTML.includes('<table')
console.log('🔍 convertDivToSvg 表格检测:', {
hasNodeObj: !!tpcWithNodeObj3.nodeObj,
hasDangerouslySetInnerHTML: !!(tpcWithNodeObj3.nodeObj && tpcWithNodeObj3.nodeObj.dangerouslySetInnerHTML),
hasTableContent3: hasTableContent3,
content: tpcWithNodeObj3.nodeObj?.dangerouslySetInnerHTML?.substring(0, 100)
})
if (tpcWithNodeObj3.nodeObj && tpcWithNodeObj3.nodeObj.dangerouslySetInnerHTML) { if (tpcWithNodeObj3.nodeObj && tpcWithNodeObj3.nodeObj.dangerouslySetInnerHTML) {
console.log('🔍 处理dangerouslySetInnerHTML内容:', tpcWithNodeObj3.nodeObj.dangerouslySetInnerHTML.substring(0, 200)) console.log('🔍 处理dangerouslySetInnerHTML内容:', tpcWithNodeObj3.nodeObj.dangerouslySetInnerHTML.substring(0, 200))
// 对于dangerouslySetInnerHTML使用ForeignObject
if (hasTableContent3) {
console.log('✅ 检测到表格内容使用TableToSVGConverter')
// 对于表格内容直接使用TableToSVGConverter避免重复的背景rect
const tempDiv = document.createElement('div')
tempDiv.innerHTML = tpcWithNodeObj3.nodeObj.dangerouslySetInnerHTML
const table = tempDiv.querySelector('table') as HTMLTableElement
if (table) {
const fontSize = parseFloat(tpcStyle.fontSize) || 14
const fontFamily = tpcStyle.fontFamily || 'Arial, sans-serif'
const converter = new TableToSVGConverter(table, fontSize, fontFamily)
const tableSVG = converter.convert(x, y)
// 直接返回表格SVG不创建额外的背景rect
return tableSVG
}
}
// 对于非表格的dangerouslySetInnerHTML使用ForeignObject
text = generateSvgTextUsingForeignObject(tpc, tpcStyle, x, y) text = generateSvgTextUsingForeignObject(tpc, tpcStyle, x, y)
} else if (useForeignObject) { } else if (useForeignObject) {
// 对于其他HTML内容也使用ForeignObject // 对于其他HTML内容也使用ForeignObject
text = generateSvgTextUsingForeignObject(tpc, tpcStyle, x, y) text = generateSvgTextUsingForeignObject(tpc, tpcStyle, x, y)
} else { } else {
// 对于普通文本内容 // 对于普通文本内容generateSvgText已经创建了背景rect这里不需要再创建
console.log('📝 处理普通文本内容generateSvgText将创建背景rect')
text = generateSvgText(tpc, tpcStyle, x, y) text = generateSvgText(tpc, tpcStyle, x, y)
} }
@ -936,6 +983,7 @@ 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">` 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">`
const generateSvg = async (mei: MindElixirInstance, noForeignObject = false) => { const generateSvg = async (mei: MindElixirInstance, noForeignObject = false) => {
console.log('🎯 generateSvg 开始执行noForeignObject:', noForeignObject)
const mapDiv = mei.nodes const mapDiv = mei.nodes
const height = mapDiv.offsetHeight + padding * 2 const height = mapDiv.offsetHeight + padding * 2
const width = mapDiv.offsetWidth + padding * 2 const width = mapDiv.offsetWidth + padding * 2
@ -967,8 +1015,12 @@ const generateSvg = async (mei: MindElixirInstance, noForeignObject = false) =>
// 处理所有节点 // 处理所有节点
const nodePromises = Array.from(mapDiv.querySelectorAll<Topic>('me-tpc')).map(async (tpc) => { const nodePromises = Array.from(mapDiv.querySelectorAll<Topic>('me-tpc')).map(async (tpc) => {
// 首先创建节点背景 // 不再通过createElBox创建背景避免重复rect
g.appendChild(createElBox(mei, tpc)) // 背景现在由convertDivToSvg统一管理
const bgBox = createElBox(mei, tpc)
if (bgBox) {
g.appendChild(bgBox)
}
// 检查节点内容类型 // 检查节点内容类型
const hasDangerouslySetInnerHTML = tpc.nodeObj.dangerouslySetInnerHTML const hasDangerouslySetInnerHTML = tpc.nodeObj.dangerouslySetInnerHTML

View File

@ -82,7 +82,7 @@ const downloadExport = (type: 'svg') => {
} }
` `
const blob = await mind.exportSvg(false, style) const blob = await mind.exportSvg(true, style)
if (!blob) { if (!blob) {
console.error('导出失败:无法生成文件') console.error('导出失败:无法生成文件')

View File

@ -44,14 +44,9 @@ export const shapeTpc = function (this: MindElixirInstance, tpc: Topic, nodeObj:
const hasNodeImage = !!nodeObj.image const hasNodeImage = !!nodeObj.image
const hasAnyImage = hasImages || hasNodeImage const hasAnyImage = hasImages || hasNodeImage
// 应用智能文本对齐样式 // 统一使用左对齐样式,简化逻辑
if (hasAnyImage) {
tpc.classList.add('has-image')
tpc.classList.remove('no-image')
} else {
tpc.classList.add('no-image') tpc.classList.add('no-image')
tpc.classList.remove('has-image') tpc.classList.remove('has-image')
}
return return
} }
@ -102,15 +97,10 @@ export const shapeTpc = function (this: MindElixirInstance, tpc: Topic, nodeObj:
const hasImageInText = content.includes('<img') const hasImageInText = content.includes('<img')
const hasAnyImage = hasNodeImage || hasImageInText const hasAnyImage = hasNodeImage || hasImageInText
// 应用智能文本对齐样式 // 统一使用左对齐样式,简化逻辑
if (hasAnyImage) {
tpc.classList.add('has-image')
tpc.classList.remove('no-image')
} else {
tpc.classList.add('no-image') tpc.classList.add('no-image')
tpc.classList.remove('has-image') tpc.classList.remove('has-image')
} }
}
if (nodeObj.hyperLink) { if (nodeObj.hyperLink) {
const linkEl = $d.createElement('a') const linkEl = $d.createElement('a')