-
-
+
+
+
+
-
-
@@ -14,8 +29,15 @@
import { ref } from 'vue';
import MindMap from "./components/MindMap.vue";
import AISidebar from "./components/AISidebar.vue";
+import MarkdownTest from "./components/MarkdownTest.vue";
const mindMapRef = ref(null);
+const isTestMode = ref(false);
+
+// 切换测试模式
+const toggleTestMode = () => {
+ isTestMode.value = !isTestMode.value;
+};
// 处理开始实时生成事件
const handleStartRealtimeGeneration = () => {
@@ -65,4 +87,36 @@ body {
height: 100%;
width: 100%;
}
+
+/* 测试模式样式 */
+.test-mode-toggle {
+ position: fixed;
+ top: 20px;
+ right: 20px;
+ z-index: 1000;
+}
+
+.test-btn {
+ padding: 10px 20px;
+ background: #007bff;
+ color: white;
+ border: none;
+ border-radius: 6px;
+ cursor: pointer;
+ font-size: 14px;
+ font-weight: 500;
+ box-shadow: 0 2px 8px rgba(0, 123, 255, 0.3);
+ transition: all 0.2s ease;
+}
+
+.test-btn:hover {
+ background: #0056b3;
+ transform: translateY(-1px);
+ box-shadow: 0 4px 12px rgba(0, 123, 255, 0.4);
+}
+
+.test-mode {
+ height: 100vh;
+ overflow: auto;
+}
diff --git a/frontend/src/components/MarkdownTest.vue b/frontend/src/components/MarkdownTest.vue
new file mode 100644
index 0000000..8e8d78c
--- /dev/null
+++ b/frontend/src/components/MarkdownTest.vue
@@ -0,0 +1,228 @@
+
+
+
Markdown渲染测试
+
+
+
输入Markdown内容
+
+
+
+
+
+
+
测试用例
+
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/src/components/MindMap.vue b/frontend/src/components/MindMap.vue
index 22891ac..2c1012b 100644
--- a/frontend/src/components/MindMap.vue
+++ b/frontend/src/components/MindMap.vue
@@ -171,6 +171,11 @@
import { ref, onMounted, onUnmounted, nextTick } from 'vue';
import MindElixir from 'mind-elixir';
import { mindmapAPI } from '../api/mindmap.js';
+import {
+ smartRenderNodeContent,
+ hasMarkdownSyntax,
+ renderMarkdownToHTML
+} from '../utils/markdownRenderer.js';
// 响应式数据
const mindmapEl = ref(null);
@@ -2717,6 +2722,77 @@ const savePreviewToDatabase = async (data, title) => {
try {
// 开始保存预览数据到数据库
+ // 检查是否已经有实时渲染的思维导图
+ const currentId = String(currentMindmapId.value || '');
+ const hasRealtimeMindmap = mindElixir.value && currentId && currentId.startsWith('temp-');
+
+ if (hasRealtimeMindmap) {
+ console.log("🔄 检测到实时渲染的思维导图,将保存到数据库并更新ID");
+
+ // 保存当前位置和缩放状态
+ const currentPosition = saveCurrentPosition();
+ console.log("📍 保存当前位置:", currentPosition);
+
+ // 创建新的思维导图
+ const response = await mindmapAPI.createMindmap(title || "预览思维导图", data);
+ console.log("🔄 创建思维导图响应:", response);
+
+ if (response.data && response.data.id) {
+ const newMindmapId = response.data.id;
+ console.log("🎉 创建思维导图成功,新思维导图的ID是:", newMindmapId);
+
+ // 更新当前思维导图ID,但保持视图状态
+ const oldMindmapId = currentMindmapId.value;
+ currentMindmapId.value = newMindmapId;
+
+ // 更新MindElixir数据中的ID
+ if (mindElixir.value && mindElixir.value.data) {
+ mindElixir.value.data.mindmapId = newMindmapId;
+ mindElixir.value.data.id = newMindmapId;
+
+ // 更新所有节点的mindmapId
+ const updateNodeIds = (node) => {
+ if (node) {
+ node.mindmapId = newMindmapId;
+ if (node.children) {
+ node.children.forEach(child => updateNodeIds(child));
+ }
+ }
+ };
+
+ if (mindElixir.value.data.nodeData) {
+ updateNodeIds(mindElixir.value.data.nodeData);
+ }
+ }
+
+ console.log("✅ 已更新思维导图ID,保持视图状态");
+ console.log("🔄 从临时ID", oldMindmapId, "更新为正式ID", newMindmapId);
+
+ // 恢复位置和缩放状态
+ if (currentPosition) {
+ setTimeout(() => {
+ restorePosition(currentPosition);
+ console.log("📍 已恢复位置和缩放状态");
+ }, 100);
+ }
+
+ // 通知AISidebar组件更新历史记录
+ window.dispatchEvent(new CustomEvent('mindmap-saved', {
+ detail: {
+ mindmapId: newMindmapId,
+ title: title,
+ timestamp: Date.now(),
+ fromRealtime: true // 标记来自实时渲染
+ }
+ }));
+
+ return;
+ }
+ }
+
+ // 如果没有实时渲染的思维导图,使用原有逻辑
+ console.log("🔄 没有检测到实时渲染的思维导图,使用标准保存流程");
+
// 创建新的思维导图
const response = await mindmapAPI.createMindmap(title || "预览思维导图", data);
console.log("🔄 创建思维导图响应:", response);
@@ -3148,12 +3224,18 @@ const updateMindMapRealtime = async (data, title) => {
});
// 🔧 修复:将数据包装成MindElixir期望的格式
+ const tempId = `temp-${Date.now()}`;
const mindElixirData = {
nodeData: data, // 将节点数据放在nodeData字段中
- mindmapId: `temp-${Date.now()}`, // 临时ID
+ mindmapId: tempId, // 临时ID
+ id: tempId, // 同时设置id字段
title: title || 'AI生成中...'
};
+ // 设置当前思维导图ID为临时ID
+ currentMindmapId.value = tempId;
+ console.log('🆔 设置临时思维导图ID:', tempId);
+
// 初始化数据
const result = mindElixir.value.init(mindElixirData);
console.log('✅ 实时思维导图实例创建成功');
@@ -3176,12 +3258,24 @@ const updateMindMapRealtime = async (data, title) => {
console.log(' 当前位置已保存:', currentPosition);
// 🔧 修复:将数据包装成MindElixir期望的格式
+ const currentId = String(currentMindmapId.value || '');
+ const tempId = currentId && currentId.startsWith('temp-')
+ ? currentId
+ : `temp-${Date.now()}`;
+
const mindElixirData = {
nodeData: data, // 将节点数据放在nodeData字段中
- mindmapId: currentMindmapId.value || `temp-${Date.now()}`,
+ 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);
console.log('✅ 思维导图数据更新成功');
diff --git a/frontend/src/utils/markdownRenderer.js b/frontend/src/utils/markdownRenderer.js
new file mode 100644
index 0000000..fd664f7
--- /dev/null
+++ b/frontend/src/utils/markdownRenderer.js
@@ -0,0 +1,303 @@
+/**
+ * Markdown节点渲染器
+ * 为Mind Elixir节点提供markdown内容渲染能力
+ * 支持表格、数学公式、代码块等
+ */
+
+import { marked } from 'marked';
+
+// 配置marked选项
+marked.setOptions({
+ breaks: true,
+ gfm: true, // GitHub Flavored Markdown
+ tables: true, // 支持表格
+ sanitize: false, // 允许HTML(用于数学公式等)
+});
+
+/**
+ * 渲染markdown内容为HTML
+ * @param {string} markdown - markdown文本
+ * @returns {string} 渲染后的HTML
+ */
+export const renderMarkdownToHTML = (markdown) => {
+ if (!markdown || typeof markdown !== 'string') {
+ return '';
+ }
+
+ try {
+ // 预处理markdown
+ const processedMarkdown = preprocessMarkdown(markdown);
+
+ // 使用marked渲染
+ const html = marked.parse(processedMarkdown);
+
+ // 后处理HTML
+ return postprocessHTML(html);
+ } catch (error) {
+ console.error('Markdown渲染失败:', error);
+ return `
渲染失败: ${error.message}
`;
+ }
+};
+
+/**
+ * 预处理markdown内容
+ * @param {string} markdown - 原始markdown
+ * @returns {string} 处理后的markdown
+ */
+const preprocessMarkdown = (markdown) => {
+ return markdown
+ // 确保表格前后有空行
+ .replace(/([^\n])\n(\|.*\|)/g, '$1\n\n$2')
+ .replace(/(\|.*\|)\n([^\n|])/g, '$1\n\n$2')
+ // 处理数学公式(如果需要的话)
+ .replace(/\$\$(.*?)\$\$/g, '
$$$1$$
')
+ .replace(/\$(.*?)\$/g, '
$$1$');
+};
+
+/**
+ * 后处理HTML内容
+ * @param {string} html - 渲染后的HTML
+ * @returns {string} 处理后的HTML
+ */
+const postprocessHTML = (html) => {
+ return html
+ // 为表格添加样式类
+ .replace(/
/g, '')
+ // 为代码块添加样式类
+ .replace(//g, '')
+ .replace(//g, '');
+};
+
+/**
+ * 为Mind Elixir节点设置markdown内容
+ * @param {HTMLElement} nodeElement - 节点DOM元素
+ * @param {string} markdownContent - markdown内容
+ */
+export const setNodeMarkdownContent = (nodeElement, markdownContent) => {
+ if (!nodeElement || !markdownContent) return;
+
+ // 查找或创建内容容器
+ let contentContainer = nodeElement.querySelector('.markdown-content');
+ if (!contentContainer) {
+ contentContainer = document.createElement('div');
+ contentContainer.className = 'markdown-content';
+
+ // 将内容容器添加到节点中
+ const topicText = nodeElement.querySelector('.topic-text');
+ if (topicText) {
+ topicText.appendChild(contentContainer);
+ } else {
+ nodeElement.appendChild(contentContainer);
+ }
+ }
+
+ // 渲染markdown内容
+ const html = renderMarkdownToHTML(markdownContent);
+ contentContainer.innerHTML = html;
+
+ // 添加样式
+ addMarkdownStyles(contentContainer);
+};
+
+/**
+ * 添加markdown样式
+ * @param {HTMLElement} container - 容器元素
+ */
+const addMarkdownStyles = (container) => {
+ // 检查是否已经添加过样式
+ if (document.getElementById('markdown-node-styles')) return;
+
+ const style = document.createElement('style');
+ style.id = 'markdown-node-styles';
+ style.textContent = `
+ .markdown-content {
+ max-width: 100%;
+ overflow: hidden;
+ }
+
+ .markdown-content h1,
+ .markdown-content h2,
+ .markdown-content h3,
+ .markdown-content h4,
+ .markdown-content h5,
+ .markdown-content h6 {
+ margin: 4px 0 2px 0;
+ font-weight: 600;
+ color: #333;
+ }
+
+ .markdown-content h1 { font-size: 16px; }
+ .markdown-content h2 { font-size: 15px; }
+ .markdown-content h3 { font-size: 14px; }
+ .markdown-content h4 { font-size: 13px; }
+ .markdown-content h5 { font-size: 12px; }
+ .markdown-content h6 { font-size: 11px; }
+
+ .markdown-content p {
+ margin: 2px 0;
+ line-height: 1.3;
+ color: #666;
+ }
+
+ .markdown-content ul,
+ .markdown-content ol {
+ margin: 2px 0;
+ padding-left: 16px;
+ }
+
+ .markdown-content li {
+ margin: 1px 0;
+ line-height: 1.3;
+ color: #666;
+ }
+
+ .markdown-content strong,
+ .markdown-content b {
+ font-weight: 600;
+ color: #333;
+ }
+
+ .markdown-content em,
+ .markdown-content i {
+ font-style: italic;
+ color: #555;
+ }
+
+ .markdown-content code {
+ background: #f5f5f5;
+ padding: 1px 4px;
+ border-radius: 3px;
+ font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
+ font-size: 11px;
+ color: #d63384;
+ }
+
+ .markdown-content pre {
+ background: #f8f9fa;
+ border: 1px solid #e9ecef;
+ border-radius: 4px;
+ padding: 8px;
+ margin: 4px 0;
+ overflow-x: auto;
+ }
+
+ .markdown-content pre code {
+ background: none;
+ padding: 0;
+ color: #333;
+ font-size: 11px;
+ }
+
+ .markdown-table {
+ border-collapse: collapse;
+ width: 100%;
+ margin: 4px 0;
+ font-size: 11px;
+ }
+
+ .markdown-table th,
+ .markdown-table td {
+ border: 1px solid #ddd;
+ padding: 4px 6px;
+ text-align: left;
+ }
+
+ .markdown-table th {
+ background-color: #f8f9fa;
+ font-weight: 600;
+ color: #333;
+ }
+
+ .markdown-table tr:nth-child(even) {
+ background-color: #f8f9fa;
+ }
+
+ .markdown-content a {
+ color: #007bff;
+ text-decoration: none;
+ }
+
+ .markdown-content a:hover {
+ text-decoration: underline;
+ }
+
+ .markdown-content blockquote {
+ border-left: 3px solid #ddd;
+ margin: 4px 0;
+ padding-left: 8px;
+ color: #666;
+ font-style: italic;
+ }
+
+ .markdown-math {
+ font-family: 'Times New Roman', serif;
+ }
+
+ .markdown-error {
+ color: #dc3545;
+ background: #f8d7da;
+ border: 1px solid #f5c6cb;
+ padding: 4px 8px;
+ border-radius: 4px;
+ font-size: 11px;
+ }
+ `;
+
+ document.head.appendChild(style);
+};
+
+/**
+ * 检查内容是否包含markdown语法
+ * @param {string} content - 内容文本
+ * @returns {boolean} 是否包含markdown语法
+ */
+export const hasMarkdownSyntax = (content) => {
+ if (!content || typeof content !== 'string') return false;
+
+ // 检查常见的markdown语法
+ const markdownPatterns = [
+ /#{1,6}\s+/, // 标题
+ /\*\*.*?\*\*/, // 粗体
+ /\*.*?\*/, // 斜体
+ /`.*?`/, // 行内代码
+ /```[\s\S]*?```/, // 代码块
+ /\|.*\|/, // 表格
+ /^\s*[-*+]\s+/m, // 列表
+ /^\s*\d+\.\s+/m, // 有序列表
+ /\[.*?\]\(.*?\)/, // 链接
+ /!\[.*?\]\(.*?\)/, // 图片
+ ];
+
+ return markdownPatterns.some(pattern => pattern.test(content));
+};
+
+/**
+ * 智能渲染节点内容
+ * 根据内容类型选择渲染方式
+ * @param {HTMLElement} nodeElement - 节点DOM元素
+ * @param {string} content - 节点内容
+ */
+export const smartRenderNodeContent = (nodeElement, content) => {
+ if (!nodeElement || !content) return;
+
+ // 检查是否包含markdown语法
+ if (hasMarkdownSyntax(content)) {
+ // 使用markdown渲染
+ setNodeMarkdownContent(nodeElement, content);
+ } else {
+ // 使用普通文本渲染
+ const topicText = nodeElement.querySelector('.topic-text');
+ if (topicText) {
+ topicText.textContent = content;
+ }
+ }
+};
+
+export default {
+ renderMarkdownToHTML,
+ setNodeMarkdownContent,
+ hasMarkdownSyntax,
+ smartRenderNodeContent
+};