imetting/frontend/src/components/MindMap.jsx

207 lines
6.2 KiB
React
Raw Normal View History

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 (
<div className="mindmap-loading">
<Loader className="animate-spin" />
<p>正在加载思维导图...</p>
</div>
);
}
if (error) {
return (
<div className="mindmap-error">
<p>{error}</p>
</div>
);
}
return (
<div className="mindmap-container">
<div className="markmap-render-area">
<svg ref={svgRef} style={{ width: '100%', height: '100%' }} />
</div>
</div>
);
};
export default MindMap;