修复SVG导出表格节点问题:使用foreignObject替代原生SVG文本,解决XML语法错误
- 将表格渲染从TableToSVGConverter改为foreignObject方式 - 修复XML语法错误:字体名加引号、添加命名空间、字符转义 - 解决表格内容压缩、对齐混乱、文本溢出问题 - 实现表格自动换行和列宽自适应 - 确保SVG导出的表格布局与HTML显示一致
This commit is contained in:
parent
35766881dd
commit
2a09a6b05c
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
|
|
@ -23,7 +23,7 @@
|
|||
flex-direction: column;
|
||||
}
|
||||
</style>
|
||||
<script type="module" crossorigin src="/assets/index-3cfab743.js"></script>
|
||||
<script type="module" crossorigin src="/assets/index-3ece160d.js"></script>
|
||||
<link rel="stylesheet" href="/assets/index-356fe347.css">
|
||||
</head>
|
||||
<body>
|
||||
|
|
|
|||
|
|
@ -150,18 +150,24 @@ class TableToSVGConverter {
|
|||
// 计算这个单元格内容需要的宽度
|
||||
// 中文字符宽度大约是字体大小的1倍,英文是0.6倍
|
||||
let contentWidth = 0
|
||||
for (const char of cell.content) {
|
||||
if (/[\u4e00-\u9fa5]/.test(char)) {
|
||||
// 中文字符
|
||||
contentWidth += this.fontSize * 1.0
|
||||
} else {
|
||||
// 英文字符
|
||||
contentWidth += this.fontSize * 0.6
|
||||
const lines = cell.content.split('\n')
|
||||
|
||||
lines.forEach((line: string) => {
|
||||
let lineWidth = 0
|
||||
for (const char of line) {
|
||||
if (/[\u4e00-\u9fa5]/.test(char)) {
|
||||
// 中文字符
|
||||
lineWidth += this.fontSize * 1.0
|
||||
} else {
|
||||
// 英文字符
|
||||
lineWidth += this.fontSize * 0.6
|
||||
}
|
||||
}
|
||||
}
|
||||
contentWidth = Math.max(contentWidth, lineWidth)
|
||||
})
|
||||
|
||||
// 加上内边距
|
||||
contentWidth += 16 // 左右各8px的padding
|
||||
contentWidth += 20 // 左右各10px的padding
|
||||
|
||||
// 考虑colspan,平均分配宽度
|
||||
const avgWidthPerCol = contentWidth / cell.colspan
|
||||
|
|
@ -174,21 +180,21 @@ class TableToSVGConverter {
|
|||
|
||||
// 设置最小列宽,确保不会太窄
|
||||
columnWidths.forEach((width, index) => {
|
||||
columnWidths[index] = Math.max(width, 80) // 最小80px
|
||||
columnWidths[index] = Math.max(width, 120) // 增加最小宽度到120px
|
||||
})
|
||||
|
||||
// 计算总宽度
|
||||
const totalWidth = columnWidths.reduce((sum, width) => sum + width, 0)
|
||||
|
||||
// 计算行高,大幅减少
|
||||
this.cellHeight = Math.max(15, this.fontSize * 1.0) // 大幅减少行高
|
||||
// 计算行高
|
||||
this.cellHeight = Math.max(25, this.fontSize * 1.2) // 合理的行高
|
||||
|
||||
// 为每行计算实际高度(考虑多行文本)
|
||||
const rowHeights: number[] = new Array(maxRows).fill(this.cellHeight)
|
||||
|
||||
structure.forEach(cell => {
|
||||
const lines = cell.content.split('\n').length
|
||||
const cellHeight = Math.max(this.cellHeight, lines * this.fontSize * 1.0 + 2)
|
||||
const cellHeight = Math.max(this.cellHeight, lines * this.fontSize * 1.3 + 8)
|
||||
|
||||
// 更新这一行涉及的行的高度
|
||||
for (let row = cell.row; row < cell.row + cell.rowspan; row++) {
|
||||
|
|
@ -310,9 +316,9 @@ class TableToSVGConverter {
|
|||
if (cell.content) {
|
||||
const text = document.createElementNS(ns, 'text')
|
||||
setAttributes(text, {
|
||||
x: cellX + cellWidth / 2 + '',
|
||||
x: cellX + 10 + '', // 左对齐,左边距10px
|
||||
y: cellY + cellHeight / 2 + this.fontSize / 3 + '',
|
||||
'text-anchor': 'middle',
|
||||
'text-anchor': 'start', // 左对齐
|
||||
'dominant-baseline': 'central',
|
||||
'font-family': this.fontFamily,
|
||||
'font-size': this.fontSize + '',
|
||||
|
|
@ -328,7 +334,7 @@ class TableToSVGConverter {
|
|||
lines.forEach((line: string, index: number) => {
|
||||
const tspan = document.createElementNS(ns, 'tspan')
|
||||
setAttributes(tspan, {
|
||||
x: cellX + cellWidth / 2 + '',
|
||||
x: cellX + 10 + '', // 每行都左对齐
|
||||
dy: index === 0 ? '0' : '1.2em'
|
||||
})
|
||||
tspan.textContent = line
|
||||
|
|
@ -415,26 +421,58 @@ function generateSvgTextUsingForeignObject(tpc: HTMLElement, tpcStyle: CSSStyleD
|
|||
contentLength: htmlContent.length
|
||||
})
|
||||
|
||||
// 如果包含表格,使用新的SVG原生转换器
|
||||
// 如果包含表格,使用foreignObject方式渲染HTML表格
|
||||
if (hasTableContent) {
|
||||
console.log('🔄 检测到表格内容,使用SVG原生转换器')
|
||||
console.log('🔄 检测到表格内容,使用foreignObject方式渲染HTML表格')
|
||||
|
||||
// 创建一个临时DOM元素来解析表格
|
||||
const tempDiv = document.createElement('div')
|
||||
tempDiv.innerHTML = cleanHtmlForSvg(htmlContent)
|
||||
const table = tempDiv.querySelector('table') as HTMLTableElement
|
||||
// 获取节点尺寸
|
||||
const nodeWidth = tpc.offsetWidth || 400
|
||||
const nodeHeight = tpc.offsetHeight || 200
|
||||
|
||||
if (table) {
|
||||
const fontSize = parseFloat(tpcStyle.fontSize) || 14
|
||||
const fontFamily = tpcStyle.fontFamily || 'Arial, sans-serif'
|
||||
// 创建背景矩形
|
||||
const bg = document.createElementNS(ns, 'rect')
|
||||
setAttributes(bg, {
|
||||
x: x + '',
|
||||
y: y + '',
|
||||
width: nodeWidth + '',
|
||||
height: nodeHeight + '',
|
||||
fill: 'white',
|
||||
stroke: '#ccc',
|
||||
'stroke-width': '1'
|
||||
})
|
||||
g.appendChild(bg)
|
||||
|
||||
const converter = new TableToSVGConverter(table, fontSize, fontFamily)
|
||||
const tableSVG = converter.convert(x, y)
|
||||
// 创建foreignObject包含HTML表格
|
||||
const foreignObject = document.createElementNS(ns, 'foreignObject')
|
||||
setAttributes(foreignObject, {
|
||||
x: x + '',
|
||||
y: y + '',
|
||||
width: nodeWidth + '',
|
||||
height: nodeHeight + ''
|
||||
})
|
||||
|
||||
g.appendChild(tableSVG)
|
||||
console.log('✅ 表格已转换为SVG原生元素')
|
||||
return g
|
||||
}
|
||||
// 创建HTML内容,确保XML语法正确
|
||||
const safeFontFamily = (tpcStyle.fontFamily || 'Arial, sans-serif').replace(/"/g, '"')
|
||||
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;
|
||||
">
|
||||
${cleanHtmlForSvg(htmlContent)}
|
||||
</div>
|
||||
`
|
||||
|
||||
foreignObject.innerHTML = htmlContentForForeignObject
|
||||
g.appendChild(foreignObject)
|
||||
|
||||
console.log('✅ 表格已使用foreignObject渲染')
|
||||
return g
|
||||
}
|
||||
|
||||
if (hasHTMLContent && !hasTableContent) {
|
||||
|
|
@ -474,7 +512,7 @@ function generateSvgTextUsingForeignObject(tpc: HTMLElement, tpcStyle: CSSStyleD
|
|||
top: -9999px;
|
||||
left: -9999px;
|
||||
width: ${nodeWidth}px;
|
||||
font-family: ${tpcStyle.fontFamily};
|
||||
font-family: '${(tpcStyle.fontFamily || 'Arial').replace(/"/g, '"')}';
|
||||
font-size: ${tpcStyle.fontSize};
|
||||
color: ${tpcStyle.color};
|
||||
`
|
||||
|
|
@ -569,7 +607,7 @@ function generateSvgTextUsingForeignObject(tpc: HTMLElement, tpcStyle: CSSStyleD
|
|||
div.style.cssText = `
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
font-family: ${tpcStyle.fontFamily};
|
||||
font-family: '${(tpcStyle.fontFamily || 'Arial').replace(/"/g, '"')}';
|
||||
font-size: ${tpcStyle.fontSize};
|
||||
color: ${tpcStyle.color};
|
||||
background: transparent;
|
||||
|
|
@ -592,7 +630,7 @@ function generateSvgTextUsingForeignObject(tpc: HTMLElement, tpcStyle: CSSStyleD
|
|||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: ${tpcStyle.fontSize};
|
||||
font-family: ${tpcStyle.fontFamily};
|
||||
font-family: '${(tpcStyle.fontFamily || 'Arial').replace(/"/g, '"')}';
|
||||
margin: 0 auto 0px auto;
|
||||
border: 1px solid #ccc;
|
||||
`
|
||||
|
|
@ -772,27 +810,9 @@ function convertDivToSvg(mei: MindElixirInstance, tpc: HTMLElement, useForeignOb
|
|||
console.log('🔍 处理dangerouslySetInnerHTML内容:', tpcWithNodeObj3.nodeObj.dangerouslySetInnerHTML.substring(0, 200))
|
||||
|
||||
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'
|
||||
|
||||
// 获取实际DOM元素的尺寸,用于精确的rect高度
|
||||
const rect = tpc.getBoundingClientRect()
|
||||
const actualHeight = rect.height
|
||||
|
||||
|
||||
const converter = new TableToSVGConverter(table, fontSize, fontFamily)
|
||||
const tableSVG = converter.convert(x, y, actualHeight)
|
||||
|
||||
// 直接返回表格SVG,不创建额外的背景rect
|
||||
return tableSVG
|
||||
}
|
||||
console.log('✅ 检测到表格内容,使用foreignObject方式渲染')
|
||||
// 对于表格内容,使用foreignObject方式,确保表格布局正确
|
||||
return generateSvgTextUsingForeignObject(tpc, tpcStyle, x, y)
|
||||
}
|
||||
|
||||
// 对于非表格的dangerouslySetInnerHTML,使用ForeignObject
|
||||
|
|
@ -1039,7 +1059,7 @@ const generateSvg = async (mei: MindElixirInstance, noForeignObject = false) =>
|
|||
if (hasDangerouslySetInnerHTML || hasHTMLContent) {
|
||||
// 对于HTML内容(表格等),使用ForeignObject
|
||||
console.log('✅ 使用ForeignObject渲染HTML内容')
|
||||
g.appendChild(convertDivToSvg(mei, tpc, noForeignObject ? false : true))
|
||||
g.appendChild(convertDivToSvg(mei, tpc, !noForeignObject))
|
||||
} else if (!hasImage) {
|
||||
// 对于没有图片的普通文本内容
|
||||
g.appendChild(convertDivToSvg(mei, tpc, false))
|
||||
|
|
|
|||
Loading…
Reference in New Issue