feat(flow): 添加节点运行状态指示器

master
钟良源 4 months ago
parent 0618335670
commit b9502164b2

@ -0,0 +1,33 @@
import React from 'react';
import { NodeProps, useStore } from '@xyflow/react';
import styles from './node/style/baseOther.module.less';
// 定义节点状态类型
export type NodeStatus = 'waiting' | 'running' | 'success' | 'failed';
// 节点状态指示器组件
const NodeStatusIndicator: React.FC<{ status: NodeStatus }> = ({ status }) => {
// 根据状态返回相应的指示器样式
const getStatusIndicator = () => {
switch (status) {
case 'waiting':
return <div className={styles['status-waiting']} />;
case 'running':
return <div className={styles['status-running']} />;
case 'success':
return <div className={styles['status-success']} />;
case 'failed':
return <div className={styles['status-failed']} />;
default:
return null;
}
};
return (
<div className={styles['node-status-indicator']}>
{getStatusIndicator()}
</div>
);
};
export default NodeStatusIndicator;

@ -1,31 +1,30 @@
import React, { useMemo } from 'react'; import React from 'react';
import styles from '@/components/FlowEditor/node/style/baseOther.module.less';
import NodeContentApp from '@/pages/flowEditor/components/nodeContentApp';
import { useStore } from '@xyflow/react'; import { useStore } from '@xyflow/react';
import { defaultNodeTypes } from '@/components/FlowEditor/node/types/defaultType'; import styles from '@/components/FlowEditor/node/style/baseOther.module.less';
import NodeContent from '@/pages/flowEditor/components/nodeContentApp';
import NodeStatusIndicator, { NodeStatus } from '@/components/FlowEditor/NodeStatusIndicator';
const AppNode = ({ data, id }: { data: defaultNodeTypes; id: string }) => { import { useStore as useFlowStore } from '@xyflow/react';
const title = data.title || '基础节点';
// 生成随机背景色使用useMemo确保颜色只在节点首次创建时生成一次 const AppNode = ({ data, id }: { data: any; id: string }) => {
const backgroundColor = useMemo(() => { const title = data.title || '应用节点';
const colors = ['#e59428', '#4a90e2', '#7b68ee', '#50c878', '#ff6347', '#9370db', '#00bfff', '#ff8c00'];
return colors[Math.floor(Math.random() * colors.length)];
}, []);
// 获取节点选中状态 - 适配React Flow v12 API // 获取节点选中状态 - 适配React Flow v12 API
const isSelected = useStore((state) => const isSelected = useStore((state) =>
state.nodeLookup.get(id)?.selected || false state.nodeLookup.get(id)?.selected || false
); );
// 获取节点运行状态
const nodeStatus: NodeStatus = useFlowStore((state) =>
(state.nodeLookup.get(id)?.data?.status as NodeStatus) || 'waiting'
);
return ( return (
<div className={`${styles['node-container']} ${isSelected ? styles.selected : ''}`}> <div className={`${styles['node-container']} ${isSelected ? styles.selected : ''}`}>
<div className={styles['node-header']} style={{ backgroundColor }}> <div className={styles['node-header']} style={{ backgroundColor: '#722ed1' }}>
{title} {title}
<NodeStatusIndicator status={nodeStatus} />
</div> </div>
<NodeContent data={data} />
<NodeContentApp data={{ ...data, type: 'app' }} />
</div> </div>
); );
}; };

@ -1,13 +1,11 @@
import React from 'react'; import React from 'react';
// import styles from '@/pages/flowEditor/node/style/base.module.less'; import { useStore } from '@xyflow/react';
import styles from '@/components/FlowEditor/node/style/baseOther.module.less'; import styles from '@/components/FlowEditor/node/style/baseOther.module.less';
import NodeContent from '@/pages/flowEditor/components/nodeContent';
import NodeContentOther from '@/pages/flowEditor/components/nodeContentOther'; import NodeContentOther from '@/pages/flowEditor/components/nodeContentOther';
import { useStore } from '@xyflow/react'; import NodeStatusIndicator, { NodeStatus } from '@/components/FlowEditor/NodeStatusIndicator';
import { defaultNodeTypes } from '@/components/FlowEditor/node/types/defaultType'; import { useStore as useFlowStore } from '@xyflow/react';
const BasicNode = ({ data, id }: { data: defaultNodeTypes; id: string }) => { const BasicNode = ({ data, id }: { data: any; id: string }) => {
const title = data.title || '基础节点'; const title = data.title || '基础节点';
// 获取节点选中状态 - 适配React Flow v12 API // 获取节点选中状态 - 适配React Flow v12 API
@ -15,13 +13,17 @@ const BasicNode = ({ data, id }: { data: defaultNodeTypes; id: string }) => {
state.nodeLookup.get(id)?.selected || false state.nodeLookup.get(id)?.selected || false
); );
// 获取节点运行状态
const nodeStatus: NodeStatus = useFlowStore((state) =>
(state.nodeLookup.get(id)?.data?.status as NodeStatus) || 'waiting'
);
return ( return (
<div className={`${styles['node-container']} ${isSelected ? styles.selected : ''}`}> <div className={`${styles['node-container']} ${isSelected ? styles.selected : ''}`}>
<div className={styles['node-header']} style={{ backgroundColor: '#e59428' }}> <div className={styles['node-header']} style={{ backgroundColor: '#e59428' }}>
{title} {title}
<NodeStatusIndicator status={nodeStatus} />
</div> </div>
{/*<NodeContent data={{ ...data, type: 'basic' }} />*/}
<NodeContentOther data={{ ...data, type: 'basic' }} /> <NodeContentOther data={{ ...data, type: 'basic' }} />
</div> </div>
); );

@ -1,11 +1,10 @@
import React from 'react'; import React from 'react';
// import styles from '@/pages/flowEditor/node/style/base.module.less';
import styles from '@/components/FlowEditor/node/style/baseOther.module.less'; import styles from '@/components/FlowEditor/node/style/baseOther.module.less';
import NodeContent from '@/pages/flowEditor/components/nodeContent'; import NodeContentOther from '@/pages/flowEditor/components/nodeContentOther';
import { useStore } from '@xyflow/react'; import { useStore } from '@xyflow/react';
import { defaultNodeTypes } from '@/components/FlowEditor/node/types/defaultType'; import { defaultNodeTypes } from '@/components/FlowEditor/node/types/defaultType';
import NodeContentOther from '@/pages/flowEditor/components/nodeContentOther'; import NodeStatusIndicator, { NodeStatus } from '@/components/FlowEditor/NodeStatusIndicator';
import { useStore as useFlowStore } from '@xyflow/react';
const EndNode = ({ data, id }: { data: defaultNodeTypes; id: string }) => { const EndNode = ({ data, id }: { data: defaultNodeTypes; id: string }) => {
const title = data.title || '结束'; const title = data.title || '结束';
@ -15,13 +14,17 @@ const EndNode = ({ data, id }: { data: defaultNodeTypes; id: string }) => {
state.nodeLookup.get(id)?.selected || false state.nodeLookup.get(id)?.selected || false
); );
// 获取节点运行状态
const nodeStatus: NodeStatus = useFlowStore((state) =>
(state.nodeLookup.get(id)?.data?.status as NodeStatus) || 'waiting'
);
return ( return (
<div className={`${styles['node-container']} ${isSelected ? styles.selected : ''}`}> <div className={`${styles['node-container']} ${isSelected ? styles.selected : ''}`}>
<div className={styles['node-header']} style={{ backgroundColor: '#c05144' }}> <div className={styles['node-header']} style={{ backgroundColor: '#c05144' }}>
{title} {title}
<NodeStatusIndicator status={nodeStatus} />
</div> </div>
{/*<NodeContent data={{ ...data, type: 'end' }} />*/}
<NodeContentOther data={{ ...data, type: 'end' }} /> <NodeContentOther data={{ ...data, type: 'end' }} />
</div> </div>
); );

@ -1,11 +1,10 @@
import React from 'react'; import React from 'react';
import { useStore } from '@xyflow/react'; import { useStore } from '@xyflow/react';
// import styles from '@/pages/flowEditor/node/style/base.module.less';
import styles from '@/components/FlowEditor/node/style/baseOther.module.less'; import styles from '@/components/FlowEditor/node/style/baseOther.module.less';
import NodeContent from '@/pages/flowEditor/components/nodeContent';
import DynamicIcon from '@/components/DynamicIcon'; import DynamicIcon from '@/components/DynamicIcon';
import NodeContentOther from '@/pages/flowEditor/components/nodeContentOther'; import NodeContentOther from '@/pages/flowEditor/components/nodeContentOther';
import NodeStatusIndicator, { NodeStatus } from '@/components/FlowEditor/NodeStatusIndicator';
import { useStore as useFlowStore } from '@xyflow/react';
const setIcon = (nodeType: string) => { const setIcon = (nodeType: string) => {
let type = 'IconApps'; let type = 'IconApps';
@ -67,13 +66,18 @@ const LocalNode = ({ data, id }: { data: any; id: string }) => {
state.nodeLookup.get(id)?.selected || false state.nodeLookup.get(id)?.selected || false
); );
// 获取节点运行状态
const nodeStatus: NodeStatus = useFlowStore((state) =>
(state.nodeLookup.get(id)?.data?.status as NodeStatus) || 'waiting'
);
return ( return (
<div className={`${styles['node-container']} ${isSelected ? styles.selected : ''}`}> <div className={`${styles['node-container']} ${isSelected ? styles.selected : ''}`}>
<div className={styles['node-header']} style={{ backgroundColor: '#1890ff' }}> <div className={styles['node-header']} style={{ backgroundColor: '#1890ff' }}>
{setIcon(data.type)} {setIcon(data.type)}
{title} {title}
<NodeStatusIndicator status={nodeStatus} />
</div> </div>
{/*<NodeContent data={data} />*/}
<NodeContentOther data={data} /> <NodeContentOther data={data} />
</div> </div>
); );

@ -1,11 +1,10 @@
import React from 'react'; import React from 'react';
// import styles from '@/pages/flowEditor/node/style/base.module.less';
import styles from '@/components/FlowEditor/node/style/baseOther.module.less'; import styles from '@/components/FlowEditor/node/style/baseOther.module.less';
import NodeContent from '@/pages/flowEditor/components/nodeContent'; import NodeContentOther from '@/pages/flowEditor/components/nodeContentOther';
import { useStore } from '@xyflow/react'; import { useStore } from '@xyflow/react';
import { defaultNodeTypes } from '@/components/FlowEditor/node/types/defaultType'; import { defaultNodeTypes } from '@/components/FlowEditor/node/types/defaultType';
import NodeContentOther from '@/pages/flowEditor/components/nodeContentOther'; import NodeStatusIndicator, { NodeStatus } from '@/components/FlowEditor/NodeStatusIndicator';
import { useStore as useFlowStore } from '@xyflow/react';
const StartNode = ({ data, id }: { data: defaultNodeTypes; id: string }) => { const StartNode = ({ data, id }: { data: defaultNodeTypes; id: string }) => {
const title = data.title || '开始'; const title = data.title || '开始';
@ -15,13 +14,17 @@ const StartNode = ({ data, id }: { data: defaultNodeTypes; id: string }) => {
state.nodeLookup.get(id)?.selected || false state.nodeLookup.get(id)?.selected || false
); );
// 获取节点运行状态
const nodeStatus: NodeStatus = useFlowStore((state) =>
(state.nodeLookup.get(id)?.data?.status as NodeStatus) || 'waiting'
);
return ( return (
<div className={`${styles['node-container']} ${isSelected ? styles.selected : ''}`}> <div className={`${styles['node-container']} ${isSelected ? styles.selected : ''}`}>
<div className={styles['node-header']} style={{ backgroundColor: '#29b971' }}> <div className={styles['node-header']} style={{ backgroundColor: '#29b971' }}>
{title} {title}
<NodeStatusIndicator status={nodeStatus} />
</div> </div>
{/*<NodeContent data={{ ...data, type: 'start' }} />*/}
<NodeContentOther data={{ ...data, type: 'start' }} /> <NodeContentOther data={{ ...data, type: 'start' }} />
</div> </div>
); );

@ -13,6 +13,7 @@
border-bottom: 1px solid rgba(255, 255, 255, 0.2); border-bottom: 1px solid rgba(255, 255, 255, 0.2);
color: #000000; color: #000000;
text-align: center; text-align: center;
position: relative;
} }
.node-api-box, .node-api-box,
@ -92,4 +93,63 @@
min-height: 20px; min-height: 20px;
text-align: center; text-align: center;
} }
}
// 节点状态指示器样式
.node-status-indicator {
position: absolute;
top: -10px;
right: -10px;
width: 20px;
height: 20px;
z-index: 10;
}
.status-waiting {
width: 16px;
height: 16px;
border-radius: 50%;
background-color: #cccccc;
border: 2px solid #ffffff;
box-shadow: 0 0 4px rgba(0, 0, 0, 0.2);
}
.status-running {
width: 16px;
height: 16px;
border-radius: 50%;
background-color: #1890ff;
border: 2px solid #ffffff;
box-shadow: 0 0 4px rgba(0, 0, 0, 0.2);
animation: pulse 1.5s infinite;
}
.status-success {
width: 16px;
height: 16px;
border-radius: 50%;
background-color: #52c41a;
border: 2px solid #ffffff;
box-shadow: 0 0 4px rgba(0, 0, 0, 0.2);
}
.status-failed {
width: 16px;
height: 16px;
border-radius: 50%;
background-color: #ff4d4f;
border: 2px solid #ffffff;
box-shadow: 0 0 4px rgba(0, 0, 0, 0.2);
}
@keyframes pulse {
0% {
box-shadow: 0 0 0 0 rgba(24, 144, 255, 0.7);
}
70% {
box-shadow: 0 0 0 6px rgba(24, 144, 255, 0);
}
100% {
box-shadow: 0 0 0 0 rgba(24, 144, 255, 0);
}
} }

@ -1,4 +1,4 @@
import React, { useCallback } from 'react'; import React, { useCallback, useEffect } from 'react';
import { import {
applyNodeChanges, applyNodeChanges,
applyEdgeChanges, applyEdgeChanges,
@ -15,7 +15,7 @@ import { localNodeData } from '@/pages/flowEditor/sideBar/config/localNodeData';
import { useAlignmentGuidelines } from '@/hooks/useAlignmentGuidelines'; import { useAlignmentGuidelines } from '@/hooks/useAlignmentGuidelines';
import LocalNode from '@/components/FlowEditor/node/localNode/LocalNode'; import LocalNode from '@/components/FlowEditor/node/localNode/LocalNode';
import LoopNode from '@/components/FlowEditor/node/loopNode/LoopNode'; import LoopNode from '@/components/FlowEditor/node/loopNode/LoopNode';
import { updateCanvasDataMap } from '@/store/ideContainer'; import { updateCanvasDataMap, resetNodeStatus } from '@/store/ideContainer';
import { import {
validateAllNodes, validateAllNodes,
showValidationErrors, showValidationErrors,
@ -919,6 +919,10 @@ export const useFlowCallbacks = (
socketId socketId
}; };
runMainFlow(params); runMainFlow(params);
// 重置节点状态
dispatch(resetNodeStatus());
} }
else { else {
// 停止运行 // 停止运行
@ -959,4 +963,5 @@ export const useFlowCallbacks = (
saveFlowDataToServer, saveFlowDataToServer,
handleRun handleRun
}; };
}; };
export default useFlowCallbacks;

@ -1,4 +1,4 @@
import { useState, useRef } from 'react'; import { useState, useRef, useEffect } from 'react';
import { Node, Edge } from '@xyflow/react'; import { Node, Edge } from '@xyflow/react';
import { debounce } from 'lodash'; import { debounce } from 'lodash';
import { useSelector, useDispatch } from 'react-redux'; import { useSelector, useDispatch } from 'react-redux';
@ -9,7 +9,7 @@ import { Dispatch } from 'redux';
export const useFlowEditorState = (initialData?: any) => { export const useFlowEditorState = (initialData?: any) => {
const [nodes, setNodes] = useState<Node[]>([]); const [nodes, setNodes] = useState<Node[]>([]);
const [edges, setEdges] = useState<Edge[]>([]); const [edges, setEdges] = useState<Edge[]>([]);
const { canvasDataMap } = useSelector((state: any) => state.ideContainer); const { canvasDataMap, nodeStatusMap } = useSelector((state: any) => state.ideContainer);
const dispatch = useDispatch(); const dispatch = useDispatch();
// 添加编辑弹窗相关状态 // 添加编辑弹窗相关状态
@ -28,6 +28,19 @@ export const useFlowEditorState = (initialData?: any) => {
const [historyInitialized, setHistoryInitialized] = useState(false); const [historyInitialized, setHistoryInitialized] = useState(false);
const historyTimeoutRef = useRef<NodeJS.Timeout | null>(null); const historyTimeoutRef = useRef<NodeJS.Timeout | null>(null);
// 更新节点状态将从store获取的状态应用到节点上
useEffect(() => {
setNodes(prevNodes =>
prevNodes.map(node => ({
...node,
data: {
...node.data,
status: nodeStatusMap[node.id] || 'waiting'
}
}))
);
}, [nodeStatusMap]);
const updateCanvasDataMapDebounced = useRef( const updateCanvasDataMapDebounced = useRef(
debounce((dispatch: Dispatch<any>, canvasDataMap: any, id: string, nodes: Node[], edges: Edge[]) => { debounce((dispatch: Dispatch<any>, canvasDataMap: any, id: string, nodes: Node[], edges: Edge[]) => {
dispatch(updateCanvasDataMap({ dispatch(updateCanvasDataMap({

@ -30,7 +30,7 @@ import { getUserToken } from '@/api/user';
import useWebSocket from '@/hooks/useWebSocket'; import useWebSocket from '@/hooks/useWebSocket';
import { Message } from '@arco-design/web-react'; import { Message } from '@arco-design/web-react';
import { queryEventItemBySceneId } from '@/api/event'; import { queryEventItemBySceneId } from '@/api/event';
import { updateNodeStatus } from '@/store/ideContainer';
type UrlParamsOptions = { type UrlParamsOptions = {
identity?: string; identity?: string;
@ -79,6 +79,28 @@ function IDEContainer() {
const socketMessage = JSON.parse(event.data); const socketMessage = JSON.parse(event.data);
if (socketMessage?.socketId) dispatch(updateSocketId(socketMessage.socketId)); if (socketMessage?.socketId) dispatch(updateSocketId(socketMessage.socketId));
// 处理节点状态更新
if (socketMessage?.nodeLog) {
const { nodeId, state } = socketMessage.nodeLog;
// 将状态映射为前端使用的状态
let status = 'waiting';
switch (state) {
case 0: // 运行中
status = 'running';
break;
case 1: // 运行成功
status = 'success';
break;
case -1: // 运行失败
status = 'failed';
break;
default:// 等待运行
status = 'waiting';
break;
}
// 更新节点状态
dispatch(updateNodeStatus({ nodeId, status }));
}
} }
}); });

@ -10,6 +10,7 @@ interface IDEContainerState {
eventList: any; eventList: any;
logBarStatus?: boolean; logBarStatus?: boolean;
socketId: string; socketId: string;
nodeStatusMap: Record<string, string>; // 节点状态映射
} }
const initialState: IDEContainerState = { const initialState: IDEContainerState = {
@ -19,11 +20,13 @@ const initialState: IDEContainerState = {
canvasDataMap: {}, // 每个画布的缓存信息 canvasDataMap: {}, // 每个画布的缓存信息
projectComponentData: {}, // 工程下的组件列表 projectComponentData: {}, // 工程下的组件列表
currentAppData: {}, // 当前选中的应用数据 currentAppData: {}, // 当前选中的应用数据
eventList:[], // 工程下的事件列表 eventList: [], // 工程下的事件列表
logBarStatus: false, logBarStatus: false,
socketId: '' // 工程的socketId socketId: '', // 工程的socketId
nodeStatusMap: {} // 初始化节点状态映射
}; };
// 创建切片
const ideContainerSlice = createSlice({ const ideContainerSlice = createSlice({
name: 'ideContainer', name: 'ideContainer',
initialState, initialState,
@ -54,10 +57,20 @@ const ideContainerSlice = createSlice({
}, },
updateSocketId(state, action) { updateSocketId(state, action) {
state.socketId = action.payload; state.socketId = action.payload;
},
// 更新节点状态
updateNodeStatus: (state, { payload }) => {
const { nodeId, status } = payload;
state.nodeStatusMap[nodeId] = status;
},
// 重置节点状态
resetNodeStatus: (state) => {
state.nodeStatusMap = {};
} }
} }
}); });
// 导出动作 creators
export const { export const {
updateInfo, updateInfo,
updateMenuData, updateMenuData,
@ -67,7 +80,10 @@ export const {
updateCurrentAppData, updateCurrentAppData,
updateEventList, updateEventList,
updateLogBarStatus, updateLogBarStatus,
updateSocketId updateSocketId,
updateNodeStatus,
resetNodeStatus
} = ideContainerSlice.actions; } = ideContainerSlice.actions;
// 默认导出 reducer
export default ideContainerSlice.reducer; export default ideContainerSlice.reducer;
Loading…
Cancel
Save