之前一直在迭代 JitWord 协同文档,上期我们完成了SDK生态的扩展:

最近做了一款有意思的AI生成思维导图的工具,今天就和大家分享一下它的技术实现。
一、什么是AI Mind

AI Mind 是一款基于 AI 的智能思维导图工具,支持 AI 自动生成、实时协作、本地存储、模板库等功能。采用前后端分离架构,提供流畅的用户体验和强大的数据管理能力。
核心特性
- AI 智能生成:接入 DeepSeek 大模型,通过自然语言描述自动生成结构化思维导图
- 本地优先存储:基于 IndexedDB 的离线优先数据持久化方案
- 零感知自动保存:2 秒防抖自动保存,用户无需手动操作
- 智能数据恢复:刷新后自动加载最新文档,数据永不丢失
- 精美 UI 交互:自定义弹窗、抽屉面板、弹性动画,替代原生交互
- 模板库系统:内置 3 大产品模板,快速启动项目规划
- 移动端适配:完整的响应式设计,支持多端访问
二、技术栈
前端技术栈
| 技术 | 版本 | 用途 |
|---|---|---|
| Vue 3 | 3.5.24 | 渐进式前端框架 |
| Vite | 7.2.4 | 新一代前端构建工具 |
| Mind-Elixir | 5.5.0 | 思维导图核心渲染引擎 |
| Axios | 1.13.2 | HTTP 请求库 |
| Pinia | 3.0.4 | Vue 状态管理 |
| Less | 4.5.1 | CSS 预处理器 |
后端技术栈
| 技术 | 版本 | 用途 |
|---|---|---|
| NestJS | 11.0.1 | 企业级 Node.js 框架 |
| TypeScript | 5.7.3 | 类型安全语言 |
| Axios | 1.13.2 | HTTP 客户端 |
| RxJS | 7.8.1 | 响应式编程库 |
| JSON 文件 | - | 数据持久化(用户统计) |
核心依赖
json
// 前端核心依赖
{
"vue": "^3.5.24",
"vite": "^7.2.4",
"mind-elixir": "^5.5.0",
"axios": "^1.13.2",
"pinia": "^3.0.4"
}
// 后端核心依赖
{
"@nestjs/common": "^11.0.1",
"@nestjs/core": "^11.0.1",
"@nestjs/axios": "^4.0.1",
"@nestjs/config": "^4.0.2"
}
三、功能亮点
3.1 AI 智能生成
技术方案
- 模型选择:DeepSeek Chat(deepseek-chat)
- Prompt 工程:结构化提示词,确保输出符合 Mind-Elixir 格式
- 流式响应:支持实时流式输出(可选)
- 自定义配置:支持用户自定义 API Key、模型、提示词
核心实现
typescript
// backend/src/mindmap/ai.service.ts
async generateMindmap(description: string, customConfig?: {
apiKey?: string;
apiUrl?: string;
model?: string;
systemPrompt?: string;
}) {
const apiKey = customConfig?.apiKey || this.defaultApiKey;
const apiUrl = customConfig?.apiUrl || this.defaultApiUrl;
const model = customConfig?.model || 'deepseek-chat';
const systemPrompt = `你是一个专业的思维导图生成助手。
输出格式要求:
1. 必须是严格的JSON格式
2. 结构符合 Mind-Elixir 格式
3. 主分支2-5个,每个分支有2-4个子节点
4. direction: 1 表示右侧分支,0 表示左侧分支`;
const response = await firstValueFrom(
this.httpService.post(apiUrl, {
model: model,
messages: [
{ role: 'system', content: systemPrompt },
{ role: 'user', content: userPrompt }
],
temperature: 0.7,
max_tokens: 2000
}, {
headers: {
'Authorization': `Bearer ${apiKey}`
}
})
);
// 解析并验证数据结构
const content = response.data.choices?.[0]?.message?.content;
const mindmapData = JSON.parse(content.trim());
return mindmapData;
}
3.2 IndexedDB 本地存储
架构设计
javascript
// frontend/src/utils/mindmapDB.js
class MindMapDB {
constructor() {
this.db = null;
}
// 初始化数据库
async init() {
return new Promise((resolve, reject) => {
const request = indexedDB.open('MindMapDB', 1);
request.onupgradeneeded = (event) => {
const db = event.target.result;
const objectStore = db.createObjectStore('mindmaps', {
keyPath: 'id',
autoIncrement: true // 自动递增主键
});
// 创建索引
objectStore.createIndex('title', 'title', { unique: false });
objectStore.createIndex('updateTime', 'updateTime', { unique: false });
objectStore.createIndex('createTime', 'createTime', { unique: false });
};
request.onsuccess = (event) => {
this.db = event.target.result;
resolve(this.db);
};
});
}
}
关键技术点
1. 数据克隆问题
IndexedDB 使用结构化克隆算法,无法存储 DOM 引用、函数、循环引用等。Mind-Elixir 的 getData() 返回的对象包含不可序列化内容,需要深度克隆清洗:
javascript
const cleanDataForStorage = (data) => {
try {
return JSON.parse(JSON.stringify(data));
} catch (error) {
console.error('数据清洗失败:', error);
return data;
}
};
2. 主键类型约束
使用 autoIncrement: true 时,主键必须是数字类型:
javascript
async saveMindMap(data) {
const request = (typeof data.id === 'number')
? objectStore.put(mindmap) // 更新
: objectStore.add(mindmap); // 创建
}
3. 智能数据恢复
刷新页面后自动加载最新文档:
javascript
const initDefaultDocument = async () => {
const allDocs = await mindmapDB.getAllMindMaps();
if (allDocs && allDocs.length > 0) {
// 加载最新文档
const latestDoc = allDocs[0];
currentPageId.value = latestDoc.id;
mindInstance.refresh(latestDoc.data);
return;
}
// 无文档时创建默认文档
const newId = await mindmapDB.createMindMap('未命名思维导图');
currentPageId.value = newId;
};
3.3 零感知自动保存
实现原理
1. 防抖机制
javascript
let autoSaveTimer = null;
const handleAutoSave = () => {
if (autoSaveTimer) {
clearTimeout(autoSaveTimer);
}
autoSaveTimer = setTimeout(async () => {
await autoSavePage();
}, 2000); // 2秒防抖
};
// 监听 Mind-Elixir 操作事件
mindInstance.bus.addListener('operation', handleAutoSave);
2. 智能保存逻辑
javascript
const autoSavePage = async () => {
const rawData = mindInstance.getData();
const cleanData = cleanDataForStorage(rawData);
if (!currentPageId.value) {
// 无当前页面,创建新文档
const newId = await mindmapDB.createMindMap('未命名思维导图');
currentPageId.value = newId;
const page = await mindmapDB.getMindMap(newId);
page.data = cleanData;
await mindmapDB.saveMindMap(page);
return;
}
// 更新当前文档
const currentPage = await mindmapDB.getMindMap(currentPageId.value);
if (currentPage) {
currentPage.data = cleanData;
await mindmapDB.saveMindMap(currentPage);
}
};
3. 页面切换保护
javascript
const handleLoadPage = async (pageId) => {
// 切换前自动保存当前页面
if (currentPageId.value && currentPageId.value !== pageId) {
await autoSavePage();
}
const page = await mindmapDB.getMindMap(pageId);
mindInstance.refresh(page.data);
currentPageId.value = pageId;
};
3.4 自定义弹窗系统
UI 设计
1. Promise 封装
javascript
const showInputDialog = (title, placeholder, defaultValue = '') => {
return new Promise((resolve) => {
dialogConfig.value = {
type: 'input',
title,
placeholder,
defaultValue,
onConfirm: (value) => {
showDialog.value = false;
resolve(value);
},
onCancel: () => {
showDialog.value = false;
resolve(null);
}
};
dialogInput.value = defaultValue;
showDialog.value = true;
});
};
2. 使用示例
javascript
// 重命名页面
const handleRenamePage = async (page) => {
const newTitle = await showInputDialog(
'重命名页面',
'请输入新名称',
page.title
);
if (!newTitle || newTitle === page.title) return;
page.title = newTitle;
await mindmapDB.saveMindMap(page);
};
3. 样式设计
less
.custom-dialog {
background: white;
border-radius: 16px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.15);
animation: slideUp 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
.dialog-header {
background: linear-gradient(135deg, #f5f7fa 0%, #ffffff 100%);
h3 {
color: #0066FF; // 品牌蓝
}
}
.dialog-input {
border: 2px solid #e5e7eb;
&:focus {
border-color: #0066FF;
background: #f0f7ff;
}
}
.btn-primary {
background: linear-gradient(135deg, #0066FF 0%, #0052cc 100%);
box-shadow: 0 2px 8px rgba(0, 102, 255, 0.3);
&:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 102, 255, 0.4);
}
}
}
@keyframes slideUp {
from {
opacity: 0;
transform: translateY(30px) scale(0.95);
}
to {
opacity: 1;
transform: translateY(0) scale(1);
}
}
3.5 模板库系统
数据结构
javascript
// frontend/src/utils/templates.js
export const templates = [
{
id: 'jitword-product',
name: 'JitWord协同AI文档产品功能架构',
description: 'AI协同文档产品的完整功能架构',
data: {
nodeData: {
id: 'root',
topic: 'JitWord协同AI文档',
root: true,
children: [
{
id: 'core-features',
topic: '核心功能',
direction: 0,
children: [
{
id: 'ai-writing',
topic: 'AI智能写作',
children: [
{ id: 'ai-1', topic: '智能续写' },
{ id: 'ai-2', topic: '内容润色' }
]
}
]
}
]
}
}
}
];
应用模板
javascript
const applyTemplate = (template) => {
if (!mindInstance) return;
try {
mindInstance.refresh(template.data);
showTemplateMenu.value = false;
console.log('模版应用成功:', template.name);
} catch (error) {
console.error('应用模版失败:', error);
}
};
3.6 移动端响应式
关键断点
less
/* 768px 断点 - 平板及以下 */
@media (max-width: 768px) {
.floating-toolbar {
flex-direction: column;
gap: 8px;
.toolbar-right {
width: 100%;
overflow-x: auto;
-webkit-overflow-scrolling: touch;
scrollbar-width: none;
}
}
.group-btn {
.btn-text { display: none; } // 仅显示图标
}
.ai-input-wrapper {
top: auto !important;
bottom: 20px !important;
width: calc(100% - 40px) !important;
}
.page-drawer {
width: 90vw !important;
max-width: 360px !important;
}
}
/* 480px 断点 - 小屏手机 */
@media (max-width: 480px) {
.logo {
width: 24px !important;
height: 24px !important;
}
.title {
font-size: 16px !important;
}
}
四、核心架构设计
4.1 前端架构
frontend/
├── src/
│ ├── api/ # API 接口层
│ │ └── mindmap.js # 思维导图 API
│ ├── components/ # 组件层
│ │ ├── MindMapEditor.vue # 主编辑器组件
│ │ ├── AIConfigButton.vue # AI 配置按钮
│ │ └── AIConfigDrawer.vue # AI 配置抽屉
│ ├── utils/ # 工具层
│ │ ├── mindmapDB.js # IndexedDB 封装
│ │ ├── templates.js # 模板数据
│ │ ├── aiConfig.js # AI 配置管理
│ │ └── api.js # Axios 实例
│ ├── styles/ # 样式层
│ │ ├── variables.less # 全局变量
│ │ └── global.less # 全局样式
│ ├── App.vue # 根组件
│ └── main.js # 入口文件
├── public/
│ └── logo.png
├── index.html
├── vite.config.js # Vite 配置
└── package.json
4.2 后端架构
backend/
├── src/
│ ├── mindmap/ # 思维导图模块
│ │ ├── ai.service.ts # AI 服务
│ │ ├── storage.service.ts # 存储服务
│ │ ├── user-stats.service.ts # 用户统计
│ │ ├── mindmap.controller.ts # 控制器
│ │ ├── mindmap.module.ts # 模块定义
│ │ └── mindmap.service.ts # 业务服务
│ ├── app.module.ts # 根模块
│ └── main.ts # 入口文件
├── data/
│ └── user-stats.json # 用户统计数据
├── nest-cli.json
├── tsconfig.json
└── package.json
4.3 数据流架构
用户操作
↓
Vue 组件 (MindMapEditor.vue)
↓
Mind-Elixir 实例
↓
事件监听 (operation)
↓
防抖处理 (2秒)
↓
数据清洗 (JSON.parse(JSON.stringify))
↓
IndexedDB 存储 (mindmapDB)
↓
本地持久化
五、核心源码实现
5.1 思维导图初始化
javascript
// frontend/src/components/MindMapEditor.vue
const initMindmap = async () => {
const options = {
el: mindmapContainer.value,
direction: MindElixir.RIGHT,
draggable: true,
contextMenu: true,
toolBar: true,
nodeMenu: true,
keypress: true,
locale: 'zh_CN',
overflowHidden: false,
mainLinkStyle: 2,
mainNodeShape: 'rect',
allowUndo: true,
layoutOffset: { x: 0, y: 0 },
theme: {
name: 'vivid-blue-theme',
palette: ['#0066FF', '#00C853', '#FFB300', '#FF3D00', '#9C27B0'],
cssVar: {
'--main-color': '#0066FF',
'--main-bgcolor': '#ffffff',
'--color': '#333333',
'--bg': '#F0F7FF'
}
}
};
const initialData = {
nodeData: {
id: 'root',
topic: '开始创作',
root: true,
children: []
}
};
mindInstance = new MindElixir(options);
mindInstance.init(initialData);
// 监听操作事件,触发自动保存
mindInstance.bus.addListener('operation', handleAutoSave);
// 初始化默认文档
await initDefaultDocument();
};
5.2 AI 生成完整流程
javascript
// 前端调用
const handleGenerate = async () => {
isGenerating.value = true;
showLoading.value = true;
const aiConfig = getActiveProviderConfig();
try {
const response = await mindmapApi.generate(
aiDescription.value,
aiConfig
);
if (response.success && response.data) {
mindInstance.refresh(response.data);
showAIInput.value = false;
// AI 生成后触发自动保存
await autoSavePage();
}
} catch (error) {
alert('AI 生成失败');
} finally {
isGenerating.value = false;
showLoading.value = false;
}
};
typescript
// 后端实现
@Controller('mindmap')
export class MindmapController {
@Post('generate')
async generateMindmap(@Body() body: any) {
const { description, aiConfig } = body;
const data = await this.aiService.generateMindmap(
description,
aiConfig
);
return {
success: true,
data,
message: '生成成功'
};
}
}
5.3 用户统计限制
typescript
// backend/src/mindmap/user-stats.service.ts
@Injectable()
export class UserStatsService {
private readonly dataPath = path.join(__dirname, '../../data/user-stats.json');
async checkAndUpdateStats(userId: string): Promise<boolean> {
const stats = await this.loadStats();
const now = new Date();
const today = now.toISOString().split('T')[0];
if (!stats[userId]) {
stats[userId] = { count: 0, lastUpdate: today };
}
const userStats = stats[userId];
// 新的一天,重置计数
if (userStats.lastUpdate !== today) {
userStats.count = 0;
userStats.lastUpdate = today;
}
// 检查是否超过限制
if (userStats.count >= 10) {
return false;
}
// 更新计数
userStats.count += 1;
stats[userId] = userStats;
await this.saveStats(stats);
return true;
}
async getRemainingCount(userId: string): Promise<number> {
const stats = await this.loadStats();
const today = new Date().toISOString().split('T')[0];
if (!stats[userId] || stats[userId].lastUpdate !== today) {
return 10;
}
return Math.max(0, 10 - stats[userId].count);
}
}
六、性能优化
6.1 防抖优化
javascript
// 2秒防抖,避免频繁保存
const handleAutoSave = () => {
if (autoSaveTimer) {
clearTimeout(autoSaveTimer);
}
autoSaveTimer = setTimeout(async () => {
await autoSavePage();
}, 2000);
};
6.2 数据清洗
javascript
// JSON 序列化反序列化,过滤不可序列化内容
const cleanDataForStorage = (data) => {
try {
return JSON.parse(JSON.stringify(data));
} catch (error) {
console.error('数据清洗失败:', error);
return data;
}
};
6.3 IndexedDB 索引
javascript
// 创建索引加速查询
objectStore.createIndex('updateTime', 'updateTime', { unique: false });
// 按更新时间倒序排列
const result = request.result.sort((a, b) =>
new Date(b.updateTime) - new Date(a.updateTime)
);
6.4 懒加载
javascript
// 页面管理抽屉仅在打开时加载数据
const togglePageManager = async () => {
showPageManager.value = !showPageManager.value;
if (showPageManager.value) {
await loadPageList();
}
};
七、部署方案
7.1 开发环境
bash
# 前端
cd frontend
npm install
npm run dev # http://localhost:3000
# 后端
cd backend
npm install
npm run start:dev # http://localhost:3001
7.2 生产构建
bash
# 前端构建
cd frontend
npm run build
# 输出到 frontend/dist
# 后端构建
cd backend
npm run build
# 输出到 backend/dist
7.3 Nginx 配置
nginx
server {
listen 443 ssl;
server_name tools.pxcharts.com;
# 前端静态资源
location / {
root /var/www/ai-mind/frontend;
try_files $uri $uri/ /index.html;
}
# 后端 API 代理
location /api/ {
proxy_pass http://localhost:3001/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
八、技术难点与解决方案
8.1 IndexedDB 数据克隆失败
问题 :could not be cloned 错误
原因:Mind-Elixir 数据包含 DOM 引用、函数等不可序列化内容
解决:深度克隆清洗
javascript
const cleanData = JSON.parse(JSON.stringify(rawData));
8.2 主键类型冲突
问题 :put 操作失败
原因:混用字符串 ID 和自动递增数字 ID
解决:类型判断
javascript
const request = (typeof data.id === 'number')
? objectStore.put(mindmap)
: objectStore.add(mindmap);
8.3 Vue 响应式代理干扰
问题:对象展开破坏 keyPath
解决:创建纯净对象
javascript
const updateData = {
id: page.id,
title: newTitle,
data: cleanDataForStorage(page.data),
createTime: page.createTime,
updateTime: page.updateTime
};
8.4 移动端滚动优化
问题:横向滚动不流畅
解决:-webkit-overflow-scrolling
less
.toolbar-right {
overflow-x: auto;
-webkit-overflow-scrolling: touch;
scrollbar-width: none;
}
九、最佳实践
9.1 数据持久化
- 优先使用 IndexedDB:离线优先,容量更大
- 数据清洗:存储前清洗,避免克隆错误
- 智能恢复:刷新后自动加载最新文档
- 防抖保存:避免频繁 IO 操作
9.2 用户体验
- 零感知保存:用户无需关心保存操作
- 自定义弹窗:替代原生交互,提升品牌感
- 加载动画:Q 弹动画,增强反馈
- 响应式设计:多端一致体验
9.3 AI 集成
- Prompt 工程:结构化提示词,确保输出格式
- 错误处理:友好的错误提示
- 自定义配置:支持用户自定义模型
- 流式响应:实时展示生成过程
9.4 性能优化
- 懒加载:按需加载数据
- 防抖节流:减少高频操作
- 索引优化:加速数据库查询
- 代码分割:按需加载组件
十、总结
AI Mind 是一个技术栈现代、架构清晰、用户体验优秀的智能思维导图工具。通过 IndexedDB 实现离线优先存储、自动保存机制保证数据安全、自定义 UI 提升交互体验、AI 集成赋能内容生成。
核心优势
- 离线优先:基于 IndexedDB 的本地存储方案
- 零感知保存:2 秒防抖自动保存
- 智能恢复:刷新后自动加载最新文档
- 精美 UI:自定义弹窗、弹性动画
- AI 赋能:DeepSeek 大模型智能生成
- 移动适配:完整的响应式设计
如果大家想实现AI思维导图,也可以参考我的技术实现,如果大家有好的建议,欢迎随时交流~