From 996310d2fefa37ef0f141ac65022276f1b2bef1e Mon Sep 17 00:00:00 2001 From: ZLY Date: Tue, 21 Oct 2025 10:04:06 +0800 Subject: [PATCH] =?UTF-8?q?feat(flow):=20=E6=B7=BB=E5=8A=A0=E5=BA=94?= =?UTF-8?q?=E7=94=A8=E8=8A=82=E7=82=B9=E7=BB=84=E4=BB=B6=E5=8F=8A=E5=86=85?= =?UTF-8?q?=E5=AE=B9=E6=B8=B2=E6=9F=93=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 AppNode 组件,支持随机背景色和选中状态显示- 实现 NodeContentApp 组件,用于渲染节点的 API 和数据输入输出 - 添加句柄渲染逻辑,区分普通节点与起始/结束节点 - 支持节点底部信息展示,如等待时间、循环间隔和事件名称 - 集成 React Flow v12 的状态管理 API 获取节点选中状态- 使用 useMemo优化节点背景色生成逻辑,避免重复计算 --- .../FlowEditor/node/appNode/AppNode.tsx | 33 ++ .../flowEditor/components/nodeContentApp.tsx | 293 ++++++++++++++++++ 2 files changed, 326 insertions(+) create mode 100644 src/components/FlowEditor/node/appNode/AppNode.tsx create mode 100644 src/pages/flowEditor/components/nodeContentApp.tsx diff --git a/src/components/FlowEditor/node/appNode/AppNode.tsx b/src/components/FlowEditor/node/appNode/AppNode.tsx new file mode 100644 index 0000000..fcc67b4 --- /dev/null +++ b/src/components/FlowEditor/node/appNode/AppNode.tsx @@ -0,0 +1,33 @@ +import React, { useMemo } 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 { defaultNodeTypes } from '@/components/FlowEditor/node/types/defaultType'; + + +const AppNode = ({ data, id }: { data: defaultNodeTypes; id: string }) => { + const title = data.title || '基础节点'; + + // 生成随机背景色,使用useMemo确保颜色只在节点首次创建时生成一次 + const backgroundColor = useMemo(() => { + const colors = ['#e59428', '#4a90e2', '#7b68ee', '#50c878', '#ff6347', '#9370db', '#00bfff', '#ff8c00']; + return colors[Math.floor(Math.random() * colors.length)]; + }, []); + + // 获取节点选中状态 - 适配React Flow v12 API + const isSelected = useStore((state) => + state.nodeLookup.get(id)?.selected || false + ); + + return ( +
+
+ {title} +
+ + +
+ ); +}; + +export default AppNode; \ No newline at end of file diff --git a/src/pages/flowEditor/components/nodeContentApp.tsx b/src/pages/flowEditor/components/nodeContentApp.tsx new file mode 100644 index 0000000..46d83a4 --- /dev/null +++ b/src/pages/flowEditor/components/nodeContentApp.tsx @@ -0,0 +1,293 @@ +import React from 'react'; +import styles from '@/components/FlowEditor/node/style/baseOther.module.less'; +import { Handle, Position, useStore } from '@xyflow/react'; +import { deserializeValue, isJSON } from '@/utils/common'; +import cronstrue from 'cronstrue/i18n'; + +interface NodeContentData { + parameters?: { + dataIns?: any[]; + dataOuts?: any[]; + apiIns?: any[]; + apiOuts?: any[]; + }; + showFooter?: boolean; + type?: string; + + [key: string]: any; +} + +// 定义通用的句柄样式 +const handleStyles = { + mainSource: { + background: '#2290f6', + width: '8px', + height: '8px', + border: '2px solid #fff', + boxShadow: '0 0 4px rgba(0,0,0,0.2)' + }, + mainTarget: { + background: '#2290f6', + width: '8px', + height: '8px', + border: '2px solid #fff', + boxShadow: '0 0 4px rgba(0,0,0,0.2)' + }, + data: { + background: '#555', + width: '6px', + height: '6px', + border: '1px solid #fff', + boxShadow: '0 0 2px rgba(0,0,0,0.2)' + } +}; + +// 渲染特殊节点(开始/结束节点)的句柄 +const renderSpecialNodeHandles = (isStartNode: boolean, isEndNode: boolean, dataIns: any[], dataOuts: any[], apiIns: any[], apiOuts: any[]) => { + const renderStartNodeHandles = () => { + if (!isStartNode) return null; + + return ( + <> + {apiOuts.map((_, index) => ( + + ))} + {dataOuts.length > 0 && dataOuts.map((_, index) => ( + + ))} + + ); + }; + + const renderEndNodeHandles = () => { + if (!isEndNode) return null; + + return ( + <> + {apiIns.map((_, index) => ( + + ))} + {dataIns.length > 0 && dataIns.map((_, index) => ( + + ))} + + ); + }; + + return ( + <> + {renderStartNodeHandles()} + {renderEndNodeHandles()} + + ); +}; + +// 渲染普通节点的句柄 +const renderRegularNodeHandles = (dataIns: any[], dataOuts: any[], apiIns: any[], apiOuts: any[]) => { + return ( + <> + {apiOuts.map((_, index) => ( + + ))} + {apiIns.map((_, index) => ( + + ))} + + {/* 输入参数连接端点 */} + {dataIns.map((_, index) => ( + + ))} + + {/* 输出参数连接端点 */} + {dataOuts.map((_, index) => ( + + ))} + + ); +}; + +const formatFooter = (data: any) => { + try { + switch (data?.type) { + case 'WAIT': + const { duration } = deserializeValue(data.customDef); + const hours = Math.floor(duration / 3600); + const minutes = Math.floor((duration % 3600) / 60); + const seconds = Math.floor(duration % 60); + return `${hours}小时${minutes}分钟${seconds}秒`; + case 'CYCLE': + const { intervalSeconds } = deserializeValue(data.customDef); + return cronstrue.toString(intervalSeconds, { locale: 'zh_CN' }); + case 'EVENTSEND': + case 'EVENTLISTENE': + const { name, topic } = isJSON(data.customDef) ? JSON.parse(data.customDef) : data.customDef; + if (topic.includes('**empty**')) return ''; + return `事件: ${name}`; + default: + return ''; + } + } catch (e) { + console.log(e); + } +}; + +const NodeContent = ({ data }: { data: NodeContentData }) => { + const apiIns = data.parameters?.apiIns || []; + const apiOuts = data.parameters?.apiOuts || []; + const dataIns = data.parameters?.dataIns || []; + const dataOuts = data.parameters?.dataOuts || []; + const showFooter = formatFooter(data.component) || false; + const footerData = (showFooter && data.component) || {}; + // console.log(apiIns, apiOuts, dataIns, dataOuts); + + // 判断节点类型 + const isStartNode = data.type === 'start'; + const isEndNode = data.type === 'end'; + const isSpecialNode = isStartNode || isEndNode; + + return ( + <> + {/*content栏-api部分*/} +
+
+ {apiIns.length > 0 && ( +
+ {apiIns.map((input, index) => ( +
+ {!input.topic.includes('**empty') ? input.name : ''} +
+ ))} +
+ )} + + {apiOuts.length > 0 && ( +
+ {apiOuts.map((output, index) => ( +
+ {!output.topic.includes('**empty') ? output.name : ''} +
+ ))} +
+ )} +
+
+ {(dataIns.length > 0 || dataOuts.length > 0) && ( + <> + {/*分割*/} +
+ + {/*content栏-data部分*/} +
+
+ {dataIns.length > 0 && !isStartNode && ( +
+ {dataIns.map((input, index) => ( +
+ {input.id || `输入${index + 1}`} +
+ ))} +
+ )} + + {dataOuts.length > 0 && !isEndNode && ( +
+ {dataOuts.map((output, index) => ( +
+ {output.id || `输出${index + 1}`} +
+ ))} +
+ )} +
+
+ + )} + + {/*footer栏*/} + {/*{showFooter && (*/} + {/*
*/} + {/* {formatFooter(footerData)}*/} + {/*
*/} + {/*)}*/} + + {/* 根据节点类型渲染不同的句柄 */} + {isSpecialNode + ? renderSpecialNodeHandles(isStartNode, isEndNode, dataIns, dataOuts, apiIns, apiOuts) + : renderRegularNodeHandles(dataIns, dataOuts, apiIns, apiOuts)} + + ); +}; + +export default NodeContent; \ No newline at end of file