Skip to content

Commit 8efb103

Browse files
author
jialan
committed
feat: highlight selected nodes
1 parent 4588449 commit 8efb103

File tree

1 file changed

+102
-10
lines changed

1 file changed

+102
-10
lines changed

website/src/extensions/workbench/treeVisualizerPanel.tsx

+102-10
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { memo, useCallback, useEffect } from 'react';
1+
import { memo, useCallback, useEffect, useState } from 'react';
22
import {
33
ReactFlow,
44
Node,
@@ -28,6 +28,20 @@ enum NodeDisplayType {
2828
RuleNode = 'RuleNode'
2929
}
3030

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+
3145
// 计算文本宽度的辅助函数
3246
const calculateTextWidth = (text: string): number => {
3347
const canvas = document.createElement('canvas');
@@ -41,7 +55,13 @@ const calculateTextWidth = (text: string): number => {
4155
};
4256

4357
// 自定义节点样式
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) => {
4565
const width = calculateTextWidth(label);
4666

4767
return {
@@ -58,8 +78,13 @@ const getNodeStyle = (displayType: string, label: string) => {
5878
fontSize: '13px',
5979
width: width,
6080
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)'
6388
};
6489
};
6590

@@ -70,7 +95,11 @@ const edgeStyle = {
7095
};
7196

7297
// 设置布局方向为从上到下
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+
) => {
74103
// 每次都创建新的 dagre 图实例
75104
const dagreGraph = new dagre.graphlib.Graph();
76105
dagreGraph.setDefaultEdgeLabel(() => ({}));
@@ -116,17 +145,54 @@ const getLayoutedElements = (nodes: Node[], edges: Edge[], direction = 'TB') =>
116145
};
117146

118147
const TreeVisualizerContent = ({ parseTree }: TreeVisualizerPanelProps) => {
119-
const [nodes, setNodes, onNodesChange] = useNodesState<Node>([]);
148+
const [nodes, setNodes, onNodesChange] = useNodesState<Node<NodeData>>([]);
120149
const [edges, setEdges, onEdgesChange] = useEdgesState<Edge>([]);
150+
const [selectedNodeId, setSelectedNodeId] = useState<string | null>(null);
121151
const { fitView } = useReactFlow();
122152

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+
123183
const convertTreeToElements = useCallback((tree: SerializedTreeNode) => {
124-
const newNodes: Node[] = [];
184+
const newNodes: Node<NodeData>[] = [];
125185
const newEdges: Edge[] = [];
126186
let nodeId = 0;
187+
let rootNodeId: string | null = null;
127188

128189
const processNode = (node: SerializedTreeNode, parentId?: string): string => {
129190
const currentId = `node-${nodeId++}`;
191+
192+
if (rootNodeId === null) {
193+
rootNodeId = currentId;
194+
}
195+
130196
const nodeDisplayType = [
131197
NodeDisplayType.TerminalNode,
132198
NodeDisplayType.ErrorNode
@@ -141,10 +207,16 @@ const TreeVisualizerContent = ({ parseTree }: TreeVisualizerPanelProps) => {
141207
type: 'default',
142208
data: {
143209
label,
144-
displayType: nodeDisplayType
210+
displayType: nodeDisplayType as NodeDisplayType
145211
},
146212
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+
}),
148220
sourcePosition: Position.Bottom,
149221
targetPosition: Position.Top,
150222
draggable: false
@@ -169,7 +241,7 @@ const TreeVisualizerContent = ({ parseTree }: TreeVisualizerPanelProps) => {
169241
};
170242

171243
processNode(tree);
172-
return { nodes: newNodes, edges: newEdges };
244+
return { nodes: newNodes, edges: newEdges, rootNodeId };
173245
}, []);
174246

175247
useEffect(() => {
@@ -180,13 +252,31 @@ const TreeVisualizerContent = ({ parseTree }: TreeVisualizerPanelProps) => {
180252

181253
setNodes(layoutedElements.nodes);
182254
setEdges(layoutedElements.edges);
255+
setSelectedNodeId(elements.rootNodeId);
183256

184257
// 等待节点渲染完成后自动适应视图
185258
setTimeout(() => {
186259
fitView({ padding: 0.2, includeHiddenNodes: false });
187260
}, 100);
188261
}, [parseTree]);
189262

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+
190280
return (
191281
<div style={{ height: '100%', width: '100%' }}>
192282
<ReactFlow
@@ -198,6 +288,8 @@ const TreeVisualizerContent = ({ parseTree }: TreeVisualizerPanelProps) => {
198288
fitViewOptions={{ padding: 0.2 }}
199289
minZoom={0.1}
200290
maxZoom={2}
291+
onNodeClick={handleNodeClick}
292+
onPaneClick={handlePaneClick}
201293
defaultEdgeOptions={{
202294
type: 'smoothstep',
203295
animated: true,

0 commit comments

Comments
 (0)