| |
| |
| |
| |
| 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 { |
| |
| 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; |
|
|