🎉 优化SVG导出功能
✨ 新功能: - 实现表格转SVG原生元素转换器 - 支持rowspan和colspan的复杂表格布局 - 智能列宽计算,针对中文内容优化 - 动态行高计算,支持多行文本 🔧 修复: - 解决SVG导出时图片压线问题 - 优化图片尺寸,增大显示但不压线 - 减少图片和文字距离,布局更紧凑 - 解决表格内容拥挤问题 📊 技术改进: - 中文字符宽度:字体大小 × 1.0 - 英文字符宽度:字体大小 × 0.6 - 最小列宽:80px,最小行高:35px - 支持多行文本的精确布局计算
This commit is contained in:
parent
cef6b60db7
commit
beee48eb0c
|
|
@ -52,6 +52,296 @@ function generateSvgText(tpc: HTMLElement, tpcStyle: CSSStyleDeclaration, x: num
|
|||
return g
|
||||
}
|
||||
|
||||
// 表格转SVG转换器类
|
||||
class TableToSVGConverter {
|
||||
private table: HTMLTableElement
|
||||
private cellWidth: number
|
||||
private cellHeight: number
|
||||
private fontSize: number
|
||||
private fontFamily: string
|
||||
|
||||
constructor(table: HTMLTableElement, fontSize: number, fontFamily: string) {
|
||||
this.table = table
|
||||
this.fontSize = fontSize
|
||||
this.fontFamily = fontFamily
|
||||
this.cellWidth = 80 // 默认单元格宽度
|
||||
this.cellHeight = 30 // 默认单元格高度
|
||||
}
|
||||
|
||||
// 分析表格结构,处理rowspan和colspan
|
||||
analyzeStructure() {
|
||||
const structure: Array<{
|
||||
row: number
|
||||
col: number
|
||||
rowspan: number
|
||||
colspan: number
|
||||
content: string
|
||||
isHeader: boolean
|
||||
}> = []
|
||||
|
||||
const rows = this.table.querySelectorAll('tr')
|
||||
rows.forEach((row, rowIndex) => {
|
||||
const cells = row.querySelectorAll('td, th')
|
||||
let colIndex = 0
|
||||
|
||||
cells.forEach(cell => {
|
||||
const htmlCell = cell as HTMLTableCellElement
|
||||
const rowspan = parseInt(htmlCell.getAttribute('rowspan') || '1')
|
||||
const colspan = parseInt(htmlCell.getAttribute('colspan') || '1')
|
||||
const content = htmlCell.textContent?.trim() || ''
|
||||
const isHeader = htmlCell.tagName.toLowerCase() === 'th'
|
||||
|
||||
structure.push({
|
||||
row: rowIndex,
|
||||
col: colIndex,
|
||||
rowspan,
|
||||
colspan,
|
||||
content,
|
||||
isHeader
|
||||
})
|
||||
|
||||
colIndex += colspan
|
||||
})
|
||||
})
|
||||
|
||||
return structure
|
||||
}
|
||||
|
||||
// 计算布局尺寸
|
||||
calculateLayout(structure: any[]) {
|
||||
const maxCols = Math.max(...structure.map(cell => cell.col + cell.colspan))
|
||||
const maxRows = Math.max(...structure.map(cell => cell.row + cell.rowspan))
|
||||
|
||||
// 为每列计算最合适的宽度
|
||||
const columnWidths: number[] = new Array(maxCols).fill(0)
|
||||
|
||||
structure.forEach(cell => {
|
||||
// 计算这个单元格内容需要的宽度
|
||||
// 中文字符宽度大约是字体大小的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
|
||||
}
|
||||
}
|
||||
|
||||
// 加上内边距
|
||||
contentWidth += 16 // 左右各8px的padding
|
||||
|
||||
// 考虑colspan,平均分配宽度
|
||||
const avgWidthPerCol = contentWidth / cell.colspan
|
||||
|
||||
// 更新这一行涉及的列的最大宽度
|
||||
for (let col = cell.col; col < cell.col + cell.colspan; col++) {
|
||||
columnWidths[col] = Math.max(columnWidths[col], avgWidthPerCol)
|
||||
}
|
||||
})
|
||||
|
||||
// 设置最小列宽,确保不会太窄
|
||||
columnWidths.forEach((width, index) => {
|
||||
columnWidths[index] = Math.max(width, 80) // 最小80px
|
||||
})
|
||||
|
||||
// 计算总宽度
|
||||
const totalWidth = columnWidths.reduce((sum, width) => sum + width, 0)
|
||||
|
||||
// 计算行高,考虑多行文本
|
||||
this.cellHeight = Math.max(35, this.fontSize * 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.4 + 10)
|
||||
|
||||
// 更新这一行涉及的行的高度
|
||||
for (let row = cell.row; row < cell.row + cell.rowspan; row++) {
|
||||
rowHeights[row] = Math.max(rowHeights[row], cellHeight)
|
||||
}
|
||||
})
|
||||
|
||||
const totalHeight = rowHeights.reduce((sum, height) => sum + height, 0)
|
||||
|
||||
return {
|
||||
totalWidth,
|
||||
totalHeight,
|
||||
cols: maxCols,
|
||||
rows: maxRows,
|
||||
columnWidths,
|
||||
rowHeights
|
||||
}
|
||||
}
|
||||
|
||||
// 生成SVG元素
|
||||
generateSVG(structure: any[], layout: any, x: number, y: number) {
|
||||
const svgGroup = document.createElementNS(ns, 'g')
|
||||
|
||||
// 绘制表格边框
|
||||
const tableRect = document.createElementNS(ns, 'rect')
|
||||
setAttributes(tableRect, {
|
||||
x: x + '',
|
||||
y: y + '',
|
||||
width: layout.totalWidth + '',
|
||||
height: layout.totalHeight + '',
|
||||
fill: 'white',
|
||||
stroke: '#ccc',
|
||||
'stroke-width': '1'
|
||||
})
|
||||
svgGroup.appendChild(tableRect)
|
||||
|
||||
// 绘制垂直网格线(使用动态列宽)
|
||||
let currentX = x
|
||||
for (let i = 0; i < layout.columnWidths.length - 1; i++) {
|
||||
currentX += layout.columnWidths[i]
|
||||
const line = document.createElementNS(ns, 'line')
|
||||
setAttributes(line, {
|
||||
x1: currentX + '',
|
||||
y1: y + '',
|
||||
x2: currentX + '',
|
||||
y2: y + layout.totalHeight + '',
|
||||
stroke: '#ccc',
|
||||
'stroke-width': '1'
|
||||
})
|
||||
svgGroup.appendChild(line)
|
||||
}
|
||||
|
||||
// 绘制水平网格线(使用动态行高)
|
||||
let currentY = y
|
||||
for (let i = 0; i < layout.rowHeights.length - 1; i++) {
|
||||
currentY += layout.rowHeights[i]
|
||||
const line = document.createElementNS(ns, 'line')
|
||||
setAttributes(line, {
|
||||
x1: x + '',
|
||||
y1: currentY + '',
|
||||
x2: x + layout.totalWidth + '',
|
||||
y2: currentY + '',
|
||||
stroke: '#ccc',
|
||||
'stroke-width': '1'
|
||||
})
|
||||
svgGroup.appendChild(line)
|
||||
}
|
||||
|
||||
// 绘制单元格内容和背景
|
||||
structure.forEach(cell => {
|
||||
// 计算单元格的实际位置和尺寸
|
||||
let cellX = x
|
||||
for (let i = 0; i < cell.col; i++) {
|
||||
cellX += layout.columnWidths[i]
|
||||
}
|
||||
|
||||
let cellY = y
|
||||
for (let i = 0; i < cell.row; i++) {
|
||||
cellY += layout.rowHeights[i]
|
||||
}
|
||||
|
||||
// 计算单元格宽度(考虑colspan)
|
||||
let cellWidth = 0
|
||||
for (let i = cell.col; i < cell.col + cell.colspan; i++) {
|
||||
cellWidth += layout.columnWidths[i]
|
||||
}
|
||||
|
||||
// 计算单元格高度(考虑rowspan)
|
||||
let cellHeight = 0
|
||||
for (let i = cell.row; i < cell.row + cell.rowspan; i++) {
|
||||
cellHeight += layout.rowHeights[i]
|
||||
}
|
||||
|
||||
// 绘制单元格背景
|
||||
if (cell.isHeader) {
|
||||
const bgRect = document.createElementNS(ns, 'rect')
|
||||
setAttributes(bgRect, {
|
||||
x: cellX + '',
|
||||
y: cellY + '',
|
||||
width: cellWidth + '',
|
||||
height: cellHeight + '',
|
||||
fill: '#f5f5f5',
|
||||
stroke: 'none'
|
||||
})
|
||||
svgGroup.appendChild(bgRect)
|
||||
}
|
||||
|
||||
// 绘制文本内容
|
||||
if (cell.content) {
|
||||
const text = document.createElementNS(ns, 'text')
|
||||
setAttributes(text, {
|
||||
x: cellX + cellWidth / 2 + '',
|
||||
y: cellY + cellHeight / 2 + this.fontSize / 3 + '',
|
||||
'text-anchor': 'middle',
|
||||
'dominant-baseline': 'central',
|
||||
'font-family': this.fontFamily,
|
||||
'font-size': this.fontSize + '',
|
||||
'font-weight': cell.isHeader ? 'bold' : 'normal',
|
||||
fill: '#333'
|
||||
})
|
||||
|
||||
// 处理多行文本
|
||||
const lines = cell.content.split('\n')
|
||||
if (lines.length === 1) {
|
||||
text.textContent = cell.content
|
||||
} else {
|
||||
lines.forEach((line: string, index: number) => {
|
||||
const tspan = document.createElementNS(ns, 'tspan')
|
||||
setAttributes(tspan, {
|
||||
x: cellX + cellWidth / 2 + '',
|
||||
dy: index === 0 ? '0' : '1.2em'
|
||||
})
|
||||
tspan.textContent = line
|
||||
text.appendChild(tspan)
|
||||
})
|
||||
}
|
||||
|
||||
svgGroup.appendChild(text)
|
||||
}
|
||||
})
|
||||
|
||||
return svgGroup
|
||||
}
|
||||
|
||||
// 转换表格为SVG
|
||||
convert(x: number, y: number) {
|
||||
const structure = this.analyzeStructure()
|
||||
const layout = this.calculateLayout(structure)
|
||||
return this.generateSVG(structure, layout, x, y)
|
||||
}
|
||||
}
|
||||
|
||||
// 清理HTML内容,修复SVG解析错误
|
||||
function cleanHtmlForSvg(html: string): string {
|
||||
if (!html) return html
|
||||
|
||||
// 修复表格中的<br>标签问题 - 将<td>内的<br>替换为空格
|
||||
let cleanedHtml = html.replace(/<td[^>]*>([^<]*)<br\s*\/?>([^<]*)<\/td>/gi, (match, before, after) => {
|
||||
return `<td>${before} ${after}</td>`
|
||||
})
|
||||
|
||||
// 修复<th>中的<br>标签问题
|
||||
cleanedHtml = cleanedHtml.replace(/<th[^>]*>([^<]*)<br\s*\/?>([^<]*)<\/th>/gi, (match, before, after) => {
|
||||
return `<th>${before} ${after}</th>`
|
||||
})
|
||||
|
||||
// 移除其他可能导致SVG解析错误的标签
|
||||
cleanedHtml = cleanedHtml.replace(/<br\s*\/?>/gi, ' ')
|
||||
|
||||
// 确保所有标签都正确闭合
|
||||
cleanedHtml = cleanedHtml.replace(/<([^>]+)>/g, (match, tagContent) => {
|
||||
// 检查是否是自闭合标签
|
||||
const selfClosingTags = ['br', 'hr', 'img', 'input', 'meta', 'link']
|
||||
const tagName = tagContent.split(' ')[0].toLowerCase()
|
||||
|
||||
if (selfClosingTags.includes(tagName)) {
|
||||
return `<${tagContent} />`
|
||||
}
|
||||
return match
|
||||
})
|
||||
|
||||
return cleanedHtml
|
||||
}
|
||||
|
||||
function generateSvgTextUsingForeignObject(tpc: HTMLElement, tpcStyle: CSSStyleDeclaration, x: number, y: number) {
|
||||
const g = document.createElementNS(ns, 'g')
|
||||
|
||||
|
|
@ -77,33 +367,76 @@ function generateSvgTextUsingForeignObject(tpc: HTMLElement, tpcStyle: CSSStyleD
|
|||
contentLength: htmlContent.length
|
||||
})
|
||||
|
||||
// 如果包含表格,使用新的SVG原生转换器
|
||||
if (hasTableContent) {
|
||||
console.log('🔄 检测到表格内容,使用SVG原生转换器')
|
||||
|
||||
// 创建一个临时DOM元素来解析表格
|
||||
const tempDiv = document.createElement('div')
|
||||
tempDiv.innerHTML = cleanHtmlForSvg(htmlContent)
|
||||
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)
|
||||
|
||||
g.appendChild(tableSVG)
|
||||
console.log('✅ 表格已转换为SVG原生元素')
|
||||
return g
|
||||
}
|
||||
}
|
||||
|
||||
if (hasHTMLContent || hasTableContent) {
|
||||
// 清理HTML内容,修复SVG解析错误
|
||||
const cleanedHtml = cleanHtmlForSvg(htmlContent)
|
||||
console.log('🔍 清理后的HTML内容:', cleanedHtml.substring(0, 200))
|
||||
|
||||
// 对于HTML内容,使用foreignObject来正确渲染
|
||||
const foreignObject = document.createElementNS(ns, 'foreignObject')
|
||||
|
||||
// 获取节点的实际尺寸,并大幅增加高度以防止图片压线
|
||||
const nodeWidth = parseInt(tpcStyle.width) || 200
|
||||
const originalHeight = parseInt(tpcStyle.height) || 100
|
||||
const nodeHeight = originalHeight + 50 // 大幅增加50px高度给图片留出空间
|
||||
|
||||
setAttributes(foreignObject, {
|
||||
x: x + '',
|
||||
y: y + '',
|
||||
width: tpcStyle.width,
|
||||
height: tpcStyle.height,
|
||||
width: nodeWidth + 'px',
|
||||
height: nodeHeight + 'px',
|
||||
})
|
||||
|
||||
// 创建div容器来包含HTML内容
|
||||
const div = document.createElement('div')
|
||||
div.innerHTML = htmlContent
|
||||
div.innerHTML = cleanedHtml
|
||||
|
||||
// 应用样式,确保与思维导图显示一致,大幅增加底部padding防止图片压线
|
||||
const paddingTop = parseInt(tpcStyle.paddingTop) || 8
|
||||
const paddingBottom = Math.max(parseInt(tpcStyle.paddingBottom) || 8, 35) // 大幅增加底部padding到35px
|
||||
const paddingLeft = parseInt(tpcStyle.paddingLeft) || 8
|
||||
const paddingRight = parseInt(tpcStyle.paddingRight) || 8
|
||||
|
||||
// 应用样式,确保与思维导图显示一致
|
||||
div.style.cssText = `
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
height: auto;
|
||||
min-height: 100%;
|
||||
font-family: ${tpcStyle.fontFamily};
|
||||
font-size: ${tpcStyle.fontSize};
|
||||
color: ${tpcStyle.color};
|
||||
background: transparent;
|
||||
padding: ${tpcStyle.padding};
|
||||
padding: ${paddingTop}px ${paddingRight}px ${paddingBottom}px ${paddingLeft}px;
|
||||
box-sizing: border-box;
|
||||
overflow: visible;
|
||||
text-align: center;
|
||||
line-height: 1.4;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
`
|
||||
|
||||
// 为表格添加样式
|
||||
|
|
@ -134,12 +467,12 @@ function generateSvgTextUsingForeignObject(tpc: HTMLElement, tpcStyle: CSSStyleD
|
|||
})
|
||||
})
|
||||
|
||||
// 为其他元素添加样式
|
||||
// 优化列表样式,减少底部空白
|
||||
const lists = div.querySelectorAll('ul, ol')
|
||||
lists.forEach(list => {
|
||||
const htmlList = list as HTMLElement
|
||||
htmlList.style.cssText = `
|
||||
margin: 4px 0;
|
||||
margin: 2px 0 4px 0;
|
||||
padding-left: 20px;
|
||||
text-align: left;
|
||||
`
|
||||
|
|
@ -149,11 +482,50 @@ function generateSvgTextUsingForeignObject(tpc: HTMLElement, tpcStyle: CSSStyleD
|
|||
listItems.forEach(item => {
|
||||
const htmlItem = item as HTMLElement
|
||||
htmlItem.style.cssText = `
|
||||
margin: 2px 0;
|
||||
line-height: 1.3;
|
||||
margin: 1px 0;
|
||||
line-height: 1.2;
|
||||
padding: 0;
|
||||
`
|
||||
})
|
||||
|
||||
// 为图片元素添加特殊样式处理,彻底解决图片压线问题
|
||||
const images = div.querySelectorAll('img')
|
||||
images.forEach((img, index) => {
|
||||
const htmlImg = img as HTMLImageElement
|
||||
|
||||
// 更大的图片尺寸,减少图片和文字距离
|
||||
const availableWidth = nodeWidth - paddingLeft - paddingRight - 5 // 进一步减少边距
|
||||
const availableHeight = Math.floor(nodeHeight * 0.7) // 增加到70%高度
|
||||
|
||||
htmlImg.style.cssText = `
|
||||
max-width: ${availableWidth}px !important;
|
||||
max-height: ${availableHeight}px !important;
|
||||
width: auto !important;
|
||||
height: auto !important;
|
||||
display: block !important;
|
||||
margin: 8px auto 20px auto !important;
|
||||
border-radius: 4px;
|
||||
object-fit: contain !important;
|
||||
flex-shrink: 0 !important;
|
||||
box-sizing: border-box !important;
|
||||
position: relative !important;
|
||||
top: 0 !important;
|
||||
left: 0 !important;
|
||||
right: 0 !important;
|
||||
bottom: 0 !important;
|
||||
`
|
||||
|
||||
console.log(`🖼️ 图片 ${index + 1} 强制尺寸控制:`, {
|
||||
nodeWidth,
|
||||
nodeHeight,
|
||||
availableWidth,
|
||||
availableHeight,
|
||||
paddingLeft,
|
||||
paddingRight,
|
||||
bottomMargin: '20px'
|
||||
})
|
||||
})
|
||||
|
||||
foreignObject.appendChild(div)
|
||||
g.appendChild(foreignObject)
|
||||
console.log('✅ 使用foreignObject渲染HTML内容')
|
||||
|
|
@ -169,6 +541,15 @@ function createElBox(mei: MindElixirInstance, tpc: Topic) {
|
|||
const tpcStyle = getComputedStyle(tpc)
|
||||
const { offsetLeft: x, offsetTop: y } = getOffsetLT(mei.nodes, tpc)
|
||||
|
||||
// 检查是否有HTML内容或节点图片,如果有则增加高度
|
||||
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 originalHeight = parseInt(tpcStyle.height) || 100
|
||||
const adjustedHeight = (hasImages || hasNodeImage) ? originalHeight + 50 : originalHeight
|
||||
|
||||
const bg = document.createElementNS(ns, 'rect')
|
||||
setAttributes(bg, {
|
||||
x: x + '',
|
||||
|
|
@ -176,7 +557,7 @@ function createElBox(mei: MindElixirInstance, tpc: Topic) {
|
|||
rx: tpcStyle.borderRadius,
|
||||
ry: tpcStyle.borderRadius,
|
||||
width: tpcStyle.width,
|
||||
height: tpcStyle.height,
|
||||
height: adjustedHeight + 'px',
|
||||
fill: tpcStyle.backgroundColor,
|
||||
stroke: tpcStyle.borderColor,
|
||||
'stroke-width': tpcStyle.borderWidth,
|
||||
|
|
@ -187,6 +568,15 @@ function convertDivToSvg(mei: MindElixirInstance, tpc: HTMLElement, useForeignOb
|
|||
const tpcStyle = getComputedStyle(tpc)
|
||||
const { offsetLeft: x, offsetTop: y } = getOffsetLT(mei.nodes, tpc)
|
||||
|
||||
// 检查是否有HTML内容或节点图片,如果有则增加高度
|
||||
const tpcWithNodeObj2 = tpc as Topic
|
||||
const hasHTMLContent2 = tpcWithNodeObj2.nodeObj && tpcWithNodeObj2.nodeObj.dangerouslySetInnerHTML
|
||||
const hasImages2 = hasHTMLContent2 && tpcWithNodeObj2.nodeObj.dangerouslySetInnerHTML?.includes('<img')
|
||||
const hasNodeImage2 = tpcWithNodeObj2.nodeObj && tpcWithNodeObj2.nodeObj.image
|
||||
|
||||
const originalHeight2 = parseInt(tpcStyle.height) || 100
|
||||
const adjustedHeight2 = (hasImages2 || hasNodeImage2) ? originalHeight2 + 50 : originalHeight2
|
||||
|
||||
const bg = document.createElementNS(ns, 'rect')
|
||||
setAttributes(bg, {
|
||||
x: x + '',
|
||||
|
|
@ -194,7 +584,7 @@ function convertDivToSvg(mei: MindElixirInstance, tpc: HTMLElement, useForeignOb
|
|||
rx: tpcStyle.borderRadius,
|
||||
ry: tpcStyle.borderRadius,
|
||||
width: tpcStyle.width,
|
||||
height: tpcStyle.height,
|
||||
height: adjustedHeight2 + 'px',
|
||||
fill: tpcStyle.backgroundColor,
|
||||
stroke: tpcStyle.borderColor,
|
||||
'stroke-width': tpcStyle.borderWidth,
|
||||
|
|
@ -205,9 +595,9 @@ function convertDivToSvg(mei: MindElixirInstance, tpc: HTMLElement, useForeignOb
|
|||
let text: SVGGElement | null = null
|
||||
|
||||
// 检查是否有dangerouslySetInnerHTML内容
|
||||
const tpcWithNodeObj = tpc as Topic
|
||||
if (tpcWithNodeObj.nodeObj && tpcWithNodeObj.nodeObj.dangerouslySetInnerHTML) {
|
||||
console.log('🔍 处理dangerouslySetInnerHTML内容:', tpcWithNodeObj.nodeObj.dangerouslySetInnerHTML.substring(0, 200))
|
||||
const tpcWithNodeObj3 = tpc as Topic
|
||||
if (tpcWithNodeObj3.nodeObj && tpcWithNodeObj3.nodeObj.dangerouslySetInnerHTML) {
|
||||
console.log('🔍 处理dangerouslySetInnerHTML内容:', tpcWithNodeObj3.nodeObj.dangerouslySetInnerHTML.substring(0, 200))
|
||||
// 对于dangerouslySetInnerHTML,使用ForeignObject
|
||||
text = generateSvgTextUsingForeignObject(tpc, tpcStyle, x, y)
|
||||
} else if (useForeignObject) {
|
||||
|
|
@ -258,14 +648,34 @@ async function convertNodeImageToSvg(mei: MindElixirInstance, tpc: Topic): Promi
|
|||
|
||||
console.log('🖼️ 处理节点图片:', imageData)
|
||||
|
||||
// 计算图片位置(在节点中央)
|
||||
// 计算图片位置,为文字预留空间,限制图片尺寸防止压线
|
||||
const nodeWidth = parseInt(tpcStyle.width)
|
||||
const nodeHeight = parseInt(tpcStyle.height)
|
||||
const imgWidth = imageData.width || 200
|
||||
const imgHeight = imageData.height || 150
|
||||
const maxImgWidth = Math.min(nodeWidth - 10, 300) // 进一步减少边距,增加最大宽度
|
||||
const maxImgHeight = Math.min(nodeHeight * 0.6, 150) // 增加最大高度为节点的60%
|
||||
const imgWidth = Math.min(imageData.width || 200, maxImgWidth)
|
||||
const imgHeight = Math.min(imageData.height || 150, maxImgHeight)
|
||||
|
||||
// 计算文字内容的高度
|
||||
let textHeight = 0
|
||||
if (tpc.text && tpc.text.textContent) {
|
||||
const textContent = tpc.text.textContent.trim()
|
||||
if (textContent) {
|
||||
// 估算文字高度:行数 * 行高
|
||||
const lines = textContent.split('\n').length
|
||||
const lineHeight = parseFloat(tpcStyle.lineHeight) || parseFloat(tpcStyle.fontSize) * 1.2
|
||||
const fontSize = parseFloat(tpcStyle.fontSize)
|
||||
const paddingTop = parseInt(tpcStyle.paddingTop) || 8
|
||||
const paddingBottom = parseInt(tpcStyle.paddingBottom) || 8
|
||||
|
||||
textHeight = lines * lineHeight + paddingTop + paddingBottom
|
||||
console.log('📝 文字高度计算:', { lines, lineHeight, fontSize, textHeight })
|
||||
}
|
||||
}
|
||||
|
||||
// 图片位置:水平居中,垂直位置考虑文字高度,大幅增加底部间距
|
||||
const imgX = x + (nodeWidth - imgWidth) / 2
|
||||
const imgY = y + (nodeHeight - imgHeight) / 2
|
||||
const imgY = y + textHeight + 20 // 减少底部间距到20px,让图片和文字更接近
|
||||
|
||||
try {
|
||||
// 尝试将图片转换为base64
|
||||
|
|
@ -427,8 +837,12 @@ const generateSvg = async (mei: MindElixirInstance, noForeignObject = false) =>
|
|||
// 对于HTML内容(表格等),使用ForeignObject
|
||||
console.log('✅ 使用ForeignObject渲染HTML内容')
|
||||
g.appendChild(convertDivToSvg(mei, tpc, noForeignObject ? false : true))
|
||||
} else if (!hasImage) {
|
||||
// 对于没有图片的普通文本内容
|
||||
g.appendChild(convertDivToSvg(mei, tpc, false))
|
||||
} else {
|
||||
// 对于普通文本内容
|
||||
// 对于有图片的节点,只渲染文字部分(不包含图片)
|
||||
console.log('📝 渲染有图片节点的文字内容')
|
||||
g.appendChild(convertDivToSvg(mei, tpc, false))
|
||||
}
|
||||
|
||||
|
|
@ -458,16 +872,36 @@ const generateSvg = async (mei: MindElixirInstance, noForeignObject = false) =>
|
|||
g.appendChild(convertAToSvg(mei, hl as HTMLAnchorElement))
|
||||
})
|
||||
// 处理图片元素 - 只处理不在节点内的独立图片
|
||||
const imgPromises = Array.from(mapDiv.querySelectorAll('img')).map(async (img) => {
|
||||
// 检查图片是否在节点内,如果在节点内则跳过(因为节点已经包含了图片和文字)
|
||||
const isInNode = img.closest('me-tpc')
|
||||
if (isInNode) {
|
||||
return null // 跳过节点内的图片,因为节点已经包含了完整内容
|
||||
const allImages = Array.from(mapDiv.querySelectorAll('img'))
|
||||
console.log('🔍 发现的所有图片元素:', allImages.length)
|
||||
|
||||
const imgPromises = allImages.map(async (img, index) => {
|
||||
// 检查图片是否在节点内
|
||||
const parentNode = img.closest('me-tpc')
|
||||
const isInForeignObject = img.closest('foreignObject')
|
||||
|
||||
console.log(`🖼️ 处理图片 ${index + 1}:`, {
|
||||
isInNode: !!parentNode,
|
||||
isInForeignObject: !!isInForeignObject,
|
||||
src: img.src.substring(0, 50)
|
||||
})
|
||||
|
||||
// 如果图片在节点内,跳过独立处理(节点已经包含了完整内容)
|
||||
if (parentNode || isInForeignObject) {
|
||||
console.log('⏭️ 跳过节点内图片')
|
||||
return null
|
||||
}
|
||||
|
||||
// 只处理独立的图片元素
|
||||
console.log('✅ 处理独立图片')
|
||||
return await convertImgToSvg(mei, img)
|
||||
})
|
||||
|
||||
const imgElements = await Promise.all(imgPromises)
|
||||
imgElements.forEach(imgEl => {
|
||||
const validImgElements = imgElements.filter(imgEl => imgEl !== null)
|
||||
console.log('📊 有效图片元素数量:', validImgElements.length)
|
||||
|
||||
validImgElements.forEach(imgEl => {
|
||||
if (imgEl) {
|
||||
g.appendChild(imgEl)
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue