refactor(flowEditor): 重构流程编辑器节点组件

- 提取公共组件 NodeContent 用于渲染节点内容和连接端点
-重构 StartNode 和 EndNode 组件,使用新的 NodeContent 组件- 添加 convertFlowData 工具函数用于转换流程数据
- 新增 exampleFlowData 作为测试数据
- 更新 FlowEditor 组件,使用转换后的流程数据初始化节点和边
master
钟良源 5 months ago
parent b784cb158b
commit b9b5ec2f19

@ -0,0 +1,96 @@
import React from 'react';
import styles from '@/pages/flowEditor/node/style/base.module.less';
import { Handle, Position } from '@xyflow/react';
interface NodeContentData {
parameters?: {
inputs?: any[];
outputs?: any[];
};
showFooter?: boolean;
type?: string;
[key: string]: any;
}
const NodeContent = ({ data }: { data: NodeContentData }) => {
const inputs = data.parameters?.inputs || [];
const outputs = data.parameters?.outputs || [];
const showFooter = data.showFooter || false;
return (
<>
{/*content栏*/}
<div className={styles['node-content']}>
{inputs.length > 0 && (
<div className={styles['node-inputs']}>
{inputs.map((input, index) => (
<div key={`input-${index}`} className={styles['node-input-label']}>
{input.name || `输入${index + 1}`}
</div>
))}
</div>
)}
{outputs.length > 0 && (
<div className={styles['node-outputs']}>
{outputs.map((output, index) => (
<div key={`output-${index}`} style={{ fontSize: '12px', padding: '2px 0' }}>
{output.name || `输出${index + 1}`}
</div>
))}
</div>
)}
</div>
{/*footer栏*/}
{showFooter && (
<div className={styles['node-footer']}>
footer
</div>
)}
{/* 默认连接端点*/}
<Handle
type={data.type === 'start' ? 'source' : 'target'}
position={data.type === 'start' ? Position.Right : Position.Left}
id={data.type === 'start' ? 'start-source' : 'end-target'}
style={{
background: '#555',
top: '40px'
}}
/>
{/*输入参数连接端点*/}
{inputs.map((_, index) => (
<Handle
key={`input-handle-${index}`}
type="target"
position={Position.Left}
id={`input-${index}`}
style={{
background: '#555',
top: `${60 + index * 20}px`
}}
/>
))}
{/*输出参数连接端点*/}
{outputs.map((_, index) => (
<Handle
key={`output-handle-${index}`}
type="source"
position={Position.Right}
id={`output-${index}`}
style={{
background: '#555',
// top: `${80 + inputs.length * 20 + index * 20}px`
top: `${60 + index * 20}px`
}}
/>
))}
</>
);
};
export default NodeContent;

@ -19,6 +19,8 @@ import StartNode from './node/startNode/StartNode';
import EndNode from './node/endNode/EndNode';
import DraggableNode from './node/draggableNode/DraggableNode';
import SideBar from './sideBar/sideBar';
import { convertFlowData } from '@/utils/convertFlowData';
import { exampleFlowData } from '@/pages/flowEditor/test/exampleFlowData';
const nodeTypes: NodeTypes = {
textUpdater: TextUpdaterNode,
@ -27,41 +29,7 @@ const nodeTypes: NodeTypes = {
draggable: DraggableNode
};
const initialNodes: Node[] = [
{
id: 'start-node',
position: { x: 0, y: 0 },
data: { label: '开始' },
type: 'start'
},
{
id: 'end-node',
position: { x: 1000, y: 0 },
data: { label: '结束' },
type: 'end'
},
{
id: 'node-1',
type: 'textUpdater',
position: { x: 400, y: 0 },
data: { value: 123 }
}
];
const initialEdges: Edge[] = [
{
id: 'start-node-1',
source: 'start-node',
target: 'node-1',
sourceHandle: 'start-source'
},
{
id: 'node-1-end',
source: 'node-1',
target: 'end-node',
targetHandle: 'end-target'
}
];
const { nodes: convertedNodes, edges: convertedEdges } = convertFlowData(exampleFlowData);
const FlowEditorWithProvider: React.FC = () => {
return (
@ -75,8 +43,8 @@ const FlowEditorWithProvider: React.FC = () => {
};
const FlowEditor: React.FC = () => {
const [nodes, setNodes] = useState<Node[]>(initialNodes);
const [edges, setEdges] = useState<Edge[]>(initialEdges);
const [nodes, setNodes] = useState<Node[]>(convertedNodes);
const [edges, setEdges] = useState<Edge[]>(convertedEdges);
const reactFlowInstance = useReactFlow();
const reactFlowWrapper = useRef<HTMLDivElement>(null);

@ -1,6 +1,6 @@
import React from 'react';
import { Handle, Position } from '@xyflow/react';
import styles from '@/pages/flowEditor/node/style/base.module.less';
import NodeContent from '@/pages/flowEditor/components/nodeContent';
interface EndNodeData {
title?: string;
@ -15,9 +15,6 @@ interface EndNodeData {
const EndNode = ({ data }: { data: EndNodeData }) => {
const title = data.title || '结束';
const inputs = data.parameters?.inputs || [];
const outputs = data.parameters?.outputs || [];
const showFooter = data.showFooter || false;
return (
<div className={styles['node-container']}>
@ -25,72 +22,7 @@ const EndNode = ({ data }: { data: EndNodeData }) => {
{title}
</div>
<div className={styles['node-content']}>
{inputs.length > 0 && (
<div className={styles['node-inputs']}>
{inputs.map((input, index) => (
<div key={`input-${index}`} className={styles['node-input-label']}>
{input.name || `输入${index + 1}`}
</div>
))}
</div>
)}
{outputs.length > 0 && (
<div className={styles['node-outputs']}>
{outputs.map((output, index) => (
<div key={`output-${index}`} style={{ fontSize: '12px', padding: '2px 0' }}>
{output.name || `输出${index + 1}`}
</div>
))}
</div>
)}
</div>
{showFooter && (
<div className={styles['node-footer']}>
footer
</div>
)}
{/* Dynamic handles based on parameters */}
<Handle
type="target"
position={Position.Left}
id="end-target"
style={{
background: '#555',
top: '40px'
}}
/>
{/* Add additional handles based on parameters */}
{inputs.map((_, index) => (
<Handle
key={`input-handle-${index}`}
type="target"
position={Position.Left}
id={`input-${index}`}
style={{
background: '#555',
top: `${60 + index * 20}px`
}}
/>
))}
{outputs.map((_, index) => (
<Handle
key={`output-handle-${index}`}
type="source"
position={Position.Right}
id={`output-${index}`}
style={{
background: '#555',
// top: `${80 + inputs.length * 20 + index * 20}px`
top: `${60 + index * 20}px`
}}
/>
))}
<NodeContent data={{ ...data, type: 'end' }} />
</div>
);
};

@ -1,6 +1,6 @@
import React from 'react';
import { Handle, Position } from '@xyflow/react';
import styles from '@/pages/flowEditor/node/style/base.module.less';
import NodeContent from '@/pages/flowEditor/components/nodeContent';
interface StartNodeData {
title?: string;
@ -15,82 +15,13 @@ interface StartNodeData {
const StartNode = ({ data }: { data: StartNodeData }) => {
const title = data.title || '开始';
const inputs = data.parameters?.inputs || [];
const outputs = data.parameters?.outputs || [];
const showFooter = data.showFooter || false;
return (
<div className={styles['node-container']}>
<div className={styles['node-header']} style={{ backgroundColor: '#29b971' }}>
{title}
</div>
<div className={styles['node-content']}>
{inputs.length > 0 && (
<div className={styles['node-inputs']}>
{inputs.map((input, index) => (
<div key={`input-${index}`} className={styles['node-input-label']}>
{input.name || `输入${index + 1}`}
</div>
))}
</div>
)}
{outputs.length > 0 && (
<div className={styles['node-outputs']}>
{outputs.map((output, index) => (
<div key={`output-${index}`} style={{ fontSize: '12px', padding: '2px 0' }}>
{output.name || `输出${index + 1}`}
</div>
))}
</div>
)}
</div>
{showFooter && (
<div className={styles['node-footer']}>
footer
</div>
)}
{/* Dynamic handles based on parameters */}
<Handle
type="source"
position={Position.Right}
id="start-source"
style={{
background: '#555',
top: '40px'
}}
/>
{/* Add additional handles based on parameters */}
{inputs.map((_, index) => (
<Handle
key={`input-handle-${index}`}
type="target"
position={Position.Left}
id={`input-${index}`}
style={{
background: '#555',
top: `${60 + index * 20}px`
}}
/>
))}
{outputs.map((_, index) => (
<Handle
key={`output-handle-${index}`}
type="source"
position={Position.Right}
id={`output-${index}`}
style={{
background: '#555',
// top: `${80 + inputs.length * 20 + index * 20}px`
top: `${60 + index * 20}px`
}}
/>
))}
<NodeContent data={{ ...data }} />
</div>
);
};

@ -0,0 +1,288 @@
export const exampleFlowData = {
'main': {
'id': 'main',
'lineConfigs': [
{
'id': '437d6a3c-ae8f-4a34-a195-33dcfa9747cf',
'lineType': 'API',
'next': {
'endpointId': 'move',
'nodeId': 'node_34'
},
'prev': {
'endpointId': 'start',
'nodeId': 'start'
},
'x6': '{"vertices":[]}'
},
{
'id': '7f9b61cd-eb12-4914-af90-85ec73bf1fec',
'lineType': 'API',
'next': {
'endpointId': 'controlClaw',
'nodeId': 'node_43'
},
'prev': {
'endpointId': 'done',
'nodeId': 'node_34'
},
'x6': '{"vertices":[]}'
},
{
'id': '6f670427-d8a5-494e-ae25-ffa1491014bf',
'lineType': 'API',
'next': {
'endpointId': 'move',
'nodeId': 'node_17'
},
'prev': {
'endpointId': 'done',
'nodeId': 'node_43'
},
'x6': '{"vertices":[]}'
},
{
'id': '621f19c1-8d7c-48f7-8158-17418b118fa2',
'lineType': 'API',
'next': {
'endpointId': 'end',
'nodeId': 'end'
},
'prev': {
'endpointId': 'done',
'nodeId': 'node_17'
},
'x6': '{"vertices":[]}'
}
],
'name': '',
'nodeConfigs': [
{
'apiId': '',
'component': null,
'dataIns': [],
'dataOfPrevNodeMap': {},
'dataOuts': [],
'defaultValues': [],
'description': '',
'joinLines': {},
'nodeId': 'start',
'nodeName': '',
'x6': '{"position":{"x":-550,"y":-25}}'
},
{
'apiId': '',
'component': {
'compIdentifier': 'admin_dazu-mechanical-arm',
'compInstanceIdentifier': 'admin_dazu-mechanical-arm',
'customDef': '',
'type': 'BASIC'
},
'dataIns': [
{
'arrayType': null,
'dataType': null,
'defaultValue': null,
'desc': '',
'id': 'input'
},
{
'arrayType': null,
'dataType': 'INTEGER',
'defaultValue': 50,
'desc': '力值(百分比20-100)',
'id': 'force'
},
{
'arrayType': null,
'dataType': 'INTEGER',
'defaultValue': 850,
'desc': '位置(千分比0-1000)',
'id': 'location'
},
{
'arrayType': 'DOUBLE',
'dataType': 'ARRAY',
'defaultValue': [
64.018,
912.91,
373.288,
-171.439,
75.154,
-81.128
],
'desc': '六位坐标',
'id': 'position'
},
{
'arrayType': null,
'dataType': 'INTEGER',
'defaultValue': 20,
'desc': '速度(百分比1-100)',
'id': 'speed'
}
],
'dataOfPrevNodeMap': {},
'dataOuts': [
{
'arrayType': null,
'dataType': null,
'defaultValue': null,
'desc': '',
'id': 'output'
}
],
'defaultValues': [],
'description': '',
'joinLines': {},
'nodeId': 'node_34',
'nodeName': 'mechanical-arm',
'x6': '{"position":{"x":-360,"y":-120}}'
},
{
'apiId': '',
'component': {
'compIdentifier': 'admin_dazu-mechanical-arm',
'compInstanceIdentifier': 'admin_dazu-mechanical-arm',
'customDef': '',
'type': 'BASIC'
},
'dataIns': [
{
'arrayType': null,
'dataType': null,
'defaultValue': null,
'desc': '',
'id': 'input'
},
{
'arrayType': null,
'dataType': 'INTEGER',
'defaultValue': 50,
'desc': '力值(百分比20-100)',
'id': 'force'
},
{
'arrayType': null,
'dataType': 'INTEGER',
'defaultValue': 950,
'desc': '位置(千分比0-1000)',
'id': 'location'
},
{
'arrayType': 'DOUBLE',
'dataType': 'ARRAY',
'defaultValue': null,
'desc': '六位坐标',
'id': 'position'
},
{
'arrayType': null,
'dataType': 'INTEGER',
'defaultValue': 20,
'desc': '速度(百分比1-100)',
'id': 'speed'
}
],
'dataOfPrevNodeMap': {},
'dataOuts': [
{
'arrayType': null,
'dataType': null,
'defaultValue': null,
'desc': '',
'id': 'output'
}
],
'defaultValues': [],
'description': '',
'joinLines': {},
'nodeId': 'node_43',
'nodeName': 'mechanical-arm',
'x6': '{"position":{"x":-120,"y":-145}}'
},
{
'apiId': '',
'component': {
'compIdentifier': 'admin_dazu-mechanical-arm',
'compInstanceIdentifier': 'admin_dazu-mechanical-arm',
'customDef': '',
'type': 'BASIC'
},
'dataIns': [
{
'arrayType': null,
'dataType': null,
'defaultValue': null,
'desc': '',
'id': 'input'
},
{
'arrayType': null,
'dataType': 'INTEGER',
'defaultValue': 50,
'desc': '力值(百分比20-100)',
'id': 'force'
},
{
'arrayType': null,
'dataType': 'INTEGER',
'defaultValue': 850,
'desc': '位置(千分比0-1000)',
'id': 'location'
},
{
'arrayType': 'DOUBLE',
'dataType': 'ARRAY',
'defaultValue': [
-224.639,
216.57,
574.605,
-156,
81.787,
-74.001
],
'desc': '六位坐标',
'id': 'position'
},
{
'arrayType': null,
'dataType': 'INTEGER',
'defaultValue': 20,
'desc': '速度(百分比1-100)',
'id': 'speed'
}
],
'dataOfPrevNodeMap': {},
'dataOuts': [
{
'arrayType': null,
'dataType': null,
'defaultValue': null,
'desc': '',
'id': 'output'
}
],
'defaultValues': [],
'description': '',
'joinLines': {},
'nodeId': 'node_17',
'nodeName': 'mechanical-arm',
'x6': '{"position":{"x":120,"y":-120}}'
},
{
'apiId': '',
'component': null,
'dataIns': [],
'dataOfPrevNodeMap': {},
'dataOuts': [],
'defaultValues': [],
'description': '',
'joinLines': {},
'nodeId': 'end',
'nodeName': '',
'x6': '{"position":{"x":370,"y":0}}'
}
]
}
};

@ -0,0 +1,86 @@
/**
* flow editor nodes edges
* @param flowData -
* @returns nodes edges
*/
export const convertFlowData = (flowData: any) => {
const nodes: any[] = [];
const edges: any[] = [];
// 处理节点配置
const nodeConfigs = flowData.main?.nodeConfigs || [];
for (const nodeConfig of nodeConfigs) {
// 确定节点类型
let nodeType = 'BASIC';
if (nodeConfig.nodeId === 'start') {
nodeType = 'start';
}
else if (nodeConfig.nodeId === 'end') {
nodeType = 'end';
}
else {
nodeType = nodeConfig.component.type;
}
// 解析位置信息
let position = { x: 0, y: 0 };
try {
const x6Data = JSON.parse(nodeConfig.x6);
position = x6Data.position || { x: 0, y: 0 };
} catch (e) {
console.warn('Failed to parse position for node:', nodeConfig.nodeId);
}
// 构造节点数据
const node: any = {
id: nodeConfig.nodeId,
type: nodeType,
position,
data: {
title: nodeConfig.nodeName || nodeConfig.nodeId,
parameters: {
inputs: nodeConfig.dataIns?.map((input: any) => ({
name: input.id,
desc: input.desc,
dataType: input.dataType,
defaultValue: input.defaultValue
})) || [],
outputs: nodeConfig.dataOuts?.map((output: any) => ({
name: output.id,
desc: output.desc,
dataType: output.dataType,
defaultValue: output.defaultValue
})) || []
}
}
};
// 如果是机械臂节点,添加组件标识信息
if (nodeConfig.component) {
node.data.component = {
compIdentifier: nodeConfig.component.compIdentifier,
compInstanceIdentifier: nodeConfig.component.compInstanceIdentifier
};
}
nodes.push(node);
}
// 处理连线配置
const lineConfigs = flowData.main?.lineConfigs || [];
for (const lineConfig of lineConfigs) {
const edge: any = {
id: lineConfig.id,
source: lineConfig.prev.nodeId,
target: lineConfig.next.nodeId,
sourceHandle: lineConfig.prev.endpointId,
targetHandle: lineConfig.next.endpointId
};
edges.push(edge);
}
console.log('nodes, edges:', nodes, edges);
return { nodes, edges };
};
Loading…
Cancel
Save