feat: 修复思维导图显示和导出问题

- 修复思维导图节点和连线错位问题
- 使用dangerouslySetInnerHTML正确处理表格内容
- 修复图片尺寸不匹配导致的布局错位
- 优化图片URL验证和错误处理
- 修复SVG导出文字堆叠问题
- 调整节点文本居中对齐,避免与连线错位
- 优化工具栏图标大小和位置
- 完善图片导出功能
This commit is contained in:
lixinran 2025-10-10 13:04:03 +08:00
parent c95bbd649b
commit bbddf200cf
17 changed files with 1311 additions and 833 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

694
frontend/dist/assets/index-d78cd498.js vendored Normal file

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-7802977e.js"></script>
<link rel="stylesheet" href="/assets/index-3cdbb183.css">
<script type="module" crossorigin src="/assets/index-d78cd498.js"></script>
<link rel="stylesheet" href="/assets/index-fc0e370b.css">
</head>
<body>
<div id="app"></div>

View File

@ -170,7 +170,7 @@
import { ref, reactive } from 'vue';
import axios from 'axios';
import { marked } from 'marked';
import { hasMarkdownSyntax } from '../utils/markdownRenderer.js';
import { hasMarkdownSyntax, renderMarkdownToHTML } from '../utils/markdownRenderer.js';
// emit
const emit = defineEmits(['start-realtime-generation']);
@ -931,11 +931,20 @@ const extractImageFromContent = (content) => {
let match;
while ((match = imageRegex.exec(content)) !== null) {
const url = match[2];
const alt = match[1] || '';
// URL
if (url && url.trim() && (url.startsWith('http') || url.startsWith('data:') || url.startsWith('/'))) {
images.push({
alt: match[1] || '',
url: match[2],
alt: alt,
url: url.trim(),
fullMatch: match[0]
});
console.log(`✅ 有效图片URL: ${url}`);
} else {
console.warn(`⚠️ 跳过无效的图片URL: ${url}`);
}
}
console.log(`🔍 从内容中提取到 ${images.length} 张图片:`, images);
@ -994,13 +1003,18 @@ const markdownToJSON = (markdown) => {
// 使
if (titleImages.length > 0) {
const firstImage = titleImages[0];
if (firstImage.url && firstImage.url.trim() !== '') {
node.image = {
url: titleImages[0].url,
width: 120,
height: 80,
url: firstImage.url,
width: 200,
height: 150,
fit: 'contain'
};
console.log(`🖼️ 为标题节点设置图片: ${titleImages[0].url}`);
console.log(`✅ 成功为标题节点设置图片: ${firstImage.url}`);
} else {
console.error(`❌ 标题图片URL无效:`, firstImage);
}
}
//
@ -1063,6 +1077,12 @@ const processContentIntelligently = (content, parentNode, nodeCounter) => {
//
images.forEach((image, index) => {
console.log(`🖼️ 处理图片 ${index + 1}:`, {
alt: image.alt,
url: image.url,
urlLength: image.url.length
});
const imageNode = {
id: `node_${nodeCounter++}`,
topic: image.alt || `图片 ${index + 1}`,
@ -1070,14 +1090,21 @@ const processContentIntelligently = (content, parentNode, nodeCounter) => {
level: (parentNode.level || 0) + 1,
image: {
url: image.url,
width: 120,
height: 80,
width: 200,
height: 150,
fit: 'contain'
},
data: {}
};
//
if (!imageNode.image.url || imageNode.image.url.trim() === '') {
console.error(`❌ 图片节点 ${index + 1} URL为空:`, imageNode);
return;
}
parentNode.children.push(imageNode);
console.log(`🖼️ 创建图片节点: ${imageNode.topic} - ${image.url}`);
console.log(`✅ 成功创建图片节点: ${imageNode.topic} - ${image.url}`);
});
//
@ -1094,9 +1121,14 @@ const processContentIntelligently = (content, parentNode, nodeCounter) => {
//
if (hasTableContent(content)) {
console.log('🎯 检测到表格内容,创建表格节点');
// markdownHTML
const htmlContent = renderMarkdownToHTML(content);
console.log('🎯 表格HTML内容:', htmlContent);
const tableNode = {
id: `node_${nodeCounter++}`,
topic: content, // markdown
topic: '', // topic使dangerouslySetInnerHTML
dangerouslySetInnerHTML: htmlContent, // 使dangerouslySetInnerHTMLHTML
children: [],
level: (parentNode.level || 0) + 1,
data: {},
@ -1173,10 +1205,14 @@ const processContentIntelligently = (content, parentNode, nodeCounter) => {
//
if (hasTableContent(finalContent)) {
console.log('🎯 检测到表格内容,创建表格节点');
// markdown
// markdownHTML
const htmlContent = renderMarkdownToHTML(finalContent);
console.log('🎯 表格HTML内容:', htmlContent);
const tableNode = {
id: `node_${currentNodeCounter++}`,
topic: finalContent, // markdown
topic: '', // topic使dangerouslySetInnerHTML
dangerouslySetInnerHTML: htmlContent, // 使dangerouslySetInnerHTMLHTML
children: [],
level: (parentNode.level || 0) + 1,
data: {},

View File

@ -373,10 +373,25 @@ const loadMindmapData = async (data, keepPosition = false, shouldCenterRoot = tr
return result;
}
return text;
},
imageProxy: (url) => {
// URL
//
if (url.startsWith('http://') || url.startsWith('https://')) {
return url;
}
//
if (url.startsWith('/')) {
return window.location.origin + url;
}
//
return window.location.origin + '/' + url;
}
});
console.log('✅ Mind Elixir实例创建完成markdown函数已设置');
// MindElixir
//
console.log('🔍 初始化Mind Elixir数据:', data);
const result = mindElixir.value.init(data);
@ -387,12 +402,6 @@ const loadMindmapData = async (data, keepPosition = false, shouldCenterRoot = tr
//
restorePosition(currentPosition);
console.log('📍 初始化后立即恢复位置');
//
setTimeout(() => {
restorePosition(currentPosition);
console.log('📍 二次确认位置恢复');
}, 100);
} else if (!keepPosition && !shouldCenterRoot) {
//
console.log('📍 跳过根节点居中,等待居中新节点');
@ -535,27 +544,10 @@ const loadMindmapData = async (data, keepPosition = false, shouldCenterRoot = tr
hideWelcomePage();
bindEventListeners();
//
if (currentPosition) {
setTimeout(() => {
restorePosition(currentPosition);
//
addNodeDescriptions();
}, 500);
} else if (shouldCenterRoot) {
//
setTimeout(() => {
centerMindMap();
// console.log('🔄 ');
//
addNodeDescriptions();
}, 500);
} else {
//
//
setTimeout(() => {
addNodeDescriptions();
}, 500);
}
}, 100);
} catch (error) {
@ -3525,6 +3517,8 @@ const updateMindMapRealtime = async (data, title) => {
centerMindMap();
// console.log(' ');
// MindElixir
// 使Mind Elixirmarkdown
} else {
//
@ -4450,5 +4444,17 @@ const updateMindMapRealtime = async (data, title) => {
flex-direction: column;
align-items: flex-start;
}
/* 强制设置节点中图片的大小 */
.map-container me-tpc img,
.map-container me-tpc > img {
max-width: 200px !important;
max-height: 150px !important;
width: auto !important;
height: auto !important;
display: block !important;
margin-bottom: 8px !important;
object-fit: cover !important;
}
</style>

View File

@ -245,6 +245,8 @@
}
& > .text {
display: inline-block;
text-align: center; // 确保文本居中对齐,避免与连线错位
width: 100%; // 确保文本容器占满节点宽度
// Allow links inside markdown text to be clickable
a {
pointer-events: auto;
@ -254,6 +256,18 @@
display: block;
margin-bottom: 8px;
object-fit: cover;
max-width: 200px !important;
max-height: 150px !important;
width: auto !important;
height: auto !important;
}
// 更具体的选择器,确保覆盖所有图片
img {
max-width: 200px !important;
max-height: 150px !important;
width: auto !important;
height: auto !important;
}
}
.circle {

View File

@ -62,6 +62,21 @@ function MindElixir(
this.newTopicName = newTopicName || 'New Node'
this.contextMenu = contextMenu ?? true
this.toolBar = toolBar ?? true
console.log('🔧 MindElixir构造函数 - 工具栏配置:', {
toolBar: toolBar,
thisToolBar: this.toolBar,
finalValue: this.toolBar,
toolBarType: typeof toolBar,
toolBarTruthy: !!toolBar
});
console.log('🚨 这是测试日志 - 如果看到这个说明代码已更新');
console.log('🔧 MindElixir构造函数 - 完整配置:', {
el: ele,
toolBar: toolBar,
contextMenu: contextMenu,
keypress: keypress,
draggable: draggable
});
this.keypress = keypress ?? true
this.mouseSelectionButton = mouseSelectionButton ?? 0
this.direction = direction ?? 1

View File

@ -70,6 +70,13 @@ const methods = {
...summary,
...exportImage,
init(this: MindElixirInstance, data: MindElixirData) {
console.log('🔧 MindElixir init方法被调用')
console.log('🔧 init方法 - 当前实例配置:', {
toolBar: this.toolBar,
contextMenu: this.contextMenu,
keypress: this.keypress,
draggable: this.draggable
})
data = JSON.parse(JSON.stringify(data))
if (!data || !data.nodeData) return new Error('MindElixir: `data` is required')
if (data.direction !== undefined) {
@ -82,6 +89,11 @@ const methods = {
this.summaries = data.summaries || []
this.tidyArrow()
// plugins
console.log('🔧 检查工具栏配置:', {
toolBar: this.toolBar,
toolBarType: typeof this.toolBar,
willCallToolBar: !!this.toolBar
})
this.toolBar && toolBar(this)
if (import.meta.env.MODE !== 'lite') {
this.keypress && keypressInit(this, this.keypress)

View File

@ -28,16 +28,16 @@ function generateSvgText(tpc: HTMLElement, tpcStyle: CSSStyleDeclaration, x: num
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)
lines.forEach((line, index) => {
const text = document.createElementNS(ns, 'text')
setAttributes(text, {
x: x + parseInt(tpcStyle.paddingLeft) + '',
y:
y +
parseInt(tpcStyle.paddingTop) +
lineHightToPadding(tpcStyle.lineHeight, tpcStyle.fontSize) * (index + 1) +
parseFloat(tpcStyle.fontSize) * (index + 1) +
'',
y: y + parseInt(tpcStyle.paddingTop) + fontSize + (lineHeight * index) + '',
'text-anchor': 'start',
'font-family': tpcStyle.fontFamily,
'font-size': `${tpcStyle.fontSize}`,
@ -51,29 +51,8 @@ function generateSvgText(tpc: HTMLElement, tpcStyle: CSSStyleDeclaration, x: num
}
function generateSvgTextUsingForeignObject(tpc: HTMLElement, tpcStyle: CSSStyleDeclaration, x: number, y: number) {
let content = ''
if ((tpc as Topic).nodeObj?.dangerouslySetInnerHTML) {
content = (tpc as Topic).nodeObj.dangerouslySetInnerHTML!
} else if ((tpc as Topic).text) {
content = (tpc as Topic).text.textContent!
} else {
content = tpc.childNodes[0].textContent!
}
const foreignObject = document.createElementNS(ns, 'foreignObject')
setAttributes(foreignObject, {
x: x + parseInt(tpcStyle.paddingLeft) + '',
y: y + parseInt(tpcStyle.paddingTop) + '',
width: tpcStyle.width,
height: tpcStyle.height,
})
const div = document.createElement('div')
setAttributes(div, {
xmlns: 'http://www.w3.org/1999/xhtml',
style: `font-family: ${tpcStyle.fontFamily}; font-size: ${tpcStyle.fontSize}; font-weight: ${tpcStyle.fontWeight}; color: ${tpcStyle.color}; white-space: pre-wrap;`,
})
div.innerHTML = content
foreignObject.appendChild(div)
return foreignObject
// 直接使用原生SVG文本渲染避免foreignObject的复杂性
return generateSvgText(tpc, tpcStyle, x, y)
}
function createElBox(mei: MindElixirInstance, tpc: Topic) {
@ -140,25 +119,89 @@ function convertAToSvg(mei: MindElixirInstance, a: HTMLAnchorElement) {
return svgA
}
function convertImgToSvg(mei: MindElixirInstance, a: HTMLImageElement) {
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: aStyle.width + '',
height: aStyle.height + '',
href: a.src,
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 = (mei: MindElixirInstance, noForeignObject = false) => {
const generateSvg = async (mei: MindElixirInstance, noForeignObject = false) => {
const mapDiv = mei.nodes
const height = mapDiv.offsetHeight + padding * 2
const width = mapDiv.offsetWidth + padding * 2
@ -190,10 +233,22 @@ const generateSvg = (mei: MindElixirInstance, noForeignObject = false) => {
mapDiv.querySelectorAll<Topic>('me-tpc').forEach(tpc => {
if (tpc.nodeObj.dangerouslySetInnerHTML) {
console.log('🔍 节点有dangerouslySetInnerHTML使用ForeignObject')
g.appendChild(convertDivToSvg(mei, tpc, noForeignObject ? false : true))
} else {
g.appendChild(createElBox(mei, tpc))
g.appendChild(convertDivToSvg(mei, tpc.text, noForeignObject ? false : true))
// 检查节点是否包含HTML内容如表格、markdown等
const hasHTMLContent = !!(tpc.text && tpc.text.innerHTML && tpc.text.innerHTML !== tpc.text.textContent)
console.log('🔍 节点HTML内容检查:', {
hasText: !!tpc.text,
hasInnerHTML: !!(tpc.text && tpc.text.innerHTML),
innerHTML: tpc.text?.innerHTML?.substring(0, 100),
textContent: tpc.text?.textContent?.substring(0, 100),
hasHTMLContent,
willUseForeignObject: !noForeignObject && hasHTMLContent
})
// 修复传递tpc而不是tpc.text确保正确导出节点内容
g.appendChild(convertDivToSvg(mei, tpc, noForeignObject ? false : hasHTMLContent))
}
})
mapDiv.querySelectorAll('.tags > span').forEach(tag => {
@ -205,8 +260,13 @@ const generateSvg = (mei: MindElixirInstance, noForeignObject = false) => {
mapDiv.querySelectorAll('.hyper-link').forEach(hl => {
g.appendChild(convertAToSvg(mei, hl as HTMLAnchorElement))
})
mapDiv.querySelectorAll('img').forEach(img => {
g.appendChild(convertImgToSvg(mei, img))
// 处理图片元素
const imgPromises = Array.from(mapDiv.querySelectorAll('img')).map(async (img) => {
return await convertImgToSvg(mei, img)
})
const imgElements = await Promise.all(imgPromises)
imgElements.forEach(imgEl => {
g.appendChild(imgEl)
})
setAttributes(g, {
x: padding + '',
@ -235,29 +295,51 @@ function blobToUrl(blob: Blob): Promise<string> {
})
}
export const exportSvg = function (this: MindElixirInstance, noForeignObject = false, injectCss?: string) {
const svgEl = generateSvg(this, noForeignObject)
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 = this.exportSvg(noForeignObject, injectCss)
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
img.onerror = reject
})
}

View File

@ -2,26 +2,66 @@
position: absolute;
color: var(--panel-color);
background: var(--panel-bgcolor);
padding: 10px;
border-radius: 5px;
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.2);
padding: 8px;
border-radius: 6px;
box-shadow: 0 2px 8px 0 rgba(0, 0, 0, 0.15);
// svg {
// display: inline-block; // overwrite tailwindcss
// width: 18px !important;
// height: 18px !important;
// max-width: 18px !important;
// max-height: 18px !important;
// min-width: 18px !important;
// min-height: 18px !important;
// vertical-align: middle;
// flex-shrink: 0;
// // 确保图标内容在容器中居中
// object-fit: contain !important;
// object-position: center !important;
// }
svg {
display: inline-block; // overwrite tailwindcss
width: 18px !important;
height: 18px !important;
max-width: 18px !important;
max-height: 18px !important;
min-width: 18px !important;
min-height: 18px !important;
vertical-align: middle;
flex-shrink: 0;
}
span {
display: inline-flex;
align-items: center;
justify-content: center;
width: 36px;
height: 36px;
min-width: 36px;
min-height: 36px;
max-width: 36px;
max-height: 36px;
border-radius: 6px;
transition: background-color 0.2s ease;
flex-shrink: 0;
&:hover {
background-color: rgba(0, 0, 0, 0.05);
}
&:active {
opacity: 0.5;
opacity: 0.7;
background-color: rgba(0, 0, 0, 0.1);
}
}
}
.mind-elixir-toolbar.rb {
right: 20px;
bottom: 20px;
right: 15px;
bottom: 15px;
span + span {
margin-left: 10px;
margin-left: 8px;
}
}

View File

@ -7,8 +7,104 @@ import living from '../icons/living.svg?raw'
import zoomin from '../icons/zoomin.svg?raw'
import zoomout from '../icons/zoomout.svg?raw'
// 导出按钮的SVG图标 - 调整尺寸与其他图标保持一致
const exportSvgIcon = `<?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"><svg t="1750169419447" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2480" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M896 128H128c-35.3 0-64 28.7-64 64v640c0 35.3 28.7 64 64 64h768c35.3 0 64-28.7 64-64V192c0-35.3-28.7-64-64-64zM832 832H192V192h640v640z" p-id="2481"></path><path d="M192 256h640v64H192v-64zM192 384h640v64H192v-64zM192 512h640v64H192v-64zM192 640h448v64H192v-64zM192 768h320v64H192v-64z" p-id="2482"></path></svg>`
import './toolBar.less'
// 导出功能 - 按照官方文档实现
const downloadExport = (type: 'svg') => {
return async (mind: MindElixirInstance) => {
try {
console.log(`🖼️ 开始导出${type.toUpperCase()}文件...`)
// 获取当前思维导图的标题作为文件名
const title = mind.nodeData?.topic || '思维导图'
const filename = `${title}_${new Date().toISOString().slice(0, 10)}`
// 完整的CSS样式包括表格样式
const style = `
.topic { font-family: Arial, sans-serif; font-size: 14px; color: #333; }
.markdown-content { font-size: 12px; line-height: 1.3; }
/* 表格样式 */
.markdown-table {
border-collapse: collapse;
width: 100%;
margin: 4px 0;
font-size: 11px;
border: 1px solid #e0e0e0;
border-radius: 6px;
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
background-color: #fafafa;
overflow: hidden;
white-space: normal;
}
.markdown-table th,
.markdown-table td {
border: 1px solid #e0e0e0;
padding: 8px 12px;
text-align: left;
vertical-align: top;
position: relative;
white-space: normal;
}
.markdown-table th {
background-color: #f5f5f5;
font-weight: 600;
color: #333;
text-align: center;
border-bottom: 1px solid #d0d0d0;
}
.markdown-table td {
background-color: #fff;
}
.markdown-table tr:nth-child(even) td {
background-color: #f8f8f8;
}
.markdown-table tr:hover td {
background-color: #f0f8ff;
}
/* 移除多余的边框,保持简洁 */
.markdown-table th:not(:last-child),
.markdown-table td:not(:last-child) {
border-right: 1px solid #e0e0e0;
}
.markdown-table tr:not(:last-child) td {
border-bottom: 1px solid #e0e0e0;
}
`
const blob = await mind.exportSvg(false, style)
if (!blob) {
console.error('导出失败:无法生成文件')
return
}
// 创建下载链接
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = `${filename}.${type}`
a.click()
URL.revokeObjectURL(url)
console.log(`✅ 成功导出${type.toUpperCase()}文件: ${filename}.${type}`)
} catch (e) {
console.error('导出失败:', e)
alert(`导出${type.toUpperCase()}失败,请重试`)
}
}
}
const map: Record<string, string> = {
side,
left,
@ -17,6 +113,7 @@ const map: Record<string, string> = {
living,
zoomin,
zoomout,
exportSvg: exportSvgIcon,
}
const createButton = (id: string, name: string) => {
const button = document.createElement('span')
@ -26,24 +123,58 @@ const createButton = (id: string, name: string) => {
}
function createToolBarRBContainer(mind: MindElixirInstance) {
try {
console.log('🔧 开始创建右侧工具栏容器...')
const toolBarRBContainer = document.createElement('div')
const fc = createButton('fullscreen', 'full')
const gc = createButton('toCenter', 'living')
const zo = createButton('zoomout', 'zoomout')
const zi = createButton('zoomin', 'zoomin')
// 添加导出按钮
console.log('🔧 开始创建导出按钮...')
const exportSvg = createButton('exportSvg', 'exportSvg')
console.log('🔧 创建导出按钮:', { exportSvg })
console.log('🔧 导出按钮内容:', {
svgContent: exportSvg.innerHTML
})
// 添加工具提示
exportSvg.title = '导出SVG矢量图'
const percentage = document.createElement('span')
percentage.innerText = '100%'
toolBarRBContainer.appendChild(fc)
toolBarRBContainer.appendChild(gc)
toolBarRBContainer.appendChild(zo)
toolBarRBContainer.appendChild(zi)
toolBarRBContainer.appendChild(exportSvg)
// toolBarRBContainer.appendChild(percentage)
toolBarRBContainer.className = 'mind-elixir-toolbar rb'
// 调试信息:确认按钮数量
console.log('🔍 工具栏按钮数量:', toolBarRBContainer.children.length)
console.log('🔍 工具栏按钮列表:', Array.from(toolBarRBContainer.children).map(child => ({
id: child.id,
tagName: child.tagName,
innerHTML: child.innerHTML.substring(0, 50)
})))
fc.onclick = () => {
if (document.fullscreenElement === mind.el) {
document.exitFullscreen()
// 退出全屏后,确保交互正常
setTimeout(() => {
mind.el.focus()
console.log('📱 已退出全屏模式,恢复节点交互')
}, 100)
} else {
mind.el.requestFullscreen()
// 进入全屏后,优化交互体验
setTimeout(() => {
console.log('📱 已进入全屏模式,节点交互可能受限')
}, 100)
}
}
gc.onclick = () => {
@ -55,7 +186,39 @@ function createToolBarRBContainer(mind: MindElixirInstance) {
zi.onclick = () => {
mind.scale(mind.scaleVal + mind.scaleSensitivity)
}
// 导出按钮点击事件
exportSvg.onclick = () => {
console.log('📄 点击导出SVG按钮')
downloadExport('svg')(mind)
}
// 调试信息:确认按钮已创建
console.log('✅ 导出按钮已创建:', {
exportSvg: exportSvg,
container: toolBarRBContainer,
exportSvgVisible: exportSvg.style.display,
containerChildren: toolBarRBContainer.children.length
})
// 延迟检查按钮是否真的在DOM中
setTimeout(() => {
const svgButton = document.getElementById('exportSvg')
console.log('🔍 延迟检查按钮DOM状态:', {
svgButton: svgButton,
svgButtonVisible: svgButton ? window.getComputedStyle(svgButton).display : 'not found'
})
}, 1000)
console.log('✅ 右侧工具栏容器创建完成')
return toolBarRBContainer
} catch (error) {
console.error('❌ 创建右侧工具栏容器失败:', error)
// 返回一个基本的工具栏容器
const fallbackContainer = document.createElement('div')
fallbackContainer.className = 'mind-elixir-toolbar rb'
return fallbackContainer
}
}
function createToolBarLTContainer(mind: MindElixirInstance) {
const toolBarLTContainer = document.createElement('div')
@ -80,6 +243,28 @@ function createToolBarLTContainer(mind: MindElixirInstance) {
}
export default function (mind: MindElixirInstance) {
mind.container.append(createToolBarRBContainer(mind))
mind.container.append(createToolBarLTContainer(mind))
try {
console.log('🔧 工具栏插件开始初始化...')
console.log('🔧 MindElixir实例:', mind)
console.log('🔧 容器元素:', mind.container)
if (!mind.container) {
console.error('❌ 容器元素不存在,无法创建工具栏')
return
}
const rbContainer = createToolBarRBContainer(mind)
const ltContainer = createToolBarLTContainer(mind)
console.log('🔧 右侧工具栏容器:', rbContainer)
console.log('🔧 左侧工具栏容器:', ltContainer)
mind.container.append(rbContainer)
mind.container.append(ltContainer)
console.log('✅ 工具栏插件初始化完成,已添加导出按钮')
console.log('✅ 容器子元素数量:', mind.container.children.length)
} catch (error) {
console.error('❌ 工具栏插件初始化失败:', error)
}
}

View File

@ -19,7 +19,7 @@ const renderer = new marked.Renderer();
// 配置marked选项
marked.setOptions({
breaks: true,
breaks: false, // 禁用breaks避免在表格中产生<br>标签导致HTML不匹配
gfm: true, // GitHub Flavored Markdown
tables: true, // 支持表格
sanitize: false, // 允许HTML用于数学公式等