import React, { useState, useEffect, useRef } from 'react'; import { Transformer } from 'markmap-lib'; import { Markmap } from 'markmap-view'; import { Loader } from 'lucide-react'; /** * MindMap - 纯展示组件,用于渲染Markdown内容的思维导图 * * 设计原则: * 1. 组件只负责渲染脑图,不处理数据获取 * 2. 不包含导出功能,导出由父组件处理 * 3. 通过props传入已准备好的content * * @param {Object} props * @param {string} props.content - Markdown格式的内容(必须由父组件准备好) * @param {string} props.title - 标题(用于显示) * @param {number} props.initialScale - 初始缩放倍数,默认为1.8 */ const MindMap = ({ content, title, initialScale = 1.8 }) => { const [markdown, setMarkdown] = useState(''); const [loading, setLoading] = useState(true); const [error, setError] = useState(''); const svgRef = useRef(null); const markmapRef = useRef(null); useEffect(() => { if (content) { setMarkdown(content); setLoading(false); } else { setMarkdown('# 暂无内容\n\n等待内容生成后查看思维导图。'); setLoading(false); } }, [content]); // 提取关键短语的函数 const extractKeyPhrases = (text) => { // 移除markdown格式 const cleanText = text.replace(/\*\*([^*]+)\*\*/g, '$1'); // 按标点符号分割 const phrases = cleanText.split(/[,。;、:]/); const keyPhrases = []; phrases.forEach(phrase => { const trimmed = phrase.trim(); // 只保留包含重要关键词的短语,且长度适中 if (trimmed.length > 4 && trimmed.length < 40) { const hasKeywords = /(?:项目|收入|问题|产品|团队|开发|验收|成本|功能|市场|合作|资源|计划|目标|业务|投入|效率|协作|管理|分析|讨论|决策|优化|整合)/.test(trimmed); if (hasKeywords) { keyPhrases.push(trimmed); } } }); // 如果没有找到关键短语,至少保留一个总结性的短语 if (keyPhrases.length === 0 && phrases.length > 0) { const firstPhrase = phrases[0].trim(); if (firstPhrase.length > 0 && firstPhrase.length < 50) { keyPhrases.push(firstPhrase); } } // 最多返回3个关键短语 return keyPhrases.slice(0, 3); }; // 预处理markdown,确保格式适合生成思维导图 const preprocessMarkdownForMindMap = (markdown, rootTitle) => { if (!markdown || markdown.trim() === '') return '# 暂无内容'; let processed = markdown.trim(); // 移除分隔线 processed = processed.replace(/^---+$/gm, ''); // 检查是否有主标题,如果有就替换为rootTitle,如果没有就添加 const lines = processed.split('\n'); const firstLine = lines[0].trim(); if (firstLine.match(/^#\s+/)) { // 如果第一行是主标题,替换为rootTitle lines[0] = `# ${rootTitle || '内容总结'}`; processed = lines.join('\n'); } else { // 如果没有主标题,添加一个 processed = `# ${rootTitle || '内容总结'}\n\n${processed}`; } const processedLines = []; const contentLines = processed.split('\n'); let i = 0; while (i < contentLines.length) { const line = contentLines[i].trim(); if (line === '') { i++; continue; } // 处理标题行 - 保持标题格式不变 if (line.match(/^#+\s+/)) { // 清理标题格式,移除粗体和多余符号,但保持标题符号 let cleanTitle = line.replace(/\*\*([^*]+)\*\*/g, '$1'); // 移除粗体 processedLines.push(cleanTitle); // 获取标题级别 const titleLevel = (line.match(/^#+/) || [''])[0].length; } // 处理现有列表项(有序和无序) - 保持其原始结构 else if (line.match(/^\s*([-*+]|\d+\.)\s+/)) { // 只移除加粗格式,保留原始行,包括缩进和列表标记 const cleanedLine = line.replace(/\*\*([^*]+)\*\*/g, '$1'); processedLines.push(cleanedLine); } // 将区块引用转换为列表项 else if (line.startsWith('>')) { const content = line.replace(/^>+\s*/, ''); processedLines.push(`- ${content}`); } // 保持表格原样,让markmap自己处理 else if (line.includes('|')) { processedLines.push(line); } // 处理其他普通段落 - 保留原样 else if (line.length > 0 && !line.match(/\*\*总字数:\d+字\*\*/)) { processedLines.push(line); } i++; } // 清理结果 const result = processedLines .filter(line => line.trim().length > 0) .join('\n'); return result; }; useEffect(() => { if (loading || !markdown || !svgRef.current) return; try { const processedMarkdown = preprocessMarkdownForMindMap(markdown, title); const transformer = new Transformer(); const { root } = transformer.transform(processedMarkdown); if (markmapRef.current) { markmapRef.current.setData(root); } else { markmapRef.current = Markmap.create(svgRef.current, null, root); } markmapRef.current.fit(); // 延迟一下再次调用fit并放大,确保内容完全渲染 setTimeout(() => { if (markmapRef.current) { markmapRef.current.fit(); // 直接设置为指定的缩放倍数 try { markmapRef.current.rescale(initialScale); } catch (e) { console.log('缩放调整失败:', e); } } }, 500); } catch (error) { console.error('思维导图渲染失败:', error); setError('思维导图渲染失败'); } }, [markdown, loading, title, initialScale]); if (loading) { return (
正在加载思维导图...
{error}