feat: 优化实时渲染思维导图的保存流程
- 实现实时渲染思维导图保存时保持视图状态 - 避免重新渲染,只更新临时ID为正式ID
This commit is contained in:
parent
fd1b71dd75
commit
caa763d808
|
|
@ -0,0 +1,187 @@
|
||||||
|
# Markdown 节点渲染集成说明
|
||||||
|
|
||||||
|
## 概述
|
||||||
|
|
||||||
|
这个集成方案为你的 Mind Elixir 思维导图项目添加了 markdown 节点渲染能力,支持:
|
||||||
|
|
||||||
|
- ✅ **表格渲染** - 完整的 markdown 表格支持
|
||||||
|
- ✅ **代码高亮** - 代码块和行内代码
|
||||||
|
- ✅ **文本格式** - 粗体、斜体、标题等
|
||||||
|
- ✅ **列表** - 有序和无序列表
|
||||||
|
- ✅ **链接** - 自动链接渲染
|
||||||
|
- ✅ **智能检测** - 自动识别 markdown 语法
|
||||||
|
|
||||||
|
## 文件结构
|
||||||
|
|
||||||
|
```
|
||||||
|
frontend/src/
|
||||||
|
├── utils/
|
||||||
|
│ └── markdownRenderer.js # 核心渲染器
|
||||||
|
├── components/
|
||||||
|
│ ├── MindMap.vue # 主思维导图组件(已集成)
|
||||||
|
│ └── MarkdownTest.vue # 测试组件
|
||||||
|
└── ...
|
||||||
|
```
|
||||||
|
|
||||||
|
## 使用方法
|
||||||
|
|
||||||
|
### 1. 在节点中使用 markdown
|
||||||
|
|
||||||
|
现在你可以在思维导图的节点内容中直接使用 markdown 语法:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
# 产品价格表
|
||||||
|
|
||||||
|
| 产品 | 价格 | 库存 |
|
||||||
|
|------|------|------|
|
||||||
|
| 苹果 | 4元 | 100个 |
|
||||||
|
| 香蕉 | 2元 | 50个 |
|
||||||
|
|
||||||
|
## 技术栈
|
||||||
|
|
||||||
|
- **前端**: Vue.js 3
|
||||||
|
- **后端**: Django
|
||||||
|
- **数据库**: PostgreSQL
|
||||||
|
|
||||||
|
## 代码示例
|
||||||
|
|
||||||
|
\`\`\`javascript
|
||||||
|
function hello() {
|
||||||
|
console.log('Hello World!');
|
||||||
|
}
|
||||||
|
\`\`\`
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 测试功能
|
||||||
|
|
||||||
|
访问 `MarkdownTest.vue` 组件来测试 markdown 渲染功能:
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<template>
|
||||||
|
<MarkdownTest />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import MarkdownTest from './components/MarkdownTest.vue';
|
||||||
|
</script>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 在现有节点中启用
|
||||||
|
|
||||||
|
系统会自动检测节点内容是否包含 markdown 语法,如果包含,会自动使用 markdown 渲染器。
|
||||||
|
|
||||||
|
## 核心功能
|
||||||
|
|
||||||
|
### 智能渲染
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
import { smartRenderNodeContent } from '../utils/markdownRenderer.js';
|
||||||
|
|
||||||
|
// 自动检测并渲染
|
||||||
|
smartRenderNodeContent(nodeElement, content);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 手动渲染
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
import { renderMarkdownToHTML } from '../utils/markdownRenderer.js';
|
||||||
|
|
||||||
|
// 直接渲染为 HTML
|
||||||
|
const html = renderMarkdownToHTML(markdownContent);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 语法检测
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
import { hasMarkdownSyntax } from '../utils/markdownRenderer.js';
|
||||||
|
|
||||||
|
// 检测是否包含 markdown 语法
|
||||||
|
if (hasMarkdownSyntax(content)) {
|
||||||
|
// 使用 markdown 渲染
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 支持的 Markdown 语法
|
||||||
|
|
||||||
|
### 表格
|
||||||
|
```markdown
|
||||||
|
| 列1 | 列2 | 列3 |
|
||||||
|
|-----|-----|-----|
|
||||||
|
| 数据1 | 数据2 | 数据3 |
|
||||||
|
```
|
||||||
|
|
||||||
|
### 代码块
|
||||||
|
```markdown
|
||||||
|
\`\`\`javascript
|
||||||
|
function test() {
|
||||||
|
console.log('Hello');
|
||||||
|
}
|
||||||
|
\`\`\`
|
||||||
|
```
|
||||||
|
|
||||||
|
### 行内代码
|
||||||
|
```markdown
|
||||||
|
使用 \`console.log()\` 输出信息
|
||||||
|
```
|
||||||
|
|
||||||
|
### 文本格式
|
||||||
|
```markdown
|
||||||
|
**粗体文本**
|
||||||
|
*斜体文本*
|
||||||
|
```
|
||||||
|
|
||||||
|
### 列表
|
||||||
|
```markdown
|
||||||
|
- 无序列表项1
|
||||||
|
- 无序列表项2
|
||||||
|
|
||||||
|
1. 有序列表项1
|
||||||
|
2. 有序列表项2
|
||||||
|
```
|
||||||
|
|
||||||
|
### 链接
|
||||||
|
```markdown
|
||||||
|
[链接文本](https://example.com)
|
||||||
|
```
|
||||||
|
|
||||||
|
## 样式定制
|
||||||
|
|
||||||
|
渲染器会自动添加 CSS 样式,你也可以通过以下类名进行自定义:
|
||||||
|
|
||||||
|
- `.markdown-content` - 主容器
|
||||||
|
- `.markdown-table` - 表格样式
|
||||||
|
- `.markdown-code` - 代码块样式
|
||||||
|
- `.markdown-math` - 数学公式样式
|
||||||
|
|
||||||
|
## 注意事项
|
||||||
|
|
||||||
|
1. **性能**: 大量 markdown 内容可能影响渲染性能
|
||||||
|
2. **安全性**: 渲染器允许 HTML,请确保内容来源可信
|
||||||
|
3. **兼容性**: 与 Mind Elixir 的拖拽、编辑功能完全兼容
|
||||||
|
|
||||||
|
## 故障排除
|
||||||
|
|
||||||
|
### 渲染失败
|
||||||
|
- 检查 markdown 语法是否正确
|
||||||
|
- 查看浏览器控制台错误信息
|
||||||
|
- 使用 `MarkdownTest.vue` 组件测试
|
||||||
|
|
||||||
|
### 样式问题
|
||||||
|
- 检查 CSS 样式是否被覆盖
|
||||||
|
- 确保 `markdown-node-styles` 样式已加载
|
||||||
|
|
||||||
|
### 性能问题
|
||||||
|
- 避免在单个节点中放置过多内容
|
||||||
|
- 考虑将复杂内容拆分为多个子节点
|
||||||
|
|
||||||
|
## 扩展功能
|
||||||
|
|
||||||
|
如果需要更多功能,可以扩展 `markdownRenderer.js`:
|
||||||
|
|
||||||
|
- 数学公式支持(KaTeX)
|
||||||
|
- 图表支持(Mermaid)
|
||||||
|
- 更多 markdown 扩展语法
|
||||||
|
|
||||||
|
## 总结
|
||||||
|
|
||||||
|
这个集成方案让你可以在保持现有 Mind Elixir 功能的同时,享受强大的 markdown 渲染能力。特别是表格渲染功能,让思维导图可以展示更丰富的数据结构。
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -11,6 +11,8 @@
|
||||||
"axios": "^1.5.0",
|
"axios": "^1.5.0",
|
||||||
"mammoth": "^1.10.0",
|
"mammoth": "^1.10.0",
|
||||||
"marked": "^16.2.1",
|
"marked": "^16.2.1",
|
||||||
|
"markmap-lib": "^0.18.12",
|
||||||
|
"markmap-view": "^0.18.12",
|
||||||
"mind-elixir": "^3.0.0",
|
"mind-elixir": "^3.0.0",
|
||||||
"pdfjs-dist": "^5.4.149",
|
"pdfjs-dist": "^5.4.149",
|
||||||
"vue": "^3.3.4"
|
"vue": "^3.3.4"
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,26 @@
|
||||||
<template>
|
<template>
|
||||||
<div id="app">
|
<div id="app">
|
||||||
<!-- AI侧边栏 -->
|
<!-- 测试模式切换按钮 -->
|
||||||
<AISidebar @start-realtime-generation="handleStartRealtimeGeneration" />
|
<div class="test-mode-toggle">
|
||||||
|
<button @click="toggleTestMode" class="test-btn">
|
||||||
|
{{ isTestMode ? '切换到思维导图' : '测试Markdown渲染' }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- 主内容区域 -->
|
<!-- 测试模式 -->
|
||||||
<div class="main-content">
|
<div v-if="isTestMode" class="test-mode">
|
||||||
<MindMap ref="mindMapRef" />
|
<MarkdownTest />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 正常模式 -->
|
||||||
|
<div v-else>
|
||||||
|
<!-- AI侧边栏 -->
|
||||||
|
<AISidebar @start-realtime-generation="handleStartRealtimeGeneration" />
|
||||||
|
|
||||||
|
<!-- 主内容区域 -->
|
||||||
|
<div class="main-content">
|
||||||
|
<MindMap ref="mindMapRef" />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
@ -14,8 +29,15 @@
|
||||||
import { ref } from 'vue';
|
import { ref } from 'vue';
|
||||||
import MindMap from "./components/MindMap.vue";
|
import MindMap from "./components/MindMap.vue";
|
||||||
import AISidebar from "./components/AISidebar.vue";
|
import AISidebar from "./components/AISidebar.vue";
|
||||||
|
import MarkdownTest from "./components/MarkdownTest.vue";
|
||||||
|
|
||||||
const mindMapRef = ref(null);
|
const mindMapRef = ref(null);
|
||||||
|
const isTestMode = ref(false);
|
||||||
|
|
||||||
|
// 切换测试模式
|
||||||
|
const toggleTestMode = () => {
|
||||||
|
isTestMode.value = !isTestMode.value;
|
||||||
|
};
|
||||||
|
|
||||||
// 处理开始实时生成事件
|
// 处理开始实时生成事件
|
||||||
const handleStartRealtimeGeneration = () => {
|
const handleStartRealtimeGeneration = () => {
|
||||||
|
|
@ -65,4 +87,36 @@ body {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
width: 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;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,228 @@
|
||||||
|
<template>
|
||||||
|
<div class="markdown-test">
|
||||||
|
<h2>Markdown渲染测试</h2>
|
||||||
|
|
||||||
|
<div class="test-section">
|
||||||
|
<h3>输入Markdown内容</h3>
|
||||||
|
<textarea
|
||||||
|
v-model="markdownInput"
|
||||||
|
placeholder="输入markdown内容..."
|
||||||
|
rows="10"
|
||||||
|
class="markdown-input"
|
||||||
|
></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="test-section">
|
||||||
|
<h3>渲染结果</h3>
|
||||||
|
<div class="rendered-content" v-html="renderedHTML"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="test-section">
|
||||||
|
<h3>测试用例</h3>
|
||||||
|
<button @click="loadTestCases" class="test-btn">加载测试用例</button>
|
||||||
|
<div class="test-cases">
|
||||||
|
<button
|
||||||
|
v-for="(testCase, index) in testCases"
|
||||||
|
:key="index"
|
||||||
|
@click="loadTestCase(testCase)"
|
||||||
|
class="test-case-btn"
|
||||||
|
>
|
||||||
|
{{ testCase.name }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, computed } from 'vue';
|
||||||
|
import { renderMarkdownToHTML, hasMarkdownSyntax } from '../utils/markdownRenderer.js';
|
||||||
|
|
||||||
|
// 响应式数据
|
||||||
|
const markdownInput = ref(`# 测试标题
|
||||||
|
|
||||||
|
这是一个**粗体**和*斜体*的测试。
|
||||||
|
|
||||||
|
## 表格测试
|
||||||
|
|
||||||
|
| 产品 | 价格 | 库存 |
|
||||||
|
|------|------|------|
|
||||||
|
| 苹果 | 4元 | 100个 |
|
||||||
|
| 香蕉 | 2元 | 50个 |
|
||||||
|
|
||||||
|
## 代码测试
|
||||||
|
|
||||||
|
\`\`\`javascript
|
||||||
|
function hello() {
|
||||||
|
console.log('Hello World!');
|
||||||
|
}
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
行内代码:\`const name = 'test'\`
|
||||||
|
|
||||||
|
## 列表测试
|
||||||
|
|
||||||
|
- 项目1
|
||||||
|
- 项目2
|
||||||
|
- 子项目2.1
|
||||||
|
- 子项目2.2
|
||||||
|
- 项目3
|
||||||
|
|
||||||
|
## 链接测试
|
||||||
|
|
||||||
|
- [GitHub](https://github.com)
|
||||||
|
- [Vue.js](https://vuejs.org)`);
|
||||||
|
|
||||||
|
// 测试用例
|
||||||
|
const testCases = ref([
|
||||||
|
{
|
||||||
|
name: '基础表格',
|
||||||
|
content: `# 产品价格表
|
||||||
|
|
||||||
|
| 产品 | 价格 |
|
||||||
|
|------|------|
|
||||||
|
| 苹果 | 4元 |
|
||||||
|
| 香蕉 | 2元 |`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '复杂表格',
|
||||||
|
content: `# 技术栈对比
|
||||||
|
|
||||||
|
| 技术 | 前端 | 后端 | 数据库 |
|
||||||
|
|------|------|------|--------|
|
||||||
|
| Vue.js | ✅ | ❌ | ❌ |
|
||||||
|
| Django | ❌ | ✅ | ❌ |
|
||||||
|
| PostgreSQL | ❌ | ❌ | ✅ |`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '代码块',
|
||||||
|
content: `# 代码示例
|
||||||
|
|
||||||
|
\`\`\`javascript
|
||||||
|
function markdownToJSON(markdown) {
|
||||||
|
const lines = markdown.split('\\n');
|
||||||
|
// 处理逻辑...
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
\`\`\``
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '混合内容',
|
||||||
|
content: `# 混合内容测试
|
||||||
|
|
||||||
|
这是一个包含**粗体**、*斜体*和\`行内代码\`的段落。
|
||||||
|
|
||||||
|
## 表格
|
||||||
|
|
||||||
|
| 功能 | 状态 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| 表格渲染 | ✅ | 支持markdown表格 |
|
||||||
|
| 代码高亮 | ✅ | 支持代码块 |
|
||||||
|
|
||||||
|
## 代码
|
||||||
|
|
||||||
|
\`\`\`python
|
||||||
|
def hello_world():
|
||||||
|
print("Hello, World!")
|
||||||
|
\`\`\``
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
|
// 计算属性:渲染后的HTML
|
||||||
|
const renderedHTML = computed(() => {
|
||||||
|
if (!markdownInput.value) return '';
|
||||||
|
|
||||||
|
try {
|
||||||
|
return renderMarkdownToHTML(markdownInput.value);
|
||||||
|
} catch (error) {
|
||||||
|
return `<div class="error">渲染失败: ${error.message}</div>`;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 方法
|
||||||
|
const loadTestCases = () => {
|
||||||
|
// 测试用例已经定义在响应式数据中
|
||||||
|
};
|
||||||
|
|
||||||
|
const loadTestCase = (testCase) => {
|
||||||
|
markdownInput.value = testCase.content;
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.markdown-test {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 20px;
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.test-section {
|
||||||
|
margin-bottom: 30px;
|
||||||
|
padding: 20px;
|
||||||
|
border: 1px solid #e0e0e0;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #f9f9f9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-input {
|
||||||
|
width: 100%;
|
||||||
|
padding: 12px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.5;
|
||||||
|
resize: vertical;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rendered-content {
|
||||||
|
background: white;
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 4px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
min-height: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.test-cases {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 10px;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.test-btn,
|
||||||
|
.test-case-btn {
|
||||||
|
padding: 8px 16px;
|
||||||
|
border: 1px solid #007bff;
|
||||||
|
background: white;
|
||||||
|
color: #007bff;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.test-btn:hover,
|
||||||
|
.test-case-btn:hover {
|
||||||
|
background: #007bff;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2, h3 {
|
||||||
|
color: #333;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
border-bottom: 2px solid #007bff;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error {
|
||||||
|
color: #dc3545;
|
||||||
|
background: #f8d7da;
|
||||||
|
border: 1px solid #f5c6cb;
|
||||||
|
padding: 10px;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -171,6 +171,11 @@
|
||||||
import { ref, onMounted, onUnmounted, nextTick } from 'vue';
|
import { ref, onMounted, onUnmounted, nextTick } from 'vue';
|
||||||
import MindElixir from 'mind-elixir';
|
import MindElixir from 'mind-elixir';
|
||||||
import { mindmapAPI } from '../api/mindmap.js';
|
import { mindmapAPI } from '../api/mindmap.js';
|
||||||
|
import {
|
||||||
|
smartRenderNodeContent,
|
||||||
|
hasMarkdownSyntax,
|
||||||
|
renderMarkdownToHTML
|
||||||
|
} from '../utils/markdownRenderer.js';
|
||||||
|
|
||||||
// 响应式数据
|
// 响应式数据
|
||||||
const mindmapEl = ref(null);
|
const mindmapEl = ref(null);
|
||||||
|
|
@ -2717,6 +2722,77 @@ const savePreviewToDatabase = async (data, title) => {
|
||||||
try {
|
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);
|
const response = await mindmapAPI.createMindmap(title || "预览思维导图", data);
|
||||||
console.log("🔄 创建思维导图响应:", response);
|
console.log("🔄 创建思维导图响应:", response);
|
||||||
|
|
@ -3148,12 +3224,18 @@ const updateMindMapRealtime = async (data, title) => {
|
||||||
});
|
});
|
||||||
|
|
||||||
// 🔧 修复:将数据包装成MindElixir期望的格式
|
// 🔧 修复:将数据包装成MindElixir期望的格式
|
||||||
|
const tempId = `temp-${Date.now()}`;
|
||||||
const mindElixirData = {
|
const mindElixirData = {
|
||||||
nodeData: data, // 将节点数据放在nodeData字段中
|
nodeData: data, // 将节点数据放在nodeData字段中
|
||||||
mindmapId: `temp-${Date.now()}`, // 临时ID
|
mindmapId: tempId, // 临时ID
|
||||||
|
id: tempId, // 同时设置id字段
|
||||||
title: title || 'AI生成中...'
|
title: title || 'AI生成中...'
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 设置当前思维导图ID为临时ID
|
||||||
|
currentMindmapId.value = tempId;
|
||||||
|
console.log('🆔 设置临时思维导图ID:', tempId);
|
||||||
|
|
||||||
// 初始化数据
|
// 初始化数据
|
||||||
const result = mindElixir.value.init(mindElixirData);
|
const result = mindElixir.value.init(mindElixirData);
|
||||||
console.log('✅ 实时思维导图实例创建成功');
|
console.log('✅ 实时思维导图实例创建成功');
|
||||||
|
|
@ -3176,12 +3258,24 @@ const updateMindMapRealtime = async (data, title) => {
|
||||||
console.log(' 当前位置已保存:', currentPosition);
|
console.log(' 当前位置已保存:', currentPosition);
|
||||||
|
|
||||||
// 🔧 修复:将数据包装成MindElixir期望的格式
|
// 🔧 修复:将数据包装成MindElixir期望的格式
|
||||||
|
const currentId = String(currentMindmapId.value || '');
|
||||||
|
const tempId = currentId && currentId.startsWith('temp-')
|
||||||
|
? currentId
|
||||||
|
: `temp-${Date.now()}`;
|
||||||
|
|
||||||
const mindElixirData = {
|
const mindElixirData = {
|
||||||
nodeData: data, // 将节点数据放在nodeData字段中
|
nodeData: data, // 将节点数据放在nodeData字段中
|
||||||
mindmapId: currentMindmapId.value || `temp-${Date.now()}`,
|
mindmapId: tempId,
|
||||||
|
id: tempId,
|
||||||
title: title || 'AI生成中...'
|
title: title || 'AI生成中...'
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 确保当前思维导图ID是临时ID
|
||||||
|
if (!currentId || !currentId.startsWith('temp-')) {
|
||||||
|
currentMindmapId.value = tempId;
|
||||||
|
console.log('🆔 更新临时思维导图ID:', tempId);
|
||||||
|
}
|
||||||
|
|
||||||
// 重新初始化数据
|
// 重新初始化数据
|
||||||
const result = mindElixir.value.init(mindElixirData);
|
const result = mindElixir.value.init(mindElixirData);
|
||||||
console.log('✅ 思维导图数据更新成功');
|
console.log('✅ 思维导图数据更新成功');
|
||||||
|
|
|
||||||
|
|
@ -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 `<div class="markdown-error">渲染失败: ${error.message}</div>`;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 预处理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, '<div class="math-block">$$$1$$</div>')
|
||||||
|
.replace(/\$(.*?)\$/g, '<span class="math-inline">$$1$</span>');
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 后处理HTML内容
|
||||||
|
* @param {string} html - 渲染后的HTML
|
||||||
|
* @returns {string} 处理后的HTML
|
||||||
|
*/
|
||||||
|
const postprocessHTML = (html) => {
|
||||||
|
return html
|
||||||
|
// 为表格添加样式类
|
||||||
|
.replace(/<table>/g, '<table class="markdown-table">')
|
||||||
|
// 为代码块添加样式类
|
||||||
|
.replace(/<pre><code/g, '<pre class="markdown-code"><code')
|
||||||
|
// 为数学公式添加样式类
|
||||||
|
.replace(/<div class="math-block">/g, '<div class="math-block markdown-math">')
|
||||||
|
.replace(/<span class="math-inline">/g, '<span class="math-inline markdown-math">');
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 为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
|
||||||
|
};
|
||||||
Loading…
Reference in New Issue