1187 lines
39 KiB
TypeScript
1187 lines
39 KiB
TypeScript
import type { Topic } from '../types/dom'
|
||
import type { MindElixirInstance } from '../types'
|
||
import { setAttributes } from '../utils'
|
||
import { getOffsetLT, isTopic } from '../utils'
|
||
|
||
const ns = 'http://www.w3.org/2000/svg'
|
||
function createSvgDom(height: string, width: string) {
|
||
const svg = document.createElementNS(ns, 'svg')
|
||
setAttributes(svg, {
|
||
version: '1.1',
|
||
xmlns: ns,
|
||
height,
|
||
width,
|
||
})
|
||
return svg
|
||
}
|
||
|
||
function lineHightToPadding(lineHeight: string, fontSize: string) {
|
||
return (parseInt(lineHeight) - parseInt(fontSize)) / 2
|
||
}
|
||
|
||
function generateSvgText(tpc: HTMLElement, tpcStyle: CSSStyleDeclaration, x: number, y: number) {
|
||
const g = document.createElementNS(ns, 'g')
|
||
let content = ''
|
||
if ((tpc as Topic).text) {
|
||
content = (tpc as Topic).text.textContent!
|
||
} else {
|
||
content = tpc.childNodes[0].textContent!
|
||
}
|
||
const lines = content!.split('\n')
|
||
|
||
// 计算行高和字体大小
|
||
const lineHeight = parseFloat(tpcStyle.lineHeight) || parseFloat(tpcStyle.fontSize) * 1.2
|
||
const fontSize = parseFloat(tpcStyle.fontSize)
|
||
const paddingTop = parseInt(tpcStyle.paddingTop) || 8
|
||
const paddingLeft = parseInt(tpcStyle.paddingLeft) || 8
|
||
|
||
// 计算节点宽度和文本起始位置
|
||
const nodeWidth = tpc.offsetWidth || 200
|
||
const textX = x + paddingLeft // 纯文本节点使用左对齐
|
||
|
||
// 计算实际需要的内容高度
|
||
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',
|
||
})
|
||
g.appendChild(bg)
|
||
|
||
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优化了底部间距:', {
|
||
originalHeight: nodeHeight,
|
||
actualHeight: actualHeight,
|
||
linesCount: lines.length
|
||
})
|
||
|
||
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)
|
||
|
||
console.log('📊 TableToSVGConverter calculateLayout 计算结果:', {
|
||
maxCols,
|
||
maxRows,
|
||
totalWidth,
|
||
totalHeight,
|
||
rowHeights,
|
||
columnWidths,
|
||
cellHeight: this.cellHeight,
|
||
fontSize: this.fontSize
|
||
})
|
||
|
||
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)
|
||
console.log('📊 TableToSVGConverter 创建表格背景rect:', {
|
||
x, y, width: layout.totalWidth, height: layout.totalHeight
|
||
})
|
||
|
||
// 绘制垂直网格线(使用动态列宽)
|
||
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(/•\s*【/g, '【')
|
||
cleanedHtml = cleanedHtml.replace(/•\s*\[/g, '[')
|
||
cleanedHtml = cleanedHtml.replace(/•\s*(/g, '(')
|
||
cleanedHtml = cleanedHtml.replace(/•\s*\(/g, '(')
|
||
|
||
// 确保所有标签都正确闭合
|
||
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) {
|
||
console.log('🚀 generateSvgTextUsingForeignObject 被调用', tpc.textContent?.substring(0, 50))
|
||
const g = document.createElementNS(ns, 'g')
|
||
|
||
// 检查内容来源
|
||
const tpcWithNodeObj = tpc as Topic
|
||
let htmlContent = ''
|
||
|
||
// 优先使用dangerouslySetInnerHTML,其次使用text.innerHTML
|
||
if (tpcWithNodeObj.nodeObj && tpcWithNodeObj.nodeObj.dangerouslySetInnerHTML) {
|
||
htmlContent = tpcWithNodeObj.nodeObj.dangerouslySetInnerHTML
|
||
console.log('🔍 使用dangerouslySetInnerHTML内容:', htmlContent.substring(0, 200))
|
||
} else if (tpcWithNodeObj.text && tpcWithNodeObj.text.innerHTML) {
|
||
htmlContent = tpcWithNodeObj.text.innerHTML
|
||
console.log('🔍 使用text.innerHTML内容:', htmlContent.substring(0, 200))
|
||
}
|
||
|
||
const hasHTMLContent = htmlContent && htmlContent !== tpc.textContent
|
||
const hasTableContent = htmlContent && htmlContent.includes('<table')
|
||
|
||
console.log('🔍 HTML内容分析:', {
|
||
hasHTMLContent,
|
||
hasTableContent,
|
||
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))
|
||
|
||
// 使用getBoundingClientRect获取精确尺寸
|
||
const rect = tpc.getBoundingClientRect()
|
||
const nodeWidth = rect.width
|
||
const nodeHeight = rect.height
|
||
|
||
// 检查是否包含任何图片
|
||
const hasImages = cleanedHtml.includes('<img')
|
||
const hasNodeImage = tpcWithNodeObj.nodeObj && tpcWithNodeObj.nodeObj.image
|
||
const hasAnyImage = hasImages || hasNodeImage
|
||
|
||
console.log('🔍 图片检测详情:', {
|
||
nodeWidth,
|
||
nodeHeight,
|
||
offsetWidth: tpc.offsetWidth,
|
||
offsetHeight: tpc.offsetHeight,
|
||
rect: rect,
|
||
hasImages: hasImages,
|
||
hasNodeImage: hasNodeImage,
|
||
hasAnyImage: hasAnyImage,
|
||
cleanedHtml: cleanedHtml.substring(0, 100),
|
||
nodeObj: tpcWithNodeObj.nodeObj
|
||
})
|
||
|
||
// 尝试使用原生SVG文本渲染
|
||
try {
|
||
const tempDiv = document.createElement('div')
|
||
tempDiv.innerHTML = cleanedHtml
|
||
tempDiv.style.cssText = `
|
||
position: absolute;
|
||
top: -9999px;
|
||
left: -9999px;
|
||
width: ${nodeWidth}px;
|
||
font-family: ${tpcStyle.fontFamily};
|
||
font-size: ${tpcStyle.fontSize};
|
||
color: ${tpcStyle.color};
|
||
`
|
||
document.body.appendChild(tempDiv)
|
||
|
||
// 提取所有文本内容并转换为SVG文本
|
||
const allText = tempDiv.textContent || tempDiv.innerText || ''
|
||
const lines = allText.split('\n').filter(line => line.trim())
|
||
|
||
console.log('🔍 提取的文本内容:', lines)
|
||
|
||
if (lines.length > 0) {
|
||
const fontSize = parseFloat(tpcStyle.fontSize) || 14
|
||
const lineHeight = fontSize * 1.4
|
||
const paddingTop = parseInt(tpcStyle.paddingTop) || 8
|
||
const paddingBottom = parseInt(tpcStyle.paddingBottom) || 8
|
||
const paddingLeft = parseInt(tpcStyle.paddingLeft) || 8
|
||
|
||
// 计算实际需要的内容高度
|
||
const contentHeight = (lines.length - 1) * lineHeight + fontSize + paddingTop + paddingBottom
|
||
const actualHeight = Math.min(contentHeight, nodeHeight) // 不超过原始节点高度
|
||
|
||
// 根据是否有图片决定对齐方式
|
||
const hasImages = cleanedHtml.includes('<img')
|
||
const textX = hasImages ? x + nodeWidth / 2 : x + paddingLeft
|
||
const startY = y + fontSize + paddingTop
|
||
|
||
lines.forEach((line, index) => {
|
||
const text = document.createElementNS(ns, 'text')
|
||
setAttributes(text, {
|
||
x: textX + '',
|
||
y: startY + (lineHeight * index) + '',
|
||
'text-anchor': hasImages ? 'middle' : 'start',
|
||
'font-family': tpcStyle.fontFamily,
|
||
'font-size': tpcStyle.fontSize,
|
||
'font-weight': tpcStyle.fontWeight,
|
||
fill: tpcStyle.color,
|
||
})
|
||
text.innerHTML = line.trim()
|
||
g.appendChild(text)
|
||
})
|
||
|
||
// 创建背景矩形,使用计算出的实际高度
|
||
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',
|
||
})
|
||
g.insertBefore(bg, g.firstChild) // 将背景放在最前面
|
||
|
||
document.body.removeChild(tempDiv)
|
||
console.log('✅ 使用原生SVG文本渲染成功,优化了底部间距:', {
|
||
originalHeight: nodeHeight,
|
||
actualHeight: actualHeight,
|
||
linesCount: lines.length
|
||
})
|
||
return g
|
||
}
|
||
|
||
document.body.removeChild(tempDiv)
|
||
} catch (error) {
|
||
console.log('❌ 原生SVG文本渲染失败,回退到foreignObject:', error)
|
||
}
|
||
|
||
// 回退到foreignObject方法
|
||
const foreignObject = document.createElementNS(ns, 'foreignObject')
|
||
|
||
setAttributes(foreignObject, {
|
||
x: x + '',
|
||
y: y + '',
|
||
width: nodeWidth + 'px',
|
||
height: nodeHeight + 'px',
|
||
})
|
||
|
||
// 创建div容器来包含HTML内容
|
||
const div = document.createElement('div')
|
||
div.innerHTML = cleanedHtml
|
||
|
||
// 应用样式,确保与思维导图显示一致
|
||
const paddingTop = parseInt(tpcStyle.paddingTop) || 8
|
||
const paddingBottom = parseInt(tpcStyle.paddingBottom) || 8
|
||
const paddingLeft = parseInt(tpcStyle.paddingLeft) || 8
|
||
const paddingRight = parseInt(tpcStyle.paddingRight) || 8
|
||
|
||
div.style.cssText = `
|
||
width: 100%;
|
||
height: 100%;
|
||
font-family: ${tpcStyle.fontFamily};
|
||
font-size: ${tpcStyle.fontSize};
|
||
color: ${tpcStyle.color};
|
||
background: transparent;
|
||
padding: ${paddingTop}px ${paddingRight}px ${paddingBottom}px ${paddingLeft}px;
|
||
box-sizing: border-box;
|
||
overflow: visible;
|
||
text-align: center !important;
|
||
line-height: 1.4;
|
||
display: flex;
|
||
flex-direction: column;
|
||
justify-content: flex-start;
|
||
align-items: center;
|
||
position: relative;
|
||
`
|
||
|
||
// 为表格添加样式
|
||
const tables = div.querySelectorAll('table')
|
||
tables.forEach(table => {
|
||
const htmlTable = table as HTMLTableElement
|
||
htmlTable.style.cssText = `
|
||
width: 100%;
|
||
border-collapse: collapse;
|
||
font-size: ${tpcStyle.fontSize};
|
||
font-family: ${tpcStyle.fontFamily};
|
||
margin: 0 auto 0px auto;
|
||
border: 1px solid #ccc;
|
||
`
|
||
|
||
// 为表格单元格添加样式
|
||
const cells = table.querySelectorAll('td, th')
|
||
cells.forEach(cell => {
|
||
const htmlCell = cell as HTMLElement
|
||
htmlCell.style.cssText = `
|
||
border: 1px solid #ccc;
|
||
padding: 4px 8px;
|
||
text-align: center;
|
||
vertical-align: top;
|
||
font-size: ${parseFloat(tpcStyle.fontSize) * 0.9}px;
|
||
background: white;
|
||
`
|
||
})
|
||
})
|
||
|
||
// 优化列表样式,减少底部空白
|
||
const lists = div.querySelectorAll('ul, ol')
|
||
lists.forEach(list => {
|
||
const htmlList = list as HTMLElement
|
||
htmlList.style.cssText = `
|
||
margin: 2px 0 4px 0 !important;
|
||
padding-left: 0 !important;
|
||
text-align: center !important;
|
||
list-style-position: inside !important;
|
||
`
|
||
})
|
||
|
||
const listItems = div.querySelectorAll('li')
|
||
listItems.forEach(item => {
|
||
const htmlItem = item as HTMLElement
|
||
htmlItem.style.cssText = `
|
||
margin: 1px 0 !important;
|
||
line-height: 1.2 !important;
|
||
padding: 0 !important;
|
||
text-align: center !important;
|
||
list-style-position: inside !important;
|
||
`
|
||
})
|
||
|
||
// 为段落文本添加样式
|
||
const paragraphs = div.querySelectorAll('p')
|
||
paragraphs.forEach(p => {
|
||
const htmlP = p as HTMLElement
|
||
htmlP.style.cssText = `
|
||
margin: 2px 0 !important;
|
||
line-height: 1.4 !important;
|
||
padding: 0 !important;
|
||
text-align: center !important;
|
||
`
|
||
})
|
||
|
||
// 为所有文本元素添加样式
|
||
const textElements = div.querySelectorAll('span, div, strong, em')
|
||
textElements.forEach(element => {
|
||
const htmlElement = element as HTMLElement
|
||
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')
|
||
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 0px 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)
|
||
|
||
// 为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)
|
||
console.log('✅ 使用foreignObject渲染HTML内容,hasAnyImage:', hasAnyImage, 'text-align:', hasAnyImage ? 'center' : 'left')
|
||
} else {
|
||
// 对于纯文本内容,使用原生SVG文本渲染
|
||
return generateSvgText(tpc, tpcStyle, x, y)
|
||
}
|
||
|
||
return g
|
||
}
|
||
|
||
function createElBox(mei: MindElixirInstance, tpc: Topic) {
|
||
// 不再创建背景rect,避免与convertDivToSvg重复创建
|
||
// 背景rect现在由convertDivToSvg统一管理
|
||
console.log('📦 createElBox 被调用,但不创建背景rect以避免重复')
|
||
return null
|
||
}
|
||
function convertDivToSvg(mei: MindElixirInstance, tpc: HTMLElement, useForeignObject = false) {
|
||
console.log('🔄 convertDivToSvg 被调用,useForeignObject:', useForeignObject, 'tpc内容:', tpc.textContent?.substring(0, 50))
|
||
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 g = document.createElementNS(ns, 'g')
|
||
let text: SVGGElement | null = null
|
||
|
||
// 检查是否有dangerouslySetInnerHTML内容
|
||
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) {
|
||
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'
|
||
|
||
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)
|
||
} else if (useForeignObject) {
|
||
// 对于其他HTML内容,也使用ForeignObject
|
||
text = generateSvgTextUsingForeignObject(tpc, tpcStyle, x, y)
|
||
} else {
|
||
// 对于普通文本内容,generateSvgText已经创建了背景rect,这里不需要再创建
|
||
console.log('📝 处理普通文本内容,generateSvgText将创建背景rect')
|
||
text = generateSvgText(tpc, tpcStyle, x, y)
|
||
}
|
||
|
||
if (text) {
|
||
g.appendChild(text)
|
||
}
|
||
|
||
return g
|
||
}
|
||
|
||
function convertAToSvg(mei: MindElixirInstance, a: HTMLAnchorElement) {
|
||
const aStyle = getComputedStyle(a)
|
||
const { offsetLeft: x, offsetTop: y } = getOffsetLT(mei.nodes, a)
|
||
const svgA = document.createElementNS(ns, 'a')
|
||
const text = document.createElementNS(ns, 'text')
|
||
setAttributes(text, {
|
||
x: x + '',
|
||
y: y + parseInt(aStyle.fontSize) + '',
|
||
'text-anchor': 'start',
|
||
'font-family': aStyle.fontFamily,
|
||
'font-size': `${aStyle.fontSize}`,
|
||
'font-weight': `${aStyle.fontWeight}`,
|
||
fill: `${aStyle.color}`,
|
||
})
|
||
text.innerHTML = a.textContent!
|
||
svgA.appendChild(text)
|
||
svgA.setAttribute('href', a.href)
|
||
return svgA
|
||
}
|
||
|
||
async function convertNodeImageToSvg(mei: MindElixirInstance, tpc: Topic): Promise<SVGImageElement | null> {
|
||
const tpcStyle = getComputedStyle(tpc)
|
||
const { offsetLeft: x, offsetTop: y } = getOffsetLT(mei.nodes, tpc)
|
||
|
||
if (!tpc.nodeObj.image) {
|
||
return null
|
||
}
|
||
|
||
const imageData = tpc.nodeObj.image
|
||
const svgI = document.createElementNS(ns, 'image')
|
||
|
||
console.log('🖼️ 处理节点图片:', imageData)
|
||
|
||
// 计算图片位置,为文字预留空间,限制图片尺寸防止压线
|
||
const nodeWidth = parseInt(tpcStyle.width)
|
||
const nodeHeight = parseInt(tpcStyle.height)
|
||
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 + textHeight + 0 // 移除底部间距,让图片贴近底部
|
||
|
||
try {
|
||
// 尝试将图片转换为base64
|
||
const base64Url = await imageToBase64(imageData.url)
|
||
setAttributes(svgI, {
|
||
x: imgX + '',
|
||
y: imgY + '',
|
||
width: imgWidth + '',
|
||
height: imgHeight + '',
|
||
href: base64Url,
|
||
})
|
||
console.log('✅ 成功导出节点图片')
|
||
} catch (error) {
|
||
console.warn('⚠️ 图片转base64失败,使用原始URL:', error)
|
||
setAttributes(svgI, {
|
||
x: imgX + '',
|
||
y: imgY + '',
|
||
width: imgWidth + '',
|
||
height: imgHeight + '',
|
||
href: imageData.url,
|
||
})
|
||
}
|
||
|
||
return svgI
|
||
}
|
||
|
||
async function convertImgToSvg(mei: MindElixirInstance, a: HTMLImageElement): Promise<SVGImageElement> {
|
||
const aStyle = getComputedStyle(a)
|
||
const { offsetLeft: x, offsetTop: y } = getOffsetLT(mei.nodes, a)
|
||
const svgI = document.createElementNS(ns, 'image')
|
||
|
||
// Use imageProxy function if provided, otherwise use original URL
|
||
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)
|
||
|
||
console.log('🖼️ 图片导出尺寸:', {
|
||
computedWidth: aStyle.width,
|
||
computedHeight: aStyle.height,
|
||
actualWidth,
|
||
actualHeight,
|
||
imageUrl: imageUrl.substring(0, 50)
|
||
})
|
||
|
||
// 尝试将图片转换为base64格式以确保导出时能正确显示
|
||
try {
|
||
const base64Url = await imageToBase64(imageUrl)
|
||
setAttributes(svgI, {
|
||
x: x + '',
|
||
y: y + '',
|
||
width: actualWidth + '',
|
||
height: actualHeight + '',
|
||
href: base64Url,
|
||
})
|
||
} catch (error) {
|
||
console.warn('Failed to convert image to base64, using original URL:', error)
|
||
setAttributes(svgI, {
|
||
x: x + '',
|
||
y: y + '',
|
||
width: actualWidth + '',
|
||
height: actualHeight + '',
|
||
href: imageUrl,
|
||
})
|
||
}
|
||
|
||
return svgI
|
||
}
|
||
|
||
// 将图片URL转换为base64格式
|
||
function imageToBase64(url: string): Promise<string> {
|
||
return new Promise((resolve, reject) => {
|
||
const img = new Image()
|
||
img.crossOrigin = 'anonymous'
|
||
|
||
img.onload = () => {
|
||
try {
|
||
const canvas = document.createElement('canvas')
|
||
canvas.width = img.width
|
||
canvas.height = img.height
|
||
const ctx = canvas.getContext('2d')
|
||
|
||
if (!ctx) {
|
||
reject(new Error('Failed to get canvas context'))
|
||
return
|
||
}
|
||
|
||
ctx.drawImage(img, 0, 0)
|
||
const base64 = canvas.toDataURL('image/png')
|
||
resolve(base64)
|
||
} catch (error) {
|
||
reject(error)
|
||
}
|
||
}
|
||
|
||
img.onerror = () => {
|
||
reject(new Error('Failed to load image'))
|
||
}
|
||
|
||
img.src = url
|
||
})
|
||
}
|
||
|
||
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 generateSvg = async (mei: MindElixirInstance, noForeignObject = false) => {
|
||
console.log('🎯 generateSvg 开始执行,noForeignObject:', noForeignObject)
|
||
const mapDiv = mei.nodes
|
||
const height = mapDiv.offsetHeight + padding * 2
|
||
const width = mapDiv.offsetWidth + padding * 2
|
||
const svg = createSvgDom(height + 'px', width + 'px')
|
||
const g = document.createElementNS(ns, 'svg')
|
||
const bgColor = document.createElementNS(ns, 'rect')
|
||
setAttributes(bgColor, {
|
||
x: '0',
|
||
y: '0',
|
||
width: `${width}`,
|
||
height: `${height}`,
|
||
fill: mei.theme.cssVar['--bgcolor'] as string,
|
||
})
|
||
svg.appendChild(bgColor)
|
||
mapDiv.querySelectorAll('.subLines').forEach(item => {
|
||
const clone = item.cloneNode(true) as SVGSVGElement
|
||
const { offsetLeft, offsetTop } = getOffsetLT(mapDiv, item.parentElement as HTMLElement)
|
||
clone.setAttribute('x', `${offsetLeft}`)
|
||
clone.setAttribute('y', `${offsetTop}`)
|
||
g.appendChild(clone)
|
||
})
|
||
|
||
const mainLine = mapDiv.querySelector('.lines')?.cloneNode(true)
|
||
mainLine && g.appendChild(mainLine)
|
||
const topiclinks = mapDiv.querySelector('.topiclinks')?.cloneNode(true)
|
||
topiclinks && g.appendChild(topiclinks)
|
||
const summaries = mapDiv.querySelector('.summary')?.cloneNode(true)
|
||
summaries && g.appendChild(summaries)
|
||
|
||
// 处理所有节点
|
||
const nodePromises = Array.from(mapDiv.querySelectorAll<Topic>('me-tpc')).map(async (tpc) => {
|
||
// 不再通过createElBox创建背景,避免重复rect
|
||
// 背景现在由convertDivToSvg统一管理
|
||
const bgBox = createElBox(mei, tpc)
|
||
if (bgBox) {
|
||
g.appendChild(bgBox)
|
||
}
|
||
|
||
// 检查节点内容类型
|
||
const hasDangerouslySetInnerHTML = tpc.nodeObj.dangerouslySetInnerHTML
|
||
const hasHTMLContent = !!(tpc.text && tpc.text.innerHTML && tpc.text.innerHTML !== tpc.text.textContent)
|
||
const hasImage = tpc.nodeObj.image
|
||
|
||
console.log('🔍 节点内容分析:', {
|
||
hasDangerouslySetInnerHTML,
|
||
hasHTMLContent,
|
||
hasImage,
|
||
innerHTML: tpc.text?.innerHTML?.substring(0, 100),
|
||
textContent: tpc.text?.textContent?.substring(0, 100)
|
||
})
|
||
|
||
// 处理不同类型的节点内容
|
||
if (hasDangerouslySetInnerHTML || hasHTMLContent) {
|
||
// 对于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))
|
||
}
|
||
|
||
// 如果有图片,单独处理图片
|
||
if (hasImage) {
|
||
console.log('🖼️ 处理节点图片:', tpc.nodeObj.image)
|
||
try {
|
||
const imageSvg = await convertNodeImageToSvg(mei, tpc)
|
||
if (imageSvg) {
|
||
g.appendChild(imageSvg)
|
||
}
|
||
} catch (error) {
|
||
console.error('❌ 图片导出失败:', error)
|
||
}
|
||
}
|
||
})
|
||
|
||
// 等待所有节点处理完成
|
||
await Promise.all(nodePromises)
|
||
mapDiv.querySelectorAll('.tags > span').forEach(tag => {
|
||
g.appendChild(convertDivToSvg(mei, tag as HTMLElement))
|
||
})
|
||
mapDiv.querySelectorAll('.icons > span').forEach(icon => {
|
||
g.appendChild(convertDivToSvg(mei, icon as HTMLElement))
|
||
})
|
||
mapDiv.querySelectorAll('.hyper-link').forEach(hl => {
|
||
g.appendChild(convertAToSvg(mei, hl as HTMLAnchorElement))
|
||
})
|
||
// 处理图片元素 - 只处理不在节点内的独立图片
|
||
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)
|
||
const validImgElements = imgElements.filter(imgEl => imgEl !== null)
|
||
console.log('📊 有效图片元素数量:', validImgElements.length)
|
||
|
||
validImgElements.forEach(imgEl => {
|
||
if (imgEl) {
|
||
g.appendChild(imgEl)
|
||
}
|
||
})
|
||
setAttributes(g, {
|
||
x: padding + '',
|
||
y: padding + '',
|
||
overflow: 'visible',
|
||
})
|
||
svg.appendChild(g)
|
||
return svg
|
||
}
|
||
|
||
const generateSvgStr = (svgEl: SVGSVGElement, injectCss?: string) => {
|
||
if (injectCss) svgEl.insertAdjacentHTML('afterbegin', '<style>' + injectCss + '</style>')
|
||
return head + svgEl.outerHTML
|
||
}
|
||
|
||
function blobToUrl(blob: Blob): Promise<string> {
|
||
return new Promise((resolve, reject) => {
|
||
const reader = new FileReader()
|
||
reader.onload = evt => {
|
||
resolve(evt.target!.result as string)
|
||
}
|
||
reader.onerror = err => {
|
||
reject(err)
|
||
}
|
||
reader.readAsDataURL(blob)
|
||
})
|
||
}
|
||
|
||
export const exportSvg = async function (this: MindElixirInstance, noForeignObject = false, injectCss?: string) {
|
||
const svgEl = await generateSvg(this, noForeignObject)
|
||
const svgString = generateSvgStr(svgEl, injectCss)
|
||
const blob = new Blob([svgString], { type: 'image/svg+xml' })
|
||
return blob
|
||
}
|
||
|
||
export const exportPng = async function (this: MindElixirInstance, noForeignObject = false, injectCss?: string): Promise<Blob | null> {
|
||
const blob = await this.exportSvg(noForeignObject, injectCss)
|
||
// use base64 to bypass canvas taint
|
||
const url = await blobToUrl(blob)
|
||
return new Promise((resolve, reject) => {
|
||
const img = new Image()
|
||
img.setAttribute('crossOrigin', 'anonymous')
|
||
|
||
// 增加超时处理
|
||
const timeout = setTimeout(() => {
|
||
reject(new Error('Image loading timeout'))
|
||
}, 10000)
|
||
|
||
img.onload = () => {
|
||
clearTimeout(timeout)
|
||
try {
|
||
const canvas = document.createElement('canvas')
|
||
canvas.width = img.width
|
||
canvas.height = img.height
|
||
const ctx = canvas.getContext('2d')!
|
||
|
||
// 设置白色背景
|
||
ctx.fillStyle = '#ffffff'
|
||
ctx.fillRect(0, 0, canvas.width, canvas.height)
|
||
|
||
ctx.drawImage(img, 0, 0)
|
||
canvas.toBlob(resolve, 'image/png', 1)
|
||
} catch (error) {
|
||
reject(error)
|
||
}
|
||
}
|
||
|
||
img.onerror = (error) => {
|
||
clearTimeout(timeout)
|
||
console.error('Image loading failed:', error)
|
||
reject(error)
|
||
}
|
||
|
||
img.src = url
|
||
})
|
||
}
|