feat: 表格渲染样式重写

This commit is contained in:
lixinran 2025-09-10 18:26:48 +08:00
parent 682744e4b8
commit 53a24bfd09
267 changed files with 8965 additions and 451730 deletions

View File

@ -1,187 +0,0 @@
# 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 渲染能力。特别是表格渲染功能,让思维导图可以展示更丰富的数据结构。

View File

@ -15,6 +15,8 @@
- **拖拽操作**: 支持节点拖拽、移动、编辑 - **拖拽操作**: 支持节点拖拽、移动、编辑
- **实时保存**: 自动保存编辑内容到数据库 - **实时保存**: 自动保存编辑内容到数据库
- **缩放控制**: 支持思维导图缩放和居中显示 - **缩放控制**: 支持思维导图缩放和居中显示
- **Markdown支持**: 节点内容支持Markdown语法渲染
- **表格渲染**: 支持Markdown表格在思维导图中的显示
### 💾 数据管理 ### 💾 数据管理
- **云端存储**: 思维导图数据持久化存储 - **云端存储**: 思维导图数据持久化存储
@ -39,10 +41,12 @@
### 前端技术 ### 前端技术
- **框架**: Vue.js 3.3.4 + Vite 4.4.9 - **框架**: Vue.js 3.3.4 + Vite 4.4.9
- **思维导图**: MindElixir 3.0.0 - **思维导图**: MindElixir 3.0.0 (自定义增强版本)
- **文件处理**: mammoth.js (DOCX), pdfjs-dist (PDF) - **文件处理**: mammoth.js (DOCX), pdfjs-dist (PDF)
- **HTTP客户端**: Axios 1.5.0 - **HTTP客户端**: Axios 1.5.0
- **Markdown处理**: marked 16.2.1 - **Markdown处理**: marked 16.2.1 + 自定义渲染器
- **数学公式**: KaTeX 0.16.22
- **代码高亮**: PrismJS 1.30.0
### AI内容分析 ### AI内容分析
- **TypeScript**: 类型安全的Markdown转JSON转换 - **TypeScript**: 类型安全的Markdown转JSON转换
@ -59,7 +63,7 @@
### 1. 克隆项目 ### 1. 克隆项目
```bash ```bash
git clone <repository-url> git clone <repository-url>
cd siweidaotu cd MindMap
``` ```
### 2. 后端设置 ### 2. 后端设置
@ -128,8 +132,8 @@ npm run dev
## 📁 项目结构 ## 📁 项目结构
``` ```
siweidaotu/ MindMap/
├── backend/ # 后端代码 ├── backend/ # Django后端
│ ├── django_mindmap/ # Django项目配置 │ ├── django_mindmap/ # Django项目配置
│ │ ├── settings.py # 项目设置 │ │ ├── settings.py # 项目设置
│ │ ├── urls.py # 主URL配置 │ │ ├── urls.py # 主URL配置
@ -148,19 +152,24 @@ siweidaotu/
│ ├── src/ │ ├── src/
│ │ ├── components/ # Vue组件 │ │ ├── components/ # Vue组件
│ │ │ ├── MindMap.vue # 思维导图组件 │ │ │ ├── MindMap.vue # 思维导图组件
│ │ │ └── AISidebar.vue # AI助手侧边栏 │ │ │ ├── AISidebar.vue # AI助手侧边栏
│ │ │ └── MarkdownTest.vue # Markdown测试组件
│ │ ├── api/ # API接口 │ │ ├── api/ # API接口
│ │ │ └── mindmap.js # 思维导图API │ │ │ └── mindmap.js # 思维导图API
│ │ └── App.vue # 主应用组件 │ │ ├── lib/ # 第三方库
│ │ │ └── mind-elixir/ # MindElixir库项目使用版本
│ │ ├── utils/ # 工具函数
│ │ │ └── markdownRenderer.js # Markdown渲染器
│ │ ├── App.vue # 主应用组件
│ │ └── main.js # 应用入口
│ ├── test-*.html # 功能测试文件
│ ├── package.json # 前端依赖 │ ├── package.json # 前端依赖
│ └── vite.config.js # Vite配置 │ └── vite.config.js # Vite配置
├── others_deletable/ # 其他文件(可删除) ├── mind-elixir-core-master/ # MindElixir完整源码
│ ├── ai-content-analyzer/ # AI内容分析工具 │ ├── src/ # TypeScript源码
│ │ ├── markdownToJSON.ts # Markdown转JSON │ ├── tests/ # 测试套件
│ │ ├── json_openai.py # AI服务调用 │ ├── dist/ # 编译后文件
│ │ └── package.json # TypeScript依赖 │ └── package.json # 依赖配置
│ └── json_openai.py # 独立AI服务文件
├── json、接口文档.md # API接口文档
└── README.md # 项目文档 └── README.md # 项目文档
``` ```
@ -373,6 +382,8 @@ server {
- `双击节点`:编辑节点内容 - `双击节点`:编辑节点内容
- `右键节点`:显示操作菜单 - `右键节点`:显示操作菜单
- `拖拽节点`:移动节点位置 - `拖拽节点`:移动节点位置
- `Ctrl + E`:展开/折叠节点
- `Alt + F`:聚焦/取消聚焦节点
## 🤝 贡献指南 ## 🤝 贡献指南
@ -388,6 +399,22 @@ server {
MIT License - 详见 [LICENSE](LICENSE) 文件 MIT License - 详见 [LICENSE](LICENSE) 文件
## 🧹 项目优化
### 代码清理
项目已进行全面的代码清理和优化:
- ✅ **删除冗余文件**: 移除了重复的测试文件和调试文件
- ✅ **保留核心功能**: 保留了所有必要的测试和调试工具
- ✅ **优化项目结构**: 清晰分离了开发版本和生产版本
- ✅ **MindElixir增强**: 集成了自定义的Markdown和表格渲染功能
### 文件结构优化
- **保留**: `mind-elixir-core-master/` - 完整的源码和文档
- **保留**: `frontend/src/lib/mind-elixir/` - 项目中使用的增强版本
- **保留**: 核心测试文件 - 用于功能验证和问题调试
- **删除**: 重复的调试文件和过时的测试文件
## 🙏 致谢 ## 🙏 致谢
- [MindElixir](https://github.com/ssshooter/mind-elixir-core) - 思维导图可视化库 - [MindElixir](https://github.com/ssshooter/mind-elixir-core) - 思维导图可视化库

Binary file not shown.

View File

@ -77,7 +77,7 @@ def call_ai_api(system_prompt, user_prompt, model="glm-4.5", base_url="https://o
model=model, model=model,
messages=messages, messages=messages,
temperature=0.7, temperature=0.7,
max_tokens=8000, # 增加token限制避免内容截断 max_tokens=4000, # 减少token限制提高响应速度
stream=stream stream=stream
) )
except Exception as e: except Exception as e:

View File

@ -1,182 +0,0 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>当前问题调试</title>
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
<style>
body {
font-family: Arial, sans-serif;
margin: 20px;
background: #f5f5f5;
}
.test-container {
background: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
margin-bottom: 20px;
}
.test-title {
color: #333;
border-bottom: 2px solid #007bff;
padding-bottom: 10px;
margin-bottom: 20px;
}
/* 模拟Mind Elixir的样式 */
.topic {
background: #fff;
border: 1px solid #ddd;
border-radius: 4px;
padding: 8px;
margin: 10px 0;
position: relative;
}
.topic-text {
font-weight: 500;
color: #333;
margin-bottom: 4px;
line-height: 1.4;
}
/* 我们的markdown样式 */
.topic .topic-text.markdown-content {
font-size: 12px;
line-height: 1.3;
}
.topic .topic-text.markdown-content table {
border-collapse: collapse !important;
width: 100% !important;
margin: 4px 0 !important;
font-size: 11px !important;
border: 1px solid #ddd !important;
display: table !important;
}
.topic .topic-text.markdown-content table th,
.topic .topic-text.markdown-content table td {
border: 1px solid #ddd !important;
padding: 2px 4px !important;
text-align: left !important;
vertical-align: top !important;
display: table-cell !important;
}
.topic .topic-text.markdown-content table th {
background-color: #f5f5f5 !important;
font-weight: 600 !important;
}
.topic .topic-text.markdown-content table tr {
display: table-row !important;
}
.topic .topic-text.markdown-content table tr:nth-child(even) {
background-color: #f9f9f9 !important;
}
.topic .topic-text.markdown-content table tr:hover {
background-color: #e9ecef !important;
}
.raw-content {
background: #f8f9fa;
padding: 10px;
border-radius: 4px;
font-family: monospace;
white-space: pre-wrap;
margin-bottom: 20px;
border: 1px solid #e9ecef;
}
.debug-info {
background: #e3f2fd;
padding: 10px;
border-radius: 4px;
margin: 10px 0;
border-left: 4px solid #2196f3;
}
</style>
</head>
<body>
<div class="test-container">
<h1 class="test-title">当前问题调试 - 用户提供的表格</h1>
<h3>原始表格内容:</h3>
<div class="raw-content" id="raw-content"></div>
<h3>模拟Mind Elixir节点中的渲染</h3>
<div class="topic">
<div class="topic-text markdown-content" id="mindelixir-render"></div>
</div>
<h3>普通渲染(对比):</h3>
<div id="normal-render"></div>
<div class="debug-info" id="debug-info"></div>
</div>
<script>
// 配置marked
marked.setOptions({
breaks: true,
gfm: true,
tables: true,
sanitize: false
});
// 用户提供的表格内容
const tableContent = `| 数据类型 | 特点 | 适用场景 | 示例 |
|---------|------|---------|------|
| 数值型 | 可进行数学运算 | 统计分析、机器学习 | 年龄、收入、温度 |
| 分类型 | 表示类别属性 | 分类问题、特征工程 | 性别、职业、地区 |
| 时间序列 | 按时间顺序排列 | 趋势预测、周期性分析 | 股票价格、气温变化 |
| 文本型 | 非结构化自然语言 | NLP分析、情感识别 | 评论、文章、社交媒体内容 |`;
// 显示原始内容
document.getElementById('raw-content').textContent = tableContent;
// 渲染到Mind Elixir模拟节点
try {
const html = marked.parse(tableContent);
document.getElementById('mindelixir-render').innerHTML = html;
} catch (error) {
document.getElementById('mindelixir-render').innerHTML = `<div style="color: red;">渲染失败: ${error.message}</div>`;
}
// 普通渲染
try {
const html = marked.parse(tableContent);
document.getElementById('normal-render').innerHTML = html;
} catch (error) {
document.getElementById('normal-render').innerHTML = `<div style="color: red;">渲染失败: ${error.message}</div>`;
}
// 调试信息
const debugInfo = document.getElementById('debug-info');
const mindelixirElement = document.getElementById('mindelixir-render');
const table = mindelixirElement.querySelector('table');
if (table) {
const computedStyle = window.getComputedStyle(table);
debugInfo.innerHTML = `
<h4>表格样式检查:</h4>
<p><strong>display:</strong> ${computedStyle.display}</p>
<p><strong>border-collapse:</strong> ${computedStyle.borderCollapse}</p>
<p><strong>border:</strong> ${computedStyle.border}</p>
<p><strong>width:</strong> ${computedStyle.width}</p>
<p><strong>font-size:</strong> ${computedStyle.fontSize}</p>
<p><strong>margin:</strong> ${computedStyle.margin}</p>
<p><strong>表格行数:</strong> ${table.rows.length}</p>
<p><strong>表格列数:</strong> ${table.rows[0] ? table.rows[0].cells.length : 0}</p>
`;
} else {
debugInfo.innerHTML = '<h4>错误:</h4><p>没有找到表格元素</p>';
}
</script>
</body>
</html>

View File

@ -1,196 +0,0 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Mind Elixir样式调试</title>
<style>
body {
font-family: Arial, sans-serif;
margin: 20px;
background: #f5f5f5;
}
.test-container {
background: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
margin-bottom: 20px;
}
.test-title {
color: #333;
border-bottom: 2px solid #007bff;
padding-bottom: 10px;
margin-bottom: 20px;
}
/* 模拟Mind Elixir的样式 */
.topic {
background: #fff;
border: 1px solid #ddd;
border-radius: 4px;
padding: 8px;
margin: 10px 0;
position: relative;
}
.topic-text {
font-weight: 500;
color: #333;
margin-bottom: 4px;
line-height: 1.4;
}
/* 我们的markdown样式 */
.topic .topic-text.markdown-content {
font-size: 12px;
line-height: 1.3;
}
.topic .topic-text.markdown-content table {
border-collapse: collapse;
width: 100%;
margin: 4px 0;
font-size: 11px;
border: 1px solid #ddd !important;
}
.topic .topic-text.markdown-content table th,
.topic .topic-text.markdown-content table td {
border: 1px solid #ddd !important;
padding: 2px 4px !important;
text-align: left !important;
vertical-align: top !important;
}
.topic .topic-text.markdown-content table th {
background-color: #f5f5f5 !important;
font-weight: 600 !important;
}
.topic .topic-text.markdown-content table tr:nth-child(even) {
background-color: #f9f9f9 !important;
}
/* 对比:普通表格样式 */
.normal-table {
border-collapse: collapse;
width: 100%;
margin: 4px 0;
font-size: 11px;
border: 1px solid #ddd;
}
.normal-table th,
.normal-table td {
border: 1px solid #ddd;
padding: 2px 4px;
text-align: left;
vertical-align: top;
}
.normal-table th {
background-color: #f5f5f5;
font-weight: 600;
}
.normal-table tr:nth-child(even) {
background-color: #f9f9f9;
}
</style>
</head>
<body>
<div class="test-container">
<h1 class="test-title">Mind Elixir样式调试</h1>
<h3>测试1模拟Mind Elixir节点中的表格</h3>
<div class="topic">
<div class="topic-text markdown-content">
<table>
<thead>
<tr>
<th>评估维度</th>
<th>权重</th>
<th>评分标准</th>
<th>计算公式</th>
</tr>
</thead>
<tbody>
<tr>
<td>完整性</td>
<td>0.3</td>
<td>缺失值比例 < 5%</td>
<td>完整性 = 1 - 缺失值数量/总数据量</td>
</tr>
<tr>
<td>准确性</td>
<td>0.3</td>
<td>误差率 < 3%</td>
<td>准确性 = 1 - 错误记录数/总记录数</td>
</tr>
</tbody>
</table>
</div>
</div>
<h3>测试2普通表格对比</h3>
<table class="normal-table">
<thead>
<tr>
<th>评估维度</th>
<th>权重</th>
<th>评分标准</th>
<th>计算公式</th>
</tr>
</thead>
<tbody>
<tr>
<td>完整性</td>
<td>0.3</td>
<td>缺失值比例 < 5%</td>
<td>完整性 = 1 - 缺失值数量/总数据量</td>
</tr>
<tr>
<td>准确性</td>
<td>0.3</td>
<td>误差率 < 3%</td>
<td>准确性 = 1 - 错误记录数/总记录数</td>
</tr>
</tbody>
</table>
<h3>测试3检查样式是否被覆盖</h3>
<div id="style-check"></div>
</div>
<script>
// 检查样式是否被正确应用
const topicText = document.querySelector('.topic-text.markdown-content');
const table = topicText.querySelector('table');
const computedStyle = window.getComputedStyle(table);
const styleCheck = document.getElementById('style-check');
styleCheck.innerHTML = `
<p><strong>表格样式检查:</strong></p>
<p>border-collapse: ${computedStyle.borderCollapse}</p>
<p>border: ${computedStyle.border}</p>
<p>width: ${computedStyle.width}</p>
<p>font-size: ${computedStyle.fontSize}</p>
<p>margin: ${computedStyle.margin}</p>
`;
// 检查第一个单元格的样式
const firstCell = table.querySelector('td');
const cellStyle = window.getComputedStyle(firstCell);
styleCheck.innerHTML += `
<p><strong>单元格样式检查:</strong></p>
<p>border: ${cellStyle.border}</p>
<p>padding: ${cellStyle.padding}</p>
<p>text-align: ${cellStyle.textAlign}</p>
<p>vertical-align: ${cellStyle.verticalAlign}</p>
`;
</script>
</body>
</html>

View File

@ -1,123 +0,0 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>表格检测调试</title>
<style>
body { font-family: Arial, sans-serif; margin: 20px; }
.test-case { margin: 20px 0; padding: 15px; border: 1px solid #ddd; border-radius: 5px; }
.content { background: #f8f9fa; padding: 10px; font-family: monospace; white-space: pre-wrap; }
.result { margin-top: 10px; padding: 10px; border-radius: 3px; }
.success { background: #d4edda; color: #155724; }
.error { background: #f8d7da; color: #721c24; }
</style>
</head>
<body>
<h1>表格检测调试</h1>
<div id="results"></div>
<script>
// 复制hasTable函数
function hasTable(content) {
if (!content || typeof content !== 'string') {
console.log('🔍 hasTable: 内容为空或不是字符串');
return false;
}
console.log('🔍 hasTable: 检查内容:', content);
// 检查是否包含表格语法
const lines = content.split('\n');
let hasTableRow = false;
let hasSeparator = false;
console.log('🔍 hasTable: 分割后的行数:', lines.length);
for (const line of lines) {
const trimmedLine = line.trim();
console.log('🔍 hasTable: 检查行:', trimmedLine);
// 检查表格行(包含|字符且至少有3个部分
if (trimmedLine.includes('|') && trimmedLine.split('|').length >= 3) {
hasTableRow = true;
console.log('✅ hasTable: 找到表格行');
}
// 检查分隔符行(包含-和|,且主要由这些字符组成)
if (trimmedLine.includes('|') && trimmedLine.includes('-') && /^[\s\|\-\:]+$/.test(trimmedLine)) {
hasSeparator = true;
console.log('✅ hasTable: 找到分隔符行');
}
}
// 如果只有表格行但没有分隔符,也可能是表格
// 或者如果内容包含多个|字符,也可能是表格
const pipeCount = (content.match(/\|/g) || []).length;
const hasMultiplePipes = pipeCount >= 6; // 至少3行每行2个|
console.log('🔍 hasTable: 结果 - hasTableRow:', hasTableRow, 'hasSeparator:', hasSeparator, 'pipeCount:', pipeCount, 'hasMultiplePipes:', hasMultiplePipes);
const result = (hasTableRow && hasSeparator) || (hasTableRow && hasMultiplePipes);
console.log('🔍 hasTable: 最终结果:', result);
return result;
}
// 测试用例
const testCases = [
{
name: "数据质量评估矩阵",
content: `| 评估维度 | 权重 | 评分标准 | 计算公式 |
|---------|------|---------|---------|
| 完整性 | 0.3 | 缺失值比例 < 5% | $\\text{完整性} = 1 - \\frac{\\text{缺失值数量}}{\\text{总数据量}}$ |
| 准确性 | 0.3 | 误差率 < 3% | $\\text{准确性} = 1 - \\frac{\\text{错误记录数}}{\\text{总记录数}}$ |`
},
{
name: "产品价格表",
content: `产品价格表
| 产品 | 价格 |
|------|------|
| 苹果 | 4元 |
| 香蕉 | 2元 |`
},
{
name: "简单表格",
content: `| 姓名 | 年龄 | 城市 |
|------|------|------|
| 张三 | 25 | 北京 |
| 李四 | 30 | 上海 |`
},
{
name: "只有表格行,没有分隔符",
content: `| 姓名 | 年龄 | 城市 |
| 张三 | 25 | 北京 |
| 李四 | 30 | 上海 |`
},
{
name: "包含表格字符但不是表格",
content: `这是一个包含|字符的文本,但不是表格`
}
];
// 运行测试
const resultsDiv = document.getElementById('results');
testCases.forEach((testCase, index) => {
const testDiv = document.createElement('div');
testDiv.className = 'test-case';
const result = hasTable(testCase.content);
testDiv.innerHTML = `
<h3>测试用例 ${index + 1}: ${testCase.name}</h3>
<div class="content">${testCase.content}</div>
<div class="result ${result ? 'success' : 'error'}">
检测结果: ${result ? '✅ 检测到表格' : '❌ 未检测到表格'}
</div>
`;
resultsDiv.appendChild(testDiv);
});
</script>
</body>
</html>

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

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

594
frontend/dist/assets/index-a09f7810.js vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -23,8 +23,8 @@
flex-direction: column; flex-direction: column;
} }
</style> </style>
<script type="module" crossorigin src="/assets/index-d6c20d61.js"></script> <script type="module" crossorigin src="/assets/index-a09f7810.js"></script>
<link rel="stylesheet" href="/assets/index-44efe7b9.css"> <link rel="stylesheet" href="/assets/index-5b39da23.css">
</head> </head>
<body> <body>
<div id="app"></div> <div id="app"></div>

470
frontend/node_modules/.vite/deps/@viselect_vanilla.js generated vendored Normal file
View File

@ -0,0 +1,470 @@
import "./chunk-FOSKEDPS.js";
// src/lib/mind-elixir/node_modules/@viselect/vanilla/dist/viselect.mjs
var X = class {
constructor() {
this._listeners = /* @__PURE__ */ new Map(), this.on = this.addEventListener, this.off = this.removeEventListener, this.emit = this.dispatchEvent;
}
addEventListener(e, t) {
const s = this._listeners.get(e) ?? /* @__PURE__ */ new Set();
return this._listeners.set(e, s), s.add(t), this;
}
removeEventListener(e, t) {
var s;
return (s = this._listeners.get(e)) == null || s.delete(t), this;
}
dispatchEvent(e, ...t) {
let s = true;
for (const i of this._listeners.get(e) ?? [])
s = i(...t) !== false && s;
return s;
}
unbindAllListeners() {
this._listeners.clear();
}
};
var L = (l, e = "px") => typeof l == "number" ? l + e : l;
var y = ({ style: l }, e, t) => {
if (typeof e == "object")
for (const [s, i] of Object.entries(e))
i !== void 0 && (l[s] = L(i));
else
t !== void 0 && (l[e] = L(t));
};
var M = (l = 0, e = 0, t = 0, s = 0) => {
const i = { x: l, y: e, width: t, height: s, top: e, left: l, right: l + t, bottom: e + s };
return { ...i, toJSON: () => JSON.stringify(i) };
};
var Y = (l) => {
let e, t = -1, s = false;
return {
next: (...i) => {
e = i, s || (s = true, t = requestAnimationFrame(() => {
l(...e), s = false;
}));
},
cancel: () => {
cancelAnimationFrame(t), s = false;
}
};
};
var k = (l, e, t = "touch") => {
switch (t) {
case "center": {
const s = e.left + e.width / 2, i = e.top + e.height / 2;
return s >= l.left && s <= l.right && i >= l.top && i <= l.bottom;
}
case "cover":
return e.left >= l.left && e.top >= l.top && e.right <= l.right && e.bottom <= l.bottom;
case "touch":
return l.right >= e.left && l.left <= e.right && l.bottom >= e.top && l.top <= e.bottom;
}
};
var H = () => matchMedia("(hover: none), (pointer: coarse)").matches;
var N = () => "safari" in window;
var A = (l) => Array.isArray(l) ? l : [l];
var O = (l) => (e, t, s, i = {}) => {
(e instanceof HTMLCollection || e instanceof NodeList) && (e = Array.from(e)), t = A(t), e = A(e);
for (const o of e)
if (o)
for (const n of t)
o[l](n, s, { capture: false, ...i });
};
var S = O("addEventListener");
var g = O("removeEventListener");
var x = (l) => {
var i;
const { clientX: e, clientY: t, target: s } = ((i = l.touches) == null ? void 0 : i[0]) ?? l;
return { x: e, y: t, target: s };
};
var E = (l, e = document) => A(l).map(
(t) => typeof t == "string" ? Array.from(e.querySelectorAll(t)) : t instanceof Element ? t : null
).flat().filter(Boolean);
var q = (l, e) => e.some((t) => typeof t == "number" ? l.button === t : typeof t == "object" ? t.button !== l.button ? false : t.modifiers.every((s) => {
switch (s) {
case "alt":
return l.altKey;
case "ctrl":
return l.ctrlKey || l.metaKey;
case "shift":
return l.shiftKey;
}
}) : false);
var { abs: b, max: C, min: B, ceil: R } = Math;
var D = (l = []) => ({
stored: l,
selected: [],
touched: [],
changed: { added: [], removed: [] }
});
var T = class T2 extends X {
constructor(e) {
var o, n, r, a, u;
super(), this._selection = D(), this._targetBoundaryScrolled = true, this._selectables = [], this._areaLocation = { y1: 0, x2: 0, y2: 0, x1: 0 }, this._areaRect = M(), this._singleClick = true, this._scrollAvailable = true, this._scrollingActive = false, this._scrollSpeed = { x: 0, y: 0 }, this._scrollDelta = { x: 0, y: 0 }, this._lastMousePosition = { x: 0, y: 0 }, this.enable = this._toggleStartEvents, this.disable = this._toggleStartEvents.bind(this, false), this._options = {
selectionAreaClass: "selection-area",
selectionContainerClass: void 0,
selectables: [],
document: window.document,
startAreas: ["html"],
boundaries: ["html"],
container: "body",
...e,
behaviour: {
overlap: "invert",
intersect: "touch",
triggers: [0],
...e.behaviour,
startThreshold: (o = e.behaviour) != null && o.startThreshold ? typeof e.behaviour.startThreshold == "number" ? e.behaviour.startThreshold : { x: 10, y: 10, ...e.behaviour.startThreshold } : { x: 10, y: 10 },
scrolling: {
speedDivider: 10,
manualSpeed: 750,
...(n = e.behaviour) == null ? void 0 : n.scrolling,
startScrollMargins: {
x: 0,
y: 0,
...(a = (r = e.behaviour) == null ? void 0 : r.scrolling) == null ? void 0 : a.startScrollMargins
}
}
},
features: {
range: true,
touch: true,
deselectOnBlur: false,
...e.features,
singleTap: {
allow: true,
intersect: "native",
...(u = e.features) == null ? void 0 : u.singleTap
}
}
};
for (const _ of Object.getOwnPropertyNames(Object.getPrototypeOf(this)))
typeof this[_] == "function" && (this[_] = this[_].bind(this));
const { document: t, selectionAreaClass: s, selectionContainerClass: i } = this._options;
this._area = t.createElement("div"), this._clippingElement = t.createElement("div"), this._clippingElement.appendChild(this._area), this._area.classList.add(s), i && this._clippingElement.classList.add(i), y(this._area, {
willChange: "top, left, bottom, right, width, height",
top: 0,
left: 0,
position: "fixed"
}), y(this._clippingElement, {
overflow: "hidden",
position: "fixed",
transform: "translate3d(0, 0, 0)",
// https://stackoverflow.com/a/38268846
pointerEvents: "none",
zIndex: "1"
}), this._frame = Y((_) => {
this._recalculateSelectionAreaRect(), this._updateElementSelection(), this._emitEvent("move", _), this._redrawSelectionArea();
}), this.enable();
}
_toggleStartEvents(e = true) {
const { document: t, features: s } = this._options, i = e ? S : g;
i(t, "mousedown", this._onTapStart), s.touch && i(t, "touchstart", this._onTapStart, { passive: false });
}
_onTapStart(e, t = false) {
const { x: s, y: i, target: o } = x(e), { document: n, startAreas: r, boundaries: a, features: u, behaviour: _ } = this._options, c = o.getBoundingClientRect();
if (e instanceof MouseEvent && !q(e, _.triggers))
return;
const p = E(r, n), m = E(a, n);
this._targetElement = m.find(
(v) => k(v.getBoundingClientRect(), c)
);
const f = e.composedPath(), d = p.find((v) => f.includes(v));
if (this._targetBoundary = m.find((v) => f.includes(v)), !this._targetElement || !d || !this._targetBoundary || !t && this._emitEvent("beforestart", e) === false)
return;
this._areaLocation = { x1: s, y1: i, x2: 0, y2: 0 };
const h = n.scrollingElement ?? n.body;
this._scrollDelta = { x: h.scrollLeft, y: h.scrollTop }, this._singleClick = true, this.clearSelection(false, true), S(n, ["touchmove", "mousemove"], this._delayedTapMove, { passive: false }), S(n, ["mouseup", "touchcancel", "touchend"], this._onTapStop), S(n, "scroll", this._onScroll), u.deselectOnBlur && (this._targetBoundaryScrolled = false, S(this._targetBoundary, "scroll", this._onStartAreaScroll));
}
_onSingleTap(e) {
const { singleTap: { intersect: t }, range: s } = this._options.features, i = x(e);
let o;
if (t === "native")
o = i.target;
else if (t === "touch") {
this.resolveSelectables();
const { x: r, y: a } = i;
o = this._selectables.find((u) => {
const { right: _, left: c, top: p, bottom: m } = u.getBoundingClientRect();
return r < _ && r > c && a < m && a > p;
});
}
if (!o)
return;
for (this.resolveSelectables(); !this._selectables.includes(o); )
if (o.parentElement)
o = o.parentElement;
else {
this._targetBoundaryScrolled || this.clearSelection();
return;
}
const { stored: n } = this._selection;
if (this._emitEvent("start", e), e.shiftKey && s && this._latestElement) {
const r = this._latestElement, [a, u] = r.compareDocumentPosition(o) & 4 ? [o, r] : [r, o], _ = [...this._selectables.filter(
(c) => c.compareDocumentPosition(a) & 4 && c.compareDocumentPosition(u) & 2
), a, u];
this.select(_), this._latestElement = r;
} else
n.includes(o) && (n.length === 1 || e.ctrlKey || n.every((r) => this._selection.stored.includes(r))) ? this.deselect(o) : (this.select(o), this._latestElement = o);
}
_delayedTapMove(e) {
const { container: t, document: s, behaviour: { startThreshold: i } } = this._options, { x1: o, y1: n } = this._areaLocation, { x: r, y: a } = x(e);
if (
// Single number for both coordinates
typeof i == "number" && b(r + a - (o + n)) >= i || // Different x and y threshold
typeof i == "object" && b(r - o) >= i.x || b(a - n) >= i.y
) {
if (g(s, ["mousemove", "touchmove"], this._delayedTapMove, { passive: false }), this._emitEvent("beforedrag", e) === false) {
g(s, ["mouseup", "touchcancel", "touchend"], this._onTapStop);
return;
}
S(s, ["mousemove", "touchmove"], this._onTapMove, { passive: false }), y(this._area, "display", "block"), E(t, s)[0].appendChild(this._clippingElement), this.resolveSelectables(), this._singleClick = false, this._targetRect = this._targetElement.getBoundingClientRect(), this._scrollAvailable = this._targetElement.scrollHeight !== this._targetElement.clientHeight || this._targetElement.scrollWidth !== this._targetElement.clientWidth, this._scrollAvailable && (S(this._targetElement, "wheel", this._wheelScroll, { passive: false }), S(this._options.document, "keydown", this._keyboardScroll, { passive: false }), this._selectables = this._selectables.filter((u) => this._targetElement.contains(u))), this._setupSelectionArea(), this._emitEvent("start", e), this._onTapMove(e);
}
this._handleMoveEvent(e);
}
_setupSelectionArea() {
const { _clippingElement: e, _targetElement: t, _area: s } = this, i = this._targetRect = t.getBoundingClientRect();
this._scrollAvailable ? (y(e, {
top: i.top,
left: i.left,
width: i.width,
height: i.height
}), y(s, {
marginTop: -i.top,
marginLeft: -i.left
})) : (y(e, {
top: 0,
left: 0,
width: "100%",
height: "100%"
}), y(s, {
marginTop: 0,
marginLeft: 0
}));
}
_onTapMove(e) {
const { _scrollSpeed: t, _areaLocation: s, _options: i, _frame: o } = this, { speedDivider: n } = i.behaviour.scrolling, r = this._targetElement, { x: a, y: u } = x(e);
if (s.x2 = a, s.y2 = u, this._lastMousePosition.x = a, this._lastMousePosition.y = u, this._scrollAvailable && !this._scrollingActive && (t.y || t.x)) {
this._scrollingActive = true;
const _ = () => {
if (!t.x && !t.y) {
this._scrollingActive = false;
return;
}
const { scrollTop: c, scrollLeft: p } = r;
t.y && (r.scrollTop += R(t.y / n), s.y1 -= r.scrollTop - c), t.x && (r.scrollLeft += R(t.x / n), s.x1 -= r.scrollLeft - p), o.next(e), requestAnimationFrame(_);
};
requestAnimationFrame(_);
} else
o.next(e);
this._handleMoveEvent(e);
}
_handleMoveEvent(e) {
const { features: t } = this._options;
(t.touch && H() || this._scrollAvailable && N()) && e.preventDefault();
}
_onScroll() {
const { _scrollDelta: e, _options: { document: t } } = this, { scrollTop: s, scrollLeft: i } = t.scrollingElement ?? t.body;
this._areaLocation.x1 += e.x - i, this._areaLocation.y1 += e.y - s, e.x = i, e.y = s, this._setupSelectionArea(), this._frame.next(null);
}
_onStartAreaScroll() {
this._targetBoundaryScrolled = true, g(this._targetElement, "scroll", this._onStartAreaScroll);
}
_wheelScroll(e) {
const { manualSpeed: t } = this._options.behaviour.scrolling, s = e.deltaY ? e.deltaY > 0 ? 1 : -1 : 0, i = e.deltaX ? e.deltaX > 0 ? 1 : -1 : 0;
this._scrollSpeed.y += s * t, this._scrollSpeed.x += i * t, this._onTapMove(e), e.preventDefault();
}
_keyboardScroll(e) {
const { manualSpeed: t } = this._options.behaviour.scrolling, s = e.key === "ArrowLeft" ? -1 : e.key === "ArrowRight" ? 1 : 0, i = e.key === "ArrowUp" ? -1 : e.key === "ArrowDown" ? 1 : 0;
this._scrollSpeed.x += Math.sign(s) * t, this._scrollSpeed.y += Math.sign(i) * t, e.preventDefault(), this._onTapMove({
clientX: this._lastMousePosition.x,
clientY: this._lastMousePosition.y,
preventDefault: () => {
}
});
}
_recalculateSelectionAreaRect() {
const { _scrollSpeed: e, _areaLocation: t, _targetElement: s, _options: i } = this, { scrollTop: o, scrollHeight: n, clientHeight: r, scrollLeft: a, scrollWidth: u, clientWidth: _ } = s, c = this._targetRect, { x1: p, y1: m } = t;
let { x2: f, y2: d } = t;
const { behaviour: { scrolling: { startScrollMargins: h } } } = i;
f < c.left + h.x ? (e.x = a ? -b(c.left - f + h.x) : 0, f = f < c.left ? c.left : f) : f > c.right - h.x ? (e.x = u - a - _ ? b(c.left + c.width - f - h.x) : 0, f = f > c.right ? c.right : f) : e.x = 0, d < c.top + h.y ? (e.y = o ? -b(c.top - d + h.y) : 0, d = d < c.top ? c.top : d) : d > c.bottom - h.y ? (e.y = n - o - r ? b(c.top + c.height - d - h.y) : 0, d = d > c.bottom ? c.bottom : d) : e.y = 0;
const v = B(p, f), w = B(m, d), j = C(p, f), K = C(m, d);
this._areaRect = M(v, w, j - v, K - w);
}
_redrawSelectionArea() {
const { x: e, y: t, width: s, height: i } = this._areaRect, { style: o } = this._area;
o.left = `${e}px`, o.top = `${t}px`, o.width = `${s}px`, o.height = `${i}px`;
}
_onTapStop(e, t) {
var n;
const { document: s, features: i } = this._options, { _singleClick: o } = this;
g(this._targetElement, "scroll", this._onStartAreaScroll), g(s, ["mousemove", "touchmove"], this._delayedTapMove), g(s, ["touchmove", "mousemove"], this._onTapMove), g(s, ["mouseup", "touchcancel", "touchend"], this._onTapStop), g(s, "scroll", this._onScroll), this._keepSelection(), e && o && i.singleTap.allow ? this._onSingleTap(e) : !o && !t && (this._updateElementSelection(), this._emitEvent("stop", e)), this._scrollSpeed.x = 0, this._scrollSpeed.y = 0, g(this._targetElement, "wheel", this._wheelScroll, { passive: true }), g(this._options.document, "keydown", this._keyboardScroll, { passive: true }), this._clippingElement.remove(), (n = this._frame) == null || n.cancel(), y(this._area, "display", "none");
}
_updateElementSelection() {
const { _selectables: e, _options: t, _selection: s, _areaRect: i } = this, { stored: o, selected: n, touched: r } = s, { intersect: a, overlap: u } = t.behaviour, _ = u === "invert", c = [], p = [], m = [];
for (let d = 0; d < e.length; d++) {
const h = e[d];
if (k(i, h.getBoundingClientRect(), a)) {
if (n.includes(h))
o.includes(h) && !r.includes(h) && r.push(h);
else if (_ && o.includes(h)) {
m.push(h);
continue;
} else
p.push(h);
c.push(h);
}
}
_ && p.push(...o.filter((d) => !n.includes(d)));
const f = u === "keep";
for (let d = 0; d < n.length; d++) {
const h = n[d];
!c.includes(h) && !// Check if the user wants to keep previously selected elements, e.g.,
// not make them part of the current selection as soon as they're touched.
(f && o.includes(h)) && m.push(h);
}
s.selected = c, s.changed = { added: p, removed: m }, this._latestElement = void 0;
}
_emitEvent(e, t) {
return this.emit(e, {
event: t,
store: this._selection,
selection: this
});
}
_keepSelection() {
const { _options: e, _selection: t } = this, { selected: s, changed: i, touched: o, stored: n } = t, r = s.filter((a) => !n.includes(a));
switch (e.behaviour.overlap) {
case "drop": {
t.stored = [
...r,
...n.filter((a) => !o.includes(a))
// Elements not touched
];
break;
}
case "invert": {
t.stored = [
...r,
...n.filter((a) => !i.removed.includes(a))
// Elements not removed from selection
];
break;
}
case "keep": {
t.stored = [
...n,
...s.filter((a) => !n.includes(a))
// Newly added
];
break;
}
}
}
/**
* Manually triggers the start of a selection
* @param evt A MouseEvent / TouchEvent-like object
* @param silent If beforestart should be fired
*/
trigger(e, t = true) {
this._onTapStart(e, t);
}
/**
* Can be used if during a selection elements have been added
* Will update everything that can be selected
*/
resolveSelectables() {
this._selectables = E(this._options.selectables, this._options.document);
}
/**
* Same as deselecting, but for all elements currently selected
* @param includeStored If the store should also get cleared
* @param quiet If move / stop events should be fired
*/
clearSelection(e = true, t = false) {
const { selected: s, stored: i, changed: o } = this._selection;
o.added = [], o.removed.push(
...s,
...e ? i : []
), t || (this._emitEvent("move", null), this._emitEvent("stop", null)), this._selection = D(e ? [] : i);
}
/**
* @returns {Array} Selected elements
*/
getSelection() {
return this._selection.stored;
}
/**
* @returns {HTMLElement} The selection area element
*/
getSelectionArea() {
return this._area;
}
/**
* @returns {Element[]} Available selectable elements for current selection
*/
getSelectables() {
return this._selectables;
}
/**
* Set the location of the selection area
* @param location A partial AreaLocation object
*/
setAreaLocation(e) {
Object.assign(this._areaLocation, e), this._redrawSelectionArea();
}
/**
* @returns {AreaLocation} The current location of the selection area
*/
getAreaLocation() {
return this._areaLocation;
}
/**
* Cancel the current selection process, pass true to fire a stop event after cancel
* @param keepEvent If a stop event should be fired
*/
cancel(e = false) {
this._onTapStop(null, !e);
}
/**
* Unbinds all events and removes the area-element.
*/
destroy() {
this.cancel(), this.disable(), this._clippingElement.remove(), super.unbindAllListeners();
}
/**
* Adds elements to the selection
* @param query CSS Query, can be an array of queries
* @param quiet If this should not trigger the move event
*/
select(e, t = false) {
const { changed: s, selected: i, stored: o } = this._selection, n = E(e, this._options.document).filter(
(r) => !i.includes(r) && !o.includes(r)
);
return o.push(...n), i.push(...n), s.added.push(...n), s.removed = [], this._latestElement = void 0, t || (this._emitEvent("move", null), this._emitEvent("stop", null)), n;
}
/**
* Removes a particular element from the selection
* @param query CSS Query, can be an array of queries
* @param quiet If this should not trigger the move event
*/
deselect(e, t = false) {
const { selected: s, stored: i, changed: o } = this._selection, n = E(e, this._options.document).filter(
(r) => s.includes(r) || i.includes(r)
);
this._selection.stored = i.filter((r) => !n.includes(r)), this._selection.selected = s.filter((r) => !n.includes(r)), this._selection.changed.added = [], this._selection.changed.removed.push(
...n.filter((r) => !o.removed.includes(r))
), this._latestElement = void 0, t || (this._emitEvent("move", null), this._emitEvent("stop", null));
}
};
T.version = "3.9.0";
var P = T;
export {
P as default
};
/*! Bundled license information:
@viselect/vanilla/dist/viselect.mjs:
(*! @viselect/vanilla v3.9.0 MIT | https://github.com/Simonwep/selection/tree/master/packages/vanilla *)
*/
//# sourceMappingURL=@viselect_vanilla.js.map

File diff suppressed because one or more lines are too long

View File

@ -1,35 +1,35 @@
{ {
"hash": "76772e52", "hash": "76772e52",
"browserHash": "b5df73c3", "browserHash": "a4ef7769",
"optimized": { "optimized": {
"axios": { "axios": {
"src": "../../axios/index.js", "src": "../../axios/index.js",
"file": "axios.js", "file": "axios.js",
"fileHash": "d5a6fb13", "fileHash": "923e7809",
"needsInterop": false "needsInterop": false
}, },
"mammoth": { "mammoth": {
"src": "../../mammoth/lib/index.js", "src": "../../mammoth/lib/index.js",
"file": "mammoth.js", "file": "mammoth.js",
"fileHash": "8e0b13e7", "fileHash": "2d23e669",
"needsInterop": true "needsInterop": true
}, },
"marked": { "marked": {
"src": "../../marked/lib/marked.esm.js", "src": "../../marked/lib/marked.esm.js",
"file": "marked.js", "file": "marked.js",
"fileHash": "330cfdd3", "fileHash": "e6802b70",
"needsInterop": false "needsInterop": false
}, },
"pdfjs-dist": { "pdfjs-dist": {
"src": "../../pdfjs-dist/build/pdf.mjs", "src": "../../pdfjs-dist/build/pdf.mjs",
"file": "pdfjs-dist.js", "file": "pdfjs-dist.js",
"fileHash": "63546997", "fileHash": "b0d6a144",
"needsInterop": false "needsInterop": false
}, },
"prismjs": { "prismjs": {
"src": "../../prismjs/prism.js", "src": "../../prismjs/prism.js",
"file": "prismjs.js", "file": "prismjs.js",
"fileHash": "3730e60a", "fileHash": "0c7d8fc7",
"needsInterop": true "needsInterop": true
}, },
"prismjs/components/prism-css": { "prismjs/components/prism-css": {
@ -65,13 +65,19 @@
"vue": { "vue": {
"src": "../../vue/dist/vue.runtime.esm-bundler.js", "src": "../../vue/dist/vue.runtime.esm-bundler.js",
"file": "vue.js", "file": "vue.js",
"fileHash": "a7d38f14", "fileHash": "b4ce9a46",
"needsInterop": false "needsInterop": false
}, },
"katex": { "katex": {
"src": "../../katex/dist/katex.mjs", "src": "../../katex/dist/katex.mjs",
"file": "katex.js", "file": "katex.js",
"fileHash": "d08a8bdf", "fileHash": "e7ed5c7a",
"needsInterop": false
},
"@viselect/vanilla": {
"src": "../../../src/lib/mind-elixir/node_modules/@viselect/vanilla/dist/viselect.mjs",
"file": "@viselect_vanilla.js",
"fileHash": "b39569df",
"needsInterop": false "needsInterop": false
} }
}, },

View File

@ -631,7 +631,7 @@ Level 4 标题用 #####
// - // -
const controller = new AbortController(); const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 120000); // 2 const timeoutId = setTimeout(() => controller.abort(), 60000); // 1
const response = await fetch('http://127.0.0.1:8000/api/ai/generate-markdown', { const response = await fetch('http://127.0.0.1:8000/api/ai/generate-markdown', {
method: 'POST', method: 'POST',

View File

@ -1510,7 +1510,8 @@ const createAINode = async (parentNode, question, answer) => {
.replace(/^#+\s*/gm, '') // .replace(/^#+\s*/gm, '') //
.replace(/\*\*(.*?)\*\*/g, '$1') // .replace(/\*\*(.*?)\*\*/g, '$1') //
.replace(/\*(.*?)\*/g, '$1') // .replace(/\*(.*?)\*/g, '$1') //
.replace(/^\s*[-*+]\s*/gm, '• ') // //
.replace(/^\s*[-*+]\s*(?![|])/gm, '• ') //
.replace(/\n{3,}/g, '\n\n') // .replace(/\n{3,}/g, '\n\n') //
.trim(); .trim();
}; };
@ -4209,40 +4210,85 @@ const updateMindMapRealtime = async (data, title) => {
line-height: 1.3; line-height: 1.3;
} }
/* Markdown表格样式 - 使用更高优先级 */ /* 强制表格样式 - 最高优先级 */
.topic table,
.topic .text table,
.topic .topic-text table,
.topic .topic-text.markdown-content table { .topic .topic-text.markdown-content table {
border-collapse: collapse !important; border-collapse: collapse !important;
width: 100% !important; width: 100% !important;
margin: 4px 0 !important; margin: 4px 0 !important;
font-size: 11px !important; font-size: 11px !important;
border: 1px solid #ddd !important; border: 2px solid #333 !important;
border-radius: 6px !important;
box-shadow: 0 2px 8px rgba(0,0,0,0.08) !important;
background-color: #fafafa !important;
overflow: hidden !important;
display: table !important; display: table !important;
white-space: normal !important; /* 覆盖MindElixir的pre-wrap */
} }
.topic table th,
.topic table td,
.topic .text table th,
.topic .text table td,
.topic .topic-text table th,
.topic .topic-text table td,
.topic .topic-text.markdown-content table th, .topic .topic-text.markdown-content table th,
.topic .topic-text.markdown-content table td { .topic .topic-text.markdown-content table td {
border: 1px solid #ddd !important; border: 2px solid #333 !important;
padding: 2px 4px !important; padding: 6px 8px !important;
text-align: left !important; text-align: left !important;
vertical-align: top !important; vertical-align: top !important;
display: table-cell !important; display: table-cell !important;
position: relative !important;
white-space: normal !important; /* 覆盖MindElixir的pre-wrap */
} }
.topic .topic-text.markdown-content table th { .topic .topic-text.markdown-content table th,
.topic .text table th,
.topic table th {
background-color: #f5f5f5 !important; background-color: #f5f5f5 !important;
font-weight: 600 !important; font-weight: 600 !important;
color: #333 !important;
text-align: center !important;
border-bottom: 2px solid #333 !important;
} }
.topic .topic-text.markdown-content table tr { .topic .topic-text.markdown-content table td,
.topic .text table td,
.topic table td {
background-color: #fff !important;
}
.topic .topic-text.markdown-content table tr,
.topic .text table tr,
.topic table tr {
display: table-row !important; display: table-row !important;
white-space: normal !important; /* 覆盖MindElixir的pre-wrap */
} }
.topic .topic-text.markdown-content table tr:nth-child(even) { .topic .topic-text.markdown-content table tr:nth-child(even) td,
background-color: #f9f9f9 !important; .topic .text table tr:nth-child(even) td {
background-color: #f8f8f8 !important;
} }
.topic .topic-text.markdown-content table tr:hover { .topic .topic-text.markdown-content table tr:hover td,
background-color: #e9ecef !important; .topic .text table tr:hover td {
background-color: #f0f8ff !important;
}
/* 简洁的边框效果 */
.topic .topic-text.markdown-content table th:not(:last-child),
.topic .topic-text.markdown-content table td:not(:last-child),
.topic .text table th:not(:last-child),
.topic .text table td:not(:last-child) {
border-right: 1px solid #e0e0e0 !important;
}
.topic .topic-text.markdown-content table tr:not(:last-child) td,
.topic .text table tr:not(:last-child) td {
border-bottom: 1px solid #e0e0e0 !important;
} }
/* Markdown代码样式 */ /* Markdown代码样式 */

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 704 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 331 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 303 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 140 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 210 KiB

View File

@ -1,430 +0,0 @@
<p align="center">
<a href="https://docs.mind-elixir.com" target="_blank" rel="noopener noreferrer">
<img width="150" src="https://raw.githubusercontent.com/ssshooter/mind-elixir-core/master/images/logo2.png" alt="mindelixir logo2">
</a>
<h1 align="center">Mind Elixir</h1>
</p>
<p align="center">
<a href="https://www.npmjs.com/package/mind-elixir">
<img src="https://img.shields.io/npm/v/mind-elixir" alt="versión">
</a>
<a href="https://github.com/ssshooter/mind-elixir-core/blob/master/LICENSE">
<img src="https://img.shields.io/npm/l/mind-elixir" alt="licencia">
</a>
<a href="https://app.codacy.com/gh/ssshooter/mind-elixir-core?utm_source=github.com&utm_medium=referral&utm_content=ssshooter/mind-elixir-core&utm_campaign=Badge_Grade_Settings">
<img src="https://api.codacy.com/project/badge/Grade/09fadec5bf094886b30cea6aabf3a88b" alt="calidad del código">
</a>
<a href="https://bundlephobia.com/result?p=mind-elixir">
<img src="https://badgen.net/bundlephobia/dependency-count/mind-elixir" alt="cantidad de dependencias">
</a>
<a href="https://packagephobia.com/result?p=mind-elixir">
<img src="https://packagephobia.com/badge?p=mind-elixir" alt="tamaño del paquete">
</a>
</p>
[English](/readme.md) |
[中文](/readme/zh.md) |
[Español](/readme/es.md) |
[Français](/readme/fr.md) |
[Português](/readme/pt.md) |
[Русский](/readme/ru.md) |
[日本語](/readme/ja.md) |
[한국어](/readme/ko.md)
Mind elixir es un núcleo de mapas mentales de JavaScript de código abierto. Puedes usarlo con cualquier framework frontend que prefieras.
Características:
- Ligero
- Alto rendimiento
- Agnóstico al framework
- Pluginable
- Plugin de arrastrar y soltar / edición de nodos incorporado
- Exportar como SVG / PNG / Html
- Resumir nodos
- Operaciones en masa soportadas
- Deshacer / Rehacer
- Atajos eficientes
- Estiliza fácilmente tu nodo con variables CSS
<details>
<summary>Tabla de Contenidos</summary>
- [Prueba ahora](#prueba-ahora)
- [Playground](#playground)
- [Documentación](#documentación)
- [Uso](#uso)
- [Instalar](#instalar)
- [NPM](#npm)
- [Etiqueta de script](#etiqueta-de-script)
- [Inicializar](#inicializar)
- [Estructura de Datos](#estructura-de-datos)
- [Manejo de Eventos](#manejo-de-eventos)
- [Exportación e Importación de Datos](#exportación-e-importación-de-datos)
- [Guardias de Operación](#guardias-de-operación)
- [Exportar como Imagen](#exportar-como-imagen)
- [Solución 1](#solución-1)
- [Solución 2](#solución-2)
- [Tema](#tema)
- [Atajos](#atajos)
- [Ecosistema](#ecosistema)
- [Desarrollo](#desarrollo)
- [Agradecimientos](#agradecimientos)
- [Contribuidores](#contribuidores)
</details>
## Prueba ahora
![mindelixir](https://raw.githubusercontent.com/ssshooter/mind-elixir-core/master/images/screenshot2.png)
https://mind-elixir.com/
### Playground
- Vanilla JS - https://codepen.io/ssshooter/pen/OJrJowN
- React - https://codesandbox.io/s/mind-elixir-3-x-react-18-x-vy9fcq
- Vue3 - https://codesandbox.io/s/mind-elixir-3-x-vue3-lth484
- Vue2 - https://codesandbox.io/s/mind-elixir-3-x-vue-2-x-5kdfjp
## Documentación
https://docs.mind-elixir.com/
## Uso
### Instalar
#### NPM
```bash
npm i mind-elixir -S
```
```javascript
import MindElixir from 'mind-elixir'
```
#### Etiqueta de script
```html
<script type="module" src="https://cdn.jsdelivr.net/npm/mind-elixir/dist/MindElixir.js"></script>
```
### Inicializar
```html
<div id="map"></div>
<style>
#map {
height: 500px;
width: 100%;
}
</style>
```
**Cambio Importante** desde la versión 1.0.0, `data` debe ser pasado a `init()`, no `options`.
```javascript
import MindElixir from 'mind-elixir'
import example from 'mind-elixir/dist/example1'
let options = {
el: '#map', // o HTMLDivElement
direction: MindElixir.LEFT,
draggable: true, // por defecto true
contextMenu: true, // por defecto true
toolBar: true, // por defecto true
nodeMenu: true, // por defecto true
keypress: true, // por defecto true
locale: 'en', // [zh_CN,zh_TW,en,ja,pt,ru] esperando PRs
overflowHidden: false, // por defecto false
mainLinkStyle: 2, // [1,2] por defecto 1
mouseSelectionButton: 0, // 0 para botón izquierdo, 2 para botón derecho, por defecto 0
contextMenuOption: {
focus: true,
link: true,
extend: [
{
name: 'Editar nodo',
onclick: () => {
alert('menú extendido')
},
},
],
},
before: {
insertSibling(el, obj) {
return true
},
async addChild(el, obj) {
await sleep()
return true
},
},
}
let mind = new MindElixir(options)
mind.install(plugin) // instala tu plugin
// crea nuevos datos de mapa
const data = MindElixir.new('nuevo tema')
// o `example`
// o los datos devueltos por `.getData()`
mind.init(data)
// obtener un nodo
MindElixir.E('node-id')
```
### Estructura de Datos
```javascript
// estructura completa de datos de nodo hasta ahora
const nodeData = {
topic: 'tema del nodo',
id: 'bd1c24420cd2c2f5',
style: { fontSize: '32', color: '#3298db', background: '#ecf0f1' },
expanded: true,
parent: null,
tags: ['Etiqueta'],
icons: ['😀'],
hyperLink: 'https://github.com/ssshooter/mind-elixir-core',
image: {
url: 'https://raw.githubusercontent.com/ssshooter/mind-elixir-core/master/images/logo2.png', // requerido
// necesitas consultar la altura y el ancho de la imagen y calcular el valor apropiado para mostrar la imagen
height: 90, // requerido
width: 90, // requerido
},
children: [
{
topic: 'hijo',
id: 'xxxx',
// ...
},
],
}
```
### Manejo de Eventos
```javascript
mind.bus.addListener('operation', operation => {
console.log(operation)
// return {
// name: nombre de la acción,
// obj: objeto objetivo
// }
// name: [insertSibling|addChild|removeNode|beginEdit|finishEdit]
// obj: objetivo
// name: moveNode
// obj: {from:objetivo1,to:objetivo2}
})
mind.bus.addListener('selectNodes', nodes => {
console.log(nodes)
})
mind.bus.addListener('expandNode', node => {
console.log('expandNode: ', node)
})
```
### Exportación e Importación de Datos
```javascript
// exportación de datos
const data = mind.getData() // objeto javascript, ver src/example.js
mind.getDataString() // objeto en cadena
// importación de datos
// iniciar
let mind = new MindElixir(options)
mind.init(data)
// actualización de datos
mind.refresh(data)
```
### Guardias de Operación
```javascript
let mind = new MindElixir({
// ...
before: {
insertSibling(el, obj) {
console.log(el, obj)
if (this.currentNode.nodeObj.parent.root) {
return false
}
return true
},
async addChild(el, obj) {
await sleep()
if (this.currentNode.nodeObj.parent.root) {
return false
}
return true
},
},
})
```
## Exportar como Imagen
### Solución 1
```typescript
const mind = {
/** instancia de mind elixir */
}
const downloadPng = async () => {
const blob = await mind.exportPng() // ¡Obtén un Blob!
if (!blob) return
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = 'nombrearchivo.png'
a.click()
URL.revokeObjectURL(url)
}
```
### Solución 2
Instala `@ssshooter/modern-screenshot`, luego:
```typescript
import { domToPng } from '@ssshooter/modern-screenshot'
const download = async () => {
const dataUrl = await domToPng(mind.nodes, {
onCloneNode: node => {
const n = node as HTMLDivElement
n.style.position = ''
n.style.top = ''
n.style.left = ''
n.style.bottom = ''
n.style.right = ''
},
padding: 300,
quality: 1,
})
const link = document.createElement('a')
link.download = 'captura.png'
link.href = dataUrl
link.click()
}
```
## Tema
```javascript
const options = {
// ...
theme: {
name: 'Oscuro',
// paleta de colores de las líneas principales
palette: ['#848FA0', '#748BE9', '#D2F9FE', '#4145A5', '#789AFA', '#706CF4', '#EF987F', '#775DD5', '#FCEECF', '#DA7FBC'],
// sobrescribir variables css
cssVar: {
'--main-color': '#ffffff',
'--main-bgcolor': '#4c4f69',
'--color': '#cccccc',
'--bgcolor': '#252526',
'--panel-color': '255, 255, 255',
'--panel-bgcolor': '45, 55, 72',
},
// todas las variables ver /src/index.less
},
// ...
}
// ...
mind.changeTheme({
name: 'Latte',
palette: ['#dd7878', '#ea76cb', '#8839ef', '#e64553', '#fe640b', '#df8e1d', '#40a02b', '#209fb5', '#1e66f5', '#7287fd'],
cssVar: {
'--main-color': '#444446',
'--main-bgcolor': '#ffffff',
'--color': '#777777',
'--bgcolor': '#f6f6f6',
},
})
```
Ten en cuenta que Mind Elixir no observará el cambio de `prefers-color-scheme`. Por favor, cambia el tema **manualmente** cuando el esquema cambie.
## Atajos
| Atajo | Función |
| ------------------ | -------------------------------- |
| Enter | Insertar Nodo Hermano |
| Tab | Insertar Nodo Hijo |
| F1 | Centrar el Mapa |
| F2 | Comenzar a Editar el Nodo Actual |
| ↑ | Seleccionar el Nodo Hermano Anterior |
| ↓ | Seleccionar el Nodo Hermano Siguiente |
| ← / → | Seleccionar Padre o Primer Hijo |
| PageUp / Alt + ↑ | Mover Nodo Arriba |
| PageDown / Alt + ↓ | Mover Nodo Abajo |
| Ctrl + ↑ | Cambiar Patrón de Diseño a Lado |
| Ctrl + ← | Cambiar Patrón de Diseño a Izquierda |
| Ctrl + → | Cambiar Patrón de Diseño a Derecha |
| Ctrl + C | Copiar el Nodo Actual |
| Ctrl + V | Pegar el Nodo Copiado |
| Ctrl + "+" | Acercar el Mapa Mental |
| Ctrl + "-" | Alejar el Mapa Mental |
| Ctrl + 0 | Restablecer Nivel de Zoom |
## Ecosistema
- [@mind-elixir/node-menu](https://github.com/ssshooter/node-menu)
- [@mind-elixir/node-menu-neo](https://github.com/ssshooter/node-menu-neo)
- [@mind-elixir/export-xmind](https://github.com/ssshooter/export-xmind)
- [@mind-elixir/export-html](https://github.com/ssshooter/export-html)
- [mind-elixir-react](https://github.com/ssshooter/mind-elixir-react)
¡Las PRs son bienvenidas!
## Desarrollo
```
pnpm i
pnpm dev
```
Prueba los archivos generados con `dev.dist.ts`:
```
pnpm build
pnpm link ./
```
Actualiza la documentación:
```
# Instalar api-extractor
pnpm install -g @microsoft/api-extractor
# Mantener /src/docs.ts
# Generar documentación
pnpm doc
pnpm doc:md
```
## Agradecimientos
- [@viselect/vanilla](https://github.com/simonwep/selection/tree/master/packages/vanilla)
## Contribuidores
¡Gracias por tus contribuciones a Mind Elixir! Tu apoyo y dedicación hacen que este proyecto sea mejor.
<a href="https://github.com/SSShooter/mind-elixir-core/graphs/contributors">
<img src="https://contrib.rocks/image?repo=SSShooter/mind-elixir-core&columns=6" />
</a>

View File

@ -1,430 +0,0 @@
<p align="center">
<a href="https://docs.mind-elixir.com" target="_blank" rel="noopener noreferrer">
<img width="150" src="https://raw.githubusercontent.com/ssshooter/mind-elixir-core/master/images/logo2.png" alt="mindelixir logo2">
</a>
<h1 align="center">Mind Elixir</h1>
</p>
<p align="center">
<a href="https://www.npmjs.com/package/mind-elixir">
<img src="https://img.shields.io/npm/v/mind-elixir" alt="version">
</a>
<a href="https://github.com/ssshooter/mind-elixir-core/blob/master/LICENSE">
<img src="https://img.shields.io/npm/l/mind-elixir" alt="license">
</a>
<a href="https://app.codacy.com/gh/ssshooter/mind-elixir-core?utm_source=github.com&utm_medium=referral&utm_content=ssshooter/mind-elixir-core&utm_campaign=Badge_Grade_Settings">
<img src="https://api.codacy.com/project/badge/Grade/09fadec5bf094886b30cea6aabf3a88b" alt="code quality">
</a>
<a href="https://bundlephobia.com/result?p=mind-elixir">
<img src="https://badgen.net/bundlephobia/dependency-count/mind-elixir" alt="dependency-count">
</a>
<a href="https://packagephobia.com/result?p=mind-elixir">
<img src="https://packagephobia.com/badge?p=mind-elixir" alt="package size">
</a>
</p>
[English](/readme.md) |
[中文](/readme/zh.md) |
[Español](/readme/es.md) |
[Français](/readme/fr.md) |
[Português](/readme/pt.md) |
[Русский](/readme/ru.md) |
[日本語](/readme/ja.md) |
[한국어](/readme/ko.md)
Mind Elixir est un noyau JavaScript open source pour créer des cartes mentales. Vous pouvez l'utiliser avec n'importe quel framework frontend de votre choix.
Caractéristiques :
- Léger
- Haute performance
- Indépendant du framework
- Extensible via plugins
- Plugin intégré pour le glisser-déposer / édition de nœuds
- Export en SVG / PNG / Html
- Résumé des nœuds
- Opérations en masse supportées
- Annuler / Refaire
- Raccourcis efficaces
- Stylisation facile des nœuds avec les variables CSS
<details>
<summary>Table des matières</summary>
- [Essayer maintenant](#essayer-maintenant)
- [Playground](#playground)
- [Documentation](#documentation)
- [Utilisation](#utilisation)
- [Installation](#installation)
- [NPM](#npm)
- [Balise script](#balise-script)
- [Initialisation](#initialisation)
- [Structure des données](#structure-des-données)
- [Gestion des événements](#gestion-des-événements)
- [Export et import des données](#export-et-import-des-données)
- [Gardes d'opération](#gardes-dopération)
- [Exporter en image](#exporter-en-image)
- [Solution 1](#solution-1)
- [Solution 2](#solution-2)
- [Thème](#thème)
- [Raccourcis](#raccourcis)
- [Écosystème](#écosystème)
- [Développement](#développement)
- [Remerciements](#remerciements)
- [Contributeurs](#contributeurs)
</details>
## Essayer maintenant
![mindelixir](https://raw.githubusercontent.com/ssshooter/mind-elixir-core/master/images/screenshot2.png)
https://mind-elixir.com/
### Playground
- Vanilla JS - https://codepen.io/ssshooter/pen/OJrJowN
- React - https://codesandbox.io/s/mind-elixir-3-x-react-18-x-vy9fcq
- Vue3 - https://codesandbox.io/s/mind-elixir-3-x-vue3-lth484
- Vue2 - https://codesandbox.io/s/mind-elixir-3-x-vue-2-x-5kdfjp
## Documentation
https://docs.mind-elixir.com/
## Utilisation
### Installation
#### NPM
```bash
npm i mind-elixir -S
```
```javascript
import MindElixir from 'mind-elixir'
```
#### Balise script
```html
<script type="module" src="https://cdn.jsdelivr.net/npm/mind-elixir/dist/MindElixir.js"></script>
```
### Initialisation
```html
<div id="map"></div>
<style>
#map {
height: 500px;
width: 100%;
}
</style>
```
**Changement majeur** depuis la version 1.0.0, `data` doit être passé à `init()`, et non `options`.
```javascript
import MindElixir from 'mind-elixir'
import example from 'mind-elixir/dist/example1'
let options = {
el: '#map', // ou HTMLDivElement
direction: MindElixir.LEFT,
draggable: true, // par défaut true
contextMenu: true, // par défaut true
toolBar: true, // par défaut true
nodeMenu: true, // par défaut true
keypress: true, // par défaut true
locale: 'en', // [zh_CN,zh_TW,en,ja,pt,ru] en attente de PRs
overflowHidden: false, // par défaut false
mainLinkStyle: 2, // [1,2] par défaut 1
mouseSelectionButton: 0, // 0 pour le bouton gauche, 2 pour le bouton droit, par défaut 0
contextMenuOption: {
focus: true,
link: true,
extend: [
{
name: 'Édition de nœud',
onclick: () => {
alert('menu étendu')
},
},
],
},
before: {
insertSibling(el, obj) {
return true
},
async addChild(el, obj) {
await sleep()
return true
},
},
}
let mind = new MindElixir(options)
mind.install(plugin) // installer votre plugin
// créer de nouvelles données de carte
const data = MindElixir.new('nouveau sujet')
// ou `example`
// ou les données retournées par `.getData()`
mind.init(data)
// obtenir un nœud
MindElixir.E('node-id')
```
### Structure des données
```javascript
// structure complète des données de nœud jusqu'à présent
const nodeData = {
topic: 'sujet du nœud',
id: 'bd1c24420cd2c2f5',
style: { fontSize: '32', color: '#3298db', background: '#ecf0f1' },
expanded: true,
parent: null,
tags: ['Tag'],
icons: ['😀'],
hyperLink: 'https://github.com/ssshooter/mind-elixir-core',
image: {
url: 'https://raw.githubusercontent.com/ssshooter/mind-elixir-core/master/images/logo2.png', // requis
// vous devez interroger la hauteur et la largeur de l'image et calculer la valeur appropriée pour afficher l'image
height: 90, // requis
width: 90, // requis
},
children: [
{
topic: 'enfant',
id: 'xxxx',
// ...
},
],
}
```
### Gestion des événements
```javascript
mind.bus.addListener('operation', operation => {
console.log(operation)
// return {
// name: nom de l'action,
// obj: objet cible
// }
// name: [insertSibling|addChild|removeNode|beginEdit|finishEdit]
// obj: cible
// name: moveNode
// obj: {from:cible1,to:cible2}
})
mind.bus.addListener('selectNodes', nodes => {
console.log(nodes)
})
mind.bus.addListener('expandNode', node => {
console.log('expandNode: ', node)
})
```
### Export et import des données
```javascript
// export des données
const data = mind.getData() // objet javascript, voir src/example.js
mind.getDataString() // objet en chaîne
// import des données
// initialisation
let mind = new MindElixir(options)
mind.init(data)
// mise à jour des données
mind.refresh(data)
```
### Gardes d'opération
```javascript
let mind = new MindElixir({
// ...
before: {
insertSibling(el, obj) {
console.log(el, obj)
if (this.currentNode.nodeObj.parent.root) {
return false
}
return true
},
async addChild(el, obj) {
await sleep()
if (this.currentNode.nodeObj.parent.root) {
return false
}
return true
},
},
})
```
## Exporter en image
### Solution 1
```typescript
const mind = {
/** instance mind elixir */
}
const downloadPng = async () => {
const blob = await mind.exportPng() // Obtenez un Blob !
if (!blob) return
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = 'filename.png'
a.click()
URL.revokeObjectURL(url)
}
```
### Solution 2
Installer `@ssshooter/modern-screenshot`, puis :
```typescript
import { domToPng } from '@ssshooter/modern-screenshot'
const download = async () => {
const dataUrl = await domToPng(mind.nodes, {
onCloneNode: node => {
const n = node as HTMLDivElement
n.style.position = ''
n.style.top = ''
n.style.left = ''
n.style.bottom = ''
n.style.right = ''
},
padding: 300,
quality: 1,
})
const link = document.createElement('a')
link.download = 'screenshot.png'
link.href = dataUrl
link.click()
}
```
## Thème
```javascript
const options = {
// ...
theme: {
name: 'Dark',
// palette de couleurs des lignes principales
palette: ['#848FA0', '#748BE9', '#D2F9FE', '#4145A5', '#789AFA', '#706CF4', '#EF987F', '#775DD5', '#FCEECF', '#DA7FBC'],
// remplacer les variables css
cssVar: {
'--main-color': '#ffffff',
'--main-bgcolor': '#4c4f69',
'--color': '#cccccc',
'--bgcolor': '#252526',
'--panel-color': '255, 255, 255',
'--panel-bgcolor': '45, 55, 72',
},
// toutes les variables voir /src/index.less
},
// ...
}
// ...
mind.changeTheme({
name: 'Latte',
palette: ['#dd7878', '#ea76cb', '#8839ef', '#e64553', '#fe640b', '#df8e1d', '#40a02b', '#209fb5', '#1e66f5', '#7287fd'],
cssVar: {
'--main-color': '#444446',
'--main-bgcolor': '#ffffff',
'--color': '#777777',
'--bgcolor': '#f6f6f6',
},
})
```
Soyez conscient que Mind Elixir n'observera pas le changement de `prefers-color-scheme`. Veuillez changer le thème **manuellement** lorsque le schéma change.
## Raccourcis
| Raccourci | Fonction |
| ------------------ | ----------------------------------------- |
| Entrée | Insérer un nœud frère |
| Tab | Insérer un nœud enfant |
| F1 | Centrer la carte |
| F2 | Commencer l'édition du nœud actuel |
| ↑ | Sélectionner le nœud frère précédent |
| ↓ | Sélectionner le nœud frère suivant |
| ← / → | Sélectionner le parent ou le premier enfant|
| PageUp / Alt + ↑ | Déplacer le nœud vers le haut |
| PageDown / Alt + ↓| Déplacer le nœud vers le bas |
| Ctrl + ↑ | Changer la disposition en mode latéral |
| Ctrl + ← | Changer la disposition vers la gauche |
| Ctrl + → | Changer la disposition vers la droite |
| Ctrl + C | Copier le nœud actuel |
| Ctrl + V | Coller le nœud copié |
| Ctrl + "+" | Zoomer la carte mentale |
| Ctrl + "-" | Dézoomer la carte mentale |
| Ctrl + 0 | Réinitialiser le niveau de zoom |
## Écosystème
- [@mind-elixir/node-menu](https://github.com/ssshooter/node-menu)
- [@mind-elixir/node-menu-neo](https://github.com/ssshooter/node-menu-neo)
- [@mind-elixir/export-xmind](https://github.com/ssshooter/export-xmind)
- [@mind-elixir/export-html](https://github.com/ssshooter/export-html)
- [mind-elixir-react](https://github.com/ssshooter/mind-elixir-react)
Les PRs sont les bienvenues !
## Développement
```
pnpm i
pnpm dev
```
Tester les fichiers générés avec `dev.dist.ts` :
```
pnpm build
pnpm link ./
```
Mettre à jour la documentation :
```
# Installer api-extractor
pnpm install -g @microsoft/api-extractor
# Maintenir /src/docs.ts
# Générer la documentation
pnpm doc
pnpm doc:md
```
## Remerciements
- [@viselect/vanilla](https://github.com/simonwep/selection/tree/master/packages/vanilla)
## Contributeurs
Merci pour vos contributions à Mind Elixir ! Votre soutien et votre dévouement rendent ce projet meilleur.
<a href="https://github.com/SSShooter/mind-elixir-core/graphs/contributors">
<img src="https://contrib.rocks/image?repo=SSShooter/mind-elixir-core&columns=6" />
</a>

View File

@ -1,429 +0,0 @@
<p align="center">
<a href="https://docs.mind-elixir.com" target="_blank" rel="noopener noreferrer">
<img width="150" src="https://raw.githubusercontent.com/ssshooter/mind-elixir-core/master/images/logo2.png" alt="mindelixir logo2">
</a>
<h1 align="center">Mind Elixir</h1>
</p>
<p align="center">
<a href="https://www.npmjs.com/package/mind-elixir">
<img src="https://img.shields.io/npm/v/mind-elixir" alt="version">
</a>
<a href="https://github.com/ssshooter/mind-elixir-core/blob/master/LICENSE">
<img src="https://img.shields.io/npm/l/mind-elixir" alt="license">
</a>
<a href="https://app.codacy.com/gh/ssshooter/mind-elixir-core?utm_source=github.com&utm_medium=referral&utm_content=ssshooter/mind-elixir-core&utm_campaign=Badge_Grade_Settings">
<img src="https://api.codacy.com/project/badge/Grade/09fadec5bf094886b30cea6aabf3a88b" alt="code quality">
</a>
<a href="https://bundlephobia.com/result?p=mind-elixir">
<img src="https://badgen.net/bundlephobia/dependency-count/mind-elixir" alt="dependency-count">
</a>
<a href="https://packagephobia.com/result?p=mind-elixir">
<img src="https://packagephobia.com/badge?p=mind-elixir" alt="package size">
</a>
</p>
[English](/readme.md) |
[中文](/readme/zh.md) |
[Español](/readme/es.md) |
[Français](/readme/fr.md) |
[Português](/readme/pt.md) |
[Русский](/readme/ru.md) |
[日本語](/readme/ja.md) |
[한국어](/readme/ko.md)
Mind Elixirは、オープンソースのJavaScriptマインドマップコアです。お好みのフロントエンドフレームワークと組み合わせて使用できます。
特徴:
- 軽量
- 高パフォーマンス
- フレームワーク非依存
- プラグイン対応
- ドラッグ&ドロップ/ノード編集プラグイン内蔵
- SVG/PNG/HTMLとしてエクスポート可能
- ノードの要約
- 一括操作対応
- 元に戻す/やり直し
- 効率的なショートカット
- CSSカスタマイズが容易
<details>
<summary>目次</summary>
- [デモを試す](#デモを試す)
- [プレイグラウンド](#プレイグラウンド)
- [ドキュメント](#ドキュメント)
- [使い方](#使い方)
- [インストール](#インストール)
- [NPM](#npm)
- [スクリプトタグ](#スクリプトタグ)
- [初期化](#初期化)
- [データ構造](#データ構造)
- [イベントハンドリング](#イベントハンドリング)
- [データのエクスポートとインポート](#データのエクスポートとインポート)
- [操作ガード](#操作ガード)
- [画像としてエクスポート](#画像としてエクスポート)
- [方法1](#方法1)
- [方法2](#方法2)
- [テーマ](#テーマ)
- [ショートカット](#ショートカット)
- [エコシステム](#エコシステム)
- [開発](#開発)
- [謝辞](#謝辞)
- [貢献者](#貢献者)
</details>
## デモを試す
![mindelixir](https://raw.githubusercontent.com/ssshooter/mind-elixir-core/master/images/screenshot2.png)
https://mind-elixir.com/
### プレイグラウンド
- Vanilla JS - https://codepen.io/ssshooter/pen/OJrJowN
- React - https://codesandbox.io/s/mind-elixir-3-x-react-18-x-vy9fcq
- Vue3 - https://codesandbox.io/s/mind-elixir-3-x-vue3-lth484
- Vue2 - https://codesandbox.io/s/mind-elixir-3-x-vue-2-x-5kdfjp
## ドキュメント
https://docs.mind-elixir.com/
## 使い方
### インストール
#### NPM
```bash
npm i mind-elixir -S
```
```javascript
import MindElixir from 'mind-elixir'
```
#### スクリプトタグ
```html
<script type="module" src="https://cdn.jsdelivr.net/npm/mind-elixir/dist/MindElixir.js"></script>
```
### 初期化
```html
<div id="map"></div>
<style>
#map {
height: 500px;
width: 100%;
}
</style>
```
**重要な変更** バージョン1.0.0以降、`data`は`options`ではなく`init()`に渡す必要があります。
```javascript
import MindElixir from 'mind-elixir'
import example from 'mind-elixir/dist/example1'
let options = {
el: '#map', // またはHTMLDivElement
direction: MindElixir.LEFT,
draggable: true, // デフォルトはtrue
contextMenu: true, // デフォルトはtrue
toolBar: true, // デフォルトはtrue
nodeMenu: true, // デフォルトはtrue
keypress: true, // デフォルトはtrue
locale: 'ja', // [zh_CN,zh_TW,en,ja,pt,ru] PRs募集中
overflowHidden: false, // デフォルトはfalse
mainLinkStyle: 2, // [1,2] デフォルトは1
mouseSelectionButton: 0, // 0は左クリック、2は右クリック、デフォルトは0
contextMenuOption: {
focus: true,
link: true,
extend: [
{
name: 'ノード編集',
onclick: () => {
alert('拡張メニュー')
},
},
],
},
before: {
insertSibling(el, obj) {
return true
},
async addChild(el, obj) {
await sleep()
return true
},
},
}
let mind = new MindElixir(options)
mind.install(plugin) // プラグインのインストール
// 新しいマップデータの作成
const data = MindElixir.new('新しいトピック')
// または `example`
// または `.getData()`の戻り値
mind.init(data)
// ノードの取得
MindElixir.E('node-id')
```
### データ構造
```javascript
// 現在のノードデータ構造
const nodeData = {
topic: 'ノードのトピック',
id: 'bd1c24420cd2c2f5',
style: { fontSize: '32', color: '#3298db', background: '#ecf0f1' },
expanded: true,
parent: null,
tags: ['タグ'],
icons: ['😀'],
hyperLink: 'https://github.com/ssshooter/mind-elixir-core',
image: {
url: 'https://raw.githubusercontent.com/ssshooter/mind-elixir-core/master/images/logo2.png', // 必須
height: 90, // 必須
width: 90, // 必須
},
children: [
{
topic: '子ノード',
id: 'xxxx',
// ...
},
],
}
```
### イベントハンドリング
```javascript
mind.bus.addListener('operation', operation => {
console.log(operation)
// return {
// name: action name,
// obj: target object
// }
// name: [insertSibling|addChild|removeNode|beginEdit|finishEdit]
// obj: target
// name: moveNode
// obj: {from:target1,to:target2}
})
mind.bus.addListener('selectNodes', nodes => {
console.log(nodes)
})
mind.bus.addListener('expandNode', node => {
console.log('expandNode: ', node)
})
```
### データのエクスポートとインポート
```javascript
// データのエクスポート
const data = mind.getData() // JavaScriptオブジェクト、src/example.jsを参照
mind.getDataString() // オブジェクトを文字列化
// データのインポート
// 初期化
let mind = new MindElixir(options)
mind.init(data)
// データの更新
mind.refresh(data)
```
### 操作ガード
```javascript
let mind = new MindElixir({
// ...
before: {
insertSibling(el, obj) {
console.log(el, obj)
if (this.currentNode.nodeObj.parent.root) {
return false
}
return true
},
async addChild(el, obj) {
await sleep()
if (this.currentNode.nodeObj.parent.root) {
return false
}
return true
},
},
})
```
## 画像としてエクスポート
### 方法1
```typescript
const mind = {
/** mind elixir instance */
}
const downloadPng = async () => {
const blob = await mind.exportPng() // Blobを取得
if (!blob) return
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = 'filename.png'
a.click()
URL.revokeObjectURL(url)
}
```
### 方法2
`@ssshooter/modern-screenshot`をインストールし、次に実行します:
```typescript
import { domToPng } from '@ssshooter/modern-screenshot'
const download = async () => {
const dataUrl = await domToPng(mind.nodes, {
onCloneNode: node => {
const n = node as HTMLDivElement
n.style.position = ''
n.style.top = ''
n.style.left = ''
n.style.bottom = ''
n.style.right = ''
},
padding: 300,
quality: 1,
})
const link = document.createElement('a')
link.download = 'screenshot.png'
link.href = dataUrl
link.click()
}
```
## テーマ
```javascript
const options = {
// ...
theme: {
name: 'Dark',
// メインラインのカラーパレット
palette: ['#848FA0', '#748BE9', '#D2F9FE', '#4145A5', '#789AFA', '#706CF4', '#EF987F', '#775DD5', '#FCEECF', '#DA7FBC'],
// CSS変数の上書き
cssVar: {
'--main-color': '#ffffff',
'--main-bgcolor': '#4c4f69',
'--color': '#cccccc',
'--bgcolor': '#252526',
'--panel-color': '255, 255, 255',
'--panel-bgcolor': '45, 55, 72',
},
// すべての変数は/src/index.lessを参照
},
// ...
}
// ...
mind.changeTheme({
name: 'Latte',
palette: ['#dd7878', '#ea76cb', '#8839ef', '#e64553', '#fe640b', '#df8e1d', '#40a02b', '#209fb5', '#1e66f5', '#7287fd'],
cssVar: {
'--main-color': '#444446',
'--main-bgcolor': '#ffffff',
'--color': '#777777',
'--bgcolor': '#f6f6f6',
},
})
```
Mind Elixirは`prefers-color-scheme`の変更を監視しません。スキームが変更された場合は、テーマを**手動で**変更してください。
## ショートカット
| ショートカット | 機能 |
| ------------------ | -------------------------------- |
| Enter | 兄弟ノードを挿入 |
| Tab | 子ノードを挿入 |
| F1 | マップを中央に配置 |
| F2 | 現在のノードの編集を開始 |
| ↑ | 前の兄弟ノードを選択 |
| ↓ | 次の兄弟ノードを選択 |
| ← / → | 親または最初の子ノードを選択 |
| PageUp / Alt + ↑ | ノードを上に移動 |
| PageDown / Alt + ↓ | ノードを下に移動 |
| Ctrl + ↑ | レイアウトパターンをサイドに変更 |
| Ctrl + ← | レイアウトパターンを左に変更 |
| Ctrl + → | レイアウトパターンを右に変更 |
| Ctrl + C | 現在のノードをコピー |
| Ctrl + V | コピーしたノードを貼り付け |
| Ctrl + "+" | マインドマップをズームイン |
| Ctrl + "-" | マインドマップをズームアウト |
| Ctrl + 0 | ズームレベルをリセット |
## エコシステム
- [@mind-elixir/node-menu](https://github.com/ssshooter/node-menu)
- [@mind-elixir/node-menu-neo](https://github.com/ssshooter/node-menu-neo)
- [@mind-elixir/export-xmind](https://github.com/ssshooter/export-xmind)
- [@mind-elixir/export-html](https://github.com/ssshooter/export-html)
- [mind-elixir-react](https://github.com/ssshooter/mind-elixir-react)
PRsは大歓迎です
## 開発
```
pnpm i
pnpm dev
```
`dev.dist.ts`で生成されたファイルをテストします:
```
pnpm build
pnpm link ./
```
ドキュメントを更新します:
```
# api-extractorをインストール
pnpm install -g @microsoft/api-extractor
# /src/docs.tsを維持
# ドキュメントを生成
pnpm doc
pnpm doc:md
```
## 謝辞
- [@viselect/vanilla](https://github.com/simonwep/selection/tree/master/packages/vanilla)
## 貢献者
Mind Elixirへの貢献に感謝しますあなたのサポートと献身がこのプロジェクトをより良くします。
<a href="https://github.com/SSShooter/mind-elixir-core/graphs/contributors">
<img src="https://contrib.rocks/image?repo=SSShooter/mind-elixir-core&columns=6" />
</a>

View File

@ -1,430 +0,0 @@
<p align="center">
<a href="https://docs.mind-elixir.com" target="_blank" rel="noopener noreferrer">
<img width="150" src="https://raw.githubusercontent.com/ssshooter/mind-elixir-core/master/images/logo2.png" alt="mindelixir logo2">
</a>
<h1 align="center">Mind Elixir</h1>
</p>
<p align="center">
<a href="https://www.npmjs.com/package/mind-elixir">
<img src="https://img.shields.io/npm/v/mind-elixir" alt="version">
</a>
<a href="https://github.com/ssshooter/mind-elixir-core/blob/master/LICENSE">
<img src="https://img.shields.io/npm/l/mind-elixir" alt="license">
</a>
<a href="https://app.codacy.com/gh/ssshooter/mind-elixir-core?utm_source=github.com&utm_medium=referral&utm_content=ssshooter/mind-elixir-core&utm_campaign=Badge_Grade_Settings">
<img src="https://api.codacy.com/project/badge/Grade/09fadec5bf094886b30cea6aabf3a88b" alt="code quality">
</a>
<a href="https://bundlephobia.com/result?p=mind-elixir">
<img src="https://badgen.net/bundlephobia/dependency-count/mind-elixir" alt="dependency-count">
</a>
<a href="https://packagephobia.com/result?p=mind-elixir">
<img src="https://packagephobia.com/badge?p=mind-elixir" alt="package size">
</a>
</p>
[English](/readme.md) |
[中文](/readme/zh.md) |
[Español](/readme/es.md) |
[Français](/readme/fr.md) |
[Português](/readme/pt.md) |
[Русский](/readme/ru.md) |
[日本語](/readme/ja.md) |
[한국어](/readme/ko.md)
Mind Elixir는 오픈 소스 JavaScript 마인드맵 코어입니다. 원하는 프론트엔드 프레임워크와 함께 사용할 수 있습니다.
특징:
- 경량화
- 고성능
- 프레임워크에 구애받지 않음
- 플러그인 지원
- 드래그 앤 드롭 / 노드 편집 플러그인 내장
- SVG / PNG / HTML 내보내기
- 노드 요약
- 대량 작업 지원
- 실행 취소 / 다시 실행
- 효율적인 단축키
- CSS 변수로 쉽게 노드 스타일링
<details>
<summary>목차</summary>
- [지금 시작하기](#지금-시작하기)
- [플레이그라운드](#플레이그라운드)
- [문서](#문서)
- [사용법](#사용법)
- [설치](#설치)
- [NPM](#npm)
- [스크립트 태그](#스크립트-태그)
- [초기화](#초기화)
- [데이터 구조](#데이터-구조)
- [이벤트 처리](#이벤트-처리)
- [데이터 내보내기와 가져오기](#데이터-내보내기와-가져오기)
- [작업 가드](#작업-가드)
- [이미지로 내보내기](#이미지로-내보내기)
- [방법 1](#방법-1)
- [방법 2](#방법-2)
- [테마](#테마)
- [단축키](#단축키)
- [생태계](#생태계)
- [개발](#개발)
- [감사의 말](#감사의-말)
- [기여자](#기여자)
</details>
## 지금 시작하기
![mindelixir](https://raw.githubusercontent.com/ssshooter/mind-elixir-core/master/images/screenshot2.png)
https://mind-elixir.com/
### 플레이그라운드
- Vanilla JS - https://codepen.io/ssshooter/pen/OJrJowN
- React - https://codesandbox.io/s/mind-elixir-3-x-react-18-x-vy9fcq
- Vue3 - https://codesandbox.io/s/mind-elixir-3-x-vue3-lth484
- Vue2 - https://codesandbox.io/s/mind-elixir-3-x-vue-2-x-5kdfjp
## 문서
https://docs.mind-elixir.com/
## 사용법
### 설치
#### NPM
```bash
npm i mind-elixir -S
```
```javascript
import MindElixir from 'mind-elixir'
```
#### 스크립트 태그
```html
<script type="module" src="https://cdn.jsdelivr.net/npm/mind-elixir/dist/MindElixir.js"></script>
```
### 초기화
```html
<div id="map"></div>
<style>
#map {
height: 500px;
width: 100%;
}
</style>
```
**주요 변경사항** 1.0.0 버전부터 `data``options`가 아닌 `init()`에 전달되어야 합니다.
```javascript
import MindElixir from 'mind-elixir'
import example from 'mind-elixir/dist/example1'
let options = {
el: '#map', // or HTMLDivElement
direction: MindElixir.LEFT,
draggable: true, // default true
contextMenu: true, // default true
toolBar: true, // default true
nodeMenu: true, // default true
keypress: true, // default true
locale: 'en', // [zh_CN,zh_TW,en,ja,pt,ru] waiting for PRs
overflowHidden: false, // default false
mainLinkStyle: 2, // [1,2] default 1
mouseSelectionButton: 0, // 0 for left button, 2 for right button, default 0
contextMenuOption: {
focus: true,
link: true,
extend: [
{
name: 'Node edit',
onclick: () => {
alert('extend menu')
},
},
],
},
before: {
insertSibling(el, obj) {
return true
},
async addChild(el, obj) {
await sleep()
return true
},
},
}
let mind = new MindElixir(options)
mind.install(plugin) // install your plugin
// create new map data
const data = MindElixir.new('new topic')
// or `example`
// or the data return from `.getData()`
mind.init(data)
// get a node
MindElixir.E('node-id')
```
### 데이터 구조
```javascript
// whole node data structure up to now
const nodeData = {
topic: 'node topic',
id: 'bd1c24420cd2c2f5',
style: { fontSize: '32', color: '#3298db', background: '#ecf0f1' },
expanded: true,
parent: null,
tags: ['Tag'],
icons: ['😀'],
hyperLink: 'https://github.com/ssshooter/mind-elixir-core',
image: {
url: 'https://raw.githubusercontent.com/ssshooter/mind-elixir-core/master/images/logo2.png', // required
// you need to query the height and width of the image and calculate the appropriate value to display the image
height: 90, // required
width: 90, // required
},
children: [
{
topic: 'child',
id: 'xxxx',
// ...
},
],
}
```
### 이벤트 처리
```javascript
mind.bus.addListener('operation', operation => {
console.log(operation)
// return {
// name: action name,
// obj: target object
// }
// name: [insertSibling|addChild|removeNode|beginEdit|finishEdit]
// obj: target
// name: moveNode
// obj: {from:target1,to:target2}
})
mind.bus.addListener('selectNodes', nodes => {
console.log(nodes)
})
mind.bus.addListener('expandNode', node => {
console.log('expandNode: ', node)
})
```
### 데이터 내보내기와 가져오기
```javascript
// data export
const data = mind.getData() // javascript object, see src/example.js
mind.getDataString() // stringify object
// data import
// initiate
let mind = new MindElixir(options)
mind.init(data)
// data update
mind.refresh(data)
```
### 작업 가드
```javascript
let mind = new MindElixir({
// ...
before: {
insertSibling(el, obj) {
console.log(el, obj)
if (this.currentNode.nodeObj.parent.root) {
return false
}
return true
},
async addChild(el, obj) {
await sleep()
if (this.currentNode.nodeObj.parent.root) {
return false
}
return true
},
},
})
```
## 이미지로 내보내기
### 방법 1
```typescript
const mind = {
/** mind elixir instance */
}
const downloadPng = async () => {
const blob = await mind.exportPng() // Get a Blob!
if (!blob) return
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = 'filename.png'
a.click()
URL.revokeObjectURL(url)
}
```
### 방법 2
Install `@ssshooter/modern-screenshot`, then:
```typescript
import { domToPng } from '@ssshooter/modern-screenshot'
const download = async () => {
const dataUrl = await domToPng(mind.nodes, {
onCloneNode: node => {
const n = node as HTMLDivElement
n.style.position = ''
n.style.top = ''
n.style.left = ''
n.style.bottom = ''
n.style.right = ''
},
padding: 300,
quality: 1,
})
const link = document.createElement('a')
link.download = 'screenshot.png'
link.href = dataUrl
link.click()
}
```
## 테마
```javascript
const options = {
// ...
theme: {
name: 'Dark',
// main lines color palette
palette: ['#848FA0', '#748BE9', '#D2F9FE', '#4145A5', '#789AFA', '#706CF4', '#EF987F', '#775DD5', '#FCEECF', '#DA7FBC'],
// overwrite css variables
cssVar: {
'--main-color': '#ffffff',
'--main-bgcolor': '#4c4f69',
'--color': '#cccccc',
'--bgcolor': '#252526',
'--panel-color': '255, 255, 255',
'--panel-bgcolor': '45, 55, 72',
},
// all variables see /src/index.less
},
// ...
}
// ...
mind.changeTheme({
name: 'Latte',
palette: ['#dd7878', '#ea76cb', '#8839ef', '#e64553', '#fe640b', '#df8e1d', '#40a02b', '#209fb5', '#1e66f5', '#7287fd'],
cssVar: {
'--main-color': '#444446',
'--main-bgcolor': '#ffffff',
'--color': '#777777',
'--bgcolor': '#f6f6f6',
},
})
```
Be aware that Mind Elixir will not observe the change of `prefers-color-scheme`. Please change the theme **manually** when the scheme changes.
## 단축키
| 단축키 | 기능 |
| ----------------- | -------------------------- |
| Enter | 형제 노드 삽입 |
| Tab | 자식 노드 삽입 |
| F1 | 맵 중앙 정렬 |
| F2 | 현재 노드 편집 시작 |
| ↑ | 이전 형제 노드 선택 |
| ↓ | 다음 형제 노드 선택 |
| ← / → | 부모 또는 첫 자식 노드 선택 |
| PageUp / Alt + ↑ | 노드 위로 이동 |
| PageDown / Alt + ↓| 노드 아래로 이동 |
| Ctrl + ↑ | 레이아웃을 측면으로 변경 |
| Ctrl + ← | 레이아웃을 왼쪽으로 변경 |
| Ctrl + → | 레이아웃을 오른쪽으로 변경 |
| Ctrl + C | 현재 노드 복사 |
| Ctrl + V | 복사된 노드 붙여넣기 |
| Ctrl + "+" | 마인드맵 확대 |
| Ctrl + "-" | 마인드맵 축소 |
| Ctrl + 0 | 확대/축소 수준 초기화 |
## 생태계
- [@mind-elixir/node-menu](https://github.com/ssshooter/node-menu)
- [@mind-elixir/node-menu-neo](https://github.com/ssshooter/node-menu-neo)
- [@mind-elixir/export-xmind](https://github.com/ssshooter/export-xmind)
- [@mind-elixir/export-html](https://github.com/ssshooter/export-html)
- [mind-elixir-react](https://github.com/ssshooter/mind-elixir-react)
PR은 언제나 환영입니다!
## 개발
```
pnpm i
pnpm dev
```
Test generated files with `dev.dist.ts`:
```
pnpm build
pnpm link ./
```
Update docs:
```
# Install api-extractor
pnpm install -g @microsoft/api-extractor
# Maintain /src/docs.ts
# Generate docs
pnpm doc
pnpm doc:md
```
## 감사의 말
- [@viselect/vanilla](https://github.com/simonwep/selection/tree/master/packages/vanilla)
## 기여자
Mind Elixir에 기여해 주셔서 감사합니다! 여러분의 지원과 헌신이 이 프로젝트를 더 좋게 만들어 갑니다.
<a href="https://github.com/SSShooter/mind-elixir-core/graphs/contributors">
<img src="https://contrib.rocks/image?repo=SSShooter/mind-elixir-core&columns=6" />
</a>

View File

@ -1,430 +0,0 @@
<p align="center">
<a href="https://docs.mind-elixir.com" target="_blank" rel="noopener noreferrer">
<img width="150" src="https://raw.githubusercontent.com/ssshooter/mind-elixir-core/master/images/logo2.png" alt="mindelixir logo2">
</a>
<h1 align="center">Mind Elixir</h1>
</p>
<p align="center">
<a href="https://www.npmjs.com/package/mind-elixir">
<img src="https://img.shields.io/npm/v/mind-elixir" alt="version">
</a>
<a href="https://github.com/ssshooter/mind-elixir-core/blob/master/LICENSE">
<img src="https://img.shields.io/npm/l/mind-elixir" alt="license">
</a>
<a href="https://app.codacy.com/gh/ssshooter/mind-elixir-core?utm_source=github.com&utm_medium=referral&utm_content=ssshooter/mind-elixir-core&utm_campaign=Badge_Grade_Settings">
<img src="https://api.codacy.com/project/badge/Grade/09fadec5bf094886b30cea6aabf3a88b" alt="code quality">
</a>
<a href="https://bundlephobia.com/result?p=mind-elixir">
<img src="https://badgen.net/bundlephobia/dependency-count/mind-elixir" alt="dependency-count">
</a>
<a href="https://packagephobia.com/result?p=mind-elixir">
<img src="https://packagephobia.com/badge?p=mind-elixir" alt="package size">
</a>
</p>
[English](/readme.md) |
[中文](/readme/zh.md) |
[Español](/readme/es.md) |
[Français](/readme/fr.md) |
[Português](/readme/pt.md) |
[Русский](/readme/ru.md) |
[日本語](/readme/ja.md) |
[한국어](/readme/ko.md)
Mind Elixir é um núcleo JavaScript de mapa mental de código aberto. Você pode usá-lo com qualquer framework frontend de sua preferência.
Características:
- Leve
- Alto desempenho
- Independente de framework
- Plugável
- Plugin integrado de arrastar e soltar / edição de nós
- Exportação como SVG / PNG / Html
- Resumo de nós
- Suporte a operações em massa
- Desfazer / Refazer
- Atalhos eficientes
- Estilização fácil dos nós com variáveis CSS
<details>
<summary>Índice</summary>
- [Experimente agora](#experimente-agora)
- [Playground](#playground)
- [Documentação](#documentação)
- [Uso](#uso)
- [Instalação](#instalação)
- [NPM](#npm)
- [Tag de script](#tag-de-script)
- [Inicialização](#inicialização)
- [Estrutura de Dados](#estrutura-de-dados)
- [Manipulação de Eventos](#manipulação-de-eventos)
- [Exportação e Importação de Dados](#exportação-e-importação-de-dados)
- [Guardas de Operação](#guardas-de-operação)
- [Exportar como Imagem](#exportar-como-imagem)
- [Solução 1](#solução-1)
- [Solução 2](#solução-2)
- [Tema](#tema)
- [Atalhos](#atalhos)
- [Ecossistema](#ecossistema)
- [Desenvolvimento](#desenvolvimento)
- [Agradecimentos](#agradecimentos)
- [Contribuidores](#contribuidores)
</details>
## Experimente agora
![mindelixir](https://raw.githubusercontent.com/ssshooter/mind-elixir-core/master/images/screenshot2.png)
https://mind-elixir.com/
### Playground
- Vanilla JS - https://codepen.io/ssshooter/pen/OJrJowN
- React - https://codesandbox.io/s/mind-elixir-3-x-react-18-x-vy9fcq
- Vue3 - https://codesandbox.io/s/mind-elixir-3-x-vue3-lth484
- Vue2 - https://codesandbox.io/s/mind-elixir-3-x-vue-2-x-5kdfjp
## Documentação
https://docs.mind-elixir.com/
## Uso
### Instalação
#### NPM
```bash
npm i mind-elixir -S
```
```javascript
import MindElixir from 'mind-elixir'
```
#### Tag de script
```html
<script type="module" src="https://cdn.jsdelivr.net/npm/mind-elixir/dist/MindElixir.js"></script>
```
### Inicialização
```html
<div id="map"></div>
<style>
#map {
height: 500px;
width: 100%;
}
</style>
```
**Mudança Importante** desde a versão 1.0.0, `data` deve ser passado para `init()`, não para `options`.
```javascript
import MindElixir from 'mind-elixir'
import example from 'mind-elixir/dist/example1'
let options = {
el: '#map', // ou HTMLDivElement
direction: MindElixir.LEFT,
draggable: true, // padrão true
contextMenu: true, // padrão true
toolBar: true, // padrão true
nodeMenu: true, // padrão true
keypress: true, // padrão true
locale: 'pt', // [zh_CN,zh_TW,en,ja,pt,ru] aguardando PRs
overflowHidden: false, // padrão false
mainLinkStyle: 2, // [1,2] padrão 1
mouseSelectionButton: 0, // 0 para botão esquerdo, 2 para botão direito, padrão 0
contextMenuOption: {
focus: true,
link: true,
extend: [
{
name: 'Editar Nó',
onclick: () => {
alert('menu estendido')
},
},
],
},
before: {
insertSibling(el, obj) {
return true
},
async addChild(el, obj) {
await sleep()
return true
},
},
}
let mind = new MindElixir(options)
mind.install(plugin) // instale seu plugin
// criar novos dados do mapa
const data = MindElixir.new('novo tópico')
// ou `example`
// ou os dados retornados de `.getData()`
mind.init(data)
// obter um nó
MindElixir.E('node-id')
```
### Estrutura de Dados
```javascript
// estrutura completa de dados do nó até agora
const nodeData = {
topic: 'tópico do nó',
id: 'bd1c24420cd2c2f5',
style: { fontSize: '32', color: '#3298db', background: '#ecf0f1' },
expanded: true,
parent: null,
tags: ['Tag'],
icons: ['😀'],
hyperLink: 'https://github.com/ssshooter/mind-elixir-core',
image: {
url: 'https://raw.githubusercontent.com/ssshooter/mind-elixir-core/master/images/logo2.png', // obrigatório
// você precisa consultar a altura e largura da imagem e calcular o valor apropriado para exibir a imagem
height: 90, // obrigatório
width: 90, // obrigatório
},
children: [
{
topic: 'filho',
id: 'xxxx',
// ...
},
],
}
```
### Manipulação de Eventos
```javascript
mind.bus.addListener('operation', operation => {
console.log(operation)
// retorna {
// name: nome da ação,
// obj: objeto alvo
// }
// name: [insertSibling|addChild|removeNode|beginEdit|finishEdit]
// obj: alvo
// name: moveNode
// obj: {from:alvo1,to:alvo2}
})
mind.bus.addListener('selectNodes', nodes => {
console.log(nodes)
})
mind.bus.addListener('expandNode', node => {
console.log('expandNode: ', node)
})
```
### Exportação e Importação de Dados
```javascript
// exportação de dados
const data = mind.getData() // objeto javascript, veja src/example.js
mind.getDataString() // objeto em string
// importação de dados
// inicialização
let mind = new MindElixir(options)
mind.init(data)
// atualização de dados
mind.refresh(data)
```
### Guardas de Operação
```javascript
let mind = new MindElixir({
// ...
before: {
insertSibling(el, obj) {
console.log(el, obj)
if (this.currentNode.nodeObj.parent.root) {
return false
}
return true
},
async addChild(el, obj) {
await sleep()
if (this.currentNode.nodeObj.parent.root) {
return false
}
return true
},
},
})
```
## Exportar como Imagem
### Solução 1
```typescript
const mind = {
/** instância do mind elixir */
}
const downloadPng = async () => {
const blob = await mind.exportPng() // Obter um Blob!
if (!blob) return
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = 'arquivo.png'
a.click()
URL.revokeObjectURL(url)
}
```
### Solução 2
Instale `@ssshooter/modern-screenshot`, depois:
```typescript
import { domToPng } from '@ssshooter/modern-screenshot'
const download = async () => {
const dataUrl = await domToPng(mind.nodes, {
onCloneNode: node => {
const n = node as HTMLDivElement
n.style.position = ''
n.style.top = ''
n.style.left = ''
n.style.bottom = ''
n.style.right = ''
},
padding: 300,
quality: 1,
})
const link = document.createElement('a')
link.download = 'screenshot.png'
link.href = dataUrl
link.click()
}
```
## Tema
```javascript
const options = {
// ...
theme: {
name: 'Dark',
// main lines color palette
palette: ['#848FA0', '#748BE9', '#D2F9FE', '#4145A5', '#789AFA', '#706CF4', '#EF987F', '#775DD5', '#FCEECF', '#DA7FBC'],
// overwrite css variables
cssVar: {
'--main-color': '#ffffff',
'--main-bgcolor': '#4c4f69',
'--color': '#cccccc',
'--bgcolor': '#252526',
'--panel-color': '255, 255, 255',
'--panel-bgcolor': '45, 55, 72',
},
// all variables see /src/index.less
},
// ...
}
// ...
mind.changeTheme({
name: 'Latte',
palette: ['#dd7878', '#ea76cb', '#8839ef', '#e64553', '#fe640b', '#df8e1d', '#40a02b', '#209fb5', '#1e66f5', '#7287fd'],
cssVar: {
'--main-color': '#444446',
'--main-bgcolor': '#ffffff',
'--color': '#777777',
'--bgcolor': '#f6f6f6',
},
})
```
Be aware that Mind Elixir will not observe the change of `prefers-color-scheme`. Please change the theme **manually** when the scheme changes.
## Atalhos
| Atalho | Função |
| ------------------ | --------------------------------- |
| Enter | Inserir Nó Irmão |
| Tab | Inserir Nó Filho |
| F1 | Centralizar o Mapa |
| F2 | Começar a Editar o Nó Atual |
| ↑ | Selecionar o Nó Irmão Anterior |
| ↓ | Selecionar o Próximo Nó Irmão |
| ← / → | Selecionar Pai ou Primeiro Filho |
| PageUp / Alt + ↑ | Mover Nó para Cima |
| PageDown / Alt + ↓ | Mover Nó para Baixo |
| Ctrl + ↑ | Mudar Padrão de Layout para Lado |
| Ctrl + ← | Mudar Padrão de Layout para Esquerda |
| Ctrl + → | Mudar Padrão de Layout para Direita |
| Ctrl + C | Copiar o Nó Atual |
| Ctrl + V | Colar o Nó Copiado |
| Ctrl + "+" | Aumentar Zoom do Mapa Mental |
| Ctrl + "-" | Diminuir Zoom do Mapa Mental |
| Ctrl + 0 | Redefinir Nível de Zoom |
## Ecossistema
- [@mind-elixir/node-menu](https://github.com/ssshooter/node-menu)
- [@mind-elixir/node-menu-neo](https://github.com/ssshooter/node-menu-neo)
- [@mind-elixir/export-xmind](https://github.com/ssshooter/export-xmind)
- [@mind-elixir/export-html](https://github.com/ssshooter/export-html)
- [mind-elixir-react](https://github.com/ssshooter/mind-elixir-react)
PRs são bem-vindos!
## Desenvolvimento
```
pnpm i
pnpm dev
```
Testar arquivos gerados com `dev.dist.ts`:
```
pnpm build
pnpm link ./
```
Atualizar documentação:
```
# Instalar api-extractor
pnpm install -g @microsoft/api-extractor
# Manter /src/docs.ts
# Gerar documentação
pnpm doc
pnpm doc:md
```
## Agradecimentos
- [@viselect/vanilla](https://github.com/simonwep/selection/tree/master/packages/vanilla)
## Contribuidores
Obrigado por suas contribuições ao Mind Elixir! Seu apoio e dedicação tornam este projeto melhor.
<a href="https://github.com/SSShooter/mind-elixir-core/graphs/contributors">
<img src="https://contrib.rocks/image?repo=SSShooter/mind-elixir-core&columns=6" />
</a>

View File

@ -1,430 +0,0 @@
<p align="center">
<a href="https://docs.mind-elixir.com" target="_blank" rel="noopener noreferrer">
<img width="150" src="https://raw.githubusercontent.com/ssshooter/mind-elixir-core/master/images/logo2.png" alt="mindelixir logo2">
</a>
<h1 align="center">Mind Elixir</h1>
</p>
<p align="center">
<a href="https://www.npmjs.com/package/mind-elixir">
<img src="https://img.shields.io/npm/v/mind-elixir" alt="version">
</a>
<a href="https://github.com/ssshooter/mind-elixir-core/blob/master/LICENSE">
<img src="https://img.shields.io/npm/l/mind-elixir" alt="license">
</a>
<a href="https://app.codacy.com/gh/ssshooter/mind-elixir-core?utm_source=github.com&utm_medium=referral&utm_content=ssshooter/mind-elixir-core&utm_campaign=Badge_Grade_Settings">
<img src="https://api.codacy.com/project/badge/Grade/09fadec5bf094886b30cea6aabf3a88b" alt="code quality">
</a>
<a href="https://bundlephobia.com/result?p=mind-elixir">
<img src="https://badgen.net/bundlephobia/dependency-count/mind-elixir" alt="dependency-count">
</a>
<a href="https://packagephobia.com/result?p=mind-elixir">
<img src="https://packagephobia.com/badge?p=mind-elixir" alt="package size">
</a>
</p>
[English](/readme.md) |
[中文](/readme/zh.md) |
[Español](/readme/es.md) |
[Français](/readme/fr.md) |
[Português](/readme/pt.md) |
[Русский](/readme/ru.md) |
[日本語](/readme/ja.md) |
[한국어](/readme/ko.md)
Mind Elixir - это библиотека с открытым исходным кодом для создания интеллект-карт на JavaScript. Вы можете использовать её с любым frontend-фреймворком.
Особенности:
- Легковесность
- Высокая производительность
- Независимость от фреймворков
- Расширяемость с помощью плагинов
- Встроенные плагины для перетаскивания и редактирования узлов
- Экспорт в SVG / PNG / HTML
- Возможность сворачивать узлы
- Поддержка массовых операций
- Отмена / Повтор действий
- Эффективные горячие клавиши
- Простая стилизация узлов с помощью CSS переменных
<details>
<summary>Содержание</summary>
- [Попробовать сейчас](#попробовать-сейчас)
- [Playground](#playground)
- [Документация](#документация)
- [Использование](#использование)
- [Установка](#установка)
- [NPM](#npm)
- [Script tag](#script-tag)
- [Инициализация](#инициализация)
- [Структура данных](#структура-данных)
- [Обработка событий](#обработка-событий)
- [Экспорт и импорт данных](#экспорт-и-импорт-данных)
- [Контроль операций](#контроль-операций)
- [Экспорт в изображение](#экспорт-в-изображение)
- [Способ 1](#способ-1)
- [Способ 2](#способ-2)
- [Тема](#тема)
- [Горячие клавиши](#горячие-клавиши)
- [Экосистема](#экосистема)
- [Разработка](#разработка)
- [Благодарности](#благодарности)
- [Контрибьюторы](#контрибьюторы)
</details>
## Попробовать сейчас
![mindelixir](https://raw.githubusercontent.com/ssshooter/mind-elixir-core/master/images/screenshot2.png)
https://mind-elixir.com/
### Playground
- Vanilla JS - https://codepen.io/ssshooter/pen/OJrJowN
- React - https://codesandbox.io/s/mind-elixir-3-x-react-18-x-vy9fcq
- Vue3 - https://codesandbox.io/s/mind-elixir-3-x-vue3-lth484
- Vue2 - https://codesandbox.io/s/mind-elixir-3-x-vue-2-x-5kdfjp
## Документация
https://docs.mind-elixir.com/
## Использование
### Установка
#### NPM
```bash
npm i mind-elixir -S
```
```javascript
import MindElixir from 'mind-elixir'
```
#### Script tag
```html
<script type="module" src="https://cdn.jsdelivr.net/npm/mind-elixir/dist/MindElixir.js"></script>
```
### Инициализация
```html
<div id="map"></div>
<style>
#map {
height: 500px;
width: 100%;
}
</style>
```
**Breaking Change** since 1.0.0, `data` should be passed to `init()`, not `options`.
```javascript
import MindElixir from 'mind-elixir'
import example from 'mind-elixir/dist/example1'
let options = {
el: '#map', // or HTMLDivElement
direction: MindElixir.LEFT,
draggable: true, // default true
contextMenu: true, // default true
toolBar: true, // default true
nodeMenu: true, // default true
keypress: true, // default true
locale: 'en', // [zh_CN,zh_TW,en,ja,pt,ru] waiting for PRs
overflowHidden: false, // default false
mainLinkStyle: 2, // [1,2] default 1
mouseSelectionButton: 0, // 0 for left button, 2 for right button, default 0
contextMenuOption: {
focus: true,
link: true,
extend: [
{
name: 'Node edit',
onclick: () => {
alert('extend menu')
},
},
],
},
before: {
insertSibling(el, obj) {
return true
},
async addChild(el, obj) {
await sleep()
return true
},
},
}
let mind = new MindElixir(options)
mind.install(plugin) // install your plugin
// create new map data
const data = MindElixir.new('new topic')
// or `example`
// or the data return from `.getData()`
mind.init(data)
// get a node
MindElixir.E('node-id')
```
### Структура данных
```javascript
// whole node data structure up to now
const nodeData = {
topic: 'node topic',
id: 'bd1c24420cd2c2f5',
style: { fontSize: '32', color: '#3298db', background: '#ecf0f1' },
expanded: true,
parent: null,
tags: ['Tag'],
icons: ['😀'],
hyperLink: 'https://github.com/ssshooter/mind-elixir-core',
image: {
url: 'https://raw.githubusercontent.com/ssshooter/mind-elixir-core/master/images/logo2.png', // required
// you need to query the height and width of the image and calculate the appropriate value to display the image
height: 90, // required
width: 90, // required
},
children: [
{
topic: 'child',
id: 'xxxx',
// ...
},
],
}
```
### Обработка событий
```javascript
mind.bus.addListener('operation', operation => {
console.log(operation)
// return {
// name: action name,
// obj: target object
// }
// name: [insertSibling|addChild|removeNode|beginEdit|finishEdit]
// obj: target
// name: moveNode
// obj: {from:target1,to:target2}
})
mind.bus.addListener('selectNodes', nodes => {
console.log(nodes)
})
mind.bus.addListener('expandNode', node => {
console.log('expandNode: ', node)
})
```
### Экспорт и импорт данных
```javascript
// data export
const data = mind.getData() // javascript object, see src/example.js
mind.getDataString() // stringify object
// data import
// initiate
let mind = new MindElixir(options)
mind.init(data)
// data update
mind.refresh(data)
```
### Контроль операций
```javascript
let mind = new MindElixir({
// ...
before: {
insertSibling(el, obj) {
console.log(el, obj)
if (this.currentNode.nodeObj.parent.root) {
return false
}
return true
},
async addChild(el, obj) {
await sleep()
if (this.currentNode.nodeObj.parent.root) {
return false
}
return true
},
},
})
```
## Экспорт в изображение
### Способ 1
```typescript
const mind = {
/** mind elixir instance */
}
const downloadPng = async () => {
const blob = await mind.exportPng() // Get a Blob!
if (!blob) return
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = 'filename.png'
a.click()
URL.revokeObjectURL(url)
}
```
### Способ 2
Install `@ssshooter/modern-screenshot`, then:
```typescript
import { domToPng } from '@ssshooter/modern-screenshot'
const download = async () => {
const dataUrl = await domToPng(mind.nodes, {
onCloneNode: node => {
const n = node as HTMLDivElement
n.style.position = ''
n.style.top = ''
n.style.left = ''
n.style.bottom = ''
n.style.right = ''
},
padding: 300,
quality: 1,
})
const link = document.createElement('a')
link.download = 'screenshot.png'
link.href = dataUrl
link.click()
}
```
## Тема
```javascript
const options = {
// ...
theme: {
name: 'Dark',
// main lines color palette
palette: ['#848FA0', '#748BE9', '#D2F9FE', '#4145A5', '#789AFA', '#706CF4', '#EF987F', '#775DD5', '#FCEECF', '#DA7FBC'],
// overwrite css variables
cssVar: {
'--main-color': '#ffffff',
'--main-bgcolor': '#4c4f69',
'--color': '#cccccc',
'--bgcolor': '#252526',
'--panel-color': '255, 255, 255',
'--panel-bgcolor': '45, 55, 72',
},
// all variables see /src/index.less
},
// ...
}
// ...
mind.changeTheme({
name: 'Latte',
palette: ['#dd7878', '#ea76cb', '#8839ef', '#e64553', '#fe640b', '#df8e1d', '#40a02b', '#209fb5', '#1e66f5', '#7287fd'],
cssVar: {
'--main-color': '#444446',
'--main-bgcolor': '#ffffff',
'--color': '#777777',
'--bgcolor': '#f6f6f6',
},
})
```
Be aware that Mind Elixir will not observe the change of `prefers-color-scheme`. Please change the theme **manually** when the scheme changes.
## Горячие клавиши
| Комбинация клавиш | Функция |
| ------------------- | -------------------------------------- |
| Enter | Вставить соседний узел |
| Tab | Вставить дочерний узел |
| F1 | Центрировать карту |
| F2 | Начать редактирование текущего узла |
| ↑ | Выбрать предыдущий узел |
| ↓ | Выбрать следующий узел |
| ← / → | Выбрать родительский или первый дочерний узел |
| PageUp / Alt + ↑ | Переместить узел вверх |
| PageDown / Alt + ↓ | Переместить узел вниз |
| Ctrl + ↑ | Изменить шаблон расположения на боковой|
| Ctrl + ← | Изменить шаблон расположения на левый |
| Ctrl + → | Изменить шаблон расположения на правый |
| Ctrl + C | Копировать текущий узел |
| Ctrl + V | Вставить скопированный узел |
| Ctrl + "+" | Увеличить масштаб карты |
| Ctrl + "-" | Уменьшить масштаб карты |
| Ctrl + 0 | Сбросить масштаб |
## Экосистема
- [@mind-elixir/node-menu](https://github.com/ssshooter/node-menu)
- [@mind-elixir/node-menu-neo](https://github.com/ssshooter/node-menu-neo)
- [@mind-elixir/export-xmind](https://github.com/ssshooter/export-xmind)
- [@mind-elixir/export-html](https://github.com/ssshooter/export-html)
- [mind-elixir-react](https://github.com/ssshooter/mind-elixir-react)
PRs are welcome!
## Разработка
```
pnpm i
pnpm dev
```
Test generated files with `dev.dist.ts`:
```
pnpm build
pnpm link ./
```
Update docs:
```
# Install api-extractor
pnpm install -g @microsoft/api-extractor
# Maintain /src/docs.ts
# Generate docs
pnpm doc
pnpm doc:md
```
## Благодарности
- [@viselect/vanilla](https://github.com/simonwep/selection/tree/master/packages/vanilla)
## Контрибьюторы
Спасибо за ваш вклад в Mind Elixir! Ваша поддержка и преданность делают этот проект лучше.
<a href="https://github.com/SSShooter/mind-elixir-core/graphs/contributors">
<img src="https://contrib.rocks/image?repo=SSShooter/mind-elixir-core&columns=6" />
</a>

View File

@ -1,430 +0,0 @@
<p align="center">
<a href="https://docs.mind-elixir.com" target="_blank" rel="noopener noreferrer">
<img width="150" src="https://raw.githubusercontent.com/ssshooter/mind-elixir-core/master/images/logo2.png" alt="mindelixir logo2">
</a>
<h1 align="center">Mind Elixir</h1>
</p>
<p align="center">
<a href="https://www.npmjs.com/package/mind-elixir">
<img src="https://img.shields.io/npm/v/mind-elixir" alt="version">
</a>
<a href="https://github.com/ssshooter/mind-elixir-core/blob/master/LICENSE">
<img src="https://img.shields.io/npm/l/mind-elixir" alt="license">
</a>
<a href="https://app.codacy.com/gh/ssshooter/mind-elixir-core?utm_source=github.com&utm_medium=referral&utm_content=ssshooter/mind-elixir-core&utm_campaign=Badge_Grade_Settings">
<img src="https://api.codacy.com/project/badge/Grade/09fadec5bf094886b30cea6aabf3a88b" alt="code quality">
</a>
<a href="https://bundlephobia.com/result?p=mind-elixir">
<img src="https://badgen.net/bundlephobia/dependency-count/mind-elixir" alt="dependency-count">
</a>
<a href="https://packagephobia.com/result?p=mind-elixir">
<img src="https://packagephobia.com/badge?p=mind-elixir" alt="package size">
</a>
</p>
[English](/readme.md) |
[中文](/readme/zh.md) |
[Español](/readme/es.md) |
[Français](/readme/fr.md) |
[Português](/readme/pt.md) |
[Русский](/readme/ru.md) |
[日本語](/readme/ja.md) |
[한국어](/readme/ko.md)
Mind elixir 是一个开源的 JavaScript 思维导图核心。你可以在任何前端框架中使用它。
特点:
- 轻量级
- 高性能
- 框架无关
- 插件化
- 内置拖放 / 节点编辑插件
- 导出为 SVG / PNG / Html
- 总结节点
- 支持批量操作
- 撤销 / 重做
- 高效快捷键
- 轻松使用 CSS 变量样式化节点
<details>
<summary>目录</summary>
- [立即试用](#立即试用)
- [演示](#演示)
- [文档](#文档)
- [使用](#使用)
- [安装](#安装)
- [NPM](#npm)
- [Script 标签](#script-标签)
- [初始化](#初始化)
- [数据结构](#数据结构)
- [事件处理](#事件处理)
- [数据导出和导入](#数据导出和导入)
- [操作守卫](#操作守卫)
- [导出为图片](#导出为图片)
- [方案 1](#方案-1)
- [方案 2](#方案-2)
- [主题](#主题)
- [快捷键](#快捷键)
- [生态](#生态)
- [开发](#开发)
- [感谢](#感谢)
- [贡献者](#贡献者)
</details>
## 立即试用
![mindelixir](https://raw.githubusercontent.com/ssshooter/mind-elixir-core/master/images/screenshot2.png)
https://mind-elixir.com/
### 演示
- 原生 JS - https://codepen.io/ssshooter/pen/OJrJowN
- React - https://codesandbox.io/s/mind-elixir-3-x-react-18-x-vy9fcq
- Vue3 - https://codesandbox.io/s/mind-elixir-3-x-vue3-lth484
- Vue2 - https://codesandbox.io/s/mind-elixir-3-x-vue-2-x-5kdfjp
## 文档
https://docs.mind-elixir.com/
## 使用
### 安装
#### NPM
```bash
npm i mind-elixir -S
```
```javascript
import MindElixir from 'mind-elixir'
```
#### Script 标签
```html
<script type="module" src="https://cdn.jsdelivr.net/npm/mind-elixir/dist/MindElixir.js"></script>
```
### 初始化
```html
<div id="map"></div>
<style>
#map {
height: 500px;
width: 100%;
}
</style>
```
**重大变更** 自 1.0.0 起,`data` 应传递给 `init()`,而不是 `options`
```javascript
import MindElixir from 'mind-elixir'
import example from 'mind-elixir/dist/example1'
let options = {
el: '#map', // 或 HTMLDivElement
direction: MindElixir.LEFT,
draggable: true, // 默认 true
contextMenu: true, // 默认 true
toolBar: true, // 默认 true
nodeMenu: true, // 默认 true
keypress: true, // 默认 true
locale: 'en', // [zh_CN,zh_TW,en,ja,pt,ru] 等待 PRs
overflowHidden: false, // 默认 false
mainLinkStyle: 2, // [1,2] 默认 1
mouseSelectionButton: 0, // 0 为左键2 为右键,默认 0
contextMenuOption: {
focus: true,
link: true,
extend: [
{
name: '节点编辑',
onclick: () => {
alert('扩展菜单')
},
},
],
},
before: {
insertSibling(el, obj) {
return true
},
async addChild(el, obj) {
await sleep()
return true
},
},
}
let mind = new MindElixir(options)
mind.install(plugin) // 安装你的插件
// 创建新的地图数据
const data = MindElixir.new('new topic')
// 或 `example`
// 或从 `.getData()` 返回的数据
mind.init(data)
// 获取一个节点
MindElixir.E('node-id')
```
### 数据结构
```javascript
// 到目前为止的整个节点数据结构
const nodeData = {
topic: '节点主题',
id: 'bd1c24420cd2c2f5',
style: { fontSize: '32', color: '#3298db', background: '#ecf0f1' },
expanded: true,
parent: null,
tags: ['标签'],
icons: ['😀'],
hyperLink: 'https://github.com/ssshooter/mind-elixir-core',
image: {
url: 'https://raw.githubusercontent.com/ssshooter/mind-elixir-core/master/images/logo2.png', // 必填
// 你需要查询图片的高度和宽度,并计算显示图片的适当值
height: 90, // 必填
width: 90, // 必填
},
children: [
{
topic: '子节点',
id: 'xxxx',
// ...
},
],
}
```
### 事件处理
```javascript
mind.bus.addListener('operation', operation => {
console.log(operation)
// 返回 {
// name: 操作名称,
// obj: 目标对象
// }
// name: [insertSibling|addChild|removeNode|beginEdit|finishEdit]
// obj: 目标
// name: moveNode
// obj: {from:目标1,to:目标2}
})
mind.bus.addListener('selectNodes', nodes => {
console.log(nodes)
})
mind.bus.addListener('expandNode', node => {
console.log('expandNode: ', node)
})
```
### 数据导出和导入
```javascript
// 数据导出
const data = mind.getData() // JavaScript 对象,见 src/example.js
mind.getDataString() // 字符串化对象
// 数据导入
// 初始化
let mind = new MindElixir(options)
mind.init(data)
// 数据更新
mind.refresh(data)
```
### 操作守卫
```javascript
let mind = new MindElixir({
// ...
before: {
insertSibling(el, obj) {
console.log(el, obj)
if (this.currentNode.nodeObj.parent.root) {
return false
}
return true
},
async addChild(el, obj) {
await sleep()
if (this.currentNode.nodeObj.parent.root) {
return false
}
return true
},
},
})
```
## 导出为图片
### 方案 1
```typescript
const mind = {
/** mind elixir 实例 */
}
const downloadPng = async () => {
const blob = await mind.exportPng() // 获取 Blob
if (!blob) return
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = 'filename.png'
a.click()
URL.revokeObjectURL(url)
}
```
### 方案 2
安装 `@ssshooter/modern-screenshot`,然后:
```typescript
import { domToPng } from '@ssshooter/modern-screenshot'
const download = async () => {
const dataUrl = await domToPng(mind.nodes, {
onCloneNode: node => {
const n = node as HTMLDivElement
n.style.position = ''
n.style.top = ''
n.style.left = ''
n.style.bottom = ''
n.style.right = ''
},
padding: 300,
quality: 1,
})
const link = document.createElement('a')
link.download = 'screenshot.png'
link.href = dataUrl
link.click()
}
```
## 主题
```javascript
const options = {
// ...
theme: {
name: 'Dark',
// 主线颜色调色板
palette: ['#848FA0', '#748BE9', '#D2F9FE', '#4145A5', '#789AFA', '#706CF4', '#EF987F', '#775DD5', '#FCEECF', '#DA7FBC'],
// 覆盖 CSS 变量
cssVar: {
'--main-color': '#ffffff',
'--main-bgcolor': '#4c4f69',
'--color': '#cccccc',
'--bgcolor': '#252526',
'--panel-color': '255, 255, 255',
'--panel-bgcolor': '45, 55, 72',
},
// 所有变量见 /src/index.less
},
// ...
}
// ...
mind.changeTheme({
name: 'Latte',
palette: ['#dd7878', '#ea76cb', '#8839ef', '#e64553', '#fe640b', '#df8e1d', '#40a02b', '#209fb5', '#1e66f5', '#7287fd'],
cssVar: {
'--main-color': '#444446',
'--main-bgcolor': '#ffffff',
'--color': '#777777',
'--bgcolor': '#f6f6f6',
},
})
```
请注意Mind Elixir 不会观察 `prefers-color-scheme` 的变化。当方案变化时,请**手动**更改主题。
## 快捷键
| 快捷键 | 功能 |
| ------------------ | -------------------------------- |
| Enter | 插入兄弟节点 |
| Tab | 插入子节点 |
| F1 | 居中地图 |
| F2 | 开始编辑当前节点 |
| ↑ | 选择上一个兄弟节点 |
| ↓ | 选择下一个兄弟节点 |
| ← / → | 选择父节点或第一个子节点 |
| PageUp / Alt + ↑ | 上移节点 |
| PageDown / Alt + ↓ | 下移节点 |
| Ctrl + ↑ | 更改布局模式为侧面 |
| Ctrl + ← | 更改布局模式为左侧 |
| Ctrl + → | 更改布局模式为右侧 |
| Ctrl + C | 复制当前节点 |
| Ctrl + V | 粘贴复制的节点 |
| Ctrl + "+" | 放大思维导图 |
| Ctrl + "-" | 缩小思维导图 |
| Ctrl + 0 | 重置缩放级别 |
## 生态
- [@mind-elixir/node-menu](https://github.com/ssshooter/node-menu)
- [@mind-elixir/node-menu-neo](https://github.com/ssshooter/node-menu-neo)
- [@mind-elixir/export-xmind](https://github.com/ssshooter/export-xmind)
- [@mind-elixir/export-html](https://github.com/ssshooter/export-html)
- [mind-elixir-react](https://github.com/ssshooter/mind-elixir-react)
欢迎 PR
## 开发
```
pnpm i
pnpm dev
```
使用 `dev.dist.ts` 测试生成的文件:
```
pnpm build
pnpm link ./
```
更新文档:
```
# 安装 api-extractor
pnpm install -g @microsoft/api-extractor
# 维护 /src/docs.ts
# 生成文档
pnpm doc
pnpm doc:md
```
## 感谢
- [@viselect/vanilla](https://github.com/simonwep/selection/tree/master/packages/vanilla)
## 贡献者
感谢你们对 Mind Elixir 的贡献!你们的支持和奉献使这个项目变得更好。
<a href="https://github.com/SSShooter/mind-elixir-core/graphs/contributors">
<img src="https://contrib.rocks/image?repo=SSShooter/mind-elixir-core&columns=6" />
</a>

View File

@ -1,39 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<title>Mind Elixir</title>
<style>
/* test tailwind compatibility */
*,
::before,
::after {
box-sizing: border-box;
border-width: 0;
border-style: solid;
border-color: #e5e7eb;
}
#map,
#map2 {
margin-top: 30px;
height: 300px;
width: 100%;
}
</style>
</head>
<body>
<div id="map"></div>
<div id="map2"></div>
<script type="module">
import MindElixir from '/src/index.ts'
window.MindElixir = MindElixir
window.E = MindElixir.E
</script>
</body>
</html>

View File

@ -307,29 +307,52 @@ const addMarkdownStyles = (container) => {
width: 100%; width: 100%;
margin: 4px 0; margin: 4px 0;
font-size: 11px; font-size: 11px;
border: 1px solid #ddd; 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; /* 覆盖MindElixir的pre-wrap */
} }
.markdown-table th, .markdown-table th,
.markdown-table td { .markdown-table td {
border: 1px solid #ddd; border: 1px solid #e0e0e0;
padding: 4px 6px; padding: 8px 12px;
text-align: left; text-align: left;
vertical-align: top; vertical-align: top;
position: relative;
white-space: normal; /* 覆盖MindElixir的pre-wrap */
} }
.markdown-table th { .markdown-table th {
background-color: #f8f9fa; background-color: #f5f5f5;
font-weight: 600; font-weight: 600;
color: #333; color: #333;
text-align: center;
border-bottom: 1px solid #d0d0d0;
} }
.markdown-table tr:nth-child(even) { .markdown-table td {
background-color: #f8f9fa; background-color: #fff;
} }
.markdown-table tr:hover { .markdown-table tr:nth-child(even) td {
background-color: #e9ecef; 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;
} }
.markdown-content a { .markdown-content a {
@ -477,6 +500,7 @@ export const smartRenderNodeContent = (content) => {
} }
}; };
export default { export default {
renderMarkdownToHTML, renderMarkdownToHTML,
setNodeMarkdownContent, setNodeMarkdownContent,

View File

@ -1,261 +0,0 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>增强Markdown渲染测试</title>
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/prismjs@1.29.0/components/prism-core.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/prismjs@1.29.0/plugins/autoloader/prism-autoloader.min.js"></script>
<link href="https://cdn.jsdelivr.net/npm/prismjs@1.29.0/themes/prism.min.css" rel="stylesheet">
<style>
body {
font-family: Arial, sans-serif;
margin: 20px;
background-color: #f5f5f5;
}
.test-container {
background: white;
border: 1px solid #ddd;
border-radius: 8px;
padding: 20px;
margin: 20px 0;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.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;
border: 1px solid #e9ecef;
}
.markdown-content pre {
background: #f8f9fa;
border: 1px solid #e9ecef;
border-radius: 4px;
padding: 8px;
margin: 4px 0;
overflow-x: auto;
position: relative;
}
.markdown-content pre code {
background: none;
padding: 0;
color: #333;
font-size: 11px;
border: none;
display: block;
white-space: pre;
}
.markdown-table {
border-collapse: collapse;
width: 100%;
margin: 4px 0;
font-size: 11px;
border: 1px solid #ddd;
}
.markdown-table th,
.markdown-table td {
border: 1px solid #ddd;
padding: 4px 6px;
text-align: left;
vertical-align: top;
}
.markdown-table th {
background-color: #f8f9fa;
font-weight: 600;
color: #333;
}
.markdown-table tr:nth-child(even) {
background-color: #f8f9fa;
}
.markdown-table tr:hover {
background-color: #e9ecef;
}
.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;
}
</style>
</head>
<body>
<h1>增强Markdown渲染测试</h1>
<div class="test-container">
<h2>测试1: 代码块 + 表格组合</h2>
<div id="test1" class="markdown-content"></div>
</div>
<div class="test-container">
<h2>测试2: 复杂表格</h2>
<div id="test2" class="markdown-content"></div>
</div>
<div class="test-container">
<h2>测试3: 你的测试内容</h2>
<div id="test3" class="markdown-content"></div>
</div>
<script>
// 配置marked
marked.setOptions({
breaks: true,
gfm: true,
tables: true,
sanitize: false,
});
// 后处理函数
function postprocessHTML(html) {
let processedHTML = html
.replace(/<table>/g, '<table class="markdown-table">')
.replace(/<pre><code/g, '<pre class="markdown-code"><code');
// 创建临时DOM元素来处理语法高亮
const tempDiv = document.createElement('div');
tempDiv.innerHTML = processedHTML;
// 为代码块添加语法高亮
const codeBlocks = tempDiv.querySelectorAll('pre code');
codeBlocks.forEach(block => {
const language = block.className.match(/language-(\w+)/);
if (language) {
try {
block.innerHTML = Prism.highlight(block.textContent, Prism.languages[language[1]], language[1]);
} catch (e) {
console.warn('语法高亮失败:', e);
}
}
});
return tempDiv.innerHTML;
}
// 测试1: 代码块 + 表格组合
const test1Markdown = `\`\`\`js
console.log('hello, JavaScript')
\`\`\`
| Products | Price |
|----------|-------|
| Apple | 4 |
| Banana | 2 |`;
document.getElementById('test1').innerHTML = postprocessHTML(marked.parse(test1Markdown));
// 测试2: 复杂表格
const test2Markdown = `| 评估维度 | 权重 | 评分标准 | 计算公式 |
|---------|------|---------|---------|
| 完整性 | 0.3 | 缺失值比例 < 5% | $\\text{完整性} = 1 - \\frac{\\text{缺失值数量}}{\\text{总数据量}}$ |
| 准确性 | 0.3 | 误差率 < 3% | $\\text{准确性} = 1 - \\frac{\\text{错误记录数}}{\\text{总记录数}}$ |`;
document.getElementById('test2').innerHTML = postprocessHTML(marked.parse(test2Markdown));
// 测试3: 你的测试内容
const test3Markdown = `测试标题
这是一个**粗体**和*斜体*的测试。
表格测试
| 产品 | 价格 | 库存 |
|------|------|------|
| 苹果 | 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)`;
document.getElementById('test3').innerHTML = postprocessHTML(marked.parse(test3Markdown));
</script>
</body>
</html>

View File

@ -1,127 +0,0 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>表格渲染测试</title>
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
<style>
body {
font-family: Arial, sans-serif;
margin: 20px;
background: #f5f5f5;
}
.test-container {
background: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
margin-bottom: 20px;
}
.test-title {
color: #333;
border-bottom: 2px solid #007bff;
padding-bottom: 10px;
margin-bottom: 20px;
}
.markdown-table {
border-collapse: collapse;
width: 100%;
margin: 4px 0;
font-size: 11px;
border: 1px solid #ddd;
}
.markdown-table th,
.markdown-table td {
border: 1px solid #ddd;
padding: 4px 6px;
text-align: left;
vertical-align: top;
}
.markdown-table th {
background-color: #f8f9fa;
font-weight: 600;
color: #333;
}
.markdown-table tr:nth-child(even) {
background-color: #f8f9fa;
}
.markdown-table tr:hover {
background-color: #e9ecef;
}
.raw-content {
background: #f8f9fa;
padding: 10px;
border-radius: 4px;
font-family: monospace;
white-space: pre-wrap;
margin-bottom: 20px;
}
</style>
</head>
<body>
<div class="test-container">
<h1 class="test-title">Markdown表格渲染测试</h1>
<h3>测试内容1数据质量评估矩阵</h3>
<div class="raw-content" id="raw1"></div>
<div id="rendered1"></div>
<h3>测试内容2产品价格表</h3>
<div class="raw-content" id="raw2"></div>
<div id="rendered2"></div>
<h3>测试内容3简单表格</h3>
<div class="raw-content" id="raw3"></div>
<div id="rendered3"></div>
</div>
<script>
// 配置marked
marked.setOptions({
breaks: true,
gfm: true,
tables: true,
sanitize: false
});
// 测试内容
const testContent1 = `| 评估维度 | 权重 | 评分标准 | 计算公式 |
|---------|------|---------|---------|
| 完整性 | 0.3 | 缺失值比例 < 5% | $\\text{完整性} = 1 - \\frac{\\text{缺失值数量}}{\\text{总数据量}}$ |
| 准确性 | 0.3 | 误差率 < 3% | $\\text{准确性} = 1 - \\frac{\\text{错误记录数}}{\\text{总记录数}}$ |
| 一致性 | 0.2 | 数据格式统一度 > 95% | $\\text{一致性} = \\frac{\\text{符合格式标准记录数}}{\\text{总记录数}}$ |
| 及时性 | 0.2 | 数据更新频率 ≤ 24小时 | $\\text{及时性} = \\frac{\\text{最近24小时更新数据量}}{\\text{总数据量}}$ |`;
const testContent2 = `产品价格表
| 产品 | 价格 |
|------|------|
| 苹果 | 4元 |
| 香蕉 | 2元 |`;
const testContent3 = `| 姓名 | 年龄 | 城市 |
|------|------|------|
| 张三 | 25 | 北京 |
| 李四 | 30 | 上海 |`;
// 渲染函数
function renderContent(rawId, renderedId, content) {
document.getElementById(rawId).textContent = content;
try {
const html = marked.parse(content);
// 为表格添加样式类
const processedHtml = html.replace(/<table>/g, '<table class="markdown-table">');
document.getElementById(renderedId).innerHTML = processedHtml;
} catch (error) {
document.getElementById(renderedId).innerHTML = `<div style="color: red;">渲染失败: ${error.message}</div>`;
}
}
// 渲染所有测试内容
renderContent('raw1', 'rendered1', testContent1);
renderContent('raw2', 'rendered2', testContent2);
renderContent('raw3', 'rendered3', testContent3);
</script>
</body>
</html>

View File

@ -1,120 +0,0 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>表格渲染测试</title>
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
<style>
body {
font-family: Arial, sans-serif;
margin: 20px;
}
.test-container {
border: 1px solid #ccc;
padding: 20px;
margin: 10px 0;
}
.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;
}
</style>
</head>
<body>
<h1>表格渲染测试</h1>
<div class="test-container">
<h2>测试1: 简单表格</h2>
<div id="test1"></div>
</div>
<div class="test-container">
<h2>测试2: 复杂表格</h2>
<div id="test2"></div>
</div>
<div class="test-container">
<h2>测试3: 你的测试表格</h2>
<div id="test3"></div>
</div>
<script>
// 配置marked
marked.setOptions({
breaks: true,
gfm: true,
tables: true,
sanitize: false,
});
// 测试1: 简单表格
const test1Markdown = `| 产品 | 价格 |
|------|------|
| 苹果 | 4元 |
| 香蕉 | 2元 |`;
document.getElementById('test1').innerHTML = marked.parse(test1Markdown);
// 测试2: 复杂表格
const test2Markdown = `| 评估维度 | 权重 | 评分标准 | 计算公式 |
|---------|------|---------|---------|
| 完整性 | 0.3 | 缺失值比例 < 5% | $\\text{完整性} = 1 - \\frac{\\text{缺失值数量}}{\\text{总数据量}}$ |
| 准确性 | 0.3 | 误差率 < 3% | $\\text{准确性} = 1 - \\frac{\\text{错误记录数}}{\\text{总记录数}}$ |`;
document.getElementById('test2').innerHTML = marked.parse(test2Markdown);
// 测试3: 你的测试表格
const test3Markdown = `| 产品 | 价格 | 库存 |
|------|------|------|
| 苹果 | 4元 | 100个 |
| 香蕉 | 2元 | 50个 |`;
document.getElementById('test3').innerHTML = marked.parse(test3Markdown);
// 测试表格检测函数
function hasTable(content) {
if (!content || typeof content !== 'string') return false;
const lines = content.split('\n');
let hasTableRow = false;
let hasSeparator = false;
for (const line of lines) {
const trimmedLine = line.trim();
// 检查表格行(包含|字符)
if (trimmedLine.includes('|') && trimmedLine.split('|').length >= 3) {
hasTableRow = true;
}
// 检查分隔符行(包含-和|
if (trimmedLine.includes('|') && trimmedLine.includes('-') && /^[\s\|\-\:]+$/.test(trimmedLine)) {
hasSeparator = true;
}
}
return hasTableRow && hasSeparator;
}
// 测试表格检测
console.log('测试1表格检测:', hasTable(test1Markdown));
console.log('测试2表格检测:', hasTable(test2Markdown));
console.log('测试3表格检测:', hasTable(test3Markdown));
</script>
</body>
</html>

View File

@ -1,629 +0,0 @@
# AI思维导图系统API接口文档
## 📋 概述
本文档描述了AI思维导图系统的RESTful API接口包括思维导图管理、节点操作和AI服务等核心功能。
## 🔧 基础信息
- **Base URL**: `http://127.0.0.1:8000/api`
- **Content-Type**: `application/json`
- **数据格式**: 遵循MindElixir规范
## 📊 数据格式规范
### MindElixir节点格式
```json
{
"id": "string", // 唯一标识符,必填
"topic": "string", // 节点标题,必填
"data": {
"des": "string" // 节点描述,可选
},
"children": [ /* Node[] */ ], // 子节点数组,必填
"mindmapId": "string" // 思维导图ID可选
}
```
### 完整思维导图数据格式
```json
{
"nodeData": {
"id": "string", // 根节点ID
"topic": "string", // 根节点标题
"data": {
"des": "string" // 根节点描述
},
"children": [ /* Node[] */ ] // 子节点数组
}
}
```
### 数据库节点格式
```json
{
"id": "string", // 节点ID
"isRoot": boolean, // 是否为根节点
"parentId": "string", // 父节点ID
"childrenCount": number, // 子节点数量
"depth": number, // 节点深度
"title": "string", // 节点标题
"des": "string", // 节点描述
"createDate": "datetime", // 创建时间
"updateDate": "datetime", // 更新时间
"delete": boolean // 是否删除
}
```
## 🗺️ 思维导图管理接口
### 获取思维导图
**接口:** `GET /api/mindMaps/{id}`
**描述:** 根据ID获取思维导图的完整数据返回MindElixir格式的树形结构
**路径参数:**
- `id` (integer): 思维导图ID
**请求示例:**
```bash
GET /api/mindMaps/1
```
**响应示例:**
```json
{
"nodeData": {
"id": "root",
"topic": "数字教育平台设计要点",
"data": {
"des": "思维导图的根节点"
},
"children": [
{
"id": "node-1",
"topic": "用户体验设计",
"data": {
"des": "用户体验设计的重要性"
},
"children": [
{
"id": "node-1-1",
"topic": "界面交互设计",
"data": {
"des": "核心功能入口应控制在3次点击内可达"
},
"children": []
}
]
}
]
}
}
```
**错误响应:**
```json
{
"detail": "mindMap not found"
}
```
### 创建思维导图
**接口:** `POST /api/mindMaps`
**描述:** 创建新的思维导图,支持传入初始数据或创建空思维导图
**请求体:**
```json
{
"title": "新的思维导图", // 可选,思维导图名称
"data": { // 可选,初始思维导图数据
"topic": "根节点标题",
"data": {
"des": "根节点描述"
},
"children": []
}
}
```
**请求示例:**
```bash
POST /api/mindMaps
Content-Type: application/json
{
"title": "AI技术发展",
"data": {
"topic": "AI技术发展",
"data": {
"des": "人工智能技术发展历程"
},
"children": []
}
}
```
**响应示例:**
```json
{
"id": 1,
"title": "AI技术发展",
"nodeData": {
"id": "root",
"topic": "AI技术发展",
"data": {
"des": "人工智能技术发展历程"
},
"children": []
}
}
```
## 🔗 节点操作接口
### 批量添加节点
**接口:** `POST /api/mindMaps/addNodes`
**描述:** 向指定思维导图中批量添加节点,支持添加子节点和兄弟节点
**请求体:**
```json
{
"mindMapId": 1, // 思维导图ID
"nodes": [ // 节点数组
{
"title": "子节点1", // 节点标题
"des": "节点描述", // 节点描述
"parentId": "parent-id", // 父节点ID
"isRoot": false // 是否为根节点
}
]
}
```
**请求示例:**
```bash
POST /api/mindMaps/addNodes
Content-Type: application/json
{
"mindMapId": 1,
"nodes": [
{
"title": "机器学习",
"des": "机器学习算法和应用",
"parentId": "root"
},
{
"title": "深度学习",
"des": "深度学习技术发展",
"parentId": "root"
}
]
}
```
**响应示例:**
```json
{
"success": true,
"message": "成功创建 2 个节点",
"data": {
"mindMapId": "1",
"nodes": [
{
"id": "node-1",
"isRoot": false,
"parentId": "root",
"childrenCount": 0,
"depth": 1,
"title": "机器学习",
"des": "机器学习算法和应用",
"createDate": "2023-07-01T10:30:00Z",
"updateDate": "2023-07-01T10:30:00Z",
"delete": false
},
{
"id": "node-2",
"isRoot": false,
"parentId": "root",
"childrenCount": 0,
"depth": 1,
"title": "深度学习",
"des": "深度学习技术发展",
"createDate": "2023-07-01T10:30:00Z",
"updateDate": "2023-07-01T10:30:00Z",
"delete": false
}
]
}
}
```
**错误响应:**
```json
{
"detail": "mindMapId is required"
}
```
### 更新节点
**接口:** `PATCH /api/mindMaps/updateNode`
**描述:** 更新指定节点的标题、描述或父节点关系
**请求体:**
```json
{
"id": "node_123", // 节点ID必填
"newTitle": "更新后的节点标题", // 可选,新标题
"newDes": "更新后的节点描述内容", // 可选,新描述
"newParentId": "parent_node_id_123" // 可选新父节点ID
}
```
**请求示例:**
```bash
PATCH /api/mindMaps/updateNode
Content-Type: application/json
{
"id": "node-1",
"newTitle": "机器学习算法",
"newDes": "包括监督学习、无监督学习和强化学习",
"newParentId": "root"
}
```
**响应示例:**
```json
{
"success": true,
"message": "节点更新成功",
"data": {
"id": "node-1",
"isRoot": false,
"parentId": "root",
"childrenCount": 0,
"depth": 1,
"title": "机器学习算法",
"des": "包括监督学习、无监督学习和强化学习",
"createDate": "2023-01-01T00:00:00Z",
"updateDate": "2023-10-05T15:30:00Z",
"delete": false
},
"updatedFields": ["title", "des", "parentId"]
}
```
**错误响应:**
```json
{
"detail": "id is required"
}
```
### 批量删除节点
**接口:** `DELETE /api/mindMaps/deleteNodes`
**描述:** 批量删除节点及其所有子节点(软删除),自动更新父节点的子节点计数
**请求体:**
```json
{
"nodeIds": ["node_123", "node_456", "node_789"] // 要删除的节点ID数组
}
```
**请求示例:**
```bash
DELETE /api/mindMaps/deleteNodes
Content-Type: application/json
{
"nodeIds": ["node-1", "node-2"]
}
```
**响应示例:**
```json
{
"success": true,
"message": "节点删除成功",
"data": {
"deletedCount": 5,
"deletedNodeIds": ["node-1", "node-2", "node-1-1", "node-1-2", "node-2-1"]
}
}
```
**错误响应:**
```json
{
"detail": "nodeIds is required"
}
```
## 🤖 AI服务接口
### AI生成Markdown
**接口:** `POST /api/ai/generate-markdown`
**描述:** 调用AI服务分析文档内容生成结构化的Markdown格式思维导图
**请求体:**
```json
{
"system_prompt": "AI系统提示词", // 可选,自定义系统提示
"user_prompt": "用户输入内容", // 必填,要分析的内容
"model": "glm-4.5", // 可选AI模型名称
"base_url": "https://open.bigmodel.cn/api/paas/v4/", // 可选API地址
"api_key": "your_api_key" // 可选API密钥
}
```
**请求示例:**
```bash
POST /api/ai/generate-markdown
Content-Type: application/json
{
"system_prompt": "你是一位专业的文档分析专家请将以下内容转换为结构化的Markdown格式",
"user_prompt": "人工智能技术发展历程包括早期发展、现代发展等阶段...",
"model": "glm-4.5"
}
```
**响应示例:**
```json
{
"markdown": "# 人工智能技术发展历程\n\n## 早期发展\n- 1950年代概念提出\n- 图灵测试的提出\n\n## 现代发展\n- 深度学习技术突破\n- 应用领域扩展",
"success": true
}
```
**错误响应:**
```json
{
"error": "用户提示词不能为空",
"success": false
}
```
## 📊 数据格式转换说明
### 前端MindElixir格式 vs 后端数据库格式
**前端MindElixir期望格式**
```json
{
"nodeData": {
"id": "root",
"topic": "根节点标题",
"data": {
"des": "节点描述"
},
"children": [
{
"id": "child-1",
"topic": "子节点标题",
"data": {
"des": "子节点描述"
},
"children": []
}
]
}
}
```
**后端数据库存储格式:**
```json
{
"id": "mindmap-123",
"title": "思维导图标题",
"nodes": [
{
"id": "node-1",
"title": "根节点标题",
"des": "节点描述",
"isRoot": true,
"parentId": null,
"depth": 0,
"childrenCount": 1,
"createDate": "2023-07-01T00:00:00Z",
"updateDate": "2023-07-01T00:00:00Z",
"delete": false
}
],
"createDate": "2023-07-01T00:00:00Z",
"updateDate": "2023-07-01T00:00:00Z",
"delete": false
}
```
### 关键字段映射
| 前端MindElixir | 后端数据库 | 说明 |
|---------------|-----------|------|
| `topic` | `title` | 节点标题 |
| `data.des` | `des` | 节点描述 |
| `children` | 通过`parentId`关系构建 | 子节点关系 |
| `id` | `id` | 节点唯一标识 |
### 数据转换规则
1. **获取思维导图时**后端将扁平节点列表转换为MindElixir期望的树形结构
2. **保存节点时**:前端发送扁平格式,后端存储为关系型数据
3. **过滤规则**:自动过滤掉标题为"根节点标题"且没有子节点的空根节点
所有操作后端都会自动更新 `depth`、`childrenCount`。
## 📝 示例思维导图结构
```
数字教育平台设计要点
├── 用户体验设计
│ ├── 界面交互设计
│ │ └── 核心功能入口应控制在3次点击内可达
│ └── 内容架构规划
│ └── 合理的内容组织帮助用户构建知识体系
├── 互动功能开发
│ └── 学习互动设计
│ └── 平台应提供即时反馈机制
└── 数据安全保障
└── 隐私保护措施
├── 个人信息和学习数据采用AES-256加密存储
└── 实施基于角色的权限管理
```
## 🔧 错误码说明
| 错误码 | 说明 | 解决方案 |
|--------|------|----------|
| 400 | 请求参数错误 | 检查请求体格式和必填字段 |
| 404 | 资源不存在 | 确认ID是否正确 |
| 500 | 服务器内部错误 | 检查服务器日志 |
## 📚 使用示例
### 完整工作流程示例
1. **创建思维导图**
```bash
POST /api/mindMaps
{
"title": "AI技术发展"
}
```
2. **添加节点**
```bash
POST /api/mindMaps/addNodes
{
"mindMapId": 1,
"nodes": [
{
"title": "机器学习",
"des": "机器学习算法和应用",
"parentId": "root"
}
]
}
```
3. **更新节点**
```bash
PATCH /api/mindMaps/updateNode
{
"id": "node-1",
"newTitle": "机器学习算法",
"newDes": "包括监督学习、无监督学习等"
}
```
4. **获取完整数据**
```bash
GET /api/mindMaps/1
```
## 🚀 快速开始
1. 启动后端服务:`python manage.py runserver`
2. 启动前端服务:`cd frontend && npm run dev`
3. 访问前端界面:`http://localhost:5173`
4. 使用API接口`http://localhost:8000/api`
## 📞 技术支持
如有问题,请查看:
- 项目README文档
- 代码注释和示例
- 错误日志和调试信息
## 初始化Git仓库并上传
### 1. 初始化Git仓库
```bash
cd /Users/natalie/Documents/test/siweidaotu
git init
```
### 2. 创建.gitignore文件
```bash
<code_block_to_apply_changes_from>
```
### 3. 配置Git用户信息如果还没有配置
```bash
git config --global user.name "你的用户名"
git config --global user.email "你的邮箱"
```
### 4. 添加远程仓库
```bash
git remote add origin http://test-www.writebug.com:3000/lixinran/MindMap.git
```
### 5. 添加所有文件到Git
```bash
git add .
```
### 6. 提交更改
```bash
git commit -m "Initial commit: AI思维导图生成器项目
- 基于Django + Vue.js的智能思维导图生成和管理系统
- 支持AI驱动的文档分析和可视化思维导图创建
- 包含完整的前后端代码和文档"
```
### 7. 设置主分支并推送
```bash
git branch -M main
git push -u origin main
```
## 如果推送时遇到认证问题
当执行 `git push -u origin main` 时,可能会提示输入用户名和密码:
- **用户名**输入你的Git账户用户名
- **密码**输入你的Git账户密码或访问令牌
## 验证步骤
推送完成后,你可以:
1. **检查Git状态**
```bash
git status
```
2. **查看远程仓库**
```bash
git remote -v
```
3. **访问仓库网站**
打开 [http://test-www.writebug.com:3000/lixinran/MindMap](http://test-www.writebug.com:3000/lixinran/MindMap) 查看是否能看到项目文件
请按照这些步骤操作,如果遇到任何错误信息,请告诉我具体的错误内容。

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 704 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 331 KiB

Some files were not shown because too many files have changed in this diff Show More