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;