1
- import { memo , useCallback , useEffect } from 'react' ;
1
+ import { memo , useCallback , useEffect , useState } from 'react' ;
2
2
import {
3
3
ReactFlow ,
4
4
Node ,
@@ -28,6 +28,20 @@ enum NodeDisplayType {
28
28
RuleNode = 'RuleNode'
29
29
}
30
30
31
+ interface NodeData {
32
+ label : string ;
33
+ displayType : NodeDisplayType ;
34
+ [ key : string ] : string ;
35
+ }
36
+
37
+ interface NodeStyleProps {
38
+ displayType : NodeDisplayType ;
39
+ label : string ;
40
+ isSelected ?: boolean ;
41
+ isChild ?: boolean ;
42
+ hasSelection ?: boolean ;
43
+ }
44
+
31
45
// 计算文本宽度的辅助函数
32
46
const calculateTextWidth = ( text : string ) : number => {
33
47
const canvas = document . createElement ( 'canvas' ) ;
@@ -41,7 +55,13 @@ const calculateTextWidth = (text: string): number => {
41
55
} ;
42
56
43
57
// 自定义节点样式
44
- const getNodeStyle = ( displayType : string , label : string ) => {
58
+ const getNodeStyle = ( {
59
+ displayType,
60
+ label,
61
+ isSelected = false ,
62
+ isChild = false ,
63
+ hasSelection = false
64
+ } : NodeStyleProps ) => {
45
65
const width = calculateTextWidth ( label ) ;
46
66
47
67
return {
@@ -58,8 +78,13 @@ const getNodeStyle = (displayType: string, label: string) => {
58
78
fontSize : '13px' ,
59
79
width : width ,
60
80
textAlign : 'center' as const ,
61
- boxShadow : '0 2px 6px rgba(0,0,0,0.1)' ,
62
- transition : 'all 0.2s ease'
81
+ transition : 'all 0.2s ease' ,
82
+ opacity : hasSelection && ! isSelected && ! isChild ? 0.3 : 1 ,
83
+ boxShadow : isSelected
84
+ ? '0 0 10px #4a90e2'
85
+ : isChild
86
+ ? '0 0 6px #4a90e2'
87
+ : '0 2px 6px rgba(0,0,0,0.1)'
63
88
} ;
64
89
} ;
65
90
@@ -70,7 +95,11 @@ const edgeStyle = {
70
95
} ;
71
96
72
97
// 设置布局方向为从上到下
73
- const getLayoutedElements = ( nodes : Node [ ] , edges : Edge [ ] , direction = 'TB' ) => {
98
+ const getLayoutedElements = < T extends Record < string , any > > (
99
+ nodes : Node < T > [ ] ,
100
+ edges : Edge [ ] ,
101
+ direction = 'TB'
102
+ ) => {
74
103
// 每次都创建新的 dagre 图实例
75
104
const dagreGraph = new dagre . graphlib . Graph ( ) ;
76
105
dagreGraph . setDefaultEdgeLabel ( ( ) => ( { } ) ) ;
@@ -116,17 +145,54 @@ const getLayoutedElements = (nodes: Node[], edges: Edge[], direction = 'TB') =>
116
145
} ;
117
146
118
147
const TreeVisualizerContent = ( { parseTree } : TreeVisualizerPanelProps ) => {
119
- const [ nodes , setNodes , onNodesChange ] = useNodesState < Node > ( [ ] ) ;
148
+ const [ nodes , setNodes , onNodesChange ] = useNodesState < Node < NodeData > > ( [ ] ) ;
120
149
const [ edges , setEdges , onEdgesChange ] = useEdgesState < Edge > ( [ ] ) ;
150
+ const [ selectedNodeId , setSelectedNodeId ] = useState < string | null > ( null ) ;
121
151
const { fitView } = useReactFlow ( ) ;
122
152
153
+ // 获取节点的所有子节点ID
154
+ const getChildNodeIds = useCallback (
155
+ ( nodeId : string | null ) : string [ ] => {
156
+ if ( ! nodeId ) return [ ] ;
157
+ const childIds : string [ ] = [ ] ;
158
+ const queue = [ nodeId ] ;
159
+
160
+ while ( queue . length > 0 ) {
161
+ const currentId = queue . shift ( ) ! ;
162
+ edges . forEach ( ( edge ) => {
163
+ if ( edge . source === currentId ) {
164
+ childIds . push ( edge . target ) ;
165
+ queue . push ( edge . target ) ;
166
+ }
167
+ } ) ;
168
+ }
169
+
170
+ return childIds ;
171
+ } ,
172
+ [ edges ]
173
+ ) ;
174
+
175
+ const handleNodeClick = useCallback ( ( event : React . MouseEvent , node : Node ) => {
176
+ setSelectedNodeId ( node . id ) ;
177
+ } , [ ] ) ;
178
+
179
+ const handlePaneClick = useCallback ( ( ) => {
180
+ setSelectedNodeId ( null ) ;
181
+ } , [ ] ) ;
182
+
123
183
const convertTreeToElements = useCallback ( ( tree : SerializedTreeNode ) => {
124
- const newNodes : Node [ ] = [ ] ;
184
+ const newNodes : Node < NodeData > [ ] = [ ] ;
125
185
const newEdges : Edge [ ] = [ ] ;
126
186
let nodeId = 0 ;
187
+ let rootNodeId : string | null = null ;
127
188
128
189
const processNode = ( node : SerializedTreeNode , parentId ?: string ) : string => {
129
190
const currentId = `node-${ nodeId ++ } ` ;
191
+
192
+ if ( rootNodeId === null ) {
193
+ rootNodeId = currentId ;
194
+ }
195
+
130
196
const nodeDisplayType = [
131
197
NodeDisplayType . TerminalNode ,
132
198
NodeDisplayType . ErrorNode
@@ -141,10 +207,16 @@ const TreeVisualizerContent = ({ parseTree }: TreeVisualizerPanelProps) => {
141
207
type : 'default' ,
142
208
data : {
143
209
label,
144
- displayType : nodeDisplayType
210
+ displayType : nodeDisplayType as NodeDisplayType
145
211
} ,
146
212
position : { x : 0 , y : 0 } ,
147
- style : getNodeStyle ( nodeDisplayType , label ) ,
213
+ style : getNodeStyle ( {
214
+ displayType : nodeDisplayType as NodeDisplayType ,
215
+ label,
216
+ isSelected : false ,
217
+ isChild : false ,
218
+ hasSelection : false
219
+ } ) ,
148
220
sourcePosition : Position . Bottom ,
149
221
targetPosition : Position . Top ,
150
222
draggable : false
@@ -169,7 +241,7 @@ const TreeVisualizerContent = ({ parseTree }: TreeVisualizerPanelProps) => {
169
241
} ;
170
242
171
243
processNode ( tree ) ;
172
- return { nodes : newNodes , edges : newEdges } ;
244
+ return { nodes : newNodes , edges : newEdges , rootNodeId } ;
173
245
} , [ ] ) ;
174
246
175
247
useEffect ( ( ) => {
@@ -180,13 +252,31 @@ const TreeVisualizerContent = ({ parseTree }: TreeVisualizerPanelProps) => {
180
252
181
253
setNodes ( layoutedElements . nodes ) ;
182
254
setEdges ( layoutedElements . edges ) ;
255
+ setSelectedNodeId ( elements . rootNodeId ) ;
183
256
184
257
// 等待节点渲染完成后自动适应视图
185
258
setTimeout ( ( ) => {
186
259
fitView ( { padding : 0.2 , includeHiddenNodes : false } ) ;
187
260
} , 100 ) ;
188
261
} , [ parseTree ] ) ;
189
262
263
+ useEffect ( ( ) => {
264
+ const childIds = getChildNodeIds ( selectedNodeId ) ;
265
+
266
+ setNodes ( ( nodes ) =>
267
+ nodes . map ( ( node ) => ( {
268
+ ...node ,
269
+ style : getNodeStyle ( {
270
+ displayType : node . data . displayType ,
271
+ label : node . data . label ,
272
+ isSelected : node . id === selectedNodeId ,
273
+ isChild : childIds . includes ( node . id ) ,
274
+ hasSelection : selectedNodeId !== null // 只有当有选中节点时才降低其他节点亮度
275
+ } )
276
+ } ) )
277
+ ) ;
278
+ } , [ selectedNodeId , getChildNodeIds ] ) ;
279
+
190
280
return (
191
281
< div style = { { height : '100%' , width : '100%' } } >
192
282
< ReactFlow
@@ -198,6 +288,8 @@ const TreeVisualizerContent = ({ parseTree }: TreeVisualizerPanelProps) => {
198
288
fitViewOptions = { { padding : 0.2 } }
199
289
minZoom = { 0.1 }
200
290
maxZoom = { 2 }
291
+ onNodeClick = { handleNodeClick }
292
+ onPaneClick = { handlePaneClick }
201
293
defaultEdgeOptions = { {
202
294
type : 'smoothstep' ,
203
295
animated : true ,
0 commit comments