@@ -10,9 +10,14 @@ import {
10
10
Background ,
11
11
BackgroundVariant ,
12
12
Controls ,
13
- ReactFlowProvider
13
+ ReactFlowProvider ,
14
+ useReactFlow ,
15
+ getNodesBounds ,
16
+ getViewportForBounds ,
17
+ ControlButton
14
18
} from '@xyflow/react' ;
15
19
import dagre from 'dagre' ;
20
+ import { toPng } from 'html-to-image' ;
16
21
import { SerializedTreeNode } from 'monaco-sql-languages/esm/languageService' ;
17
22
18
23
import '@xyflow/react/dist/style.css' ;
@@ -45,12 +50,12 @@ interface NodeStyleProps {
45
50
const calculateTextWidth = ( text : string ) : number => {
46
51
const canvas = document . createElement ( 'canvas' ) ;
47
52
const context = canvas . getContext ( '2d' ) ;
48
- if ( ! context ) return 120 ;
53
+ if ( ! context ) return 80 ;
49
54
50
- context . font = '13px Monaco, monospace' ;
55
+ context . font = '14px Monaco, monospace' ;
51
56
const metrics = context . measureText ( text ) ;
52
57
// 添加内边距和一些缓冲空间
53
- return Math . max ( 120 , Math . ceil ( metrics . width + 40 ) ) ;
58
+ return Math . max ( 80 , Math . ceil ( metrics . width + 20 ) ) ;
54
59
} ;
55
60
56
61
// 自定义节点样式
@@ -64,31 +69,25 @@ const getNodeStyle = ({
64
69
const width = calculateTextWidth ( label ) ;
65
70
66
71
return {
67
- padding : '8px 12px' ,
68
- border : '2px solid #4a90e2' ,
69
- borderRadius : '6px' ,
72
+ padding : '8px 0px' ,
73
+ borderRadius : '8px' ,
70
74
backgroundColor :
71
75
displayType === NodeDisplayType . TerminalNode
72
- ? 'rgb(136 205 255 )'
76
+ ? 'rgba(22, 163, 74, 0.8 )'
73
77
: displayType === NodeDisplayType . ErrorNode
74
- ? 'rgb(255 205 210 )'
75
- : '#fff ' ,
76
- color : '#2c3e50 ' ,
77
- fontSize : '13px ' ,
78
+ ? 'rgba(220, 38, 38, 0.8 )'
79
+ : 'rgba(27, 126, 191, 0.8) ' ,
80
+ color : '#ffffff ' ,
81
+ fontSize : '12px ' ,
78
82
width : width ,
79
83
textAlign : 'center' as const ,
80
84
transition : 'all 0.2s ease' ,
81
- opacity : hasSelection && ! isSelected && ! isChild ? 0.3 : 1 ,
82
- boxShadow : isSelected
83
- ? '0 0 10px #4a90e2'
84
- : isChild
85
- ? '0 0 6px #4a90e2'
86
- : '0 2px 6px rgba(0,0,0,0.1)'
85
+ opacity : hasSelection && ! isSelected && ! isChild ? 0.3 : 1
87
86
} ;
88
87
} ;
89
88
90
89
const edgeStyle = {
91
- stroke : '#4a90e2 ' ,
90
+ stroke : 'rgb(14, 99, 156) ' ,
92
91
strokeWidth : 2
93
92
} ;
94
93
@@ -142,6 +141,70 @@ const getLayoutedElements = <T extends Record<string, any>>(
142
141
return { nodes : layoutedNodes , edges } ;
143
142
} ;
144
143
144
+ const downloadImage = ( dataUrl : string ) => {
145
+ const a = document . createElement ( 'a' ) ;
146
+ a . setAttribute ( 'download' , `parse-tree-${ new Date ( ) . toLocaleString ( ) } .png` ) ;
147
+ a . setAttribute ( 'href' , dataUrl ) ;
148
+ a . click ( ) ;
149
+ } ;
150
+
151
+ const DownloadButton = ( ) => {
152
+ const { getNodes } = useReactFlow ( ) ;
153
+
154
+ const onClick = ( ) => {
155
+ const nodes = getNodes ( ) ;
156
+ if ( nodes . length === 0 ) return ;
157
+
158
+ const nodesBounds = getNodesBounds ( nodes ) ;
159
+
160
+ const padding = 30 ;
161
+ const imageWidth = nodesBounds . width + padding ;
162
+ const imageHeight = nodesBounds . height + padding ;
163
+
164
+ // 计算用于导出的视口变换
165
+ const viewport = getViewportForBounds ( nodesBounds , imageWidth , imageHeight , 0.1 , 2 , 0.1 ) ;
166
+
167
+ const viewportElement = document . querySelector ( '.react-flow__viewport' ) as HTMLElement ;
168
+ if ( ! viewportElement ) return ;
169
+
170
+ toPng ( viewportElement , {
171
+ backgroundColor : '#ffffff' ,
172
+ width : imageWidth ,
173
+ height : imageHeight ,
174
+ style : {
175
+ width : `${ imageWidth } px` ,
176
+ height : `${ imageHeight } px` ,
177
+ transform : `translate(${ viewport . x } px, ${ viewport . y } px) scale(${ viewport . zoom } )`
178
+ }
179
+ } )
180
+ . then ( ( dataUrl : string ) => {
181
+ downloadImage ( dataUrl ) ;
182
+ } )
183
+ . catch ( ( error : Error ) => {
184
+ console . error ( 'Download image failed:' , error ) ;
185
+ } ) ;
186
+ } ;
187
+
188
+ return (
189
+ < ControlButton onClick = { onClick } title = "Download Image" >
190
+ < svg
191
+ width = "16"
192
+ height = "16"
193
+ viewBox = "0 0 24 24"
194
+ fill = "none"
195
+ stroke = "currentColor"
196
+ strokeWidth = "2"
197
+ strokeLinecap = "round"
198
+ strokeLinejoin = "round"
199
+ >
200
+ < path d = "M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
201
+ < polyline points = "7,10 12,15 17,10" />
202
+ < line x1 = "12" y1 = "15" x2 = "12" y2 = "3" />
203
+ </ svg >
204
+ </ ControlButton >
205
+ ) ;
206
+ } ;
207
+
145
208
const TreeVisualizerContent = ( { parseTree } : TreeVisualizerPanelProps ) => {
146
209
const [ nodes , setNodes , onNodesChange ] = useNodesState < Node < NodeData > > ( [ ] ) ;
147
210
const [ edges , setEdges , onEdgesChange ] = useEdgesState < Edge > ( [ ] ) ;
@@ -280,6 +343,7 @@ const TreeVisualizerContent = ({ parseTree }: TreeVisualizerPanelProps) => {
280
343
edges = { edges }
281
344
onNodesChange = { onNodesChange }
282
345
onEdgesChange = { onEdgesChange }
346
+ colorMode = "dark"
283
347
fitView
284
348
fitViewOptions = { { padding : 0.2 } }
285
349
minZoom = { 0.1 }
@@ -308,7 +372,7 @@ const TreeVisualizerContent = ({ parseTree }: TreeVisualizerPanelProps) => {
308
372
gap = { 12 }
309
373
size = { 1 }
310
374
color = "#91919a"
311
- style = { { opacity : 0.6 } }
375
+ style = { { opacity : 0.7 } }
312
376
/>
313
377
< Controls
314
378
showInteractive = { false }
@@ -317,7 +381,9 @@ const TreeVisualizerContent = ({ parseTree }: TreeVisualizerPanelProps) => {
317
381
flexDirection : 'column' ,
318
382
gap : '8px'
319
383
} }
320
- />
384
+ >
385
+ < DownloadButton />
386
+ </ Controls >
321
387
</ ReactFlow >
322
388
</ div >
323
389
) ;
0 commit comments