基于Vue3+Nestjs从零实现一款AI思维导图工具

之前一直在迭代 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 数据持久化

  1. 优先使用 IndexedDB:离线优先,容量更大
  2. 数据清洗:存储前清洗,避免克隆错误
  3. 智能恢复:刷新后自动加载最新文档
  4. 防抖保存:避免频繁 IO 操作

9.2 用户体验

  1. 零感知保存:用户无需关心保存操作
  2. 自定义弹窗:替代原生交互,提升品牌感
  3. 加载动画:Q 弹动画,增强反馈
  4. 响应式设计:多端一致体验

9.3 AI 集成

  1. Prompt 工程:结构化提示词,确保输出格式
  2. 错误处理:友好的错误提示
  3. 自定义配置:支持用户自定义模型
  4. 流式响应:实时展示生成过程

9.4 性能优化

  1. 懒加载:按需加载数据
  2. 防抖节流:减少高频操作
  3. 索引优化:加速数据库查询
  4. 代码分割:按需加载组件

十、总结

AI Mind 是一个技术栈现代、架构清晰、用户体验优秀的智能思维导图工具。通过 IndexedDB 实现离线优先存储、自动保存机制保证数据安全、自定义 UI 提升交互体验、AI 集成赋能内容生成。

核心优势

  1. 离线优先:基于 IndexedDB 的本地存储方案
  2. 零感知保存:2 秒防抖自动保存
  3. 智能恢复:刷新后自动加载最新文档
  4. 精美 UI:自定义弹窗、弹性动画
  5. AI 赋能:DeepSeek 大模型智能生成
  6. 移动适配:完整的响应式设计

如果大家想实现AI思维导图,也可以参考我的技术实现,如果大家有好的建议,欢迎随时交流~

相关推荐
EdgeOne边缘安全加速平台2 小时前
一键管控 AI 爬虫,腾讯 EdgeOne 基础 Bot 管理能力免费开放
人工智能·爬虫
maoku662 小时前
从关键词到语义:向量数据库如何让AI真正理解你的需求
数据库·人工智能
寻道码路2 小时前
【MCP探索实践】Google GenAI Toolbox:Google开源的企业级AI数据库中间件、5分钟搞定LLM-SQL安全互联
数据库·人工智能·sql·开源·aigc
QBoson2 小时前
综述:多尺度模拟与机器学习在高熵合金研究中的当前应用现状
人工智能·机器学习
njsgcs2 小时前
agentscope Mem0LongTermMemory记忆写入和查询工具2个py
人工智能
renhongxia12 小时前
COVLM-RL:利用VLM引导强化学习实现自动驾驶的关键面向对象推理
人工智能·深度学习·机器学习·语言模型·自动驾驶·逻辑回归
学习的周周啊2 小时前
ClawdBot(openclaw) + Cloudflare Tunnel + Zero-Trust 零基础保姆教程
网络·人工智能·python·clawdbot
CELLGENE BIOSCIENCE2 小时前
精准检测,洞见未来|赛唐生物应邀出席2026张江药谷产业发展闭门交流会,共话药物质量安全新篇章
大数据·人工智能
啊阿狸不会拉杆2 小时前
《机器学习导论》第 1 章 - 引言
人工智能·python·算法·机器学习·ai·numpy·matplotlib