File size: 3,878 Bytes
ad74240 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 | /**
* 思维导图插件组件
* 使用 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;
|