feat: 优化思维导图编辑和显示功能
- 修复编辑模式下表格渲染问题,使用新的节点结构逻辑 - 实现Markdown到HTML转换,支持表格和图片正确显示 - 添加generateTopicFromMarkdown函数,自动生成节点简短标题 - 优化保存逻辑,同时更新HTML、Markdown和topic字段 - 修复思维导图生成过程中的页面空白问题,优先使用增量更新 - 应用自定义紫色主题,统一线条和节点配色方案 - 简化控制台输出,只保留关键调试信息
This commit is contained in:
parent
68b0ba1ccf
commit
318974511c
Binary file not shown.
|
|
@ -209,6 +209,18 @@ import '../lib/mind-elixir/dist/style.css';
|
||||||
import { renderMarkdownToHTML } from '../utils/markdownRenderer.js';
|
import { renderMarkdownToHTML } from '../utils/markdownRenderer.js';
|
||||||
import Vditor from 'vditor';
|
import Vditor from 'vditor';
|
||||||
import 'vditor/dist/index.css';
|
import 'vditor/dist/index.css';
|
||||||
|
import { marked } from 'marked';
|
||||||
|
|
||||||
|
// 配置marked库
|
||||||
|
marked.setOptions({
|
||||||
|
breaks: true, // 支持换行
|
||||||
|
gfm: true, // 支持GitHub风格的Markdown
|
||||||
|
tables: true, // 支持表格
|
||||||
|
pedantic: false, // 不严格模式
|
||||||
|
sanitize: false, // 不过滤HTML标签
|
||||||
|
smartLists: true, // 智能列表
|
||||||
|
smartypants: false // 不转换引号
|
||||||
|
});
|
||||||
|
|
||||||
// 使用Vditor渲染Markdown内容
|
// 使用Vditor渲染Markdown内容
|
||||||
const renderMarkdownWithVditor = async (markdown) => {
|
const renderMarkdownWithVditor = async (markdown) => {
|
||||||
|
|
@ -445,41 +457,233 @@ const closeImagePreview = () => {
|
||||||
imagePreviewError.value = '';
|
imagePreviewError.value = '';
|
||||||
};
|
};
|
||||||
|
|
||||||
// HTML转Markdown函数
|
/**
|
||||||
const convertHTMLToMarkdown = (html) => {
|
* 从Markdown内容生成简短标题
|
||||||
|
* 用于节点显示
|
||||||
|
*/
|
||||||
|
const generateTopicFromMarkdown = (markdown) => {
|
||||||
|
if (!markdown || typeof markdown !== 'string') {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 移除Markdown语法,提取纯文本
|
||||||
|
let text = markdown
|
||||||
|
// 移除标题标记
|
||||||
|
.replace(/^#{1,6}\s+/gm, '')
|
||||||
|
// 移除加粗和斜体
|
||||||
|
.replace(/\*\*(.*?)\*\*/g, '$1')
|
||||||
|
.replace(/\*(.*?)\*/g, '$1')
|
||||||
|
// 移除代码块
|
||||||
|
.replace(/```[\s\S]*?```/g, '')
|
||||||
|
.replace(/`([^`]*)`/g, '$1')
|
||||||
|
// 移除链接,保留文本
|
||||||
|
.replace(/\[([^\]]+)\]\([^)]+\)/g, '$1')
|
||||||
|
// 移除图片
|
||||||
|
.replace(/!\[([^\]]*)\]\([^)]+\)/g, '')
|
||||||
|
// 移除表格分隔符
|
||||||
|
.replace(/^\|.*\|$/gm, '')
|
||||||
|
.replace(/^[-|:\s]+$/gm, '')
|
||||||
|
// 移除列表标记
|
||||||
|
.replace(/^[-*+]\s+/gm, '')
|
||||||
|
.replace(/^\d+\.\s+/gm, '')
|
||||||
|
// 移除引用标记
|
||||||
|
.replace(/^>\s*/gm, '')
|
||||||
|
// 移除多余空白
|
||||||
|
.replace(/\n+/g, ' ')
|
||||||
|
.replace(/\s+/g, ' ')
|
||||||
|
.trim();
|
||||||
|
|
||||||
|
// 限制长度,取前50个字符
|
||||||
|
if (text.length > 50) {
|
||||||
|
text = text.substring(0, 50) + '...';
|
||||||
|
}
|
||||||
|
|
||||||
|
return text || '无标题';
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ 生成标题失败:', error);
|
||||||
|
return '无标题';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Markdown转HTML转换函数
|
||||||
|
* 支持表格、图片等复杂内容的转换
|
||||||
|
*/
|
||||||
|
const convertMarkdownToHTML = (markdown) => {
|
||||||
|
if (!markdown || typeof markdown !== 'string') {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
let html = markdown;
|
||||||
|
|
||||||
|
// 处理表格 - 简化版本,处理更灵活
|
||||||
|
// 先尝试处理标准Markdown表格
|
||||||
|
html = html.replace(/\|(.+)\|\n\|[-\s|]+\|\n((?:\|.+\|\n?)*)/g, (match, header, rows) => {
|
||||||
|
const headers = header.split('|').map(h => h.trim()).filter(h => h);
|
||||||
|
const rowLines = rows.trim().split('\n').filter(line => line.trim());
|
||||||
|
|
||||||
|
let tableHTML = '<table class="markdown-table" style="border-collapse: collapse; width: 100%; margin: 1em 0;">\n';
|
||||||
|
|
||||||
|
// 表头
|
||||||
|
if (headers.length > 0) {
|
||||||
|
tableHTML += '<thead><tr>';
|
||||||
|
headers.forEach(header => {
|
||||||
|
tableHTML += `<th style="border: 1px solid #ddd; padding: 8px; background: #f5f5f5; font-weight: bold;">${header}</th>`;
|
||||||
|
});
|
||||||
|
tableHTML += '</tr></thead>\n';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 表体
|
||||||
|
if (rowLines.length > 0) {
|
||||||
|
tableHTML += '<tbody>';
|
||||||
|
rowLines.forEach(row => {
|
||||||
|
const cells = row.split('|').map(c => c.trim()).filter(c => c);
|
||||||
|
if (cells.length > 0) {
|
||||||
|
tableHTML += '<tr>';
|
||||||
|
cells.forEach(cell => {
|
||||||
|
tableHTML += `<td style="border: 1px solid #ddd; padding: 8px;">${cell}</td>`;
|
||||||
|
});
|
||||||
|
tableHTML += '</tr>';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
tableHTML += '</tbody>';
|
||||||
|
}
|
||||||
|
|
||||||
|
tableHTML += '</table>';
|
||||||
|
return tableHTML;
|
||||||
|
});
|
||||||
|
|
||||||
|
// 如果没有找到标准表格,尝试处理简单的列表格式
|
||||||
|
if (!html.includes('<table')) {
|
||||||
|
// 将列表项转换为简单的表格
|
||||||
|
html = html.replace(/^([^::]+)[::]\s*(.+)$/gm, '<div style="display: flex; margin: 0.5em 0; border-bottom: 1px solid #eee; padding: 0.5em 0;"><div style="font-weight: bold; min-width: 120px;">$1:</div><div style="flex: 1;">$2</div></div>');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理图片
|
||||||
|
html = html.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, '<img src="$2" alt="$1" style="max-width: 100%; height: auto; margin: 1em 0; border-radius: 4px;" />');
|
||||||
|
|
||||||
|
// 处理标题
|
||||||
|
html = html.replace(/^### (.*$)/gim, '<h3>$1</h3>');
|
||||||
|
html = html.replace(/^## (.*$)/gim, '<h2>$1</h2>');
|
||||||
|
html = html.replace(/^# (.*$)/gim, '<h1>$1</h1>');
|
||||||
|
|
||||||
|
// 处理加粗
|
||||||
|
html = html.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>');
|
||||||
|
|
||||||
|
// 处理斜体
|
||||||
|
html = html.replace(/\*(.*?)\*/g, '<em>$1</em>');
|
||||||
|
|
||||||
|
// 处理代码块
|
||||||
|
html = html.replace(/```([^`]*)```/g, '<pre style="background: #f5f5f5; padding: 1em; border-radius: 4px; overflow-x: auto;"><code>$1</code></pre>');
|
||||||
|
|
||||||
|
// 处理行内代码
|
||||||
|
html = html.replace(/`([^`]*)`/g, '<code style="background: #f5f5f5; padding: 0.2em 0.4em; border-radius: 3px; font-family: monospace;">$1</code>');
|
||||||
|
|
||||||
|
// 处理链接
|
||||||
|
html = html.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" style="color: #1a73e8; text-decoration: none;">$1</a>');
|
||||||
|
|
||||||
|
// 处理列表
|
||||||
|
html = html.replace(/^- (.*$)/gim, '<li>$1</li>');
|
||||||
|
html = html.replace(/(<li>.*<\/li>)/s, '<ul style="margin: 1em 0; padding-left: 2em;">$1</ul>');
|
||||||
|
|
||||||
|
// 处理换行
|
||||||
|
html = html.replace(/\n\n/g, '</p><p>');
|
||||||
|
html = html.replace(/\n/g, '<br>');
|
||||||
|
|
||||||
|
// 包装段落
|
||||||
|
if (!html.startsWith('<')) {
|
||||||
|
html = '<p>' + html + '</p>';
|
||||||
|
}
|
||||||
|
|
||||||
|
return html;
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Markdown转HTML转换失败:', error);
|
||||||
|
return markdown; // 回退到原始Markdown
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 改进的HTML转Markdown转换函数
|
||||||
|
* 支持表格、图片等复杂内容的转换
|
||||||
|
*/
|
||||||
|
const convertHTMLToMarkdownImproved = (html) => {
|
||||||
if (!html || typeof html !== 'string') {
|
if (!html || typeof html !== 'string') {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
console.log('🔄 开始转换HTML到Markdown:', html.substring(0, 100) + '...');
|
// 优先使用Vditor的html2md功能
|
||||||
|
|
||||||
// 使用Vditor的html2md功能(如果可用)
|
|
||||||
if (typeof Vditor?.html2md === 'function') {
|
if (typeof Vditor?.html2md === 'function') {
|
||||||
const markdown = Vditor.html2md(html);
|
console.log('✅ 使用Vditor.html2md转换HTML');
|
||||||
console.log('✅ 使用Vditor.html2md转换成功:', markdown.substring(0, 100) + '...');
|
return Vditor.html2md(html);
|
||||||
return markdown;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 回退到简单转换逻辑
|
console.log('⚠️ Vditor.html2md不可用,使用改进的转换逻辑');
|
||||||
console.log('⚠️ Vditor.html2md不可用,使用简单转换逻辑');
|
|
||||||
let markdown = html
|
// 创建临时DOM元素进行解析
|
||||||
// 处理表格 - 保持表格结构
|
const tempDiv = document.createElement('div');
|
||||||
.replace(/<table[^>]*>/gi, '\n')
|
tempDiv.innerHTML = html;
|
||||||
.replace(/<\/table>/gi, '\n')
|
|
||||||
.replace(/<thead[^>]*>/gi, '')
|
let markdown = '';
|
||||||
.replace(/<\/thead>/gi, '')
|
|
||||||
.replace(/<tbody[^>]*>/gi, '')
|
// 处理表格
|
||||||
.replace(/<\/tbody>/gi, '')
|
const tables = tempDiv.querySelectorAll('table');
|
||||||
.replace(/<tr[^>]*>/gi, '')
|
tables.forEach((table, tableIndex) => {
|
||||||
.replace(/<\/tr>/gi, ' |\n')
|
console.log(`📊 处理表格 ${tableIndex + 1}`);
|
||||||
.replace(/<th[^>]*>/gi, '| ')
|
|
||||||
.replace(/<\/th>/gi, ' ')
|
// 处理表头
|
||||||
.replace(/<td[^>]*>/gi, '| ')
|
const thead = table.querySelector('thead');
|
||||||
.replace(/<\/td>/gi, ' ')
|
if (thead) {
|
||||||
// 图片处理
|
const headerRow = thead.querySelector('tr');
|
||||||
.replace(/<img[^>]*src="([^"]+)"[^>]*alt="([^"]*)"[^>]*>/gi, '')
|
if (headerRow) {
|
||||||
.replace(/<img[^>]*src="([^"]+)"[^>]*>/gi, '')
|
const headers = Array.from(headerRow.querySelectorAll('th, td')).map(cell =>
|
||||||
|
(cell.textContent || '').trim().replace(/\|/g, '\\|')
|
||||||
|
);
|
||||||
|
markdown += '| ' + headers.join(' | ') + ' |\n';
|
||||||
|
markdown += '| ' + headers.map(() => '---').join(' | ') + ' |\n';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理表体
|
||||||
|
const tbody = table.querySelector('tbody') || table;
|
||||||
|
const rows = tbody.querySelectorAll('tr');
|
||||||
|
rows.forEach(row => {
|
||||||
|
// 跳过表头行(如果已经在thead中处理过)
|
||||||
|
if (thead && thead.contains(row)) return;
|
||||||
|
|
||||||
|
const cells = Array.from(row.querySelectorAll('td, th')).map(cell =>
|
||||||
|
(cell.textContent || '').trim().replace(/\|/g, '\\|')
|
||||||
|
);
|
||||||
|
if (cells.length > 0) {
|
||||||
|
markdown += '| ' + cells.join(' | ') + ' |\n';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
markdown += '\n';
|
||||||
|
});
|
||||||
|
|
||||||
|
// 处理图片
|
||||||
|
const images = tempDiv.querySelectorAll('img');
|
||||||
|
images.forEach(img => {
|
||||||
|
const src = img.src || img.getAttribute('src') || '';
|
||||||
|
const alt = img.alt || img.getAttribute('alt') || '';
|
||||||
|
if (src) {
|
||||||
|
markdown += `\n\n`;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 处理其他内容(移除已处理的表格和图片)
|
||||||
|
const processedDiv = tempDiv.cloneNode(true);
|
||||||
|
processedDiv.querySelectorAll('table, img').forEach(el => el.remove());
|
||||||
|
|
||||||
|
// 转换剩余内容
|
||||||
|
let remainingContent = processedDiv.innerHTML;
|
||||||
|
|
||||||
|
// 基本HTML到Markdown转换
|
||||||
|
remainingContent = remainingContent
|
||||||
// 换行与段落
|
// 换行与段落
|
||||||
.replace(/<br\s*\/?>/gi, '\n')
|
.replace(/<br\s*\/?>/gi, '\n')
|
||||||
.replace(/<\/p>/gi, '\n\n')
|
.replace(/<\/p>/gi, '\n\n')
|
||||||
|
|
@ -494,48 +698,32 @@ const convertHTMLToMarkdown = (html) => {
|
||||||
.replace(/<\/ul>/gi, '\n')
|
.replace(/<\/ul>/gi, '\n')
|
||||||
.replace(/<li[^>]*>/gi, '- ')
|
.replace(/<li[^>]*>/gi, '- ')
|
||||||
.replace(/<\/li>/gi, '\n')
|
.replace(/<\/li>/gi, '\n')
|
||||||
|
// 标题
|
||||||
|
.replace(/<h1[^>]*>(.*?)<\/h1>/gi, '# $1\n\n')
|
||||||
|
.replace(/<h2[^>]*>(.*?)<\/h2>/gi, '## $1\n\n')
|
||||||
|
.replace(/<h3[^>]*>(.*?)<\/h3>/gi, '### $1\n\n')
|
||||||
|
.replace(/<h4[^>]*>(.*?)<\/h4>/gi, '#### $1\n\n')
|
||||||
|
.replace(/<h5[^>]*>(.*?)<\/h5>/gi, '##### $1\n\n')
|
||||||
|
.replace(/<h6[^>]*>(.*?)<\/h6>/gi, '###### $1\n\n')
|
||||||
|
// 链接
|
||||||
|
.replace(/<a[^>]*href="([^"]*)"[^>]*>(.*?)<\/a>/gi, '[$2]($1)')
|
||||||
|
// 代码
|
||||||
|
.replace(/<code[^>]*>(.*?)<\/code>/gi, '`$1`')
|
||||||
|
.replace(/<pre[^>]*>(.*?)<\/pre>/gi, '```\n$1\n```\n\n')
|
||||||
// 清除剩余标签
|
// 清除剩余标签
|
||||||
.replace(/<[^>]+>/g, '')
|
.replace(/<[^>]+>/g, '')
|
||||||
// 规范换行
|
// 规范换行
|
||||||
.replace(/\n{3,}/g, '\n\n')
|
.replace(/\n{3,}/g, '\n\n')
|
||||||
.trim();
|
.trim();
|
||||||
|
|
||||||
// 处理表格格式 - 添加表头分隔线
|
markdown += remainingContent;
|
||||||
const lines = markdown.split('\n');
|
|
||||||
const processedLines = [];
|
|
||||||
let inTable = false;
|
|
||||||
|
|
||||||
for (let i = 0; i < lines.length; i++) {
|
console.log('✅ HTML转Markdown转换完成:', markdown.substring(0, 200) + '...');
|
||||||
const line = lines[i];
|
|
||||||
if (line.includes('|') && line.trim().length > 0) {
|
|
||||||
if (!inTable) {
|
|
||||||
inTable = true;
|
|
||||||
processedLines.push(line);
|
|
||||||
// 添加表头分隔线
|
|
||||||
const headerCells = line.split('|').filter(cell => cell.trim());
|
|
||||||
if (headerCells.length > 0) {
|
|
||||||
const separator = '| ' + headerCells.map(() => '---').join(' | ') + ' |';
|
|
||||||
processedLines.push(separator);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
processedLines.push(line);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (inTable) {
|
|
||||||
inTable = false;
|
|
||||||
}
|
|
||||||
processedLines.push(line);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
markdown = processedLines.join('\n');
|
|
||||||
|
|
||||||
console.log('✅ 简单转换逻辑完成:', markdown.substring(0, 100) + '...');
|
|
||||||
return markdown;
|
return markdown;
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('❌ HTML转Markdown失败:', error);
|
console.error('❌ HTML转Markdown转换失败:', error);
|
||||||
return html; // 转换失败时返回原始HTML
|
return html; // 回退到原始HTML
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -565,15 +753,55 @@ const openRichTextEditor = (nodeObj, nodeElement) => {
|
||||||
allKeys: Object.keys(nodeObj)
|
allKeys: Object.keys(nodeObj)
|
||||||
});
|
});
|
||||||
|
|
||||||
// 将HTML内容转换为Markdown格式供编辑
|
// 使用新的节点结构逻辑
|
||||||
|
let htmlContent = '';
|
||||||
|
|
||||||
|
// 优先使用dangerouslySetInnerHTML(用于WYSIWYG编辑)
|
||||||
if (nodeObj.dangerouslySetInnerHTML) {
|
if (nodeObj.dangerouslySetInnerHTML) {
|
||||||
editorContent.value = convertHTMLToMarkdown(nodeObj.dangerouslySetInnerHTML);
|
htmlContent = nodeObj.dangerouslySetInnerHTML;
|
||||||
console.log('📝 转换后的Markdown内容:', editorContent.value);
|
console.log('📝 使用dangerouslySetInnerHTML内容:', htmlContent.substring(0, 200) + '...');
|
||||||
} else if (nodeObj.topic) {
|
|
||||||
editorContent.value = nodeObj.topic;
|
|
||||||
} else {
|
|
||||||
editorContent.value = '';
|
|
||||||
}
|
}
|
||||||
|
// 其次使用markdown字段(如果存在)
|
||||||
|
else if (nodeObj.markdown) {
|
||||||
|
console.log('📝 使用markdown内容,转换为HTML:', nodeObj.markdown.substring(0, 200) + '...');
|
||||||
|
try {
|
||||||
|
htmlContent = marked.parse(nodeObj.markdown);
|
||||||
|
// 更新dangerouslySetInnerHTML
|
||||||
|
nodeObj.dangerouslySetInnerHTML = htmlContent;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Markdown转HTML失败:', error);
|
||||||
|
htmlContent = nodeObj.markdown;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 最后回退到data或topic(兼容旧数据)
|
||||||
|
else {
|
||||||
|
const markdownContent = nodeObj.data || nodeObj.topic || '';
|
||||||
|
console.log('📝 使用兼容模式,从data/topic获取内容:', markdownContent.substring(0, 200) + '...');
|
||||||
|
|
||||||
|
// 检查是否包含Markdown格式内容
|
||||||
|
const hasMarkdownContent = markdownContent.includes('|') && markdownContent.includes('-') || // 表格
|
||||||
|
markdownContent.includes('![') || // 图片
|
||||||
|
markdownContent.includes('#') || // 标题
|
||||||
|
markdownContent.includes('**') || // 粗体
|
||||||
|
markdownContent.includes('`'); // 代码
|
||||||
|
|
||||||
|
if (hasMarkdownContent) {
|
||||||
|
try {
|
||||||
|
htmlContent = marked.parse(markdownContent);
|
||||||
|
// 更新节点结构
|
||||||
|
nodeObj.markdown = markdownContent;
|
||||||
|
nodeObj.dangerouslySetInnerHTML = htmlContent;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Markdown转HTML失败:', error);
|
||||||
|
htmlContent = markdownContent;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
htmlContent = markdownContent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
editorContent.value = htmlContent;
|
||||||
|
console.log('🎯 WYSIWYG模式HTML内容:', htmlContent.substring(0, 200) + '...');
|
||||||
|
|
||||||
// 检查是否有MindElixir原生图片
|
// 检查是否有MindElixir原生图片
|
||||||
if (nodeObj.image && !editorContent.value.includes('![')) {
|
if (nodeObj.image && !editorContent.value.includes('![')) {
|
||||||
|
|
@ -622,13 +850,6 @@ const initVditor = async () => {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('🎯 开始初始化Vditor编辑器...');
|
|
||||||
console.log('🔍 vditorContainer元素:', vditorContainer.value);
|
|
||||||
console.log('🔍 容器可见性:', vditorContainer.value.offsetParent !== null);
|
|
||||||
console.log('🔍 容器尺寸:', {
|
|
||||||
width: vditorContainer.value.offsetWidth,
|
|
||||||
height: vditorContainer.value.offsetHeight
|
|
||||||
});
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 先清理容器
|
// 先清理容器
|
||||||
|
|
@ -638,7 +859,7 @@ const initVditor = async () => {
|
||||||
vditorInstance = new Vditor(vditorContainer.value, {
|
vditorInstance = new Vditor(vditorContainer.value, {
|
||||||
height: 400,
|
height: 400,
|
||||||
placeholder: '请输入节点内容...',
|
placeholder: '请输入节点内容...',
|
||||||
mode: 'wysiwyg', // 所见即所得模式
|
mode: 'wysiwyg', // 所见即所得模式,使用HTML内容
|
||||||
theme: 'classic',
|
theme: 'classic',
|
||||||
toolbarConfig: {
|
toolbarConfig: {
|
||||||
pin: true // 固定工具栏
|
pin: true // 固定工具栏
|
||||||
|
|
@ -691,17 +912,12 @@ const initVditor = async () => {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
after: () => {
|
after: () => {
|
||||||
console.log('✅ Vditor编辑器初始化完成');
|
|
||||||
|
|
||||||
// 等待编辑器完全渲染后再设置内容
|
// 等待编辑器完全渲染后再设置内容
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
// ✅ 直接设置内容,让Vditor自动处理Markdown/HTML转换
|
// ✅ 直接设置内容,让Vditor自动处理Markdown/HTML转换
|
||||||
if (editorContent.value && editorContent.value.trim()) {
|
if (editorContent.value && editorContent.value.trim()) {
|
||||||
vditorInstance.setValue(editorContent.value);
|
vditorInstance.setValue(editorContent.value);
|
||||||
console.log('✅ 内容已设置到WYSIWYG编辑器:', editorContent.value.substring(0, 100) + '...');
|
|
||||||
console.log('✅ 编辑器当前模式: wysiwyg (所见即所得模式)');
|
|
||||||
} else {
|
} else {
|
||||||
console.warn('⚠️ editorContent.value为空,设置空内容');
|
|
||||||
vditorInstance.setValue('');
|
vditorInstance.setValue('');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -714,34 +930,18 @@ const initVditor = async () => {
|
||||||
// 获取Markdown格式的内容
|
// 获取Markdown格式的内容
|
||||||
const markdownContent = vditorInstance.getValue();
|
const markdownContent = vditorInstance.getValue();
|
||||||
editorContent.value = markdownContent;
|
editorContent.value = markdownContent;
|
||||||
console.log('📝 编辑器内容变化(Markdown):', markdownContent.substring(0, 50) + '...');
|
|
||||||
});
|
});
|
||||||
} else {
|
|
||||||
console.warn('⚠️ Vditor实例没有on方法');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查工具栏是否正确渲染
|
// 检查工具栏是否正确渲染
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
const toolbar = document.querySelector('.vditor-toolbar');
|
const toolbar = document.querySelector('.vditor-toolbar');
|
||||||
const editor = document.querySelector('.vditor-wysiwyg');
|
const editor = document.querySelector('.vditor-wysiwyg');
|
||||||
console.log('🔍 Vditor工具栏:', toolbar);
|
|
||||||
console.log('🔍 Vditor编辑器区域:', editor);
|
|
||||||
|
|
||||||
if (!toolbar || !editor) {
|
|
||||||
console.error('❌ Vditor组件没有正确渲染');
|
|
||||||
console.error('工具栏状态:', toolbar ? '存在' : '不存在');
|
|
||||||
console.error('编辑器状态:', editor ? '存在' : '不存在');
|
|
||||||
} else {
|
|
||||||
console.log('✅ Vditor组件渲染成功');
|
|
||||||
console.log('工具栏按钮数量:', toolbar.querySelectorAll('button').length);
|
|
||||||
console.log('工具栏高度:', toolbar.offsetHeight);
|
|
||||||
}
|
|
||||||
}, 100);
|
}, 100);
|
||||||
}, 100);
|
}, 100);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log('✅ Vditor编辑器创建成功');
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('❌ Vditor编辑器初始化失败:', error);
|
console.error('❌ Vditor编辑器初始化失败:', error);
|
||||||
console.error('错误详情:', error.stack);
|
console.error('错误详情:', error.stack);
|
||||||
|
|
@ -820,14 +1020,13 @@ const saveRichTextChanges = async () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
// ✅ 使用Vditor内置API获取内容
|
// ✅ 使用Vditor内置API获取内容
|
||||||
const markdownContent = vditorInstance ? vditorInstance.getValue() : editorContent.value;
|
const htmlContent = vditorInstance ? vditorInstance.getValue() : editorContent.value;
|
||||||
const htmlContent = vditorInstance ? vditorInstance.getHTML() : editorContent.value;
|
const markdownContent = vditorInstance ? vditorInstance.getMarkdown() : '';
|
||||||
const contentToSave = markdownContent.trim() || '';
|
|
||||||
|
|
||||||
console.log('📝 获取到的Markdown内容:', contentToSave.substring(0, 100) + '...');
|
|
||||||
console.log('📝 获取到的HTML内容:', htmlContent.substring(0, 100) + '...');
|
console.log('📝 获取到的HTML内容:', htmlContent.substring(0, 100) + '...');
|
||||||
|
console.log('📝 获取到的Markdown内容:', markdownContent.substring(0, 100) + '...');
|
||||||
|
|
||||||
// 更新节点数据
|
// 更新节点数据 - 使用新的节点结构
|
||||||
const titleChanged = editorTitle.value !== (currentNode.value.title || '');
|
const titleChanged = editorTitle.value !== (currentNode.value.title || '');
|
||||||
console.log('🔍 标题变化检查:', {
|
console.log('🔍 标题变化检查:', {
|
||||||
editorTitle: editorTitle.value,
|
editorTitle: editorTitle.value,
|
||||||
|
|
@ -835,10 +1034,11 @@ const saveRichTextChanges = async () => {
|
||||||
titleChanged: titleChanged
|
titleChanged: titleChanged
|
||||||
});
|
});
|
||||||
|
|
||||||
// 更新节点内容
|
// 更新节点内容 - 新的节点结构
|
||||||
currentNode.value.topic = contentToSave; // 保存原始Markdown内容
|
currentNode.value.dangerouslySetInnerHTML = htmlContent; // 保存HTML内容(用于渲染)
|
||||||
|
currentNode.value.markdown = markdownContent || htmlContent; // 保存Markdown内容(用于导出)
|
||||||
|
currentNode.value.topic = generateTopicFromMarkdown(markdownContent || htmlContent); // 生成简短标题
|
||||||
currentNode.value.title = editorTitle.value; // 更新标题
|
currentNode.value.title = editorTitle.value; // 更新标题
|
||||||
currentNode.value.dangerouslySetInnerHTML = htmlContent; // 保存渲染后的HTML
|
|
||||||
|
|
||||||
// 更新DOM元素
|
// 更新DOM元素
|
||||||
if (currentNodeElement.value) {
|
if (currentNodeElement.value) {
|
||||||
|
|
@ -864,7 +1064,9 @@ const saveRichTextChanges = async () => {
|
||||||
newTitle: editorTitle.value, // 使用更新后的标题
|
newTitle: editorTitle.value, // 使用更新后的标题
|
||||||
newDes: currentNode.value.data?.des || "",
|
newDes: currentNode.value.data?.des || "",
|
||||||
newParentId: currentNode.value.parentId || currentNode.value.parent?.id,
|
newParentId: currentNode.value.parentId || currentNode.value.parent?.id,
|
||||||
newDangerouslySetInnerHTML: htmlContent || "" // 保存富文本内容
|
newDangerouslySetInnerHTML: htmlContent || "", // 保存HTML内容(用于渲染)
|
||||||
|
newMarkdown: markdownContent || htmlContent, // 保存Markdown内容(用于导出)
|
||||||
|
newTopic: currentNode.value.topic // 保存生成的简短标题
|
||||||
};
|
};
|
||||||
|
|
||||||
console.log("🔍 直接发送到后端的更新数据:", updateData);
|
console.log("🔍 直接发送到后端的更新数据:", updateData);
|
||||||
|
|
@ -3656,24 +3858,18 @@ const openCustomEditModal = (nodeObj, nodeElement) => {
|
||||||
// 确保模态框完全渲染后再初始化Vditor编辑器
|
// 确保模态框完全渲染后再初始化Vditor编辑器
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
console.log('🔄 准备初始化Vditor,容器状态:', vditorContainer.value);
|
|
||||||
|
|
||||||
// 检查容器是否真正可见
|
// 检查容器是否真正可见
|
||||||
if (vditorContainer.value && vditorContainer.value.offsetParent !== null) {
|
if (vditorContainer.value && vditorContainer.value.offsetParent !== null) {
|
||||||
console.log('✅ 容器已可见,开始初始化Vditor');
|
|
||||||
initVditor();
|
initVditor();
|
||||||
} else {
|
} else {
|
||||||
console.warn('⚠️ 容器未准备好,延迟初始化');
|
|
||||||
// 多次尝试,确保容器准备好
|
// 多次尝试,确保容器准备好
|
||||||
let attempts = 0;
|
let attempts = 0;
|
||||||
const maxAttempts = 10;
|
const maxAttempts = 10;
|
||||||
const checkContainer = () => {
|
const checkContainer = () => {
|
||||||
attempts++;
|
attempts++;
|
||||||
if (vditorContainer.value && vditorContainer.value.offsetParent !== null) {
|
if (vditorContainer.value && vditorContainer.value.offsetParent !== null) {
|
||||||
console.log('✅ 容器在第', attempts, '次尝试后可见');
|
|
||||||
initVditor();
|
initVditor();
|
||||||
} else if (attempts < maxAttempts) {
|
} else if (attempts < maxAttempts) {
|
||||||
console.log('🔄 第', attempts, '次检查,容器仍未准备好,继续等待...');
|
|
||||||
setTimeout(checkContainer, 100);
|
setTimeout(checkContainer, 100);
|
||||||
} else {
|
} else {
|
||||||
console.error('❌ 容器在', maxAttempts, '次尝试后仍未准备好');
|
console.error('❌ 容器在', maxAttempts, '次尝试后仍未准备好');
|
||||||
|
|
@ -4785,6 +4981,7 @@ const updateMindMapRealtime = async (data, title, eventDetail = null) => {
|
||||||
infinite: true,
|
infinite: true,
|
||||||
maxScale: 5,
|
maxScale: 5,
|
||||||
minScale: 0.1,
|
minScale: 0.1,
|
||||||
|
theme: customTheme, // 使用自定义紫色主题
|
||||||
markdown: (text, nodeObj) => {
|
markdown: (text, nodeObj) => {
|
||||||
// 检查内容是否包含markdown语法(包括图片和数学公式)
|
// 检查内容是否包含markdown语法(包括图片和数学公式)
|
||||||
if (text.includes('|') || text.includes('**') || text.includes('`') || text.includes('#') || text.includes('$') || text.includes('![')) {
|
if (text.includes('|') || text.includes('**') || text.includes('`') || text.includes('#') || text.includes('$') || text.includes('![')) {
|
||||||
|
|
@ -4869,36 +5066,73 @@ const updateMindMapRealtime = async (data, title, eventDetail = null) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
// 完整更新:仅在必要时重新初始化
|
// 优化:避免不必要的完整更新,优先使用增量更新
|
||||||
console.log('🔄 执行完整更新');
|
console.log('🔄 尝试增量更新替代完整更新');
|
||||||
|
|
||||||
const currentId = String(currentMindmapId.value || '');
|
// 检查是否可以增量更新
|
||||||
const tempId = currentId && currentId.startsWith('temp-')
|
if (mindElixir.value && mindElixir.value.data && mindElixir.value.data.nodeData) {
|
||||||
? currentId
|
console.log('✅ 使用增量更新,避免页面空白');
|
||||||
: `temp-${Date.now()}`;
|
|
||||||
|
|
||||||
const mindElixirData = {
|
// 更新根节点标题
|
||||||
nodeData: data,
|
if (mindElixir.value.data.nodeData && data.topic) {
|
||||||
mindmapId: tempId,
|
const rootNode = Object.values(mindElixir.value.data.nodeData)[0];
|
||||||
id: tempId,
|
if (rootNode && rootNode.topic !== data.topic) {
|
||||||
title: title || 'AI生成中...'
|
rootNode.topic = data.topic;
|
||||||
};
|
// 更新DOM中的标题显示
|
||||||
|
const rootElement = mindmapEl.value.querySelector(`[data-id="${rootNode.id}"]`);
|
||||||
|
if (rootElement) {
|
||||||
|
const textElement = rootElement.querySelector('.topic-text');
|
||||||
|
if (textElement) {
|
||||||
|
textElement.textContent = data.topic;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 确保当前思维导图ID是临时ID
|
// 增量更新子节点
|
||||||
if (!currentId || !currentId.startsWith('temp-')) {
|
if (data.children && data.children.length > 0) {
|
||||||
currentMindmapId.value = tempId;
|
updateNodesIncremental(data.children, mindElixir.value.data.nodeData);
|
||||||
console.log('🆔 更新临时思维导图ID:', tempId);
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// 重新初始化数据
|
// 恢复位置,避免视图跳动
|
||||||
const result = mindElixir.value.init(mindElixirData);
|
if (currentPosition) {
|
||||||
|
setTimeout(() => {
|
||||||
|
restorePosition(currentPosition);
|
||||||
|
console.log('📍 优化增量更新后恢复位置');
|
||||||
|
}, 50);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 只有在确实无法增量更新时才执行完整更新
|
||||||
|
console.log('⚠️ 无法增量更新,执行完整更新');
|
||||||
|
|
||||||
// 恢复位置
|
const currentId = String(currentMindmapId.value || '');
|
||||||
if (currentPosition) {
|
const tempId = currentId && currentId.startsWith('temp-')
|
||||||
setTimeout(() => {
|
? currentId
|
||||||
restorePosition(currentPosition);
|
: `temp-${Date.now()}`;
|
||||||
console.log('📍 完整更新后恢复位置');
|
|
||||||
}, 100);
|
const mindElixirData = {
|
||||||
|
nodeData: data,
|
||||||
|
mindmapId: tempId,
|
||||||
|
id: tempId,
|
||||||
|
title: title || 'AI生成中...'
|
||||||
|
};
|
||||||
|
|
||||||
|
// 确保当前思维导图ID是临时ID
|
||||||
|
if (!currentId || !currentId.startsWith('temp-')) {
|
||||||
|
currentMindmapId.value = tempId;
|
||||||
|
console.log('🆔 更新临时思维导图ID:', tempId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重新初始化数据
|
||||||
|
const result = mindElixir.value.init(mindElixirData);
|
||||||
|
|
||||||
|
// 恢复位置
|
||||||
|
if (currentPosition) {
|
||||||
|
setTimeout(() => {
|
||||||
|
restorePosition(currentPosition);
|
||||||
|
console.log('📍 完整更新后恢复位置');
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -53,13 +53,10 @@ renderer.image = function(href, title, text) {
|
||||||
|
|
||||||
// 处理图片URL,确保能正确显示
|
// 处理图片URL,确保能正确显示
|
||||||
let processedUrl = hrefStr;
|
let processedUrl = hrefStr;
|
||||||
|
|
||||||
// 暂时禁用代理URL转换,直接使用原始URL
|
|
||||||
// 这样可以避免代理服务配置问题导致的图片显示异常
|
|
||||||
if (hrefStr.includes('cdn-mineru.openxlab.org.cn')) {
|
if (hrefStr.includes('cdn-mineru.openxlab.org.cn')) {
|
||||||
// 直接使用原始URL,不进行代理转换
|
// 将外部CDN URL转换为代理URL
|
||||||
processedUrl = hrefStr;
|
const urlPath = hrefStr.replace('https://cdn-mineru.openxlab.org.cn', '');
|
||||||
console.log('🖼️ 使用原始CDN URL:', processedUrl);
|
processedUrl = `/proxy-image${urlPath}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 生成图片HTML
|
// 生成图片HTML
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue