agent / frontend /src /components /plugins /MindmapViewer.jsx
samlax12's picture
Upload 139 files
ad74240 verified
/**
* 思维导图插件组件
* 使用 markmap 从 Markdown 文本渲染交互式 SVG 思维导图
*/
import { useState, useRef, useEffect, useCallback } from 'react';
import { Button, Empty, Typography, Modal } from 'antd';
import { ExpandOutlined, DownloadOutlined } from '@ant-design/icons';
const { Text } = Typography;
const MindmapViewer = ({ markdown = '' }) => {
const svgRef = useRef(null);
const markmapRef = useRef(null);
const [fullscreen, setFullscreen] = useState(false);
const [hasContent, setHasContent] = useState(false);
const renderMap = useCallback(async (svgEl, md) => {
if (!svgEl || !md) return;
try {
// 动态导入 markmap(避免 SSR 问题)
const { Transformer } = await import('markmap-lib');
const { Markmap } = await import('markmap-view');
const transformer = new Transformer();
const { root } = transformer.transform(md);
// 清除旧内容
svgEl.innerHTML = '';
// 渲染
markmapRef.current = Markmap.create(svgEl, {
autoFit: true,
duration: 300,
maxWidth: 300,
}, root);
setHasContent(true);
} catch (e) {
console.error('Markmap 渲染失败:', e);
}
}, []);
useEffect(() => {
if (markdown && svgRef.current) {
renderMap(svgRef.current, markdown);
}
}, [markdown, renderMap]);
const handleExport = () => {
if (!svgRef.current) return;
const svgData = new XMLSerializer().serializeToString(svgRef.current);
const blob = new Blob([svgData], { type: 'image/svg+xml' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'mindmap.svg';
a.click();
URL.revokeObjectURL(url);
};
return (
<div style={{ display: 'flex', flexDirection: 'column', height: '100%', gap: '8px' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<Text strong style={{ fontSize: '13px' }}>思维导图</Text>
<div>
{hasContent && (
<>
<Button
size="small"
icon={<ExpandOutlined />}
onClick={() => setFullscreen(true)}
style={{ marginRight: '8px' }}
>
全屏
</Button>
<Button
size="small"
icon={<DownloadOutlined />}
onClick={handleExport}
>
导出 SVG
</Button>
</>
)}
</div>
</div>
<div style={{
flex: 1,
borderRadius: '6px',
border: '1px solid rgba(0,0,0,0.1)',
overflow: 'hidden',
background: '#fff',
minHeight: '300px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}>
{markdown ? (
<svg
ref={svgRef}
style={{ width: '100%', height: '100%', minHeight: '300px' }}
/>
) : (
<Empty description="等待生成思维导图..." />
)}
</div>
{/* 全屏模态框 */}
<Modal
open={fullscreen}
onCancel={() => setFullscreen(false)}
footer={null}
width="90vw"
style={{ top: '5vh' }}
styles={{ body: { height: '80vh', padding: '12px' } }}
destroyOnClose
afterOpenChange={(open) => {
if (open && markdown) {
// 全屏模态框中重新渲染
setTimeout(() => {
const modalSvg = document.querySelector('.ant-modal-body svg');
if (modalSvg) renderMap(modalSvg, markdown);
}, 100);
}
}}
>
<svg style={{ width: '100%', height: '100%' }} />
</Modal>
</div>
);
};
export default MindmapViewer;