diff --git a/src/components/FlowEditor/NodeStatusIndicator.tsx b/src/components/FlowEditor/NodeStatusIndicator.tsx new file mode 100644 index 0000000..d3a1350 --- /dev/null +++ b/src/components/FlowEditor/NodeStatusIndicator.tsx @@ -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
; + case 'running': + return
; + case 'success': + return
; + case 'failed': + return
; + default: + return null; + } + }; + + return ( +
+ {getStatusIndicator()} +
+ ); +}; + +export default NodeStatusIndicator; \ No newline at end of file diff --git a/src/components/FlowEditor/node/appNode/AppNode.tsx b/src/components/FlowEditor/node/appNode/AppNode.tsx index fcc67b4..5dcee09 100644 --- a/src/components/FlowEditor/node/appNode/AppNode.tsx +++ b/src/components/FlowEditor/node/appNode/AppNode.tsx @@ -1,31 +1,30 @@ -import React, { useMemo } from 'react'; -import styles from '@/components/FlowEditor/node/style/baseOther.module.less'; -import NodeContentApp from '@/pages/flowEditor/components/nodeContentApp'; +import React from 'react'; import { useStore } from '@xyflow/react'; -import { defaultNodeTypes } from '@/components/FlowEditor/node/types/defaultType'; - - -const AppNode = ({ data, id }: { data: defaultNodeTypes; id: string }) => { - const title = data.title || '基础节点'; +import styles from '@/components/FlowEditor/node/style/baseOther.module.less'; +import NodeContent from '@/pages/flowEditor/components/nodeContentApp'; +import NodeStatusIndicator, { NodeStatus } from '@/components/FlowEditor/NodeStatusIndicator'; +import { useStore as useFlowStore } from '@xyflow/react'; - // 生成随机背景色,使用useMemo确保颜色只在节点首次创建时生成一次 - const backgroundColor = useMemo(() => { - const colors = ['#e59428', '#4a90e2', '#7b68ee', '#50c878', '#ff6347', '#9370db', '#00bfff', '#ff8c00']; - return colors[Math.floor(Math.random() * colors.length)]; - }, []); +const AppNode = ({ data, id }: { data: any; id: string }) => { + const title = data.title || '应用节点'; // 获取节点选中状态 - 适配React Flow v12 API const isSelected = useStore((state) => state.nodeLookup.get(id)?.selected || false ); + // 获取节点运行状态 + const nodeStatus: NodeStatus = useFlowStore((state) => + (state.nodeLookup.get(id)?.data?.status as NodeStatus) || 'waiting' + ); + return (
-
+
{title} +
- - +
); }; diff --git a/src/components/FlowEditor/node/basicNode/BasicNode.tsx b/src/components/FlowEditor/node/basicNode/BasicNode.tsx index 6e04c51..22912c2 100644 --- a/src/components/FlowEditor/node/basicNode/BasicNode.tsx +++ b/src/components/FlowEditor/node/basicNode/BasicNode.tsx @@ -1,13 +1,11 @@ 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 NodeContent from '@/pages/flowEditor/components/nodeContent'; import NodeContentOther from '@/pages/flowEditor/components/nodeContentOther'; -import { useStore } from '@xyflow/react'; -import { defaultNodeTypes } from '@/components/FlowEditor/node/types/defaultType'; - +import NodeStatusIndicator, { NodeStatus } from '@/components/FlowEditor/NodeStatusIndicator'; +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 || '基础节点'; // 获取节点选中状态 - 适配React Flow v12 API @@ -15,13 +13,17 @@ const BasicNode = ({ data, id }: { data: defaultNodeTypes; id: string }) => { state.nodeLookup.get(id)?.selected || false ); + // 获取节点运行状态 + const nodeStatus: NodeStatus = useFlowStore((state) => + (state.nodeLookup.get(id)?.data?.status as NodeStatus) || 'waiting' + ); + return (
{title} +
- - {/**/}
); diff --git a/src/components/FlowEditor/node/endNode/EndNode.tsx b/src/components/FlowEditor/node/endNode/EndNode.tsx index 6eb7d3a..48bfae1 100644 --- a/src/components/FlowEditor/node/endNode/EndNode.tsx +++ b/src/components/FlowEditor/node/endNode/EndNode.tsx @@ -1,11 +1,10 @@ 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 NodeContent from '@/pages/flowEditor/components/nodeContent'; +import NodeContentOther from '@/pages/flowEditor/components/nodeContentOther'; import { useStore } from '@xyflow/react'; 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 title = data.title || '结束'; @@ -15,13 +14,17 @@ const EndNode = ({ data, id }: { data: defaultNodeTypes; id: string }) => { state.nodeLookup.get(id)?.selected || false ); + // 获取节点运行状态 + const nodeStatus: NodeStatus = useFlowStore((state) => + (state.nodeLookup.get(id)?.data?.status as NodeStatus) || 'waiting' + ); + return (
{title} +
- - {/**/}
); diff --git a/src/components/FlowEditor/node/localNode/LocalNode.tsx b/src/components/FlowEditor/node/localNode/LocalNode.tsx index e694256..0d543f3 100644 --- a/src/components/FlowEditor/node/localNode/LocalNode.tsx +++ b/src/components/FlowEditor/node/localNode/LocalNode.tsx @@ -1,11 +1,10 @@ import React from '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 NodeContent from '@/pages/flowEditor/components/nodeContent'; import DynamicIcon from '@/components/DynamicIcon'; 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) => { let type = 'IconApps'; @@ -67,13 +66,18 @@ const LocalNode = ({ data, id }: { data: any; id: string }) => { state.nodeLookup.get(id)?.selected || false ); + // 获取节点运行状态 + const nodeStatus: NodeStatus = useFlowStore((state) => + (state.nodeLookup.get(id)?.data?.status as NodeStatus) || 'waiting' + ); + return (
{setIcon(data.type)} {title} +
- {/**/}
); diff --git a/src/components/FlowEditor/node/startNode/StartNode.tsx b/src/components/FlowEditor/node/startNode/StartNode.tsx index cbc76ef..986a528 100644 --- a/src/components/FlowEditor/node/startNode/StartNode.tsx +++ b/src/components/FlowEditor/node/startNode/StartNode.tsx @@ -1,11 +1,10 @@ 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 NodeContent from '@/pages/flowEditor/components/nodeContent'; +import NodeContentOther from '@/pages/flowEditor/components/nodeContentOther'; import { useStore } from '@xyflow/react'; 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 title = data.title || '开始'; @@ -15,13 +14,17 @@ const StartNode = ({ data, id }: { data: defaultNodeTypes; id: string }) => { state.nodeLookup.get(id)?.selected || false ); + // 获取节点运行状态 + const nodeStatus: NodeStatus = useFlowStore((state) => + (state.nodeLookup.get(id)?.data?.status as NodeStatus) || 'waiting' + ); + return (
{title} +
- - {/**/}
); diff --git a/src/components/FlowEditor/node/style/baseOther.module.less b/src/components/FlowEditor/node/style/baseOther.module.less index d6c76d3..f499b98 100644 --- a/src/components/FlowEditor/node/style/baseOther.module.less +++ b/src/components/FlowEditor/node/style/baseOther.module.less @@ -13,6 +13,7 @@ border-bottom: 1px solid rgba(255, 255, 255, 0.2); color: #000000; text-align: center; + position: relative; } .node-api-box, @@ -92,4 +93,63 @@ min-height: 20px; 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); + } } \ No newline at end of file diff --git a/src/hooks/useFlowCallbacks.ts b/src/hooks/useFlowCallbacks.ts index 6075916..1873399 100644 --- a/src/hooks/useFlowCallbacks.ts +++ b/src/hooks/useFlowCallbacks.ts @@ -1,4 +1,4 @@ -import React, { useCallback } from 'react'; +import React, { useCallback, useEffect } from 'react'; import { applyNodeChanges, applyEdgeChanges, @@ -15,7 +15,7 @@ import { localNodeData } from '@/pages/flowEditor/sideBar/config/localNodeData'; import { useAlignmentGuidelines } from '@/hooks/useAlignmentGuidelines'; import LocalNode from '@/components/FlowEditor/node/localNode/LocalNode'; import LoopNode from '@/components/FlowEditor/node/loopNode/LoopNode'; -import { updateCanvasDataMap } from '@/store/ideContainer'; +import { updateCanvasDataMap, resetNodeStatus } from '@/store/ideContainer'; import { validateAllNodes, showValidationErrors, @@ -919,6 +919,10 @@ export const useFlowCallbacks = ( socketId }; runMainFlow(params); + + // 重置节点状态 + dispatch(resetNodeStatus()); + } else { // 停止运行 @@ -959,4 +963,5 @@ export const useFlowCallbacks = ( saveFlowDataToServer, handleRun }; -}; \ No newline at end of file +}; +export default useFlowCallbacks; diff --git a/src/hooks/useFlowEditorState.ts b/src/hooks/useFlowEditorState.ts index 04220ca..2285ff6 100644 --- a/src/hooks/useFlowEditorState.ts +++ b/src/hooks/useFlowEditorState.ts @@ -1,4 +1,4 @@ -import { useState, useRef } from 'react'; +import { useState, useRef, useEffect } from 'react'; import { Node, Edge } from '@xyflow/react'; import { debounce } from 'lodash'; import { useSelector, useDispatch } from 'react-redux'; @@ -9,7 +9,7 @@ import { Dispatch } from 'redux'; export const useFlowEditorState = (initialData?: any) => { const [nodes, setNodes] = useState([]); const [edges, setEdges] = useState([]); - const { canvasDataMap } = useSelector((state: any) => state.ideContainer); + const { canvasDataMap, nodeStatusMap } = useSelector((state: any) => state.ideContainer); const dispatch = useDispatch(); // 添加编辑弹窗相关状态 @@ -28,6 +28,19 @@ export const useFlowEditorState = (initialData?: any) => { const [historyInitialized, setHistoryInitialized] = useState(false); const historyTimeoutRef = useRef(null); + // 更新节点状态,将从store获取的状态应用到节点上 + useEffect(() => { + setNodes(prevNodes => + prevNodes.map(node => ({ + ...node, + data: { + ...node.data, + status: nodeStatusMap[node.id] || 'waiting' + } + })) + ); + }, [nodeStatusMap]); + const updateCanvasDataMapDebounced = useRef( debounce((dispatch: Dispatch, canvasDataMap: any, id: string, nodes: Node[], edges: Edge[]) => { dispatch(updateCanvasDataMap({ diff --git a/src/pages/ideContainer/index.tsx b/src/pages/ideContainer/index.tsx index a3c0cbb..0cd0b0b 100644 --- a/src/pages/ideContainer/index.tsx +++ b/src/pages/ideContainer/index.tsx @@ -30,7 +30,7 @@ import { getUserToken } from '@/api/user'; import useWebSocket from '@/hooks/useWebSocket'; import { Message } from '@arco-design/web-react'; import { queryEventItemBySceneId } from '@/api/event'; - +import { updateNodeStatus } from '@/store/ideContainer'; type UrlParamsOptions = { identity?: string; @@ -79,6 +79,28 @@ function IDEContainer() { const socketMessage = JSON.parse(event.data); 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 })); + } } }); diff --git a/src/store/ideContainer.ts b/src/store/ideContainer.ts index ce32245..ad782c7 100644 --- a/src/store/ideContainer.ts +++ b/src/store/ideContainer.ts @@ -10,6 +10,7 @@ interface IDEContainerState { eventList: any; logBarStatus?: boolean; socketId: string; + nodeStatusMap: Record; // 节点状态映射 } const initialState: IDEContainerState = { @@ -19,11 +20,13 @@ const initialState: IDEContainerState = { canvasDataMap: {}, // 每个画布的缓存信息 projectComponentData: {}, // 工程下的组件列表 currentAppData: {}, // 当前选中的应用数据 - eventList:[], // 工程下的事件列表 + eventList: [], // 工程下的事件列表 logBarStatus: false, - socketId: '' // 工程的socketId + socketId: '', // 工程的socketId + nodeStatusMap: {} // 初始化节点状态映射 }; +// 创建切片 const ideContainerSlice = createSlice({ name: 'ideContainer', initialState, @@ -54,10 +57,20 @@ const ideContainerSlice = createSlice({ }, updateSocketId(state, action) { state.socketId = action.payload; + }, + // 更新节点状态 + updateNodeStatus: (state, { payload }) => { + const { nodeId, status } = payload; + state.nodeStatusMap[nodeId] = status; + }, + // 重置节点状态 + resetNodeStatus: (state) => { + state.nodeStatusMap = {}; } } }); +// 导出动作 creators export const { updateInfo, updateMenuData, @@ -67,7 +80,10 @@ export const { updateCurrentAppData, updateEventList, updateLogBarStatus, - updateSocketId + updateSocketId, + updateNodeStatus, + resetNodeStatus } = ideContainerSlice.actions; +// 默认导出 reducer export default ideContainerSlice.reducer; \ No newline at end of file