Compare commits

...

2 Commits

@ -0,0 +1,113 @@
# 新增 Flow 节点模板(基于当前重构版本)
这份模板按你现在的代码结构整理,目标是:**新增节点只改最少文件**。
---
## 1. 最小接入步骤
新增一个普通节点(非 LOOP通常只要 3 步:
1. 新建节点组件(可先复用 `LocalNode` 风格)
2. 在 `nodeRegistry` 注册类型映射
3. 在节点配置源(`localNodeData`)增加定义
> `useFlowCallbacks` 已统一走 `resolveNodeDefinition + buildRuntimeNode + ensureNodeTypeRegistered`,不需要再在多个入口手写创建逻辑。
---
## 2. 模板代码
### 2.1 新建节点组件
**路径示例**`src/components/FlowEditor/node/httpNode/HttpNode.tsx`
```tsx
import React from 'react';
import LocalNode from '@/components/FlowEditor/node/localNode/LocalNode';
const HttpNode = (props: any) => {
// 第一版可以直接复用 LocalNode 渲染行为
return <LocalNode {...props} />;
};
export default HttpNode;
```
---
### 2.2 注册节点类型映射(关键)
**文件**`src/utils/flow/nodeRegistry.ts`
```ts
import HttpNode from '@/components/FlowEditor/node/httpNode/HttpNode';
export const resolveNodeComponent = (nodeType: string) => {
switch (nodeType) {
// ...已有 case
case 'HTTP':
return HttpNode;
default:
return LocalNode;
}
};
```
---
### 2.3 增加节点定义(用于菜单与创建)
**文件**`src/pages/flowEditor/sideBar/config/localNodeData.ts`
```ts
export const localNodeData = [
// ...已有节点
{
nodeType: 'HTTP',
nodeName: 'HTTP请求',
data: {
title: 'HTTP请求',
type: 'HTTP',
parameters: {
apiIns: [{ name: 'start', desc: '', dataType: '', defaultValue: '' }],
apiOuts: [{ name: 'done', desc: '', dataType: '', defaultValue: '' }],
dataIns: [],
dataOuts: [],
},
component: {
type: 'HTTP',
customDef: '{}',
},
},
},
];
```
---
## 3. 可选:节点专用编辑器
如果你希望配置面板是独立 UI再补
1. 新建编辑器组件(`src/components/FlowEditor/nodeEditors/...`
2. 在编辑器路由/映射处按 `nodeType` 挂载
不做这一步也能先跑通新增节点。
---
## 4. 当前推荐的接入方式
新增节点时不要再手写分散逻辑,优先复用:
- `resolveNodeDefinition(...)`
- `buildRuntimeNode(...)`
- `ensureNodeTypeRegistered(...)`
这些已覆盖:
- 侧栏拖拽到画布onDrop
- 画布空白处添加addNodeOnPane
- 在边上插入节点addNodeOnEdge

@ -0,0 +1,149 @@
# 新增 LOOP 类节点模板(基于当前重构版本)
这份模板用于你现在这套 Flow Core`loopFactory + nodeRegistry + useFlowCallbacks`)下,新增“成对节点/分支类”能力。
> 适用场景:像 `LOOP_START/LOOP_END` 这种需要一次创建多个节点和连边的节点族。
---
## 1. 推荐做法(先看)
不要在 `useFlowCallbacks` 里直接手写大段节点/边构造。按现在的模式:
1. 在 `src/utils/flow/` 新增工厂(如 `xxxFactory.ts`
2. 暴露:
- 节点对(或节点组)构造函数
- 组内连接边构造函数
- 外部插入连接边构造函数(可选)
3. 在 `useFlowCallbacks` 只负责调用工厂 + setNodes/setEdges + snapshot
---
## 2. 工厂模板
**路径示例**`src/utils/flow/retryFactory.ts`
```ts
import { Edge } from '@xyflow/react';
export const createRetryNodePair = (position: { x: number; y: number }) => {
const retryStartNode: any = {
id: `RETRY_START-${Date.now()}`,
type: 'RETRY',
position: { x: position.x, y: position.y },
data: {
title: '重试开始',
type: 'RETRY_START',
parameters: {
apiIns: [{ name: 'start', desc: '', dataType: '', defaultValue: '' }],
apiOuts: [{ name: 'done', desc: '', dataType: '', defaultValue: '' }],
dataIns: [],
dataOuts: [],
},
component: {},
},
};
const retryEndNode: any = {
id: `RETRY_END-${Date.now()}`,
type: 'RETRY',
position: { x: position.x + 400, y: position.y },
data: {
title: '重试结束',
type: 'RETRY_END',
parameters: {
apiIns: [
{ name: 'continue', desc: '', dataType: '', defaultValue: '' },
],
apiOuts: [{ name: 'break', desc: '', dataType: '', defaultValue: '' }],
dataIns: [],
dataOuts: [],
},
component: {
type: 'RETRY_END',
customDef: JSON.stringify({ retryStartNodeId: retryStartNode.id }),
retryStartNodeId: retryStartNode.id,
},
},
};
retryStartNode.data.component = {
type: 'RETRY_START',
customDef: JSON.stringify({ retryEndNodeId: retryEndNode.id }),
};
return { retryStartNode, retryEndNode };
};
export const createRetryGroupEdge = (
retryStartId: string,
retryEndId: string
): Edge => ({
id: `${retryStartId}-${retryEndId}-group`,
source: retryStartId,
target: retryEndId,
sourceHandle: `${retryStartId}-group`,
targetHandle: `${retryEndId}-group`,
type: 'custom',
});
```
---
## 3. useFlowCallbacks 接入模板
```ts
import {
createRetryNodePair,
createRetryGroupEdge,
} from '@/utils/flow/retryFactory';
import { ensureNodeTypeRegistered } from '@/utils/flow/nodeRegistry';
import { dispatchFlowSnapshotAsync } from '@/utils/flow/snapshot';
const addRetryNodeWithStartEnd = useCallback(
(position: { x: number; y: number }) => {
const { retryStartNode, retryEndNode } = createRetryNodePair(position);
const groupEdge = createRetryGroupEdge(retryStartNode.id, retryEndNode.id);
ensureNodeTypeRegistered('RETRY', '重试');
setNodes((nds) => {
const newNodes = [...nds, retryStartNode, retryEndNode];
dispatchFlowSnapshotAsync({
nodes: [...newNodes],
edges: [...edges, groupEdge],
});
return newNodes;
});
setEdges((eds) => [...eds, groupEdge]);
},
[edges]
);
```
---
## 4. 类型映射模板
**文件**`src/utils/flow/nodeRegistry.ts`
```ts
case 'RETRY':
return LoopNode; // 或你自己的专用节点组件
```
> 早期可复用 `LoopNode/LocalNode`,后续再拆专用展示组件。
---
## 5. 新增“边上插入该类节点”的模板(可选)
如需在边上插入节点组,建议像 `LOOP` 一样提供工厂函数:
- `createXxxNodePair(position)`
- `createXxxGroupEdge(startId, endId)`
- `createXxxInsertConnectionEdges({ sourceId, sourceHandle, targetId, targetHandle, ... })`
这样 `addNodeOnEdge` 里只拼装调用,不再手写边结构。

File diff suppressed because it is too large Load Diff

@ -3,6 +3,7 @@ import { Node, Edge } from '@xyflow/react';
import { debounce } from 'lodash';
import { useSelector, useDispatch, shallowEqual } from 'react-redux';
import { updateCanvasDataMap } from '@/store/ideContainer';
import { getCurrentAppKey } from '@/utils/flow/runtime';
import { Dispatch } from 'redux';
@ -11,29 +12,22 @@ export const useFlowEditorState = (initialData?: any, readOnly?: boolean) => {
const [edges, setEdges] = useState<Edge[]>([]);
// 使用 shallowEqual 比较器来避免不必要的重新渲染
const ideContainerState = useSelector((state: any) => ({
const ideContainerState = useSelector(
(state: any) => ({
canvasDataMap: state.ideContainer.canvasDataMap,
appRuntimeData: state.ideContainer.appRuntimeData,
currentAppData: state.ideContainer.currentAppData,
}), shallowEqual);
}),
shallowEqual
);
const { canvasDataMap, appRuntimeData, currentAppData } = ideContainerState;
const dispatch = useDispatch();
// 辅助函数:获取当前应用/子流程的唯一标识符
const getCurrentAppKey = (currentAppData: any) => {
if (!currentAppData) return null;
// 如果是子流程key包含'sub'使用key作为标识符
if (currentAppData.key && currentAppData.key.includes('sub')) {
return currentAppData.key;
}
// 否则使用id
return currentAppData.id;
};
// 获取当前应用的运行状态
const currentAppKey = getCurrentAppKey(currentAppData);
const currentAppIsRunning = currentAppKey && appRuntimeData[currentAppKey]
const currentAppIsRunning =
currentAppKey && appRuntimeData[currentAppKey]
? appRuntimeData[currentAppKey].isRunning
: false;
@ -44,7 +38,10 @@ export const useFlowEditorState = (initialData?: any, readOnly?: boolean) => {
// 添加节点选择弹窗状态
const [edgeForNodeAdd, setEdgeForNodeAdd] = useState<Edge | null>(null);
const [positionForNodeAdd, setPositionForNodeAdd] = useState<{ x: number, y: number } | null>(null);
const [positionForNodeAdd, setPositionForNodeAdd] = useState<{
x: number;
y: number;
} | null>(null);
// 在组件顶部添加历史记录相关状态
const [historyInitialized, setHistoryInitialized] = useState(false);
@ -53,16 +50,18 @@ export const useFlowEditorState = (initialData?: any, readOnly?: boolean) => {
// 更新节点状态将从store获取的状态应用到节点上
useEffect(() => {
// 获取当前应用对应的节点状态映射
const currentNodeStatusMap = currentAppKey && appRuntimeData[currentAppKey]
const currentNodeStatusMap =
currentAppKey && appRuntimeData[currentAppKey]
? appRuntimeData[currentAppKey].nodeStatusMap
: {};
// 检查 initialData 中是否包含节点状态(用于查看历史实例)
const hasInitialDataStatus = initialData?.components &&
const hasInitialDataStatus =
initialData?.components &&
Object.values(initialData.components).some((comp: any) => comp.status);
setNodes(prevNodes =>{
return prevNodes.map(node => {
setNodes((prevNodes) => {
return prevNodes.map((node) => {
// 如果是只读模式(历史实例查看),优先使用节点自身的历史状态
// 如果是正常运行模式,只使用运行时状态
let nodeStatus = 'waiting';
@ -70,8 +69,11 @@ export const useFlowEditorState = (initialData?: any, readOnly?: boolean) => {
if (readOnly) {
// 只读模式:使用历史状态
nodeStatus = node.data.status as string || 'waiting';
showStatus = hasInitialDataStatus as boolean || node.data.isStatusVisible as boolean || false;
nodeStatus = (node.data.status as string) || 'waiting';
showStatus =
(hasInitialDataStatus as boolean) ||
(node.data.isStatusVisible as boolean) ||
false;
} else {
// 正常模式:只使用运行时状态
nodeStatus = currentNodeStatusMap[node.id] || 'waiting';
@ -83,21 +85,39 @@ export const useFlowEditorState = (initialData?: any, readOnly?: boolean) => {
data: {
...node.data,
status: nodeStatus,
isStatusVisible: showStatus
}
isStatusVisible: showStatus,
},
};
});
});
}, [appRuntimeData, currentAppKey, currentAppIsRunning, initialData?.id, initialData?.components, nodes.length, readOnly]);
}, [
appRuntimeData,
currentAppKey,
currentAppIsRunning,
initialData?.id,
initialData?.components,
nodes.length,
readOnly,
]);
const updateCanvasDataMapDebounced = useRef(
debounce((dispatch: Dispatch<any>, canvasDataMap: any, id: string, nodes: Node[], edges: Edge[]) => {
dispatch(updateCanvasDataMap({
debounce(
(
dispatch: Dispatch<any>,
canvasDataMap: any,
id: string,
nodes: Node[],
edges: Edge[]
) => {
dispatch(
updateCanvasDataMap({
...canvasDataMap,
[id]: { nodes, edges }
}));
}, 500)
[id]: { nodes, edges },
})
);
},
500
)
).current;
return {
@ -127,6 +147,6 @@ export const useFlowEditorState = (initialData?: any, readOnly?: boolean) => {
dispatch,
// Initial data
initialData: initialData
initialData: initialData,
};
};

@ -27,6 +27,7 @@ import { NodeTypes } from '@xyflow/react';
import { useSelector } from 'react-redux';
import HandlerBar from '@/pages/flowEditor/components/handlerBar';
import PublishFlowModal from '@/pages/flowEditor/components/publishFlowModal';
import { getCurrentAppKey } from '@/utils/flow/runtime';
const edgeTypes = {
custom: CustomEdge
@ -52,8 +53,10 @@ interface FlowEditorMainProps {
setIsDelete: React.Dispatch<React.SetStateAction<boolean>>;
edgeForNodeAdd: Edge | null;
setEdgeForNodeAdd: React.Dispatch<React.SetStateAction<Edge | null>>;
positionForNodeAdd: { x: number, y: number } | null;
setPositionForNodeAdd: React.Dispatch<React.SetStateAction<{ x: number, y: number } | null>>;
positionForNodeAdd: { x: number; y: number } | null;
setPositionForNodeAdd: React.Dispatch<
React.SetStateAction<{ x: number; y: number } | null>
>;
isRunning: boolean;
initialData: any;
canvasDataMap: any;
@ -81,7 +84,11 @@ interface FlowEditorMainProps {
copyNode: (node: Node) => void;
pasteNode: (position: { x: number; y: number }) => void;
addNodeOnEdge: (nodeType: string, node: any) => void;
addNodeOnPane: (nodeType: string, position: { x: number, y: number }, node?: any) => void;
addNodeOnPane: (
nodeType: string,
position: { x: number; y: number },
node?: any
) => void;
handleAddNode: (nodeType: string, node: any) => void;
saveFlowDataToServer: () => void;
handleRun: (running: boolean) => void;
@ -145,7 +152,8 @@ const FlowEditorMain: React.FC<FlowEditorMainProps> = (props) => {
handleReRun
} = props;
const { getGuidelines, clearGuidelines, AlignmentGuides } = useAlignmentGuidelines();
const { getGuidelines, clearGuidelines, AlignmentGuides } =
useAlignmentGuidelines();
const { undo, redo, canUndo, canRedo } = useHistory();
const reactFlowId = useMemo(() => new Date().getTime().toString(), []);
@ -162,7 +170,7 @@ const FlowEditorMain: React.FC<FlowEditorMainProps> = (props) => {
if (isVisible) {
// 显示节点 - 从隐藏节点集合中移除
setHiddenNodes(prev => {
setHiddenNodes((prev) => {
const newSet = new Set(prev);
newSet.delete(appId);
return newSet;
@ -170,62 +178,34 @@ const FlowEditorMain: React.FC<FlowEditorMainProps> = (props) => {
}
else {
// 隐藏节点 - 添加到隐藏节点集合
setHiddenNodes(prev => new Set(prev).add(appId));
setHiddenNodes((prev) => new Set(prev).add(appId));
}
};
document.addEventListener('toggleNodeVisibility', handleToggleNodeVisibility as EventListener);
document.addEventListener(
'toggleNodeVisibility',
handleToggleNodeVisibility as EventListener
);
return () => {
document.removeEventListener('toggleNodeVisibility', handleToggleNodeVisibility as EventListener);
document.removeEventListener(
'toggleNodeVisibility',
handleToggleNodeVisibility as EventListener
);
};
}, []);
// 从Redux store中获取当前应用的运行状态
const { appRuntimeData, currentAppData } = useSelector((state: any) => state.ideContainer);
// 辅助函数:获取当前应用/子流程的唯一标识符
const getCurrentAppKey = () => {
if (!currentAppData) return null;
// 如果是子流程key包含'sub'使用key作为标识符
if (currentAppData.key && currentAppData.key.includes('sub')) {
return currentAppData.key;
}
// 否则使用id
return currentAppData.id;
};
const { appRuntimeData, currentAppData } = useSelector(
(state: any) => state.ideContainer
);
const currentAppKey = getCurrentAppKey();
const currentAppIsRunning = currentAppKey && appRuntimeData[currentAppKey]
const currentAppKey = getCurrentAppKey(currentAppData);
const currentAppIsRunning =
currentAppKey && appRuntimeData[currentAppKey]
? appRuntimeData[currentAppKey].isRunning
: false;
// 监听自定义事件以隐藏/显示节点
useEffect(() => {
const handleToggleNodeVisibility = (event: CustomEvent) => {
const { appId, isVisible } = event.detail;
if (isVisible) {
// 显示节点 - 从隐藏节点集合中移除
setHiddenNodes(prev => {
const newSet = new Set(prev);
newSet.delete(appId);
return newSet;
});
}
else {
// 隐藏节点 - 添加到隐藏节点集合
setHiddenNodes(prev => new Set(prev).add(appId));
}
};
document.addEventListener('toggleNodeVisibility', handleToggleNodeVisibility as EventListener);
return () => {
document.removeEventListener('toggleNodeVisibility', handleToggleNodeVisibility as EventListener);
};
}, []);
// 监听键盘事件实现快捷键
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
@ -275,7 +255,7 @@ const FlowEditorMain: React.FC<FlowEditorMainProps> = (props) => {
// Ctrl/Cmd+C 复制选中的节点
if (isModifierKey && e.key === 'c' && canEdit) {
// 获取当前选中的节点
const selectedNode = nodes.find(node => node.selected);
const selectedNode = nodes.find((node) => node.selected);
if (selectedNode) {
// 不允许复制开始和结束节点
if (selectedNode.type === 'start' || selectedNode.type === 'end') {
@ -312,7 +292,18 @@ const FlowEditorMain: React.FC<FlowEditorMainProps> = (props) => {
return () => {
document.removeEventListener('keydown', handleKeyDown);
};
}, [undo, redo, canUndo, canRedo, currentAppIsRunning, useDefault, nodes, copyNode, pasteNode, reactFlowInstance]);
}, [
undo,
redo,
canUndo,
canRedo,
currentAppIsRunning,
useDefault,
nodes,
copyNode,
pasteNode,
reactFlowInstance
]);
// 处理流程发布
const handlePublish = () => {
@ -338,7 +329,9 @@ const FlowEditorMain: React.FC<FlowEditorMainProps> = (props) => {
}, [nodes, edges]);
return (
<div ref={reactFlowWrapper} style={{ width: '100%', height: '100%', position: 'relative' }}
<div
ref={reactFlowWrapper}
style={{ width: '100%', height: '100%', position: 'relative' }}
onContextMenu={(e) => e.preventDefault()}
// 添加点击事件处理器,用于关闭添加节点菜单
onClick={() => {
@ -346,10 +339,11 @@ const FlowEditorMain: React.FC<FlowEditorMainProps> = (props) => {
setEdgeForNodeAdd(null);
setPositionForNodeAdd(null);
}
}}>
}}
>
<ReactFlow
id={reactFlowId}
nodes={nodes.map(node => {
nodes={nodes.map((node) => {
// 检查节点是否应该被隐藏
const isHidden = hiddenNodes.has(node.id);
// 应用透明度样式
@ -364,12 +358,13 @@ const FlowEditorMain: React.FC<FlowEditorMainProps> = (props) => {
}
};
})}
edges={edges.map(edge => {
edges={edges.map((edge) => {
// 检查边连接的节点是否被隐藏
const isSourceHidden = hiddenNodes.has(edge.source);
const isTargetHidden = hiddenNodes.has(edge.target);
// 如果源节点或目标节点被隐藏,则边也应用透明度
const style = (isSourceHidden || isTargetHidden) ? { opacity: 0.3 } : {};
const style =
isSourceHidden || isTargetHidden ? { opacity: 0.3 } : {};
// 更新边的数据,确保选择框也应用透明度
return {
@ -380,7 +375,9 @@ const FlowEditorMain: React.FC<FlowEditorMainProps> = (props) => {
},
data: {
...edge.data,
...(isSourceHidden || isTargetHidden ? { hidden: true, opacity: 0.3 } : { opacity: 1 })
...(isSourceHidden || isTargetHidden
? { hidden: true, opacity: 0.3 }
: { opacity: 1 })
}
};
})}
@ -410,14 +407,17 @@ const FlowEditorMain: React.FC<FlowEditorMainProps> = (props) => {
}
// 检查是否有开始或结束节点
const hasStartOrEndNode = nodes.some(node => node.type === 'start' || node.type === 'end');
const hasStartOrEndNode = nodes.some(
(node) => node.type === 'start' || node.type === 'end'
);
if (hasStartOrEndNode) {
console.warn('开始和结束节点不允许删除');
return false; // 阻止删除操作
}
// 检查是否有循环节点这里只是检查实际删除逻辑在onNodesDelete中处理
const loopNodes = nodes.filter(node =>
const loopNodes = nodes.filter(
(node) =>
node.data?.type === 'LOOP_START' || node.data?.type === 'LOOP_END'
);
@ -437,7 +437,8 @@ const FlowEditorMain: React.FC<FlowEditorMainProps> = (props) => {
}
// 检查是否有循环节点
const loopNodes = deleted.filter(node =>
const loopNodes = deleted.filter(
(node) =>
node.data?.type === 'LOOP_START' || node.data?.type === 'LOOP_END'
);
@ -446,15 +447,20 @@ const FlowEditorMain: React.FC<FlowEditorMainProps> = (props) => {
let nodesToRemove = [...deleted];
// 为每个循环节点找到其配对节点
loopNodes.forEach(loopNode => {
const component = loopNode.data?.component as { customDef?: string } | undefined;
if (loopNode.data?.type === 'LOOP_START' && component?.customDef) {
loopNodes.forEach((loopNode) => {
const component = loopNode.data?.component as
| { customDef?: string }
| undefined;
if (
loopNode.data?.type === 'LOOP_START' &&
component?.customDef
) {
try {
const customDef = JSON.parse(component.customDef);
const relatedNodeId = customDef.loopEndNodeId;
// 添加关联的结束节点到删除列表
const relatedNode = nodes.find(n => n.id === relatedNodeId);
const relatedNode = nodes.find((n) => n.id === relatedNodeId);
if (relatedNode) {
nodesToRemove.push(relatedNode);
}
@ -462,12 +468,15 @@ const FlowEditorMain: React.FC<FlowEditorMainProps> = (props) => {
console.error('解析循环开始节点数据失败:', e);
}
}
else if (loopNode.data?.type === 'LOOP_END' && component?.customDef) {
else if (
loopNode.data?.type === 'LOOP_END' &&
component?.customDef
) {
try {
const customDef = JSON.parse(component.customDef);
const relatedNodeId = customDef.loopStartNodeId;
// 添加关联的开始节点到删除列表
const relatedNode = nodes.find(n => n.id === relatedNodeId);
const relatedNode = nodes.find((n) => n.id === relatedNodeId);
if (relatedNode) {
nodesToRemove.push(relatedNode);
}
@ -478,56 +487,123 @@ const FlowEditorMain: React.FC<FlowEditorMainProps> = (props) => {
});
// 去重
nodesToRemove = nodesToRemove.filter((node, index, self) =>
index === self.findIndex(n => n.id === node.id)
nodesToRemove = nodesToRemove.filter(
(node, index, self) =>
index === self.findIndex((n) => n.id === node.id)
);
// 删除所有相关节点和边
setNodes((nds) => nds.filter((n) => !nodesToRemove.find((d) => d.id === n.id)));
setNodes((nds) =>
nds.filter((n) => !nodesToRemove.find((d) => d.id === n.id))
);
// 删除与这些节点相关的所有边
const nodeIdsToRemove = nodesToRemove.map(node => node.id);
setEdges((eds) => eds.filter((e) =>
!nodeIdsToRemove.includes(e.source) && !nodeIdsToRemove.includes(e.target)
));
const nodeIdsToRemove = nodesToRemove.map((node) => node.id);
setEdges((eds) =>
eds.filter(
(e) =>
!nodeIdsToRemove.includes(e.source) &&
!nodeIdsToRemove.includes(e.target)
)
);
}
else {
// 普通节点删除
setNodes((nds) => nds.filter((n) => !deleted.find((d) => d.id === n.id)));
setNodes((nds) =>
nds.filter((n) => !deleted.find((d) => d.id === n.id))
);
}
setIsEditModalOpen(false);
}}
onNodesChange={readOnly ? undefined : (!currentAppIsRunning ? onNodesChange : undefined)} // readOnly 或运行时禁用节点变更
onEdgesChange={readOnly ? undefined : (!currentAppIsRunning ? onEdgesChange : undefined)} // readOnly 或运行时禁用边变更
onConnect={readOnly ? undefined : (!currentAppIsRunning ? onConnect : undefined)} // readOnly 或运行时禁用连接
onReconnect={readOnly ? undefined : (!currentAppIsRunning ? onReconnect : undefined)} // readOnly 或运行时禁用重新连接
onDragOver={readOnly ? undefined : (!currentAppIsRunning ? onDragOver : undefined)} // readOnly 或运行时禁用拖拽
onDrop={readOnly ? undefined : (!currentAppIsRunning ? onDrop : undefined)} // readOnly 或运行时禁用放置
onNodeDrag={readOnly ? undefined : (!currentAppIsRunning ? onNodeDrag : undefined)} // readOnly 或运行时禁用节点拖拽
onNodesChange={
readOnly
? undefined
: !currentAppIsRunning
? onNodesChange
: undefined
} // readOnly 或运行时禁用节点变更
onEdgesChange={
readOnly
? undefined
: !currentAppIsRunning
? onEdgesChange
: undefined
} // readOnly 或运行时禁用边变更
onConnect={
readOnly ? undefined : !currentAppIsRunning ? onConnect : undefined
} // readOnly 或运行时禁用连接
onReconnect={
readOnly ? undefined : !currentAppIsRunning ? onReconnect : undefined
} // readOnly 或运行时禁用重新连接
onDragOver={
readOnly ? undefined : !currentAppIsRunning ? onDragOver : undefined
} // readOnly 或运行时禁用拖拽
onDrop={
readOnly ? undefined : !currentAppIsRunning ? onDrop : undefined
} // readOnly 或运行时禁用放置
onNodeDrag={
readOnly ? undefined : !currentAppIsRunning ? onNodeDrag : undefined
} // readOnly 或运行时禁用节点拖拽
connectionLineType={ConnectionLineType.SmoothStep}
connectionLineComponent={CustomConnectionLine}
onNodeDragStop={readOnly ? undefined : (!currentAppIsRunning ? onNodeDragStop : undefined)} // readOnly 或运行时禁用节点拖拽停止
onNodeContextMenu={readOnly ? undefined : ((useDefault || currentAppIsRunning) ? onNodeContextMenu : undefined)} // readOnly 或应用编排模式下禁用节点上下文菜单
onNodeDoubleClick={readOnly ? undefined : (!currentAppIsRunning ? onNodeDoubleClick : undefined)} // readOnly 或运行时禁用节点双击
onEdgeContextMenu={readOnly ? undefined : ((!currentAppIsRunning && useDefault) ? onEdgeContextMenu : undefined)} // readOnly 或运行时或应用编排模式下禁用边上下文菜单
onPaneClick={(useDefault || currentAppIsRunning) ? onPaneClick : undefined} // 运行时禁用面板点击
onPaneContextMenu={readOnly ? undefined : ((!currentAppIsRunning && useDefault) ? onPaneContextMenu : undefined)} // readOnly 或运行时或应用编排模式下禁用面板上下文菜单
onNodeDragStop={
readOnly
? undefined
: !currentAppIsRunning
? onNodeDragStop
: undefined
} // readOnly 或运行时禁用节点拖拽停止
onNodeContextMenu={
readOnly
? undefined
: useDefault || currentAppIsRunning
? onNodeContextMenu
: undefined
} // readOnly 或应用编排模式下禁用节点上下文菜单
onNodeDoubleClick={
readOnly
? undefined
: !currentAppIsRunning
? onNodeDoubleClick
: undefined
} // readOnly 或运行时禁用节点双击
onEdgeContextMenu={
readOnly
? undefined
: !currentAppIsRunning && useDefault
? onEdgeContextMenu
: undefined
} // readOnly 或运行时或应用编排模式下禁用边上下文菜单
onPaneClick={
useDefault || currentAppIsRunning ? onPaneClick : undefined
} // 运行时禁用面板点击
onPaneContextMenu={
readOnly
? undefined
: !currentAppIsRunning && useDefault
? onPaneContextMenu
: undefined
} // readOnly 或运行时或应用编排模式下禁用面板上下文菜单
onEdgeMouseEnter={(_event, edge) => {
setEdges((eds) => eds.map(e => {
setEdges((eds) =>
eds.map((e) => {
if (e.id === edge.id) {
return { ...e, data: { ...e.data, hovered: true } };
}
return e;
}));
})
);
}}
onEdgeMouseLeave={(_event, edge) => {
setEdges((eds) => eds.map(e => {
setEdges((eds) =>
eds.map((e) => {
if (e.id === edge.id) {
return { ...e, data: { ...e.data, hovered: false } };
}
return e;
}));
})
);
}}
fitView
selectionOnDrag={!currentAppIsRunning} // 运行时禁用拖拽选择
@ -550,14 +626,16 @@ const FlowEditorMain: React.FC<FlowEditorMainProps> = (props) => {
></ActionBar>
</Panel>
)}
{useDefault && !readOnly && <Panel position="top-right">
{useDefault && !readOnly && (
<Panel position="top-right">
<HandlerBar
onPublish={handlePublish}
isRunning={currentAppIsRunning}
setNodes={setNodes}
setEdges={setEdges}
/>
</Panel>}
</Panel>
)}
<AlignmentGuides />
</ReactFlow>
@ -572,7 +650,7 @@ const FlowEditorMain: React.FC<FlowEditorMainProps> = (props) => {
}}
>
<NodeContextMenu
node={nodes.find(n => n.id === menu.id)!}
node={nodes.find((n) => n.id === menu.id)!}
onDelete={deleteNode}
onEdit={editNode}
onCopy={copyNode}
@ -594,7 +672,7 @@ const FlowEditorMain: React.FC<FlowEditorMainProps> = (props) => {
}}
>
<EdgeContextMenu
edge={edges.find(e => e.id === menu.id)!}
edge={edges.find((e) => e.id === menu.id)!}
onDelete={deleteEdge}
onEdit={editEdge}
onAddNode={(edge) => {
@ -618,7 +696,11 @@ const FlowEditorMain: React.FC<FlowEditorMainProps> = (props) => {
>
<PaneContextMenu
position={menu.position!}
onAddNode={(nodeType: string, position: { x: number, y: number }, node: any) => {
onAddNode={(
nodeType: string,
position: { x: number; y: number },
node: any
) => {
addNodeOnPane(nodeType, position, node);
setIsEditModalOpen(false);
setMenu(null); // 关闭上下文菜单
@ -638,12 +720,18 @@ const FlowEditorMain: React.FC<FlowEditorMainProps> = (props) => {
/>
{/*统一的添加节点菜单 - 仅在默认模式且非运行时显示*/}
{!currentAppIsRunning && useDefault && (edgeForNodeAdd || positionForNodeAdd) && (
{!currentAppIsRunning &&
useDefault &&
(edgeForNodeAdd || positionForNodeAdd) && (
<div
style={{
position: 'absolute',
top: edgeForNodeAdd ? (edgeForNodeAdd.data?.y as number || 0) : (positionForNodeAdd?.y || 0),
left: edgeForNodeAdd ? ((edgeForNodeAdd.data?.x as number || 0) + 20) : (positionForNodeAdd?.x || 0),
top: edgeForNodeAdd
? (edgeForNodeAdd.data?.y as number) || 0
: positionForNodeAdd?.y || 0,
left: edgeForNodeAdd
? ((edgeForNodeAdd.data?.x as number) || 0) + 20
: positionForNodeAdd?.x || 0,
zIndex: 1000,
transform: 'none'
}}

@ -7,10 +7,11 @@ import {
IconPause,
IconSync,
IconUndo,
IconRedo
IconRedo,
} from '@arco-design/web-react/icon';
import { updateLogBarStatus } from '@/store/ideContainer';
import { useSelector, useDispatch } from 'react-redux';
import { getCurrentAppKey } from '@/utils/flow/runtime';
const ButtonGroup = Button.Group;
@ -37,30 +38,23 @@ const ActionBar: React.FC<ActionBarProps> = ({
onRun,
onPause,
onReRun,
isRunning = false
isRunning = false,
}) => {
const { logBarStatus, appRuntimeData, currentAppData } = useSelector((state: any) => state.ideContainer);
const { logBarStatus, appRuntimeData, currentAppData } = useSelector(
(state: any) => state.ideContainer
);
const dispatch = useDispatch();
// 辅助函数:获取当前应用/子流程的唯一标识符
const getCurrentAppKey = () => {
if (!currentAppData) return null;
// 如果是子流程key包含'sub'使用key作为标识符
if (currentAppData.key && currentAppData.key.includes('sub')) {
return currentAppData.key;
}
// 否则使用id
return currentAppData.id;
};
// 获取当前应用的运行状态
const currentAppKey = getCurrentAppKey();
const currentAppIsRunning = currentAppKey && appRuntimeData[currentAppKey]
const currentAppKey = getCurrentAppKey(currentAppData);
const currentAppIsRunning =
currentAppKey && appRuntimeData[currentAppKey]
? appRuntimeData[currentAppKey].isRunning
: false;
// 获取当前应用的暂停状态(如果有的话)
const currentAppIsPaused = currentAppKey && appRuntimeData[currentAppKey]
const currentAppIsPaused =
currentAppKey && appRuntimeData[currentAppKey]
? appRuntimeData[currentAppKey].isPaused
: false;
@ -84,7 +78,9 @@ const ActionBar: React.FC<ActionBarProps> = ({
return (
<div className="action-bar">
<Button onClick={onSave} type="primary" shape="round" icon={<IconSave />}></Button>
<Button onClick={onSave} type="primary" shape="round" icon={<IconSave />}>
</Button>
{useDefault && (
<>
<ButtonGroup style={{ marginLeft: 8 }}>

@ -5,6 +5,7 @@ import { updateLogBarStatus } from '@/store/ideContainer';
import { useSelector, useDispatch } from 'react-redux';
import { getNodeData } from '@/api/appIns';
import RunTimeData from './components/runTimeData';
import { getCurrentAppKey } from '@/utils/flow/runtime';
const TabPane = Tabs.TabPane;
@ -28,17 +29,17 @@ const data = [
{
key: '1',
title: '运行日志',
content: '运行时日志...'
content: '运行时日志...',
},
{
key: '2',
title: '校验日志',
content: '校验日志...'
content: '校验日志...',
},
{
key: '3',
title: '运行数据',
content: '运行数据日志...'
content: '运行数据日志...',
},
// {
// key: '4',
@ -56,28 +57,18 @@ const LogBar: React.FC<LogBarProps> = () => {
const [logContainerHeight, setLogContainerHeight] = useState('250px'); // 添加日志容器高度状态
const [runtimeData, setRuntimeData] = useState<RuntimeData>({}); // 添加运行数据状态
const [loading, setLoading] = useState(false);
const { logBarStatus, appRuntimeData, currentAppData } = useSelector((state: any) => state.ideContainer);
const { logBarStatus, appRuntimeData, currentAppData } = useSelector(
(state: any) => state.ideContainer
);
const dispatch = useDispatch();
// 辅助函数:获取当前应用/子流程的唯一标识符
const getCurrentAppKey = () => {
if (!currentAppData) return null;
// 如果是子流程key包含'sub'使用key作为标识符
if (currentAppData.key && currentAppData.key.includes('sub')) {
return currentAppData.key;
}
// 否则使用id
return currentAppData.id;
};
// 处理 Tab 点击事件
const handleTabClick = (key: string) => {
// 如果点击当前激活的 tab则切换收起状态
if (key === activeTab) {
dispatch(updateLogBarStatus(!logBarStatus));
}
else {
} else {
// 如果点击的是其他 tab则切换到该 tab 并展开
setActiveTab(key);
dispatch(updateLogBarStatus(true));
@ -87,20 +78,24 @@ const LogBar: React.FC<LogBarProps> = () => {
// 当 collapsed 状态改变时,直接更新元素的样式
useEffect(() => {
if (resizeBoxRef.current) {
resizeBoxRef.current.style.height = logBarStatus ? logContainerHeight : '0px';
resizeBoxRef.current.style.height = logBarStatus
? logContainerHeight
: '0px';
}
}, [logBarStatus, logContainerHeight]);
// 处理 ResizeBox 手动调整大小事件
const handleResize = (e: MouseEvent, size: {
const handleResize = (
e: MouseEvent,
size: {
width: number;
height: number;
}) => {
}
) => {
// 当高度接近收起状态的高度时,同步更新 logBarStatus 状态
if (size.height <= 40) {
dispatch(updateLogBarStatus(false));
}
else {
} else {
dispatch(updateLogBarStatus(true));
// 更新日志容器高度状态
setLogContainerHeight(`${size.height}px`);
@ -118,10 +113,10 @@ const LogBar: React.FC<LogBarProps> = () => {
id: Date.now(),
type,
message,
timestamp
timestamp,
};
setValidationLogs(prev => [...prev, newLog]);
setValidationLogs((prev) => [...prev, newLog]);
// 自动切换到校验日志tab并展开logBar
// setActiveTab('2');
@ -133,24 +128,24 @@ const LogBar: React.FC<LogBarProps> = () => {
id: Date.now(),
type,
message,
timestamp
timestamp,
};
setruntimeLogs(prev => [...prev, newLog]);
setruntimeLogs((prev) => [...prev, newLog]);
// 自动切换到运行日志tab并展开logBar
dispatch(updateLogBarStatus(true));
// 同时将日志添加到对应应用的运行日志中
// 如果提供了 appId优先使用提供的 appId否则使用当前激活的应用
const targetAppKey = appId || getCurrentAppKey();
const targetAppKey = appId || getCurrentAppKey(currentAppData);
if (targetAppKey) {
dispatch({
type: 'ideContainer/addRuntimeLog',
payload: {
log: newLog,
appId: targetAppKey
}
appId: targetAppKey,
},
});
}
}
@ -161,28 +156,35 @@ const LogBar: React.FC<LogBarProps> = () => {
// 清理事件监听器
return () => {
document.removeEventListener('logMessage', handleLogMessage as EventListener);
document.removeEventListener(
'logMessage',
handleLogMessage as EventListener
);
};
}, [dispatch, currentAppData]);
// 获取当前应用的运行状态
const currentAppKey = getCurrentAppKey();
const currentAppKey = getCurrentAppKey(currentAppData);
const isRunning = currentAppKey && appRuntimeData[currentAppKey]?.isRunning;
// 实现轮询获取运行数据 - 只在应用运行时轮询
useEffect(() => {
let intervalId: NodeJS.Timeout | null = null;
const appKey = getCurrentAppKey();
const appKey = getCurrentAppKey(currentAppData);
// 只有在应用正在运行且有 runId 时才开始轮询
if (appKey && appRuntimeData[appKey]?.isRunning && appRuntimeData[appKey]?.runId) {
if (
appKey &&
appRuntimeData[appKey]?.isRunning &&
appRuntimeData[appKey]?.runId
) {
const fetchRuntimeData = async () => {
try {
setLoading(true);
const response = await getNodeData(appRuntimeData[appKey].runId);
setRuntimeData(prev => ({
setRuntimeData((prev) => ({
...prev,
[appKey]: response.data
[appKey]: response.data,
}));
} catch (error) {
console.error('获取运行数据失败:', error);
@ -208,10 +210,10 @@ const LogBar: React.FC<LogBarProps> = () => {
// 当应用停止运行时,清除运行数据
useEffect(() => {
const appKey = getCurrentAppKey();
const appKey = getCurrentAppKey(currentAppData);
if (appKey && !appRuntimeData[appKey]?.isRunning) {
// 清除当前应用的运行数据
setRuntimeData(prev => {
setRuntimeData((prev) => {
const newData = { ...prev };
delete newData[appKey];
return newData;
@ -222,12 +224,25 @@ const LogBar: React.FC<LogBarProps> = () => {
// 渲染校验日志内容
const renderValidationLogs = () => {
return (
<div style={{ padding: '10px', height: 'calc(100% - 40px)', overflowY: 'auto' }}>
<div
style={{
padding: '10px',
height: 'calc(100% - 40px)',
overflowY: 'auto',
}}
>
{validationLogs.length === 0 ? (
<p></p>
) : (
validationLogs.map(log => (
<div key={log.id} style={{ marginBottom: '8px', padding: '4px', borderBottom: '1px solid #eee' }}>
validationLogs.map((log) => (
<div
key={log.id}
style={{
marginBottom: '8px',
padding: '4px',
borderBottom: '1px solid #eee',
}}
>
<div style={{ fontSize: '12px', color: '#999' }}>
{new Date(log.timestamp).toLocaleString()}
</div>
@ -244,18 +259,32 @@ const LogBar: React.FC<LogBarProps> = () => {
// 渲染运行时日志内容
const renderRuntimeLogs = () => {
// 获取当前应用的运行日志
const currentAppKey = getCurrentAppKey();
const currentAppLogs = currentAppKey && appRuntimeData[currentAppKey]
const currentAppKey = getCurrentAppKey(currentAppData);
const currentAppLogs =
currentAppKey && appRuntimeData[currentAppKey]
? appRuntimeData[currentAppKey].logs || []
: [];
return (
<div style={{ padding: '10px', height: 'calc(100% - 40px)', overflowY: 'auto' }}>
<div
style={{
padding: '10px',
height: 'calc(100% - 40px)',
overflowY: 'auto',
}}
>
{currentAppLogs.length === 0 ? (
<p></p>
) : (
currentAppLogs.map((log: LogMessage) => (
<div key={log.id} style={{ marginBottom: '8px', padding: '4px', borderBottom: '1px solid #eee' }}>
<div
key={log.id}
style={{
marginBottom: '8px',
padding: '4px',
borderBottom: '1px solid #eee',
}}
>
<div style={{ fontSize: '12px', color: '#999' }}>
{new Date(log.timestamp).toLocaleString()}
</div>
@ -271,11 +300,19 @@ const LogBar: React.FC<LogBarProps> = () => {
// 渲染运行数据内容
const renderRuntimeData = () => {
const currentAppKey = getCurrentAppKey();
const currentAppDataContent = currentAppKey ? runtimeData[currentAppKey] : null;
const currentAppKey = getCurrentAppKey(currentAppData);
const currentAppDataContent = currentAppKey
? runtimeData[currentAppKey]
: null;
return (
<div style={{ padding: '10px', height: 'calc(100% - 40px)', overflowY: 'auto' }}>
<div
style={{
padding: '10px',
height: 'calc(100% - 40px)',
overflowY: 'auto',
}}
>
{!currentAppDataContent ? (
<p></p>
) : (
@ -292,7 +329,7 @@ const LogBar: React.FC<LogBarProps> = () => {
className={styles.logBar}
directions={['top']}
style={{
height: logBarStatus ? logContainerHeight : '0px'
height: logBarStatus ? logContainerHeight : '0px',
}}
onMoving={handleResize}
>
@ -304,10 +341,13 @@ const LogBar: React.FC<LogBarProps> = () => {
>
{tabs.map((x) => (
<TabPane destroyOnHide key={x.key} title={x.title}>
{x.key === '1' ? renderRuntimeLogs() :
x.key === '2' ? renderValidationLogs() :
x.key === '3' ? renderRuntimeData() : // 添加运行数据渲染
x.content}
{x.key === '1'
? renderRuntimeLogs()
: x.key === '2'
? renderValidationLogs()
: x.key === '3'
? renderRuntimeData() // 添加运行数据渲染
: x.content}
</TabPane>
))}
</Tabs>

@ -1,4 +1,8 @@
import { createSlice } from '@reduxjs/toolkit';
import {
createDefaultAppRuntimeState,
getCurrentAppKey,
} from '@/utils/flow/runtime';
// 定义初始状态类型
interface IDEContainerState {
@ -16,22 +20,25 @@ interface IDEContainerState {
nodeStatusMap: Record<string, string>; // 节点状态映射
isRunning: boolean; // 是否正在运行
// 应用运行状态和日志数据按应用ID隔离存储
appRuntimeData: Record<string, {
appRuntimeData: Record<
string,
{
nodeStatusMap: Record<string, string>;
isRunning: boolean;
isPaused: boolean;
logs: any[];
runId: string;
eventSendNodeList: any[], // [{nodeID:topic}]
eventlisteneList: any[] // [{nodeID:topic}]
}>;
eventSendNodeList: any[]; // [{nodeID:topic}]
eventlisteneList: any[]; // [{nodeID:topic}]
}
>;
gloBalVarList: any;
// 组件开发相关状态
componentCoding: {
localProjectPath: string; // code-server路径
name: string; // 组件名称
projectId: string; // 组件标识
id: string,// 组件id
id: string; // 组件id
};
}
@ -56,20 +63,8 @@ const initialState: IDEContainerState = {
localProjectPath: '',
name: '',
projectId: '',
id: ''
}
};
// 辅助函数:获取当前应用/子流程的唯一标识符
// 对于子流程,使用 key如 sub_xxx对于主流程使用 id
const getCurrentAppKey = (currentAppData: any) => {
if (!currentAppData) return null;
// 如果是子流程key包含'sub'使用key作为标识符
if (currentAppData.key && currentAppData.key.includes('sub')) {
return currentAppData.key;
}
// 否则使用id
return currentAppData.id;
id: '',
},
};
// 创建切片
@ -90,7 +85,10 @@ const ideContainerSlice = createSlice({
state.canvasDataMap = { ...state.canvasDataMap, ...action.payload };
},
updateProjectComponentData(state, action) {
state.projectComponentData = { ...state.projectComponentData, ...action.payload };
state.projectComponentData = {
...state.projectComponentData,
...action.payload,
};
},
updateCurrentAppData(state, action) {
state.currentAppData = action.payload;
@ -98,7 +96,9 @@ const ideContainerSlice = createSlice({
// 切换应用时,同步全局 nodeStatusMap 为新应用的节点状态
const newAppKey = getCurrentAppKey(action.payload);
if (newAppKey && state.appRuntimeData[newAppKey]) {
state.nodeStatusMap = { ...state.appRuntimeData[newAppKey].nodeStatusMap };
state.nodeStatusMap = {
...state.appRuntimeData[newAppKey].nodeStatusMap,
};
state.isRunning = state.appRuntimeData[newAppKey].isRunning;
} else {
// 如果新应用没有运行时数据,清空节点状态
@ -140,15 +140,7 @@ const ideContainerSlice = createSlice({
// 更新目标应用的节点状态
if (targetAppKey) {
if (!state.appRuntimeData[targetAppKey]) {
state.appRuntimeData[targetAppKey] = {
nodeStatusMap: {},
isRunning: false,
isPaused: false,
logs: [],
runId: '',
eventSendNodeList: [],
eventlisteneList: []
};
state.appRuntimeData[targetAppKey] = createDefaultAppRuntimeState();
}
state.appRuntimeData[targetAppKey].nodeStatusMap[nodeId] = status;
}
@ -171,15 +163,7 @@ const ideContainerSlice = createSlice({
const appKey = getCurrentAppKey(state.currentAppData);
if (appKey) {
if (!state.appRuntimeData[appKey]) {
state.appRuntimeData[appKey] = {
nodeStatusMap: {},
isRunning: false,
isPaused: false,
logs: [],
runId: '',
eventSendNodeList: [],
eventlisteneList: []
};
state.appRuntimeData[appKey] = createDefaultAppRuntimeState();
}
state.appRuntimeData[appKey].isRunning = payload;
}
@ -189,15 +173,7 @@ const ideContainerSlice = createSlice({
const appKey = getCurrentAppKey(state.currentAppData);
if (appKey) {
if (!state.appRuntimeData[appKey]) {
state.appRuntimeData[appKey] = {
nodeStatusMap: {},
isRunning: false,
isPaused: false,
logs: [],
runId: '',
eventSendNodeList: [],
eventlisteneList: []
};
state.appRuntimeData[appKey] = createDefaultAppRuntimeState();
}
state.appRuntimeData[appKey].isPaused = payload;
}
@ -206,15 +182,7 @@ const ideContainerSlice = createSlice({
updateRuntimeId: (state, { payload }) => {
const appKey = getCurrentAppKey(state.currentAppData);
if (!state.appRuntimeData[appKey]) {
state.appRuntimeData[appKey] = {
nodeStatusMap: {},
isRunning: false,
isPaused: false,
logs: [],
runId: '',
eventSendNodeList: [],
eventlisteneList: []
};
state.appRuntimeData[appKey] = createDefaultAppRuntimeState();
}
state.appRuntimeData[appKey].runId = payload;
},
@ -222,31 +190,18 @@ const ideContainerSlice = createSlice({
updateEventNodeList: (state, { payload }) => {
const appKey = getCurrentAppKey(state.currentAppData);
if (!state.appRuntimeData[appKey]) {
state.appRuntimeData[appKey] = createDefaultAppRuntimeState();
}
state.appRuntimeData[appKey] = {
nodeStatusMap: {},
isRunning: false,
isPaused: false,
logs: [],
runId: '',
eventSendNodeList: [],
eventlisteneList: []
...state.appRuntimeData[appKey],
...payload,
};
}
state.appRuntimeData[appKey] = { ...state.appRuntimeData[appKey], ...payload };
},
// 添加运行日志
addRuntimeLog: (state, { payload }) => {
const { log, appId } = payload;
if (!state.appRuntimeData[appId]) {
state.appRuntimeData[appId] = {
nodeStatusMap: {},
isRunning: false,
isPaused: false,
logs: [],
runId: '',
eventSendNodeList: [],
eventlisteneList: []
};
state.appRuntimeData[appId] = createDefaultAppRuntimeState();
}
state.appRuntimeData[appId].logs.push(log);
},
@ -260,8 +215,8 @@ const ideContainerSlice = createSlice({
// 更新组件编码路径
updateComponentCodingPath(state, action) {
state.componentCoding = { ...action.payload };
}
}
},
},
});
// 导出动作 creators
@ -286,7 +241,7 @@ export const {
updateEventNodeList,
addRuntimeLog,
clearRuntimeLogs,
updateComponentCodingPath
updateComponentCodingPath,
} = ideContainerSlice.actions;
// 默认导出 reducer

@ -1,14 +1,8 @@
import { nodeTypeMap, registerNodeType } from '@/components/FlowEditor/node';
import store from '@/store/index';
import LocalNode from '@/components/FlowEditor/node/localNode/LocalNode';
import LoopNode from '@/components/FlowEditor/node/loopNode/LoopNode';
import SwitchNode from '@/components/FlowEditor/node/switchNode/SwitchNode';
import BasicNode from '@/components/FlowEditor/node/basicNode/BasicNode';
import ImageNode from '@/components/FlowEditor/node/imageNode/ImageNode';
import CodeNode from '@/components/FlowEditor/node/codeNode/CodeNode';
import RestNode from '@/components/FlowEditor/node/restNode/RestNode';
import { updateEventNodeList } from '@/store/ideContainer';
import { resolveNodeComponent } from '@/utils/flow/nodeRegistry';
/**
* flow editor nodes edges
@ -37,12 +31,14 @@ export const convertFlowData = (flowData: any, useDefault = true) => {
title: '开始',
parameters: {
apiIns: [],
apiOuts: [{ name: 'start', desc: '', dataType: '', defaultValue: '' }],
apiOuts: [
{ name: 'start', desc: '', dataType: '', defaultValue: '' },
],
dataIns: [],
dataOuts: []
dataOuts: [],
},
type: 'start',
},
type: 'start'
}
},
{
id: `end-${timestamp}`,
@ -51,16 +47,18 @@ export const convertFlowData = (flowData: any, useDefault = true) => {
data: {
title: '结束',
parameters: {
apiIns: [{ name: 'end', desc: '', dataType: '', defaultValue: '' }],
apiIns: [
{ name: 'end', desc: '', dataType: '', defaultValue: '' },
],
apiOuts: [],
dataIns: [],
dataOuts: []
dataOuts: [],
},
type: 'end',
},
},
type: 'end'
}
}
],
edges: []
edges: [],
};
}
// 否则返回空数组
@ -80,44 +78,55 @@ export const convertFlowData = (flowData: any, useDefault = true) => {
try {
const customDef = JSON.parse(nodeConfig.component.customDef);
// 使用展开运算符创建新数组,避免修改冻结对象
eventlisteneList.splice(eventlisteneList.length, 0, { [nodeId]: customDef.topic });
eventlisteneList.splice(eventlisteneList.length, 0, {
[nodeId]: customDef.topic,
});
} catch (error) {
console.log(error);
}
}
else if (nodeId.includes('EVENTSEND') && !nodeId.includes('EVENTSEND_SYNC')) {
} else if (
nodeId.includes('EVENTSEND') &&
!nodeId.includes('EVENTSEND_SYNC')
) {
try {
const customDef = JSON.parse(nodeConfig.component.customDef);
// 使用展开运算符创建新数组,避免修改冻结对象
eventSendNodeList.splice(eventSendNodeList.length, 0, { [nodeId]: customDef.topic });
eventSendNodeList.splice(eventSendNodeList.length, 0, {
[nodeId]: customDef.topic,
});
} catch (error) {
console.log(error);
}
}
if (eventlisteneList.length > 0 || eventSendNodeList.length > 0) store.dispatch(updateEventNodeList({
if (eventlisteneList.length > 0 || eventSendNodeList.length > 0)
store.dispatch(
updateEventNodeList({
eventSendNodeList: [...eventSendNodeList],
eventlisteneList: [...eventlisteneList]
}));
eventlisteneList: [...eventlisteneList],
})
);
else {
store.dispatch(updateEventNodeList({
store.dispatch(
updateEventNodeList({
eventSendNodeList: [],
eventlisteneList: []
}));
eventlisteneList: [],
})
);
}
// 确定节点类型
let nodeType = 'BASIC';
if (nodeId.includes('start')) {
nodeType = 'start';
}
else if (nodeId.includes('end')) {
} else if (nodeId.includes('end')) {
nodeType = 'end';
}
else if (nodeConfig.component?.type === 'LOOP_START' || nodeConfig.component?.type === 'LOOP_END') {
} else if (
nodeConfig.component?.type === 'LOOP_START' ||
nodeConfig.component?.type === 'LOOP_END'
) {
nodeType = 'LOOP';
}
else {
} else {
nodeType = nodeConfig.component?.type || 'BASIC';
}
@ -135,10 +144,10 @@ export const convertFlowData = (flowData: any, useDefault = true) => {
apiIns: getNodeApiIns(nodeId, nodeConfig, currentProjectCompData),
apiOuts: getNodeApiOuts(nodeId, nodeConfig, currentProjectCompData),
dataIns: nodeConfig.dataIns || [],
dataOuts: nodeConfig.dataOuts || []
dataOuts: nodeConfig.dataOuts || [],
},
type: nodeConfig.component?.type || nodeType,
},
type: nodeConfig.component?.type || nodeType
}
};
// 添加组件标识信息
@ -157,16 +166,25 @@ export const convertFlowData = (flowData: any, useDefault = true) => {
// 注册循环节点类型
if (nodeType === 'LOOP') {
const nodeMap = Array.from(Object.values(nodeTypeMap).map(key => key));
const nodeMap = Array.from(Object.values(nodeTypeMap).map((key) => key));
if (!nodeMap.includes('LOOP')) {
registerNodeType('LOOP', LoopNode, '循环');
}
}
// 注册其他节点类型
const nodeMap = Array.from(Object.values(nodeTypeMap).map(key => key));
if (!nodeMap.includes(nodeType) && nodeType !== 'start' && nodeType !== 'end' && nodeType !== 'LOOP') {
registerNodeType(nodeType, getNodeComponent(nodeType), nodeConfig.componentName);
const nodeMap = Array.from(Object.values(nodeTypeMap).map((key) => key));
if (
!nodeMap.includes(nodeType) &&
nodeType !== 'start' &&
nodeType !== 'end' &&
nodeType !== 'LOOP'
) {
registerNodeType(
nodeType,
resolveNodeComponent(nodeType),
nodeConfig.componentName
);
}
nodes.push(node);
@ -176,7 +194,15 @@ export const convertFlowData = (flowData: any, useDefault = true) => {
const addedEdges = new Set<string>();
// 创建一个映射来存储所有连接信息
const connections = new Map<string, { source: string; target: string; sourceHandle: string; targetHandle: string }>();
const connections = new Map<
string,
{
source: string;
target: string;
sourceHandle: string;
targetHandle: string;
}
>();
// 遍历所有节点,收集连接信息
for (const entry of nodeEntries) {
@ -187,7 +213,7 @@ export const convertFlowData = (flowData: any, useDefault = true) => {
if (nodeConfig.apiDownstream && Array.isArray(nodeConfig.apiDownstream)) {
nodeConfig.apiDownstream.forEach((targetArray: string[]) => {
if (Array.isArray(targetArray)) {
targetArray.forEach(target => {
targetArray.forEach((target) => {
if (typeof target === 'string' && target.includes('$$')) {
const [targetNodeId, targetHandle] = target.split('$$');
const connectionKey = `${nodeId}-${targetNodeId}`;
@ -199,17 +225,16 @@ export const convertFlowData = (flowData: any, useDefault = true) => {
if (existing) {
connections.set(connectionKey, {
...existing,
targetHandle: targetHandle
targetHandle: targetHandle,
});
}
}
else {
} else {
// 创建新的连接信息
connections.set(connectionKey, {
source: nodeId,
target: targetNodeId,
sourceHandle: '', // 将根据节点信息填充
targetHandle: targetHandle
targetHandle: targetHandle,
});
}
}
@ -222,7 +247,7 @@ export const convertFlowData = (flowData: any, useDefault = true) => {
if (nodeConfig.apiUpstream && Array.isArray(nodeConfig.apiUpstream)) {
nodeConfig.apiUpstream.forEach((sourceArray: string[]) => {
if (Array.isArray(sourceArray)) {
sourceArray.forEach(source => {
sourceArray.forEach((source) => {
if (typeof source === 'string' && source.includes('$$')) {
const [sourceNodeId, sourceHandle] = source.split('$$');
const connectionKey = `${sourceNodeId}-${nodeId}`;
@ -234,17 +259,16 @@ export const convertFlowData = (flowData: any, useDefault = true) => {
if (existing) {
connections.set(connectionKey, {
...existing,
sourceHandle: sourceHandle
sourceHandle: sourceHandle,
});
}
}
else {
} else {
// 创建新的连接信息
connections.set(connectionKey, {
source: sourceNodeId,
target: nodeId,
sourceHandle: sourceHandle,
targetHandle: '' // 将根据节点信息填充
targetHandle: '', // 将根据节点信息填充
});
}
}
@ -269,9 +293,13 @@ export const convertFlowData = (flowData: any, useDefault = true) => {
if (!finalSourceHandle) {
if (source === 'start') {
finalSourceHandle = 'start';
}
else if (sourceNode && sourceNode.data && sourceNode.data.parameters &&
sourceNode.data.parameters.apiOuts && sourceNode.data.parameters.apiOuts.length > 0) {
} else if (
sourceNode &&
sourceNode.data &&
sourceNode.data.parameters &&
sourceNode.data.parameters.apiOuts &&
sourceNode.data.parameters.apiOuts.length > 0
) {
// 查找匹配的目标句柄
const matchingApiOut = sourceNode.data.parameters.apiOuts.find(
(apiOut: any) => apiOut.name === targetHandle
@ -279,17 +307,18 @@ export const convertFlowData = (flowData: any, useDefault = true) => {
if (matchingApiOut) {
finalSourceHandle = matchingApiOut.name;
}
else {
} else {
// 如果没有精确匹配使用第一个apiOut
finalSourceHandle = sourceNode.data.parameters.apiOuts[0].name;
}
}
else if (sourceNode && sourceNode.component && sourceNode.component.type) {
} else if (
sourceNode &&
sourceNode.component &&
sourceNode.component.type
) {
// 根据节点类型获取正确的源句柄
finalSourceHandle = getNodeApiOutHandle(source, sourceNode);
}
else {
} else {
// 默认句柄
finalSourceHandle = 'done';
}
@ -301,9 +330,13 @@ export const convertFlowData = (flowData: any, useDefault = true) => {
if (!finalTargetHandle) {
if (target === 'end') {
finalTargetHandle = 'end';
}
else if (targetNode && targetNode.data && targetNode.data.parameters &&
targetNode.data.parameters.apiIns && targetNode.data.parameters.apiIns.length > 0) {
} else if (
targetNode &&
targetNode.data &&
targetNode.data.parameters &&
targetNode.data.parameters.apiIns &&
targetNode.data.parameters.apiIns.length > 0
) {
// 查找匹配的源句柄
const matchingApiIn = targetNode.data.parameters.apiIns.find(
(apiIn: any) => apiIn.name === sourceHandle
@ -311,13 +344,11 @@ export const convertFlowData = (flowData: any, useDefault = true) => {
if (matchingApiIn) {
finalTargetHandle = matchingApiIn.name;
}
else {
} else {
// 如果没有精确匹配使用第一个apiIn
finalTargetHandle = targetNode.data.parameters.apiIns[0].name;
}
}
else {
} else {
// 默认句柄
finalTargetHandle = 'start';
}
@ -338,8 +369,8 @@ export const convertFlowData = (flowData: any, useDefault = true) => {
type: 'custom',
lineType: 'api',
data: {
lineType: 'api'
}
lineType: 'api',
},
});
}
}
@ -356,9 +387,12 @@ export const convertFlowData = (flowData: any, useDefault = true) => {
// 第一个元素是源节点和句柄信息
const [sourceInfo, targetInfo] = connectionGroup;
if (typeof sourceInfo === 'string' && sourceInfo.includes('@@') &&
typeof targetInfo === 'string' && targetInfo.includes('@@')) {
if (
typeof sourceInfo === 'string' &&
sourceInfo.includes('@@') &&
typeof targetInfo === 'string' &&
targetInfo.includes('@@')
) {
const [sourceNodeId, sourceHandle] = sourceInfo.split('@@');
const [targetNodeId, targetHandle] = targetInfo.split('@@');
@ -378,8 +412,8 @@ export const convertFlowData = (flowData: any, useDefault = true) => {
type: 'custom',
lineType: 'data',
data: {
lineType: 'data'
}
lineType: 'data',
},
});
}
}
@ -403,12 +437,12 @@ export const revertFlowData = (nodes: any[], edges: any[]) => {
const flowData: any = {
id: 'main',
nodeConfigs: [],
lineConfigs: []
lineConfigs: [],
};
// 转换节点数据
if (nodes && nodes.length > 0) {
flowData.nodeConfigs = nodes.map(node => {
flowData.nodeConfigs = nodes.map((node) => {
// 确定 nodeId 和 nodeName
const nodeId = node.id || node.name;
const nodeName = node.data?.title || nodeId;
@ -418,21 +452,20 @@ export const revertFlowData = (nodes: any[], edges: any[]) => {
// 特殊处理 start 和 end 节点
if (nodeId.includes('start')) {
nodeType = 'start';
}
else if (nodeId.includes('end')) {
} else if (nodeId.includes('end')) {
nodeType = 'end';
}
// 构造 x6 数据(位置信息)
const x6 = JSON.stringify({
position: node.position
position: node.position,
});
// 构造 nodeConfig 对象
const nodeConfig: any = {
nodeId,
nodeName,
x6
x6,
};
// 处理 component 信息
@ -440,18 +473,20 @@ export const revertFlowData = (nodes: any[], edges: any[]) => {
nodeConfig.component = {
type: nodeType,
compIdentifier: node.data.component.compIdentifier || '',
compInstanceIdentifier: node.data.component.compInstanceIdentifier || '',
compId: node.data.compId || ''
compInstanceIdentifier:
node.data.component.compInstanceIdentifier || '',
compId: node.data.compId || '',
};
if (node.data.component?.customDef) nodeConfig.component.customDef = node.data.component.customDef;
}
else if (nodeType !== 'start' && nodeType !== 'end') {
if (node.data.component?.customDef)
nodeConfig.component.customDef = node.data.component.customDef;
} else if (nodeType !== 'start' && nodeType !== 'end') {
// 对于非 start/end 节点,添加基本的 component 信息
nodeConfig.component = {
type: nodeType
type: nodeType,
};
}
if (['BASIC', 'SUB'].includes(nodeType)) nodeConfig.component.compId = node.data.compId || '';
if (['BASIC', 'SUB'].includes(nodeType))
nodeConfig.component.compId = node.data.compId || '';
// 处理参数信息
const parameters = node.data?.parameters || {};
@ -463,7 +498,7 @@ export const revertFlowData = (nodes: any[], edges: any[]) => {
desc: input.desc,
dataType: input.dataType,
defaultValue: input.defaultValue,
arrayType: input.arrayType || null
arrayType: input.arrayType || null,
}));
}
@ -474,7 +509,7 @@ export const revertFlowData = (nodes: any[], edges: any[]) => {
desc: output.desc,
dataType: output.dataType,
defaultValue: output.defaultValue,
arrayType: output.arrayType || null
arrayType: output.arrayType || null,
}));
}
@ -486,22 +521,34 @@ export const revertFlowData = (nodes: any[], edges: any[]) => {
if (edges && edges.length > 0) {
flowData.lineConfigs = edges.map((edge, index) => {
// 查找源节点和目标节点以确定连线类型
const sourceNode = nodes.find(node => node.id === edge.source);
const targetNode = nodes.find(node => node.id === edge.target);
const sourceNode = nodes.find((node) => node.id === edge.source);
const targetNode = nodes.find((node) => node.id === edge.target);
let lineType = 'DATA'; // 默认为DATA类型
// 判断是否为CONVERT类型的连线
if (targetNode && ['JSONCONVERT', 'JSON2STR', 'STR2JSON'].includes(targetNode.type)) {
if (
targetNode &&
['JSONCONVERT', 'JSON2STR', 'STR2JSON'].includes(targetNode.type)
) {
lineType = 'CONVERT';
}
// 判断是否为API类型的连线
else if (edge.sourceHandle && (edge.sourceHandle === 'apiOuts' ||
sourceNode?.data?.parameters?.apiOuts?.some((out: any) => (out.name || out.id) === edge.sourceHandle))) {
else if (
edge.sourceHandle &&
(edge.sourceHandle === 'apiOuts' ||
sourceNode?.data?.parameters?.apiOuts?.some(
(out: any) => (out.name || out.id) === edge.sourceHandle
))
) {
lineType = 'API';
}
else if (edge.targetHandle && (edge.targetHandle === 'apiIns' ||
targetNode?.data?.parameters?.apiIns?.some((inp: any) => (inp.name || inp.id) === edge.targetHandle))) {
} else if (
edge.targetHandle &&
(edge.targetHandle === 'apiIns' ||
targetNode?.data?.parameters?.apiIns?.some(
(inp: any) => (inp.name || inp.id) === edge.targetHandle
))
) {
lineType = 'API';
}
@ -510,12 +557,12 @@ export const revertFlowData = (nodes: any[], edges: any[]) => {
lineType, // 添加lineType属性
prev: {
nodeId: edge.source,
endpointId: edge.sourceHandle || 'done' // 默认使用 'done'
endpointId: edge.sourceHandle || 'done', // 默认使用 'done'
},
next: {
nodeId: edge.target,
endpointId: edge.targetHandle || 'start' // 默认使用 'start'
}
endpointId: edge.targetHandle || 'start', // 默认使用 'start'
},
};
});
}
@ -530,20 +577,24 @@ export const revertFlowData = (nodes: any[], edges: any[]) => {
* @param complexKV - 使id {ID/nodeIdsub_ID}
* @returns convertFlowData
*/
export const reverseConvertFlowData = (nodes: any[], edges: any[], complexKV: any) => {
export const reverseConvertFlowData = (
nodes: any[],
edges: any[],
complexKV: any
) => {
// 初始化返回的数据结构
const flowData: any = {};
// 转换节点数据
if (nodes && nodes.length > 0) {
nodes.forEach(node => {
nodes.forEach((node) => {
const nodeId = node.id;
// 构造节点配置对象
const nodeConfig: any = {
id: nodeId,
componentName: node.data?.title || nodeId,
position: node.position || { x: 0, y: 0 }
position: node.position || { x: 0, y: 0 },
};
// 处理 component 信息
@ -570,16 +621,14 @@ export const reverseConvertFlowData = (nodes: any[], edges: any[], complexKV: an
dataIns: node.data.parameters.dataIns,
dataOuts: node.data.parameters.dataOuts,
subflowId: subflowId,
name: node.data.title
})
name: node.data.title,
}),
};
}
else if (node.data?.component) {
} else if (node.data?.component) {
nodeConfig.component = { ...node.data.component };
}
else {
} else {
nodeConfig.component = {
type: node.type
type: node.type,
};
}
@ -589,32 +638,28 @@ export const reverseConvertFlowData = (nodes: any[], edges: any[], complexKV: an
// 处理 apiIns输入API
if (parameters.apiIns && parameters.apiIns.length > 0) {
nodeConfig.apiIns = parameters.apiIns;
}
else {
} else {
nodeConfig.apiIns = [];
}
// 处理 apiOuts输出API
if (parameters.apiOuts && parameters.apiOuts.length > 0) {
nodeConfig.apiOuts = parameters.apiOuts;
}
else {
} else {
nodeConfig.apiOuts = [];
}
// 处理 dataIns输入数据
if (parameters.dataIns && parameters.dataIns.length > 0) {
nodeConfig.dataIns = parameters.dataIns;
}
else {
} else {
nodeConfig.dataIns = [];
}
// 处理 dataOuts输出数据
if (parameters.dataOuts && parameters.dataOuts.length > 0) {
nodeConfig.dataOuts = parameters.dataOuts;
}
else {
} else {
nodeConfig.dataOuts = [];
}
@ -632,33 +677,45 @@ export const reverseConvertFlowData = (nodes: any[], edges: any[], complexKV: an
// 处理连接关系
if (edges && edges.length > 0) {
// 分析边的连接关系
edges.forEach(edge => {
edges.forEach((edge) => {
const sourceNode = edge.source;
const targetNode = edge.target;
const sourceHandle = edge.sourceHandle || 'done';
const targetHandle = edge.targetHandle || 'start';
// 确定连接类型API 还是 DATA
const sourceNodeData = nodes.find(n => n.id === sourceNode);
const targetNodeData = nodes.find(n => n.id === targetNode);
const sourceNodeData = nodes.find((n) => n.id === sourceNode);
const targetNodeData = nodes.find((n) => n.id === targetNode);
const isApiConnection =
(sourceNodeData?.data?.parameters?.apiOuts?.some((out: any) => (out.name || out.id) === sourceHandle)) ||
(targetNodeData?.data?.parameters?.apiIns?.some((inp: any) => (inp.name || inp.id) === targetHandle)) ||
sourceHandle === 'start' || targetHandle === 'end' ||
sourceHandle === 'end' || targetHandle === 'start';
sourceNodeData?.data?.parameters?.apiOuts?.some(
(out: any) => (out.name || out.id) === sourceHandle
) ||
targetNodeData?.data?.parameters?.apiIns?.some(
(inp: any) => (inp.name || inp.id) === targetHandle
) ||
sourceHandle === 'start' ||
targetHandle === 'end' ||
sourceHandle === 'end' ||
targetHandle === 'start';
if (isApiConnection) {
// API 连接
// 添加下游连接
flowData[sourceNode].apiDownstream.push([`${targetNode}$$${targetHandle}`]);
flowData[sourceNode].apiDownstream.push([
`${targetNode}$$${targetHandle}`,
]);
// 添加上游连接
flowData[targetNode].apiUpstream.push([`${sourceNode}$$${sourceHandle}`]);
}
else {
flowData[targetNode].apiUpstream.push([
`${sourceNode}$$${sourceHandle}`,
]);
} else {
// 数据连接
const dataConnection = [`${sourceNode}@@${sourceHandle}`, `${targetNode}@@${targetHandle}`];
const dataConnection = [
`${sourceNode}@@${sourceHandle}`,
`${targetNode}@@${targetHandle}`,
];
flowData[sourceNode].dataDownstream.push(dataConnection);
flowData[targetNode].dataUpstream.push(dataConnection);
}
@ -669,59 +726,67 @@ export const reverseConvertFlowData = (nodes: any[], edges: any[], complexKV: an
};
// 获取节点的API输入参数
const getNodeApiIns = (nodeId: string, nodeConfig: any, currentProjectCompData: any[]) => {
const getNodeApiIns = (
nodeId: string,
nodeConfig: any,
currentProjectCompData: any[]
) => {
// JSON2STR 和 STR2JSON 不需要 API 输入
if (nodeConfig.component?.type === 'JSON2STR' || nodeConfig.component?.type === 'STR2JSON') {
if (
nodeConfig.component?.type === 'JSON2STR' ||
nodeConfig.component?.type === 'STR2JSON'
) {
return [];
}
// 对于特定类型的节点使用预定义值
if (nodeConfig.component?.type === 'LOOP_START') {
return [{ name: 'start', desc: '', dataType: '', defaultValue: '' }];
}
else if (nodeConfig.component?.type === 'LOOP_END') {
} else if (nodeConfig.component?.type === 'LOOP_END') {
return [{ name: 'continue', desc: '', dataType: '', defaultValue: '' }];
}
else if (nodeId.includes('end')) {
} else if (nodeId.includes('end')) {
return [{ name: 'end', desc: '', dataType: '', defaultValue: '' }];
}
else if (nodeConfig.component?.type === 'SUB') {
} else if (nodeConfig.component?.type === 'SUB') {
return [{ name: 'start', desc: '', dataType: '', defaultValue: '' }];
}
else {
const comp = currentProjectCompData.filter(item => {
} else {
const comp = currentProjectCompData.filter((item) => {
return (item.id || item?.comp?.id) === nodeConfig?.component?.compId;
});
if (comp && comp.length > 0) {
const apiIns = comp[0]?.def?.apis || comp[0]?.comp?.def?.apis || [];
return apiIns.map(v => {
return apiIns.map((v) => {
return {
...v,
name: v.id,
desc: v.desc,
dataType: v?.dataType || '',
defaultValue: v?.defaultValue || ''
defaultValue: v?.defaultValue || '',
};
});
}
else {
} else {
return [{ name: 'start', desc: '', dataType: '', defaultValue: '' }];
}
}
};
// 获取节点的API输出参数
const getNodeApiOuts = (nodeId: string, nodeConfig: any, currentProjectCompData: any[]) => {
const getNodeApiOuts = (
nodeId: string,
nodeConfig: any,
currentProjectCompData: any[]
) => {
// JSON2STR 和 STR2JSON 不需要 API 输出
if (nodeConfig.component?.type === 'JSON2STR' || nodeConfig.component?.type === 'STR2JSON') {
if (
nodeConfig.component?.type === 'JSON2STR' ||
nodeConfig.component?.type === 'STR2JSON'
) {
return [];
}
// 对于特定类型的节点使用预定义值
if (nodeConfig.component?.type === 'LOOP_START') {
return [{ name: 'done', desc: '', dataType: '', defaultValue: '' }];
}
else if (nodeConfig.component?.type === 'LOOP_END') {
} else if (nodeConfig.component?.type === 'LOOP_END') {
// 从customDef中获取apiOutIds数组
try {
const customDef = JSON.parse(nodeConfig.component?.customDef || '{}');
@ -731,15 +796,14 @@ const getNodeApiOuts = (nodeId: string, nodeConfig: any, currentProjectCompData:
const breakIndex = apiOutIds.indexOf('break');
if (breakIndex !== -1) {
// 返回从"break"开始的所有项
return apiOutIds.slice(breakIndex).map(id => ({
return apiOutIds.slice(breakIndex).map((id) => ({
name: id,
id: id,
desc: id,
dataType: '',
defaultValue: ''
defaultValue: '',
}));
}
else {
} else {
// 如果没有找到"break",则返回默认值
return [{ name: 'break', desc: '', dataType: '', defaultValue: '' }];
}
@ -747,8 +811,7 @@ const getNodeApiOuts = (nodeId: string, nodeConfig: any, currentProjectCompData:
// 解析失败时返回默认值
return [{ name: 'break', desc: '', dataType: '', defaultValue: '' }];
}
}
else if (nodeConfig.component?.type === 'SWITCH') {
} else if (nodeConfig.component?.type === 'SWITCH') {
// 从customDef中获取apiOutIds数组
try {
const customDef = JSON.parse(nodeConfig.component?.customDef || '{}');
@ -758,15 +821,14 @@ const getNodeApiOuts = (nodeId: string, nodeConfig: any, currentProjectCompData:
const breakIndex = apiOutIds.indexOf('default');
if (breakIndex !== -1) {
// 返回从"break"开始的所有项
return apiOutIds.slice(breakIndex).map(id => ({
return apiOutIds.slice(breakIndex).map((id) => ({
name: id,
id: id,
desc: id,
dataType: '',
defaultValue: ''
defaultValue: '',
}));
}
else {
} else {
// 如果没有找到"break",则返回默认值
return [{ name: 'default', desc: '', dataType: '', defaultValue: '' }];
}
@ -774,26 +836,25 @@ const getNodeApiOuts = (nodeId: string, nodeConfig: any, currentProjectCompData:
// 解析失败时返回默认值
return [{ name: 'done', desc: '', dataType: '', defaultValue: '' }];
}
}
else if (nodeId.includes('start')) {
} else if (nodeId.includes('start')) {
return [{ name: 'start', desc: '', dataType: '', defaultValue: '' }];
}
else if (nodeId.includes('end')) {
} else if (nodeId.includes('end')) {
return [];
}
else if (nodeConfig.component?.type === 'SUB') {
} else if (nodeConfig.component?.type === 'SUB') {
return [{ name: 'done', desc: '', dataType: '', defaultValue: '' }];
}
else {
const comp = currentProjectCompData.filter(item => item.id === nodeConfig?.component?.compId);
} else {
const comp = currentProjectCompData.filter(
(item) => item.id === nodeConfig?.component?.compId
);
if (comp && comp.length > 0) {
return [{
return [
{
...comp[0].def?.apiOut,
dataType: '',
defaultValue: ''
}];
}
else {
defaultValue: '',
},
];
} else {
return [{ name: 'done', desc: '', dataType: '', defaultValue: '' }];
}
}
@ -803,14 +864,11 @@ const getNodeApiOuts = (nodeId: string, nodeConfig: any, currentProjectCompData:
const getNodeApiOutHandle = (nodeId: string, nodeConfig: any) => {
if (nodeConfig.component?.type === 'LOOP_START') {
return 'done';
}
else if (nodeConfig.component?.type === 'LOOP_END') {
} else if (nodeConfig.component?.type === 'LOOP_END') {
return 'break';
}
else if (nodeId.includes('start')) {
} else if (nodeId.includes('start')) {
return 'start';
}
else if (nodeId.includes('end')) {
} else if (nodeId.includes('end')) {
return 'end';
}
return 'done';
@ -825,13 +883,17 @@ const getCurrentProjectStoreData = () => {
// 处理projectCompDto中的数据
if (compData.projectCompDto) {
const { mineComp = [], pubComp = [], teamWorkComp = [] } = compData.projectCompDto;
const {
mineComp = [],
pubComp = [],
teamWorkComp = [],
} = compData.projectCompDto;
// 添加mineComp数据
mineComp.forEach((item: any) => {
result.push({
...item,
type: 'mineComp'
type: 'mineComp',
});
});
@ -839,7 +901,7 @@ const getCurrentProjectStoreData = () => {
pubComp.forEach((item: any) => {
result.push({
...item,
type: 'pubComp'
type: 'pubComp',
});
});
@ -847,7 +909,7 @@ const getCurrentProjectStoreData = () => {
teamWorkComp.forEach((item: any) => {
result.push({
...item,
type: 'teamWorkComp'
type: 'teamWorkComp',
});
});
}
@ -860,7 +922,7 @@ const getCurrentProjectStoreData = () => {
mineFlow.forEach((item: any) => {
result.push({
...item,
type: 'mineFlow'
type: 'mineFlow',
});
});
@ -868,7 +930,7 @@ const getCurrentProjectStoreData = () => {
pubFlow.forEach((item: any) => {
result.push({
...item,
type: 'pubFlow'
type: 'pubFlow',
});
});
}
@ -881,7 +943,13 @@ const getCurrentProjectStoreData = () => {
const compLibsData = sessionStorage.getItem(compLibsKey);
if (compLibsData) {
const { myLibs = [], pubLibs = [], teamLibs = [], myFlow = [], pubFlow = [] } = JSON.parse(compLibsData);
const {
myLibs = [],
pubLibs = [],
teamLibs = [],
myFlow = [],
pubFlow = [],
} = JSON.parse(compLibsData);
// 处理 myLibs我的组件库
if (Array.isArray(myLibs)) {
@ -890,7 +958,7 @@ const getCurrentProjectStoreData = () => {
lib.children.forEach((item: any) => {
result.push({
...item,
type: 'mineComp'
type: 'mineComp',
});
});
}
@ -904,7 +972,7 @@ const getCurrentProjectStoreData = () => {
lib.children.forEach((item: any) => {
result.push({
...item,
type: 'pubComp'
type: 'pubComp',
});
});
}
@ -918,7 +986,7 @@ const getCurrentProjectStoreData = () => {
lib.children.forEach((item: any) => {
result.push({
...item,
type: 'teamWorkComp'
type: 'teamWorkComp',
});
});
}
@ -930,7 +998,7 @@ const getCurrentProjectStoreData = () => {
myFlow.forEach((item: any) => {
result.push({
...item,
type: 'mineFlow'
type: 'mineFlow',
});
});
}
@ -940,7 +1008,7 @@ const getCurrentProjectStoreData = () => {
pubFlow.forEach((item: any) => {
result.push({
...item,
type: 'pubFlow'
type: 'pubFlow',
});
});
}
@ -951,22 +1019,3 @@ const getCurrentProjectStoreData = () => {
}
return result;
};
// 根据节点类型获取对应的节点组件
const getNodeComponent = (nodeType: string) => {
switch (nodeType) {
case 'BASIC':
case 'SUB':
return BasicNode;
case 'SWITCH':
return SwitchNode;
case 'IMAGE':
return ImageNode;
case 'CODE':
return CodeNode;
case 'REST':
return RestNode;
default:
return LocalNode;
}
};

@ -0,0 +1,54 @@
import { Edge } from '@xyflow/react';
export const resolveInsertedNodeHandles = (node: any) => {
let sourceHandle = 'done';
let targetHandle = 'start';
if (node?.data?.parameters) {
const { apiOuts, apiIns } = node.data.parameters;
if (apiOuts && apiOuts.length > 0) {
sourceHandle = apiOuts[0].name || apiOuts[0].id || sourceHandle;
}
if (apiIns && apiIns.length > 0) {
targetHandle = apiIns[0].name || apiIns[0].id || targetHandle;
}
}
return { sourceHandle, targetHandle };
};
export const buildInsertedNodeEdges = (params: {
baseEdges: Edge[];
removedEdgeId: string;
sourceId: string;
sourceHandle?: string;
targetId: string;
targetHandle?: string;
insertedNodeId: string;
insertedNodeSourceHandle: string;
insertedNodeTargetHandle: string;
}): Edge[] => [
...params.baseEdges.filter((e) => e.id !== params.removedEdgeId),
{
id: `e${params.sourceId}-${params.insertedNodeId}`,
source: params.sourceId,
target: params.insertedNodeId,
sourceHandle: params.sourceHandle,
targetHandle: params.insertedNodeTargetHandle,
type: 'custom',
lineType: 'api',
data: { lineType: 'api' },
} as Edge,
{
id: `e${params.insertedNodeId}-${params.targetId}`,
source: params.insertedNodeId,
target: params.targetId,
sourceHandle: params.insertedNodeSourceHandle,
targetHandle: params.targetHandle,
type: 'custom',
lineType: 'api',
data: { lineType: 'api' },
} as Edge,
];

@ -0,0 +1,104 @@
import { Edge } from '@xyflow/react';
export const createLoopNodePair = (position: { x: number; y: number }) => {
const loopStartNode: any = {
id: `LOOP_START-${Date.now()}`,
type: 'LOOP',
position: { x: position.x, y: position.y },
data: {
title: '循环开始',
type: 'LOOP_START',
parameters: {
apiIns: [{ name: 'start', desc: '', dataType: '', defaultValue: '' }],
apiOuts: [{ name: 'done', desc: '', dataType: '', defaultValue: '' }],
dataIns: [],
dataOuts: [],
},
component: {},
},
};
const loopEndNode: any = {
id: `LOOP_END-${Date.now()}`,
type: 'LOOP',
position: { x: position.x + 400, y: position.y },
data: {
title: '循环结束',
type: 'LOOP_END',
parameters: {
apiIns: [
{ name: 'continue', desc: '', dataType: '', defaultValue: '' },
],
apiOuts: [{ name: 'break', desc: '', dataType: '', defaultValue: '' }],
dataIns: [
{
arrayType: null,
dataType: 'INTEGER',
defaultValue: 10,
desc: '最大循环次数',
id: 'maxTime',
},
],
dataOuts: [],
},
component: {
type: 'LOOP_END',
customDef: JSON.stringify({
apiOutIds: ['continue', 'break'],
conditions: [],
loopStartNodeId: loopStartNode.id,
}),
loopStartNodeId: loopStartNode.id,
},
},
};
loopStartNode.data.component = {
type: 'LOOP_START',
customDef: JSON.stringify({ loopEndNodeId: loopEndNode.id }),
};
return { loopStartNode, loopEndNode };
};
export const createLoopGroupEdge = (
loopStartId: string,
loopEndId: string
): Edge => ({
id: `${loopStartId}-${loopEndId}-group`,
source: loopStartId,
target: loopEndId,
sourceHandle: `${loopStartId}-group`,
targetHandle: `${loopEndId}-group`,
type: 'custom',
});
export const createLoopInsertConnectionEdges = (params: {
sourceId: string;
sourceHandle: string;
targetId: string;
targetHandle: string;
loopStartId: string;
loopEndId: string;
}): Edge[] => [
{
id: `e${params.sourceId}-${params.loopStartId}`,
source: params.sourceId,
target: params.loopStartId,
sourceHandle: params.sourceHandle,
targetHandle: 'start',
type: 'custom',
lineType: 'api',
data: { lineType: 'api' },
} as Edge,
{
id: `e${params.loopEndId}-${params.targetId}`,
source: params.loopEndId,
target: params.targetId,
sourceHandle: 'break',
targetHandle: params.targetHandle,
type: 'custom',
lineType: 'api',
data: { lineType: 'api' },
} as Edge,
];

@ -0,0 +1,79 @@
import { Node } from '@xyflow/react';
interface FlowNodeDefinition {
nodeName: string;
data: any;
id?: string;
flowHousVO?: {
id?: string;
};
}
type EventIdMode = 'id' | 'eventIdOptional';
export const createFlowNode = (
nodeType: string,
nodeDefinition: FlowNodeDefinition,
position: { x: number; y: number }
): Node => {
const node: any = {
id: `${nodeType}-${Date.now()}`,
type: nodeType,
position,
data: {
...nodeDefinition.data,
title: nodeDefinition.nodeName,
type: nodeType,
},
};
if (nodeDefinition.id || nodeDefinition?.flowHousVO?.id) {
node.data.compId = nodeDefinition.id || nodeDefinition?.flowHousVO?.id;
}
return node;
};
export const attachFlowNodeComponent = (
node: any,
nodeType: string,
nodeDefinition: FlowNodeDefinition,
eventList: any[],
eventIdMode: EventIdMode
) => {
if (nodeType === 'SWITCH') {
node.data.component = {
customDef: JSON.stringify({
apiOutIds: ['default'],
conditions: [],
}),
};
} else if (nodeType === 'SUB') {
node.data.component = {
type: nodeType,
compId: node.data.compId,
};
} else if (nodeType === 'EVENTSEND' || nodeType === 'EVENTLISTENE') {
const emptyEvent = eventList.find((item) =>
item.topic.includes('**empty**')
);
node.data.component = {
type: nodeType,
customDef: {
eventId:
eventIdMode === 'eventIdOptional'
? emptyEvent?.eventId ?? null
: emptyEvent.id,
name: emptyEvent.name,
topic: emptyEvent.topic,
},
};
} else {
node.data.component = {
type: nodeType,
compId: nodeDefinition.id,
};
}
return node;
};

@ -0,0 +1,61 @@
import {
createFlowNode,
attachFlowNodeComponent,
} from '@/utils/flow/nodeFactory';
import {
ensureNodeTypeRegistered,
resolveNodeComponent,
} from '@/utils/flow/nodeRegistry';
type EventIdMode = 'id' | 'eventIdOptional';
export const resolveNodeDefinition = (
nodeList: any[],
nodeType: string,
fallbackNode?: any
) => {
return nodeList.find((n) => n.nodeType === nodeType) || fallbackNode || null;
};
export const buildRuntimeNode = (params: {
nodeType: string;
nodeDefinition: any;
position: { x: number; y: number };
eventList: any[];
eventIdMode: EventIdMode;
}) => {
const { nodeType, nodeDefinition, position, eventList, eventIdMode } = params;
const newNode = attachFlowNodeComponent(
createFlowNode(nodeType, nodeDefinition, position),
nodeType,
nodeDefinition,
eventList,
eventIdMode
);
ensureNodeTypeRegistered(
nodeType,
nodeDefinition.nodeName,
resolveNodeComponent(nodeType)
);
return newNode;
};
export const buildDroppedNode = (
nodeData: any,
position: { x: number; y: number }
) => {
ensureNodeTypeRegistered(nodeData.nodeType, nodeData.nodeName);
return {
id: `${nodeData.nodeType}-${Date.now()}`,
type: nodeData.nodeType,
position,
data: {
...nodeData.data,
title: nodeData.nodeName,
type: nodeData.nodeType,
},
};
};

@ -0,0 +1,40 @@
import BasicNode from '@/components/FlowEditor/node/basicNode/BasicNode';
import SwitchNode from '@/components/FlowEditor/node/switchNode/SwitchNode';
import ImageNode from '@/components/FlowEditor/node/imageNode/ImageNode';
import CodeNode from '@/components/FlowEditor/node/codeNode/CodeNode';
import RestNode from '@/components/FlowEditor/node/restNode/RestNode';
import LocalNode from '@/components/FlowEditor/node/localNode/LocalNode';
import { nodeTypeMap, registerNodeType } from '@/components/FlowEditor/node';
export const resolveNodeComponent = (nodeType: string) => {
switch (nodeType) {
case 'BASIC':
case 'SUB':
return BasicNode;
case 'SWITCH':
return SwitchNode;
case 'IMAGE':
return ImageNode;
case 'CODE':
return CodeNode;
case 'REST':
return RestNode;
default:
return LocalNode;
}
};
export const ensureNodeTypeRegistered = (
nodeType: string,
nodeName: string,
component?: any
) => {
const nodeMap = Array.from(Object.values(nodeTypeMap).map((key) => key));
if (!nodeMap.includes(nodeType)) {
registerNodeType(
nodeType,
component || resolveNodeComponent(nodeType),
nodeName
);
}
};

@ -0,0 +1,34 @@
export interface FlowCurrentAppData {
id?: string;
key?: string;
}
export interface AppRuntimeState {
nodeStatusMap: Record<string, string>;
isRunning: boolean;
isPaused: boolean;
logs: any[];
runId: string;
eventSendNodeList: any[];
eventlisteneList: any[];
}
export const getCurrentAppKey = (
currentAppData: FlowCurrentAppData | null | undefined
) => {
if (!currentAppData) return null;
if (currentAppData.key && currentAppData.key.includes('sub')) {
return currentAppData.key;
}
return currentAppData.id || null;
};
export const createDefaultAppRuntimeState = (): AppRuntimeState => ({
nodeStatusMap: {},
isRunning: false,
isPaused: false,
logs: [],
runId: '',
eventSendNodeList: [],
eventlisteneList: [],
});

@ -0,0 +1,22 @@
import { Edge, Node } from '@xyflow/react';
interface FlowSnapshotDetail {
nodes: Node[];
edges: Edge[];
}
export const dispatchFlowSnapshot = (detail: FlowSnapshotDetail) => {
const event = new CustomEvent('takeSnapshot', {
detail,
});
document.dispatchEvent(event);
};
export const dispatchFlowSnapshotAsync = (
detail: FlowSnapshotDetail,
delay = 0
) => {
return setTimeout(() => {
dispatchFlowSnapshot(detail);
}, delay);
};

@ -1,10 +1,5 @@
import { defaultNodeTypes } from '@/components/FlowEditor/node/types/defaultType';
import BasicNode from '@/components/FlowEditor/node/basicNode/BasicNode';
import SwitchNode from '@/components/FlowEditor/node/switchNode/SwitchNode';
import ImageNode from '@/components/FlowEditor/node/imageNode/ImageNode';
import CodeNode from '@/components/FlowEditor/node/codeNode/CodeNode';
import RestNode from '@/components/FlowEditor/node/restNode/RestNode';
import LocalNode from '@/components/FlowEditor/node/localNode/LocalNode';
import { resolveNodeComponent } from '@/utils/flow/nodeRegistry';
// 获取handle类型 (api或data)
const getHandleType = (handleId: string, nodeParams: any) => {
@ -12,8 +7,15 @@ const getHandleType = (handleId: string, nodeParams: any) => {
const apiOuts = nodeParams.apiOuts || [];
const apiIns = nodeParams.apiIns || [];
if (apiOuts.some((api: any) => (api?.eventId || api.name || api.id) === handleId) ||
apiIns.some((api: any) => (api?.eventId || api.name || api.id) === handleId) || (handleId.includes('loop'))) {
if (
apiOuts.some(
(api: any) => (api?.eventId || api.name || api.id) === handleId
) ||
apiIns.some(
(api: any) => (api?.eventId || api.name || api.id) === handleId
) ||
handleId.includes('loop')
) {
return 'api';
}
@ -21,8 +23,10 @@ const getHandleType = (handleId: string, nodeParams: any) => {
const dataOuts = nodeParams.dataOuts || [];
const dataIns = nodeParams.dataIns || [];
if (dataOuts.some((data: any) => (data.name || data.id) === handleId) ||
dataIns.some((data: any) => (data.name || data.id) === handleId)) {
if (
dataOuts.some((data: any) => (data.name || data.id) === handleId) ||
dataIns.some((data: any) => (data.name || data.id) === handleId)
) {
return 'data';
}
@ -31,7 +35,12 @@ const getHandleType = (handleId: string, nodeParams: any) => {
};
// 验证数据类型是否匹配
const validateDataType = (sourceNode: defaultNodeTypes, targetNode: defaultNodeTypes, sourceHandleId: string, targetHandleId: string) => {
const validateDataType = (
sourceNode: defaultNodeTypes,
targetNode: defaultNodeTypes,
sourceHandleId: string,
targetHandleId: string
) => {
const sourceParams = sourceNode.data?.parameters || {};
const targetParams = targetNode.data?.parameters || {};
@ -41,13 +50,16 @@ const validateDataType = (sourceNode: defaultNodeTypes, targetNode: defaultNodeT
const sourceDataOuts = sourceParams.dataOuts || [];
// 查找源handle的数据类型
const sourceApi = sourceApiOuts.find((api: any) => api.name === sourceHandleId);
const sourceData = sourceDataOuts.find((data: any) => data.name === sourceHandleId);
const sourceApi = sourceApiOuts.find(
(api: any) => api.name === sourceHandleId
);
const sourceData = sourceDataOuts.find(
(data: any) => data.name === sourceHandleId
);
if (sourceApi) {
sourceDataType = sourceApi.dataType || '';
}
else if (sourceData) {
} else if (sourceData) {
sourceDataType = sourceData.dataType || '';
}
@ -57,13 +69,16 @@ const validateDataType = (sourceNode: defaultNodeTypes, targetNode: defaultNodeT
const targetDataIns = targetParams.dataIns || [];
// 查找目标handle的数据类型
const targetApi = targetApiIns.find((api: any) => api.name === targetHandleId);
const targetData = targetDataIns.find((data: any) => data.name === targetHandleId);
const targetApi = targetApiIns.find(
(api: any) => api.name === targetHandleId
);
const targetData = targetDataIns.find(
(data: any) => data.name === targetHandleId
);
if (targetApi) {
targetDataType = targetApi.dataType || '';
}
else if (targetData) {
} else if (targetData) {
targetDataType = targetData.dataType || '';
}
@ -76,29 +91,9 @@ const validateDataType = (sourceNode: defaultNodeTypes, targetNode: defaultNodeT
return sourceDataType === targetDataType;
};
// 根据节点类型获取对应的节点组件
const getNodeComponent = (nodeType: string) => {
switch (nodeType) {
case 'BASIC':
case 'SUB':
return BasicNode;
case 'SWITCH':
return SwitchNode;
case 'IMAGE':
return ImageNode;
case 'CODE':
return CodeNode;
case 'REST':
return RestNode;
default:
return LocalNode;
}
return resolveNodeComponent(nodeType);
};
export {
getHandleType,
validateDataType,
getNodeComponent
};
export { getHandleType, validateDataType, getNodeComponent };

Loading…
Cancel
Save