105 lines
3.2 KiB
JavaScript
105 lines
3.2 KiB
JavaScript
import React, { useEffect, useRef, useState } from 'react';
|
|
import { Transformer } from 'markmap-lib';
|
|
import { Markmap } from 'markmap-view';
|
|
import { Spin, Empty, Button, Space } from 'antd';
|
|
import { FullscreenOutlined, ZoomInOutlined, ZoomOutOutlined, SyncOutlined } from '@ant-design/icons';
|
|
|
|
const transformer = new Transformer();
|
|
|
|
const hasRenderableSize = (element) => {
|
|
if (!element) return false;
|
|
const rect = element.getBoundingClientRect();
|
|
return Number.isFinite(rect.width) && Number.isFinite(rect.height) && rect.width > 0 && rect.height > 0;
|
|
};
|
|
|
|
const MindMap = ({ content }) => {
|
|
const svgRef = useRef(null);
|
|
const markmapRef = useRef(null);
|
|
const latestRootRef = useRef(null);
|
|
const [loading, setLoading] = useState(true);
|
|
|
|
useEffect(() => {
|
|
if (!content || !svgRef.current) return;
|
|
|
|
setLoading(true);
|
|
try {
|
|
const { root } = transformer.transform(content);
|
|
latestRootRef.current = root;
|
|
|
|
if (markmapRef.current) {
|
|
markmapRef.current.setData(root);
|
|
} else {
|
|
markmapRef.current = Markmap.create(svgRef.current, {
|
|
autoFit: false,
|
|
duration: 500,
|
|
}, root);
|
|
}
|
|
|
|
requestAnimationFrame(() => {
|
|
if (svgRef.current && hasRenderableSize(svgRef.current)) {
|
|
markmapRef.current?.fit();
|
|
}
|
|
});
|
|
} catch (error) {
|
|
console.error('Markmap error:', error);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}, [content]);
|
|
|
|
useEffect(() => {
|
|
const svgElement = svgRef.current;
|
|
if (!svgElement || typeof ResizeObserver === 'undefined') {
|
|
return undefined;
|
|
}
|
|
|
|
const observer = new ResizeObserver(() => {
|
|
if (!hasRenderableSize(svgElement)) {
|
|
return;
|
|
}
|
|
|
|
if (!markmapRef.current && latestRootRef.current) {
|
|
markmapRef.current = Markmap.create(svgElement, {
|
|
autoFit: false,
|
|
duration: 500,
|
|
}, latestRootRef.current);
|
|
}
|
|
|
|
requestAnimationFrame(() => {
|
|
markmapRef.current?.fit();
|
|
});
|
|
});
|
|
|
|
observer.observe(svgElement);
|
|
return () => observer.disconnect();
|
|
}, []);
|
|
|
|
const handleFit = () => markmapRef.current?.fit();
|
|
const handleZoomIn = () => markmapRef.current?.rescale(1.2);
|
|
const handleZoomOut = () => markmapRef.current?.rescale(0.8);
|
|
|
|
if (!content) return <Empty description="暂无内容,无法生成思维导图" />;
|
|
|
|
return (
|
|
<div className="mindmap-container" style={{ position: 'relative', width: '100%', height: '100%', minHeight: 500 }}>
|
|
{loading && (
|
|
<div style={{ position: 'absolute', inset: 0, display: 'flex', alignItems: 'center', justifyContent: 'center', zIndex: 10, background: 'rgba(255,255,255,0.8)' }}>
|
|
<Spin tip="生成导图中..." />
|
|
</div>
|
|
)}
|
|
|
|
<div style={{ position: 'absolute', right: 16, top: 16, zIndex: 20 }}>
|
|
<Space direction="vertical">
|
|
<Button icon={<FullscreenOutlined />} onClick={handleFit} title="自适应" />
|
|
<Button icon={<ZoomInOutlined />} onClick={handleZoomIn} title="放大" />
|
|
<Button icon={<ZoomOutOutlined />} onClick={handleZoomOut} title="缩小" />
|
|
</Space>
|
|
</div>
|
|
|
|
<svg ref={svgRef} style={{ width: '100%', height: '100%', minHeight: 500 }} />
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default MindMap;
|