feat: 修复思维导图显示和导出问题
- 修复思维导图节点和连线错位问题 - 使用dangerouslySetInnerHTML正确处理表格内容 - 修复图片尺寸不匹配导致的布局错位 - 优化图片URL验证和错误处理 - 修复SVG导出文字堆叠问题 - 调整节点文本居中对齐,避免与连线错位 - 优化工具栏图标大小和位置 - 完善图片导出功能
This commit is contained in:
parent
c95bbd649b
commit
bbddf200cf
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
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;
|
||||
}
|
||||
</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>
|
||||
|
|
|
|||
|
|
@ -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('🎯 检测到表格内容,创建表格节点');
|
||||
// 将markdown表格转换为HTML格式
|
||||
const htmlContent = renderMarkdownToHTML(content);
|
||||
console.log('🎯 表格HTML内容:', htmlContent);
|
||||
|
||||
const tableNode = {
|
||||
id: `node_${nodeCounter++}`,
|
||||
topic: content, // 保持原始markdown格式
|
||||
topic: '', // 清空topic,使用dangerouslySetInnerHTML
|
||||
dangerouslySetInnerHTML: htmlContent, // 使用dangerouslySetInnerHTML来正确处理HTML内容
|
||||
children: [],
|
||||
level: (parentNode.level || 0) + 1,
|
||||
data: {},
|
||||
|
|
@ -1173,10 +1205,14 @@ const processContentIntelligently = (content, parentNode, nodeCounter) => {
|
|||
// 检查是否包含表格内容
|
||||
if (hasTableContent(finalContent)) {
|
||||
console.log('🎯 检测到表格内容,创建表格节点');
|
||||
// 如果是表格,保持原始markdown格式,不进行文本转换
|
||||
// 将markdown表格转换为HTML格式
|
||||
const htmlContent = renderMarkdownToHTML(finalContent);
|
||||
console.log('🎯 表格HTML内容:', htmlContent);
|
||||
|
||||
const tableNode = {
|
||||
id: `node_${currentNodeCounter++}`,
|
||||
topic: finalContent, // 保持原始markdown格式
|
||||
topic: '', // 清空topic,使用dangerouslySetInnerHTML
|
||||
dangerouslySetInnerHTML: htmlContent, // 使用dangerouslySetInnerHTML来正确处理HTML内容
|
||||
children: [],
|
||||
level: (parentNode.level || 0) + 1,
|
||||
data: {},
|
||||
|
|
|
|||
|
|
@ -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 Elixir原生markdown支持,无需手动渲染
|
||||
} 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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(用于数学公式等)
|
||||
|
|
|
|||
Loading…
Reference in New Issue