修复SVG导出文本居中问题

- 修改generateSvgText函数,使用text-anchor: middle强制文本居中
- 计算节点中心坐标,确保文本在节点中水平居中
- 解决图片说明文本左对齐的问题
- 使用原生SVG文本渲染确保居中效果
This commit is contained in:
lixinran 2025-10-10 16:29:32 +08:00
parent beee48eb0c
commit a8051a50e8
9 changed files with 296 additions and 151 deletions

Binary file not shown.

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;
}
</style>
<script type="module" crossorigin src="/assets/index-2d6d975e.js"></script>
<link rel="stylesheet" href="/assets/index-f7e35254.css">
<script type="module" crossorigin src="/assets/index-df04209b.js"></script>
<link rel="stylesheet" href="/assets/index-bc28fb44.css">
</head>
<body>
<div id="app"></div>

View File

@ -1085,13 +1085,22 @@ const processContentIntelligently = (content, parentNode, nodeCounter) => {
urlLength: image.url.length
});
// URLURLCORS
let imageUrl = image.url;
if (imageUrl.includes('cdn-mineru.openxlab.org.cn')) {
// CDN URLURL
const urlPath = imageUrl.replace('https://cdn-mineru.openxlab.org.cn', '');
imageUrl = `/proxy-image${urlPath}`;
console.log(`🔄 转换图片URL: ${image.url} -> ${imageUrl}`);
}
const imageNode = {
id: `node_${nodeCounter++}`,
topic: image.alt || `图片 ${index + 1}`,
children: [],
level: (parentNode.level || 0) + 1,
image: {
url: image.url,
url: imageUrl,
width: 200,
height: 150,
fit: 'contain'
@ -1106,7 +1115,7 @@ const processContentIntelligently = (content, parentNode, nodeCounter) => {
}
parentNode.children.push(imageNode);
console.log(`✅ 成功创建图片节点: ${imageNode.topic} - ${image.url}`);
console.log(`✅ 成功创建图片节点: ${imageNode.topic} - ${imageUrl}`);
});
//

View File

@ -35,12 +35,16 @@ function generateSvgText(tpc: HTMLElement, tpcStyle: CSSStyleDeclaration, x: num
const paddingTop = parseInt(tpcStyle.paddingTop) || 8
const paddingLeft = parseInt(tpcStyle.paddingLeft) || 8
// 计算节点宽度用于居中
const nodeWidth = tpc.offsetWidth || 200
const centerX = x + nodeWidth / 2
lines.forEach((line, index) => {
const text = document.createElementNS(ns, 'text')
setAttributes(text, {
x: x + paddingLeft + '',
x: centerX + '',
y: y + paddingTop + fontSize + (lineHeight * index) + '',
'text-anchor': 'start',
'text-anchor': 'middle',
'font-family': tpcStyle.fontFamily,
'font-size': `${tpcStyle.fontSize}`,
'font-weight': `${tpcStyle.fontWeight}`,
@ -394,13 +398,73 @@ function generateSvgTextUsingForeignObject(tpc: HTMLElement, tpcStyle: CSSStyleD
const cleanedHtml = cleanHtmlForSvg(htmlContent)
console.log('🔍 清理后的HTML内容:', cleanedHtml.substring(0, 200))
// 对于HTML内容使用foreignObject来正确渲染
const foreignObject = document.createElementNS(ns, 'foreignObject')
// 使用getBoundingClientRect获取精确尺寸
const rect = tpc.getBoundingClientRect()
const nodeWidth = rect.width
const nodeHeight = rect.height
// 获取节点的实际尺寸,并大幅增加高度以防止图片压线
const nodeWidth = parseInt(tpcStyle.width) || 200
const originalHeight = parseInt(tpcStyle.height) || 100
const nodeHeight = originalHeight + 50 // 大幅增加50px高度给图片留出空间
console.log('🔍 使用getBoundingClientRect获取尺寸:', {
nodeWidth,
nodeHeight,
offsetWidth: tpc.offsetWidth,
offsetHeight: tpc.offsetHeight,
rect: rect
})
// 尝试使用原生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 centerX = x + nodeWidth / 2
const startY = y + fontSize + (parseInt(tpcStyle.paddingTop) || 8)
lines.forEach((line, index) => {
const text = document.createElementNS(ns, 'text')
setAttributes(text, {
x: centerX + '',
y: startY + (lineHeight * index) + '',
'text-anchor': 'middle',
'font-family': tpcStyle.fontFamily,
'font-size': tpcStyle.fontSize,
'font-weight': tpcStyle.fontWeight,
fill: tpcStyle.color,
})
text.innerHTML = line.trim()
g.appendChild(text)
})
document.body.removeChild(tempDiv)
console.log('✅ 使用原生SVG文本渲染成功')
return g
}
document.body.removeChild(tempDiv)
} catch (error) {
console.log('❌ 原生SVG文本渲染失败回退到foreignObject:', error)
}
// 回退到foreignObject方法
const foreignObject = document.createElementNS(ns, 'foreignObject')
setAttributes(foreignObject, {
x: x + '',
@ -413,16 +477,15 @@ function generateSvgTextUsingForeignObject(tpc: HTMLElement, tpcStyle: CSSStyleD
const div = document.createElement('div')
div.innerHTML = cleanedHtml
// 应用样式,确保与思维导图显示一致大幅增加底部padding防止图片压线
// 应用样式,确保与思维导图显示一致
const paddingTop = parseInt(tpcStyle.paddingTop) || 8
const paddingBottom = Math.max(parseInt(tpcStyle.paddingBottom) || 8, 35) // 大幅增加底部padding到35px
const paddingBottom = parseInt(tpcStyle.paddingBottom) || 8
const paddingLeft = parseInt(tpcStyle.paddingLeft) || 8
const paddingRight = parseInt(tpcStyle.paddingRight) || 8
div.style.cssText = `
width: 100%;
height: auto;
min-height: 100%;
height: 100%;
font-family: ${tpcStyle.fontFamily};
font-size: ${tpcStyle.fontSize};
color: ${tpcStyle.color};
@ -430,7 +493,7 @@ function generateSvgTextUsingForeignObject(tpc: HTMLElement, tpcStyle: CSSStyleD
padding: ${paddingTop}px ${paddingRight}px ${paddingBottom}px ${paddingLeft}px;
box-sizing: border-box;
overflow: visible;
text-align: center;
text-align: center !important;
line-height: 1.4;
display: flex;
flex-direction: column;
@ -448,7 +511,7 @@ function generateSvgTextUsingForeignObject(tpc: HTMLElement, tpcStyle: CSSStyleD
border-collapse: collapse;
font-size: ${tpcStyle.fontSize};
font-family: ${tpcStyle.fontFamily};
margin: 0 auto;
margin: 0 auto 0px auto;
border: 1px solid #ccc;
`
@ -472,9 +535,10 @@ function generateSvgTextUsingForeignObject(tpc: HTMLElement, tpcStyle: CSSStyleD
lists.forEach(list => {
const htmlList = list as HTMLElement
htmlList.style.cssText = `
margin: 2px 0 4px 0;
padding-left: 20px;
text-align: left;
margin: 2px 0 4px 0 !important;
padding-left: 0 !important;
text-align: center !important;
list-style-position: inside !important;
`
})
@ -482,12 +546,44 @@ function generateSvgTextUsingForeignObject(tpc: HTMLElement, tpcStyle: CSSStyleD
listItems.forEach(item => {
const htmlItem = item as HTMLElement
htmlItem.style.cssText = `
margin: 1px 0;
line-height: 1.2;
padding: 0;
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.textAlign = 'center !important'
})
// 添加全局CSS样式确保所有内容居中
const style = document.createElement('style')
style.textContent = `
* {
text-align: center !important;
margin-left: auto !important;
margin-right: auto !important;
}
`
div.appendChild(style)
// 为图片元素添加特殊样式处理,彻底解决图片压线问题
const images = div.querySelectorAll('img')
images.forEach((img, index) => {
@ -503,7 +599,7 @@ function generateSvgTextUsingForeignObject(tpc: HTMLElement, tpcStyle: CSSStyleD
width: auto !important;
height: auto !important;
display: block !important;
margin: 8px auto 20px auto !important;
margin: 8px auto 0px auto !important;
border-radius: 4px;
object-fit: contain !important;
flex-shrink: 0 !important;
@ -547,17 +643,15 @@ function createElBox(mei: MindElixirInstance, tpc: Topic) {
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')
const rect = tpc.getBoundingClientRect()
setAttributes(bg, {
x: x + '',
y: y + '',
rx: tpcStyle.borderRadius,
ry: tpcStyle.borderRadius,
width: tpcStyle.width,
height: adjustedHeight + 'px',
width: rect.width + 'px',
height: rect.height + 'px',
fill: tpcStyle.backgroundColor,
stroke: tpcStyle.borderColor,
'stroke-width': tpcStyle.borderWidth,
@ -574,17 +668,15 @@ function convertDivToSvg(mei: MindElixirInstance, tpc: HTMLElement, useForeignOb
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')
const rect = tpc.getBoundingClientRect()
setAttributes(bg, {
x: x + '',
y: y + '',
rx: tpcStyle.borderRadius,
ry: tpcStyle.borderRadius,
width: tpcStyle.width,
height: adjustedHeight2 + 'px',
width: rect.width + 'px',
height: rect.height + 'px',
fill: tpcStyle.backgroundColor,
stroke: tpcStyle.borderColor,
'stroke-width': tpcStyle.borderWidth,
@ -675,7 +767,7 @@ async function convertNodeImageToSvg(mei: MindElixirInstance, tpc: Topic): Promi
// 图片位置:水平居中,垂直位置考虑文字高度,大幅增加底部间距
const imgX = x + (nodeWidth - imgWidth) / 2
const imgY = y + textHeight + 20 // 减少底部间距到20px让图片和文字更接近
const imgY = y + textHeight + 0 // 移除底部间距,让图片贴近底部
try {
// 尝试将图片转换为base64

View File

@ -9,6 +9,26 @@ export default defineConfig({
'/api': {
target: 'http://localhost:8000',
changeOrigin: true
},
'/proxy-image': {
target: 'https://cdn-mineru.openxlab.org.cn',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/proxy-image/, ''),
configure: (proxy, options) => {
proxy.on('proxyReq', (proxyReq, req, res) => {
// 添加必要的请求头
proxyReq.setHeader('User-Agent', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36')
proxyReq.setHeader('Accept', 'image/webp,image/apng,image/*,*/*;q=0.8')
proxyReq.setHeader('Accept-Language', 'zh-CN,zh;q=0.9,en;q=0.8')
})
proxy.on('proxyRes', (proxyRes, req, res) => {
// 添加CORS头
proxyRes.headers['Access-Control-Allow-Origin'] = '*'
proxyRes.headers['Access-Control-Allow-Methods'] = 'GET, POST, PUT, DELETE, OPTIONS'
proxyRes.headers['Access-Control-Allow-Headers'] = 'Content-Type, Authorization'
proxyRes.headers['Cache-Control'] = 'public, max-age=31536000'
})
}
}
}
}