修复SVG导出文本居中问题
- 修改generateSvgText函数,使用text-anchor: middle强制文本居中 - 计算节点中心坐标,确保文本在节点中水平居中 - 解决图片说明文本左对齐的问题 - 使用原生SVG文本渲染确保居中效果
This commit is contained in:
parent
beee48eb0c
commit
a8051a50e8
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
|
|
@ -23,8 +23,8 @@
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
<script type="module" crossorigin src="/assets/index-2d6d975e.js"></script>
|
<script type="module" crossorigin src="/assets/index-df04209b.js"></script>
|
||||||
<link rel="stylesheet" href="/assets/index-f7e35254.css">
|
<link rel="stylesheet" href="/assets/index-bc28fb44.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
|
|
|
||||||
|
|
@ -1085,13 +1085,22 @@ const processContentIntelligently = (content, parentNode, nodeCounter) => {
|
||||||
urlLength: image.url.length
|
urlLength: image.url.length
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 转换图片URL为代理URL,解决CORS问题
|
||||||
|
let imageUrl = image.url;
|
||||||
|
if (imageUrl.includes('cdn-mineru.openxlab.org.cn')) {
|
||||||
|
// 将外部CDN URL转换为代理URL
|
||||||
|
const urlPath = imageUrl.replace('https://cdn-mineru.openxlab.org.cn', '');
|
||||||
|
imageUrl = `/proxy-image${urlPath}`;
|
||||||
|
console.log(`🔄 转换图片URL: ${image.url} -> ${imageUrl}`);
|
||||||
|
}
|
||||||
|
|
||||||
const imageNode = {
|
const imageNode = {
|
||||||
id: `node_${nodeCounter++}`,
|
id: `node_${nodeCounter++}`,
|
||||||
topic: image.alt || `图片 ${index + 1}`,
|
topic: image.alt || `图片 ${index + 1}`,
|
||||||
children: [],
|
children: [],
|
||||||
level: (parentNode.level || 0) + 1,
|
level: (parentNode.level || 0) + 1,
|
||||||
image: {
|
image: {
|
||||||
url: image.url,
|
url: imageUrl,
|
||||||
width: 200,
|
width: 200,
|
||||||
height: 150,
|
height: 150,
|
||||||
fit: 'contain'
|
fit: 'contain'
|
||||||
|
|
@ -1106,7 +1115,7 @@ const processContentIntelligently = (content, parentNode, nodeCounter) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
parentNode.children.push(imageNode);
|
parentNode.children.push(imageNode);
|
||||||
console.log(`✅ 成功创建图片节点: ${imageNode.topic} - ${image.url}`);
|
console.log(`✅ 成功创建图片节点: ${imageNode.topic} - ${imageUrl}`);
|
||||||
});
|
});
|
||||||
|
|
||||||
// 移除图片后的内容
|
// 移除图片后的内容
|
||||||
|
|
|
||||||
|
|
@ -35,12 +35,16 @@ function generateSvgText(tpc: HTMLElement, tpcStyle: CSSStyleDeclaration, x: num
|
||||||
const paddingTop = parseInt(tpcStyle.paddingTop) || 8
|
const paddingTop = parseInt(tpcStyle.paddingTop) || 8
|
||||||
const paddingLeft = parseInt(tpcStyle.paddingLeft) || 8
|
const paddingLeft = parseInt(tpcStyle.paddingLeft) || 8
|
||||||
|
|
||||||
|
// 计算节点宽度用于居中
|
||||||
|
const nodeWidth = tpc.offsetWidth || 200
|
||||||
|
const centerX = x + nodeWidth / 2
|
||||||
|
|
||||||
lines.forEach((line, index) => {
|
lines.forEach((line, index) => {
|
||||||
const text = document.createElementNS(ns, 'text')
|
const text = document.createElementNS(ns, 'text')
|
||||||
setAttributes(text, {
|
setAttributes(text, {
|
||||||
x: x + paddingLeft + '',
|
x: centerX + '',
|
||||||
y: y + paddingTop + fontSize + (lineHeight * index) + '',
|
y: y + paddingTop + fontSize + (lineHeight * index) + '',
|
||||||
'text-anchor': 'start',
|
'text-anchor': 'middle',
|
||||||
'font-family': tpcStyle.fontFamily,
|
'font-family': tpcStyle.fontFamily,
|
||||||
'font-size': `${tpcStyle.fontSize}`,
|
'font-size': `${tpcStyle.fontSize}`,
|
||||||
'font-weight': `${tpcStyle.fontWeight}`,
|
'font-weight': `${tpcStyle.fontWeight}`,
|
||||||
|
|
@ -394,13 +398,73 @@ function generateSvgTextUsingForeignObject(tpc: HTMLElement, tpcStyle: CSSStyleD
|
||||||
const cleanedHtml = cleanHtmlForSvg(htmlContent)
|
const cleanedHtml = cleanHtmlForSvg(htmlContent)
|
||||||
console.log('🔍 清理后的HTML内容:', cleanedHtml.substring(0, 200))
|
console.log('🔍 清理后的HTML内容:', cleanedHtml.substring(0, 200))
|
||||||
|
|
||||||
// 对于HTML内容,使用foreignObject来正确渲染
|
// 使用getBoundingClientRect获取精确尺寸
|
||||||
const foreignObject = document.createElementNS(ns, 'foreignObject')
|
const rect = tpc.getBoundingClientRect()
|
||||||
|
const nodeWidth = rect.width
|
||||||
|
const nodeHeight = rect.height
|
||||||
|
|
||||||
// 获取节点的实际尺寸,并大幅增加高度以防止图片压线
|
console.log('🔍 使用getBoundingClientRect获取尺寸:', {
|
||||||
const nodeWidth = parseInt(tpcStyle.width) || 200
|
nodeWidth,
|
||||||
const originalHeight = parseInt(tpcStyle.height) || 100
|
nodeHeight,
|
||||||
const nodeHeight = originalHeight + 50 // 大幅增加50px高度给图片留出空间
|
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, {
|
setAttributes(foreignObject, {
|
||||||
x: x + '',
|
x: x + '',
|
||||||
|
|
@ -413,16 +477,15 @@ function generateSvgTextUsingForeignObject(tpc: HTMLElement, tpcStyle: CSSStyleD
|
||||||
const div = document.createElement('div')
|
const div = document.createElement('div')
|
||||||
div.innerHTML = cleanedHtml
|
div.innerHTML = cleanedHtml
|
||||||
|
|
||||||
// 应用样式,确保与思维导图显示一致,大幅增加底部padding防止图片压线
|
// 应用样式,确保与思维导图显示一致
|
||||||
const paddingTop = parseInt(tpcStyle.paddingTop) || 8
|
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 paddingLeft = parseInt(tpcStyle.paddingLeft) || 8
|
||||||
const paddingRight = parseInt(tpcStyle.paddingRight) || 8
|
const paddingRight = parseInt(tpcStyle.paddingRight) || 8
|
||||||
|
|
||||||
div.style.cssText = `
|
div.style.cssText = `
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: auto;
|
height: 100%;
|
||||||
min-height: 100%;
|
|
||||||
font-family: ${tpcStyle.fontFamily};
|
font-family: ${tpcStyle.fontFamily};
|
||||||
font-size: ${tpcStyle.fontSize};
|
font-size: ${tpcStyle.fontSize};
|
||||||
color: ${tpcStyle.color};
|
color: ${tpcStyle.color};
|
||||||
|
|
@ -430,7 +493,7 @@ 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: center;
|
text-align: center !important;
|
||||||
line-height: 1.4;
|
line-height: 1.4;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
@ -448,7 +511,7 @@ function generateSvgTextUsingForeignObject(tpc: HTMLElement, tpcStyle: CSSStyleD
|
||||||
border-collapse: collapse;
|
border-collapse: collapse;
|
||||||
font-size: ${tpcStyle.fontSize};
|
font-size: ${tpcStyle.fontSize};
|
||||||
font-family: ${tpcStyle.fontFamily};
|
font-family: ${tpcStyle.fontFamily};
|
||||||
margin: 0 auto;
|
margin: 0 auto 0px auto;
|
||||||
border: 1px solid #ccc;
|
border: 1px solid #ccc;
|
||||||
`
|
`
|
||||||
|
|
||||||
|
|
@ -472,9 +535,10 @@ function generateSvgTextUsingForeignObject(tpc: HTMLElement, tpcStyle: CSSStyleD
|
||||||
lists.forEach(list => {
|
lists.forEach(list => {
|
||||||
const htmlList = list as HTMLElement
|
const htmlList = list as HTMLElement
|
||||||
htmlList.style.cssText = `
|
htmlList.style.cssText = `
|
||||||
margin: 2px 0 4px 0;
|
margin: 2px 0 4px 0 !important;
|
||||||
padding-left: 20px;
|
padding-left: 0 !important;
|
||||||
text-align: left;
|
text-align: center !important;
|
||||||
|
list-style-position: inside !important;
|
||||||
`
|
`
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -482,12 +546,44 @@ function generateSvgTextUsingForeignObject(tpc: HTMLElement, tpcStyle: CSSStyleD
|
||||||
listItems.forEach(item => {
|
listItems.forEach(item => {
|
||||||
const htmlItem = item as HTMLElement
|
const htmlItem = item as HTMLElement
|
||||||
htmlItem.style.cssText = `
|
htmlItem.style.cssText = `
|
||||||
margin: 1px 0;
|
margin: 1px 0 !important;
|
||||||
line-height: 1.2;
|
line-height: 1.2 !important;
|
||||||
padding: 0;
|
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')
|
const images = div.querySelectorAll('img')
|
||||||
images.forEach((img, index) => {
|
images.forEach((img, index) => {
|
||||||
|
|
@ -503,7 +599,7 @@ function generateSvgTextUsingForeignObject(tpc: HTMLElement, tpcStyle: CSSStyleD
|
||||||
width: auto !important;
|
width: auto !important;
|
||||||
height: auto !important;
|
height: auto !important;
|
||||||
display: block !important;
|
display: block !important;
|
||||||
margin: 8px auto 20px auto !important;
|
margin: 8px auto 0px auto !important;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
object-fit: contain !important;
|
object-fit: contain !important;
|
||||||
flex-shrink: 0 !important;
|
flex-shrink: 0 !important;
|
||||||
|
|
@ -547,17 +643,15 @@ function createElBox(mei: MindElixirInstance, tpc: Topic) {
|
||||||
const hasImages = hasHTMLContent && tpcWithNodeObj.nodeObj.dangerouslySetInnerHTML?.includes('<img')
|
const hasImages = hasHTMLContent && tpcWithNodeObj.nodeObj.dangerouslySetInnerHTML?.includes('<img')
|
||||||
const hasNodeImage = tpcWithNodeObj.nodeObj && tpcWithNodeObj.nodeObj.image
|
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 bg = document.createElementNS(ns, 'rect')
|
||||||
|
const rect = tpc.getBoundingClientRect()
|
||||||
setAttributes(bg, {
|
setAttributes(bg, {
|
||||||
x: x + '',
|
x: x + '',
|
||||||
y: y + '',
|
y: y + '',
|
||||||
rx: tpcStyle.borderRadius,
|
rx: tpcStyle.borderRadius,
|
||||||
ry: tpcStyle.borderRadius,
|
ry: tpcStyle.borderRadius,
|
||||||
width: tpcStyle.width,
|
width: rect.width + 'px',
|
||||||
height: adjustedHeight + 'px',
|
height: rect.height + 'px',
|
||||||
fill: tpcStyle.backgroundColor,
|
fill: tpcStyle.backgroundColor,
|
||||||
stroke: tpcStyle.borderColor,
|
stroke: tpcStyle.borderColor,
|
||||||
'stroke-width': tpcStyle.borderWidth,
|
'stroke-width': tpcStyle.borderWidth,
|
||||||
|
|
@ -574,17 +668,15 @@ 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 originalHeight2 = parseInt(tpcStyle.height) || 100
|
|
||||||
const adjustedHeight2 = (hasImages2 || hasNodeImage2) ? originalHeight2 + 50 : originalHeight2
|
|
||||||
|
|
||||||
const bg = document.createElementNS(ns, 'rect')
|
const bg = document.createElementNS(ns, 'rect')
|
||||||
|
const rect = tpc.getBoundingClientRect()
|
||||||
setAttributes(bg, {
|
setAttributes(bg, {
|
||||||
x: x + '',
|
x: x + '',
|
||||||
y: y + '',
|
y: y + '',
|
||||||
rx: tpcStyle.borderRadius,
|
rx: tpcStyle.borderRadius,
|
||||||
ry: tpcStyle.borderRadius,
|
ry: tpcStyle.borderRadius,
|
||||||
width: tpcStyle.width,
|
width: rect.width + 'px',
|
||||||
height: adjustedHeight2 + 'px',
|
height: rect.height + 'px',
|
||||||
fill: tpcStyle.backgroundColor,
|
fill: tpcStyle.backgroundColor,
|
||||||
stroke: tpcStyle.borderColor,
|
stroke: tpcStyle.borderColor,
|
||||||
'stroke-width': tpcStyle.borderWidth,
|
'stroke-width': tpcStyle.borderWidth,
|
||||||
|
|
@ -675,7 +767,7 @@ async function convertNodeImageToSvg(mei: MindElixirInstance, tpc: Topic): Promi
|
||||||
|
|
||||||
// 图片位置:水平居中,垂直位置考虑文字高度,大幅增加底部间距
|
// 图片位置:水平居中,垂直位置考虑文字高度,大幅增加底部间距
|
||||||
const imgX = x + (nodeWidth - imgWidth) / 2
|
const imgX = x + (nodeWidth - imgWidth) / 2
|
||||||
const imgY = y + textHeight + 20 // 减少底部间距到20px,让图片和文字更接近
|
const imgY = y + textHeight + 0 // 移除底部间距,让图片贴近底部
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 尝试将图片转换为base64
|
// 尝试将图片转换为base64
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,26 @@ export default defineConfig({
|
||||||
'/api': {
|
'/api': {
|
||||||
target: 'http://localhost:8000',
|
target: 'http://localhost:8000',
|
||||||
changeOrigin: true
|
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'
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue