修复思维导图文本对齐和格式清理问题

- 实现智能文本对齐:纯文本节点左对齐,带图片节点居中
- 自动清理重复格式标记:移除• 【等重复格式
- 修复双击编辑后样式恢复问题:编辑完成后重新应用样式逻辑
- 优化CSS类名控制:使用.no-image和.has-image类动态控制对齐方式
- 确保SVG导出和显示效果一致性
This commit is contained in:
lixinran 2025-10-10 16:51:27 +08:00
parent a8051a50e8
commit cd9b1f5a38
3 changed files with 201 additions and 42 deletions

View File

@ -235,11 +235,61 @@
max-width: 35em;
white-space: pre-wrap;
pointer-events: all;
// 确保整个节点内容居中对齐
// 默认居中对齐,但会根据是否有图片动态调整
text-align: center !important;
flex-direction: column !important;
align-items: center !important;
justify-content: center !important;
// 没有图片的节点使用左对齐
&.no-image {
text-align: left !important;
align-items: flex-start !important;
.text {
text-align: left !important;
}
ul, ol {
text-align: left !important;
padding-left: 20px !important;
list-style-position: outside !important;
}
li {
text-align: left !important;
list-style-position: outside !important;
}
p, span, div, strong, em {
text-align: left !important;
}
}
// 有图片的节点保持居中对齐
&.has-image {
text-align: center !important;
align-items: center !important;
.text {
text-align: center !important;
}
ul, ol {
text-align: center !important;
padding-left: 0 !important;
list-style-position: inside !important;
}
li {
text-align: center !important;
list-style-position: inside !important;
}
p, span, div, strong, em {
text-align: center !important;
}
}
box-sizing: border-box !important;
// 确保节点有固定的最小高度,避免内容变化导致尺寸不稳定
min-height: 2em !important;
@ -260,7 +310,6 @@
}
& > .text {
display: block !important;
text-align: center !important; // 确保文本居中对齐,避免与连线错位
width: 100% !important; // 确保文本容器占满节点宽度
margin: 0 auto !important; // 强制居中
// 确保文本不会导致节点尺寸不稳定

View File

@ -35,16 +35,36 @@ 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
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: centerX + '',
x: textX + '',
y: y + paddingTop + fontSize + (lineHeight * index) + '',
'text-anchor': 'middle',
'text-anchor': 'start',
'font-family': tpcStyle.fontFamily,
'font-size': `${tpcStyle.fontSize}`,
'font-weight': `${tpcStyle.fontWeight}`,
@ -53,6 +73,13 @@ function generateSvgText(tpc: HTMLElement, tpcStyle: CSSStyleDeclaration, x: num
text.innerHTML = line
g.appendChild(text)
})
console.log('✅ generateSvgText优化了底部间距:', {
originalHeight: nodeHeight,
actualHeight: actualHeight,
linesCount: lines.length
})
return g
}
@ -331,6 +358,12 @@ function cleanHtmlForSvg(html: string): string {
// 移除其他可能导致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) => {
// 检查是否是自闭合标签
@ -403,12 +436,18 @@ function generateSvgTextUsingForeignObject(tpc: HTMLElement, tpcStyle: CSSStyleD
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('🔍 使用getBoundingClientRect获取尺寸:', {
nodeWidth,
nodeHeight,
offsetWidth: tpc.offsetWidth,
offsetHeight: tpc.offsetHeight,
rect: rect
rect: rect,
hasAnyImage: hasAnyImage
})
// 尝试使用原生SVG文本渲染
@ -435,15 +474,25 @@ function generateSvgTextUsingForeignObject(tpc: HTMLElement, tpcStyle: CSSStyleD
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)
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: centerX + '',
x: textX + '',
y: startY + (lineHeight * index) + '',
'text-anchor': 'middle',
'text-anchor': hasImages ? 'middle' : 'start',
'font-family': tpcStyle.fontFamily,
'font-size': tpcStyle.fontSize,
'font-weight': tpcStyle.fontWeight,
@ -453,8 +502,27 @@ function generateSvgTextUsingForeignObject(tpc: HTMLElement, tpcStyle: CSSStyleD
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文本渲染成功')
console.log('✅ 使用原生SVG文本渲染成功优化了底部间距:', {
originalHeight: nodeHeight,
actualHeight: actualHeight,
linesCount: lines.length
})
return g
}
@ -493,12 +561,12 @@ function generateSvgTextUsingForeignObject(tpc: HTMLElement, tpcStyle: CSSStyleD
padding: ${paddingTop}px ${paddingRight}px ${paddingBottom}px ${paddingLeft}px;
box-sizing: border-box;
overflow: visible;
text-align: center !important;
text-align: ${hasAnyImage ? 'center' : 'left'} !important;
line-height: 1.4;
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: center;
align-items: ${hasAnyImage ? 'center' : 'flex-start'};
position: relative;
`
@ -536,9 +604,9 @@ function generateSvgTextUsingForeignObject(tpc: HTMLElement, tpcStyle: CSSStyleD
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;
padding-left: ${hasAnyImage ? '0' : '20px'} !important;
text-align: ${hasAnyImage ? 'center' : 'left'} !important;
list-style-position: ${hasAnyImage ? 'inside' : 'outside'} !important;
`
})
@ -549,12 +617,12 @@ function generateSvgTextUsingForeignObject(tpc: HTMLElement, tpcStyle: CSSStyleD
margin: 1px 0 !important;
line-height: 1.2 !important;
padding: 0 !important;
text-align: center !important;
list-style-position: inside !important;
text-align: ${hasAnyImage ? 'center' : 'left'} !important;
list-style-position: ${hasAnyImage ? 'inside' : 'outside'} !important;
`
})
// 为段落文本添加居中样式
// 为段落文本添加样式
const paragraphs = div.querySelectorAll('p')
paragraphs.forEach(p => {
const htmlP = p as HTMLElement
@ -562,27 +630,17 @@ function generateSvgTextUsingForeignObject(tpc: HTMLElement, tpcStyle: CSSStyleD
margin: 2px 0 !important;
line-height: 1.4 !important;
padding: 0 !important;
text-align: center !important;
text-align: ${hasAnyImage ? 'center' : 'left'} !important;
`
})
// 为所有文本元素添加居中样式
// 为所有文本元素添加样式
const textElements = div.querySelectorAll('span, div, strong, em')
textElements.forEach(element => {
const htmlElement = element as HTMLElement
htmlElement.style.textAlign = 'center !important'
htmlElement.style.textAlign = `${hasAnyImage ? 'center' : 'left'} !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')
@ -642,6 +700,7 @@ function createElBox(mei: MindElixirInstance, tpc: Topic) {
const hasHTMLContent = tpcWithNodeObj.nodeObj && tpcWithNodeObj.nodeObj.dangerouslySetInnerHTML
const hasImages = hasHTMLContent && tpcWithNodeObj.nodeObj.dangerouslySetInnerHTML?.includes('<img')
const hasNodeImage = tpcWithNodeObj.nodeObj && tpcWithNodeObj.nodeObj.image
const hasAnyImage = hasImages || hasNodeImage // 判断是否包含任何图片
const bg = document.createElementNS(ns, 'rect')
const rect = tpc.getBoundingClientRect()

View File

@ -26,7 +26,33 @@ export const shapeTpc = function (this: MindElixirInstance, tpc: Topic, nodeObj:
}
if (nodeObj.dangerouslySetInnerHTML) {
tpc.innerHTML = nodeObj.dangerouslySetInnerHTML
// 清理HTML内容移除重复的格式
let cleanedHTML = nodeObj.dangerouslySetInnerHTML
// 移除• 【这种重复格式
cleanedHTML = cleanedHTML.replace(/•\s*【/g, '【')
cleanedHTML = cleanedHTML.replace(/•\s*\[/g, '[')
// 移除其他可能的重复格式
cleanedHTML = cleanedHTML.replace(/•\s*/g, '')
cleanedHTML = cleanedHTML.replace(/•\s*\(/g, '(')
tpc.innerHTML = cleanedHTML
// 检查是否包含图片,决定文本对齐方式
const hasImages = cleanedHTML.includes('<img')
const hasNodeImage = !!nodeObj.image
const hasAnyImage = hasImages || hasNodeImage
// 应用智能文本对齐样式
if (hasAnyImage) {
tpc.classList.add('has-image')
tpc.classList.remove('no-image')
} else {
tpc.classList.add('no-image')
tpc.classList.remove('has-image')
}
return
}
@ -53,15 +79,37 @@ export const shapeTpc = function (this: MindElixirInstance, tpc: Topic, nodeObj:
textEl.className = 'text'
// Check if markdown parser is provided and topic contains markdown syntax
let content = ''
if (this.markdown) {
textEl.innerHTML = this.markdown(nodeObj.topic, nodeObj)
content = this.markdown(nodeObj.topic, nodeObj)
} else {
// 直接设置文本内容图片通过MindElixir原生image属性处理
textEl.innerHTML = nodeObj.topic || ''
content = nodeObj.topic || ''
}
// 清理文本内容,移除重复的格式
content = content.replace(/•\s*【/g, '【')
content = content.replace(/•\s*\[/g, '[')
content = content.replace(/•\s*/g, '')
content = content.replace(/•\s*\(/g, '(')
textEl.innerHTML = content
tpc.appendChild(textEl)
tpc.text = textEl
// 检查是否有图片,决定文本对齐方式
const hasNodeImage = !!nodeObj.image
const hasImageInText = content.includes('<img')
const hasAnyImage = hasNodeImage || hasImageInText
// 应用智能文本对齐样式
if (hasAnyImage) {
tpc.classList.add('has-image')
tpc.classList.remove('no-image')
} else {
tpc.classList.add('no-image')
tpc.classList.remove('has-image')
}
}
if (nodeObj.hyperLink) {
@ -229,6 +277,9 @@ export const editTopic = function (this: MindElixirInstance, el: Topic) {
div.remove()
// 重新应用样式和清理逻辑
shapeTpc.call(this, el, node)
if (inputContent === originalContent) return
this.linkDiv()