yichael 4 mesi fa
parent
commit
be1c96faa4

+ 34 - 0
src/pages/blueprint/blueprint-core.js

@@ -1100,6 +1100,39 @@ export function useBlueprint(workflowName = null) {
     }
     }
   }, [nodes, connections, workflowName]);
   }, [nodes, connections, workflowName]);
   
   
+  /**
+   * 更新节点数据(用于动态节点如 Echo)
+   */
+  const updateNodeData = useCallback((nodeId, newData) => {
+    setNodes(prev => prev.map(node => {
+      if (node.id === nodeId) {
+        // 合并新数据
+        const updatedNode = {
+          ...node,
+          data: {
+            ...node.data,
+            ...newData
+          }
+        };
+        
+        // 如果是 echo 或 append 节点,需要重新生成端口
+        if (node.type === 'echo' && newData.value !== undefined) {
+          const { createEchoInputPorts } = require('./node-renderer/echo/echo-node.js');
+          const newInputs = createEchoInputPorts(newData.value);
+          updatedNode.inputs = newInputs;
+        } else if (node.type === 'append' && newData.value !== undefined) {
+          const { createAppendInputPorts } = require('./node-renderer/append/append-node.js');
+          const newInputs = createAppendInputPorts(newData.value);
+          updatedNode.inputs = newInputs;
+        }
+        
+        setIsDirty(true);
+        return updatedNode;
+      }
+      return node;
+    }));
+  }, []);
+  
   return {
   return {
     nodes,
     nodes,
     connections,
     connections,
@@ -1119,6 +1152,7 @@ export function useBlueprint(workflowName = null) {
     createConnection,
     createConnection,
     deleteConnection,
     deleteConnection,
     updatePortValue,
     updatePortValue,
+    updateNodeData,
     updateConnectionCoords,
     updateConnectionCoords,
     updateAllConnectionCoords,
     updateAllConnectionCoords,
     copySelectedNodes,
     copySelectedNodes,

+ 2 - 0
src/pages/blueprint/blueprint.jsx

@@ -38,6 +38,7 @@ function Blueprint({ workflowName: propWorkflowName = null }) {
     createConnection,
     createConnection,
     deleteConnection,
     deleteConnection,
     updatePortValue,
     updatePortValue,
+    updateNodeData,
     updateAllConnectionCoords,
     updateAllConnectionCoords,
     handleNodeSelect,
     handleNodeSelect,
     getProgressStyle,
     getProgressStyle,
@@ -115,6 +116,7 @@ function Blueprint({ workflowName: propWorkflowName = null }) {
         onConnectionDelete={deleteConnection}
         onConnectionDelete={deleteConnection}
         onCanvasContextMenu={handleCanvasContextMenu}
         onCanvasContextMenu={handleCanvasContextMenu}
         onPortValueChange={updatePortValue}
         onPortValueChange={updatePortValue}
+        onNodeUpdate={updateNodeData}
         onVariableDrop={handleVariableDrop}
         onVariableDrop={handleVariableDrop}
         onUpdateConnectionCoords={updateAllConnectionCoords}
         onUpdateConnectionCoords={updateAllConnectionCoords}
         canvasControllerRef={canvasControllerRef}
         canvasControllerRef={canvasControllerRef}

+ 2 - 0
src/pages/blueprint/canvas/blueprint-canvas.jsx

@@ -20,6 +20,7 @@ export function BlueprintCanvas({
   onConnectionDelete,
   onConnectionDelete,
   onCanvasContextMenu,
   onCanvasContextMenu,
   onPortValueChange,
   onPortValueChange,
+  onNodeUpdate,
   onVariableDrop,
   onVariableDrop,
   onUpdateConnectionCoords,
   onUpdateConnectionCoords,
   canvasControllerRef: externalControllerRef
   canvasControllerRef: externalControllerRef
@@ -207,6 +208,7 @@ export function BlueprintCanvas({
               onNodeMouseDown={handleNodeMouseDown}
               onNodeMouseDown={handleNodeMouseDown}
               onNodeDoubleClick={onNodeDoubleClick}
               onNodeDoubleClick={onNodeDoubleClick}
               onPortValueChange={onPortValueChange}
               onPortValueChange={onPortValueChange}
+              onNodeUpdate={onNodeUpdate}
             />
             />
           ) : null;
           ) : null;
         })}
         })}

+ 39 - 0
src/pages/blueprint/node-renderer/echo/echo-node.css

@@ -163,3 +163,42 @@
 .Blueprint-echo-node .Blueprint-node-port-arrow {
 .Blueprint-echo-node .Blueprint-node-port-arrow {
   pointer-events: none;
   pointer-events: none;
 }
 }
+
+/* Echo 节点底部区域 - 添加按钮 */
+.Blueprint-echo-node-footer {
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  padding: 8px;
+  border-top: 1px solid rgba(76, 175, 80, 0.2);
+  pointer-events: auto;
+}
+
+.Blueprint-echo-add-button {
+  width: 24px;
+  height: 24px;
+  background: rgba(76, 175, 80, 0.2);
+  border: 1px solid rgba(76, 175, 80, 0.5);
+  border-radius: 50%;
+  color: #4caf50;
+  font-size: 18px;
+  font-weight: bold;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  cursor: pointer;
+  transition: all 0.2s ease;
+  pointer-events: auto;
+  line-height: 1;
+  padding: 0;
+}
+
+.Blueprint-echo-add-button:hover {
+  background: rgba(76, 175, 80, 0.3);
+  border-color: #4caf50;
+  transform: scale(1.1);
+}
+
+.Blueprint-echo-add-button:active {
+  transform: scale(0.95);
+}

+ 114 - 79
src/pages/blueprint/node-renderer/echo/echo-node.jsx

@@ -16,7 +16,7 @@ import { NodeRenderer } from '../node-renderer.jsx';
 import './echo-node.css';
 import './echo-node.css';
 
 
 export function EchoNode(props) {
 export function EchoNode(props) {
-  const { node, connections, onPortMouseDown, onPortMouseUp } = props;
+  const { node, connections, onPortMouseDown, onPortMouseUp, onNodeUpdate } = props;
   const [segments, setSegments] = useState([]);
   const [segments, setSegments] = useState([]);
   
   
   console.log(`🎨 [EchoNode] 渲染 Echo 节点:`, { id: node.id, x: node.x, y: node.y, value: node.data?.value });
   console.log(`🎨 [EchoNode] 渲染 Echo 节点:`, { id: node.id, x: node.x, y: node.y, value: node.data?.value });
@@ -38,6 +38,27 @@ export function EchoNode(props) {
     );
     );
   };
   };
   
   
+  // 添加新变量的处理函数
+  const handleAddVariable = (e) => {
+    e.stopPropagation();
+    console.log(`➕ [EchoNode] 添加新变量`);
+    
+    // 在当前 value 的末尾添加一个新的变量占位符
+    const currentValue = node.data?.value || '';
+    const newVarName = `var${segments.length + 1}`;
+    const newValue = currentValue + `{{${newVarName}}}`;
+    
+    console.log(`  📝 更新 value: "${currentValue}" -> "${newValue}"`);
+    
+    // 更新节点数据
+    if (onNodeUpdate) {
+      onNodeUpdate(node.id, {
+        ...node.data,
+        value: newValue
+      });
+    }
+  };
+  
   // 数据端口
   // 数据端口
   const dataPorts = node.inputs?.filter(p => p.type === 'data') || [];
   const dataPorts = node.inputs?.filter(p => p.type === 'data') || [];
   
   
@@ -51,88 +72,102 @@ export function EchoNode(props) {
     console.log(`  🎨 [EchoNode renderCustomBody] 开始渲染,segments 数量: ${segments.length}, dataPorts 数量: ${dataPorts.length}`);
     console.log(`  🎨 [EchoNode renderCustomBody] 开始渲染,segments 数量: ${segments.length}, dataPorts 数量: ${dataPorts.length}`);
     
     
     return (
     return (
-      <div className="Blueprint-echo-node-content">
-        {segments.map((segment, index) => {
-          const port = dataPorts.find(p => p.segmentIndex === index);
-          console.log(`    🔍 渲染 segment[${index}]:`, segment, `找到的 port:`, port);
-          
-          if (!port) {
-            console.log(`    ⚠️ segment[${index}] 没有对应的 port,跳过渲染`);
-            return null;
-          }
-          
-          const connected = isPortConnected(port.id);
-          const portClass = 'port-string';
-          
-          // 文本片段:空心圆 + 输入框(既可输入也可连接)
-          if (segment.type === 'text') {
-            console.log(`      ✅ 渲染 segment[${index}] (text) - 空心圆 + 输入框`);
-            return (
-              <div key={index} className="Blueprint-echo-segment">
-                <div className="Blueprint-echo-text-segment">
-                  <div
-                    className={`Blueprint-node-port input ${portClass} ${connected ? 'connected' : 'disconnected'}`}
-                    data-port-type="data"
-                    onMouseDown={(e) => {
-                      e.stopPropagation();
-                      onPortMouseDown?.(node.id, port.id);
-                    }}
-                    onMouseUp={(e) => {
-                      e.stopPropagation();
-                      onPortMouseUp?.(node.id, port.id);
-                    }}
-                    title="文本输入(可连接变量)"
-                  >
-                    <div className="Blueprint-node-port-dot hollow"></div>
+      <>
+        <div className="Blueprint-echo-node-content">
+          {segments.map((segment, index) => {
+            const port = dataPorts.find(p => p.segmentIndex === index);
+            console.log(`    🔍 渲染 segment[${index}]:`, segment, `找到的 port:`, port);
+            
+            if (!port) {
+              console.log(`    ⚠️ segment[${index}] 没有对应的 port,跳过渲染`);
+              return null;
+            }
+            
+            const connected = isPortConnected(port.id);
+            const portClass = 'port-string';
+            
+            // 文本片段:空心圆 + 输入框(既可输入也可连接)
+            if (segment.type === 'text') {
+              console.log(`      ✅ 渲染 segment[${index}] (text) - 空心圆 + 输入框`);
+              return (
+                <div key={index} className="Blueprint-echo-segment">
+                  <div className="Blueprint-echo-text-segment">
+                    <div
+                      className={`Blueprint-node-port input ${portClass} ${connected ? 'connected' : 'disconnected'}`}
+                      data-port-type="data"
+                      onMouseDown={(e) => {
+                        e.stopPropagation();
+                        onPortMouseDown?.(node.id, port.id);
+                      }}
+                      onMouseUp={(e) => {
+                        e.stopPropagation();
+                        onPortMouseUp?.(node.id, port.id);
+                      }}
+                      title="文本输入(可连接变量)"
+                    >
+                      <div className="Blueprint-node-port-dot hollow"></div>
+                    </div>
+                    <input
+                      type="text"
+                      className="Blueprint-echo-text-input"
+                      defaultValue={segment.content}
+                      placeholder="文本"
+                      onClick={(e) => e.stopPropagation()}
+                      onMouseDown={(e) => e.stopPropagation()}
+                      onChange={(e) => {
+                        // TODO: 更新 value 字段
+                        const newValue = e.target.value;
+                        // 需要重新组合整个 value
+                      }}
+                    />
                   </div>
                   </div>
-                  <input
-                    type="text"
-                    className="Blueprint-echo-text-input"
-                    defaultValue={segment.content}
-                    placeholder="文本"
-                    onClick={(e) => e.stopPropagation()}
-                    onMouseDown={(e) => e.stopPropagation()}
-                    onChange={(e) => {
-                      // TODO: 更新 value 字段
-                      const newValue = e.target.value;
-                      // 需要重新组合整个 value
-                    }}
-                  />
                 </div>
                 </div>
-              </div>
-            );
-          }
-          
-          // 变量引用:实心圆 + 标签(专门连接变量)
-          if (segment.type === 'variable') {
-            console.log(`      ✅ 渲染 segment[${index}] (variable) - 实心圆 + 标签`);
-            return (
-              <div key={index} className="Blueprint-echo-segment">
-                <div className="Blueprint-echo-variable-segment">
-                  <div
-                    className={`Blueprint-node-port input ${portClass} ${connected ? 'connected' : 'disconnected'}`}
-                    data-port-type="data"
-                    onMouseDown={(e) => {
-                      e.stopPropagation();
-                      onPortMouseDown?.(node.id, port.id);
-                    }}
-                    onMouseUp={(e) => {
-                      e.stopPropagation();
-                      onPortMouseUp?.(node.id, port.id);
-                    }}
-                    title={`变量: ${segment.content}`}
-                  >
-                    <div className="Blueprint-node-port-dot solid"></div>
-                    <span className="Blueprint-node-port-label">{segment.content}</span>
+              );
+            }
+            
+            // 变量引用:实心圆 + 标签(专门连接变量)
+            if (segment.type === 'variable') {
+              console.log(`      ✅ 渲染 segment[${index}] (variable) - 实心圆 + 标签`);
+              return (
+                <div key={index} className="Blueprint-echo-segment">
+                  <div className="Blueprint-echo-variable-segment">
+                    <div
+                      className={`Blueprint-node-port input ${portClass} ${connected ? 'connected' : 'disconnected'}`}
+                      data-port-type="data"
+                      onMouseDown={(e) => {
+                        e.stopPropagation();
+                        onPortMouseDown?.(node.id, port.id);
+                      }}
+                      onMouseUp={(e) => {
+                        e.stopPropagation();
+                        onPortMouseUp?.(node.id, port.id);
+                      }}
+                      title={`变量: ${segment.content}`}
+                    >
+                      <div className="Blueprint-node-port-dot solid"></div>
+                      <span className="Blueprint-node-port-label">{segment.content}</span>
+                    </div>
                   </div>
                   </div>
                 </div>
                 </div>
-              </div>
-            );
-          }
-          
-          return null;
-        })}
-      </div>
+              );
+            }
+            
+            return null;
+          })}
+        </div>
+        
+        {/* 添加变量按钮 */}
+        <div className="Blueprint-echo-node-footer">
+          <button 
+            className="Blueprint-echo-add-button"
+            onClick={handleAddVariable}
+            onMouseDown={(e) => e.stopPropagation()}
+            title="添加变量"
+          >
+            +
+          </button>
+        </div>
+      </>
     );
     );
   };
   };
   
   

+ 127 - 44
src/pages/blueprint/node-renderer/if/if-node.css

@@ -1,17 +1,16 @@
-/* IF 分支节点样式(模仿 Unreal Engine Branch 节点 - 图3风格) */
+/* IF 分支节点样式 - 自定义布局 */
 .Blueprint-if-node {
 .Blueprint-if-node {
-  /* 深色圆角矩形,类似图3 */
-  min-width: 180px;
-  background: linear-gradient(180deg, #3d3d40 0%, #2a2a2d 100%);  /* 深灰渐变 */
-  border: 1px solid rgba(255, 255, 255, 0.15);  /* 微弱边框 */
+  min-width: 200px;
+  min-height: 120px;
+  background: linear-gradient(180deg, #3d3d40 0%, #2a2a2d 100%);
+  border: 1px solid rgba(255, 255, 255, 0.15);
   border-radius: 12px;
   border-radius: 12px;
   box-shadow: 
   box-shadow: 
-    0 1px 0 0 rgba(255, 255, 255, 0.2) inset,  /* 顶部光泽高光 */
+    0 1px 0 0 rgba(255, 255, 255, 0.2) inset,
     0 4px 12px rgba(0, 0, 0, 0.5),
     0 4px 12px rgba(0, 0, 0, 0.5),
-    0 0 20px rgba(156, 39, 176, 0.3);  /* 紫色外发光 */
+    0 0 20px rgba(156, 39, 176, 0.3);
   display: flex;
   display: flex;
   flex-direction: column;
   flex-direction: column;
-  align-items: stretch;
   position: relative;
   position: relative;
   overflow: visible;
   overflow: visible;
 }
 }
@@ -25,7 +24,7 @@
     0 0 20px rgba(255, 165, 0, 0.4);
     0 0 20px rgba(255, 165, 0, 0.4);
 }
 }
 
 
-/* 内部发光效果(类似图3的 Aws 节点) */
+/* 内部发光效果 */
 .Blueprint-if-node::before {
 .Blueprint-if-node::before {
   content: '';
   content: '';
   position: absolute;
   position: absolute;
@@ -38,76 +37,160 @@
   pointer-events: none;
   pointer-events: none;
 }
 }
 
 
+/* 顶部光泽效果(类似图像中的光泽头部) */
 .Blueprint-if-node-header {
 .Blueprint-if-node-header {
-  background: transparent;  /* 无独立背景,融入节点 */
+  background: linear-gradient(180deg, rgba(156, 39, 176, 0.3) 0%, rgba(156, 39, 176, 0.1) 50%, transparent 100%);
   color: #fff;
   color: #fff;
   padding: 8px 16px;
   padding: 8px 16px;
   font-size: 13px;
   font-size: 13px;
   font-weight: 600;
   font-weight: 600;
-  text-align: center;
-  letter-spacing: 1px;
+  text-align: left;
+  letter-spacing: 0.5px;
   position: relative;
   position: relative;
   z-index: 1;
   z-index: 1;
+  border-radius: 12px 12px 0 0;
 }
 }
 
 
-/* 执行端口区域 */
-.Blueprint-if-node-execution {
-  height: 36px;
+/* 自定义布局容器 - 使用 flex */
+.Blueprint-if-node-body {
+  position: relative;
+  flex: 1;
+  display: flex;
+  flex-direction: column;
+  padding: 12px 16px;
+  z-index: 1;
+  min-height: 100px;
+  justify-content: space-between;
+}
+
+/* 第一行:执行输入和 True 输出 */
+.Blueprint-if-node-top-row {
   display: flex;
   display: flex;
-  flex-direction: row;
   justify-content: space-between;
   justify-content: space-between;
+  align-items: flex-start;
+  min-height: 30px;
+}
+
+/* 第二行:条件输入区域 */
+.Blueprint-if-node-middle-row {
+  display: flex;
   align-items: center;
   align-items: center;
+  flex: 1;
+  padding: 8px 0;
+}
+
+/* 第三行:False 输出 */
+.Blueprint-if-node-bottom-row {
+  display: flex;
+  justify-content: flex-end;
+  align-items: flex-end;
+  min-height: 30px;
+}
+
+/* 左上:执行输入 */
+.Blueprint-if-node-execution-input {
+  display: flex;
+  align-items: flex-start;
+  justify-content: flex-start;
+}
+
+.Blueprint-if-node-execution-input .Blueprint-node-port {
   position: relative;
   position: relative;
-  overflow: visible;
-  padding: 0 12px;
-  z-index: 1;
+  left: 0;
 }
 }
 
 
-/* 数据端口区域(Condition) */
-.Blueprint-if-node-params {
-  padding: 8px 16px 12px;
-  overflow: visible;
+/* 右上:true 输出 */
+.Blueprint-if-node-true-output {
   display: flex;
   display: flex;
-  flex-direction: column;
-  gap: 10px;
+  align-items: flex-start;
+  justify-content: flex-end;
+}
+
+.Blueprint-if-node-true-output .Blueprint-node-port {
   position: relative;
   position: relative;
-  min-height: 40px;
-  z-index: 1;
+  right: 0;
+}
+
+/* 左中:条件输入 */
+.Blueprint-if-node-condition {
+  display: flex;
+  align-items: center;
+  width: 100%;
+}
+
+/* 空条件提示 */
+.Blueprint-if-condition-empty {
+  color: #888;
+  font-size: 11px;
+  font-style: italic;
+}
+
+/* 右下:false 输出 */
+.Blueprint-if-node-false-output {
+  display: flex;
+  align-items: flex-end;
+  justify-content: flex-end;
+}
+
+.Blueprint-if-node-false-output .Blueprint-node-port {
+  position: relative;
+  right: 0;
 }
 }
 
 
-/* 特殊样式:Condition 端口使用红色圆点(布尔类型,参考图3) */
-.Blueprint-if-node .Blueprint-node-port[data-port-type="data"] .Blueprint-node-port-dot {
-  background: #ff4444;  /* 鲜红色 */
-  border: 2px solid #ff6666;
-  width: 14px;
-  height: 14px;
-  border-radius: 50%;  /* 圆形 */
-  box-shadow: 0 0 8px rgba(255, 68, 68, 0.6);  /* 红色发光 */
+/* 条件端口样式:让 bool 类型使用红色 */
+.Blueprint-if-node .Blueprint-node-port.port-bool .Blueprint-node-port-dot {
+  background: #f44336;
+  border: none;
+  width: 12px;
+  height: 12px;
+  border-radius: 50%;
+  box-shadow: 0 0 6px rgba(244, 67, 54, 0.5);
 }
 }
 
 
-.Blueprint-if-node .Blueprint-node-port[data-port-type="data"].connected .Blueprint-node-port-dot {
-  background: #ff4444;
-  box-shadow: 0 0 12px rgba(255, 68, 68, 0.8);
+.Blueprint-if-node .Blueprint-node-port.port-bool.connected .Blueprint-node-port-dot {
+  background: #ef5350;
+  box-shadow: 0 0 10px rgba(244, 67, 54, 0.7);
 }
 }
 
 
-/* Condition 标签样式 */
+/* 条件标签样式 */
 .Blueprint-if-node .Blueprint-node-port[data-port-type="data"] .Blueprint-node-port-label {
 .Blueprint-if-node .Blueprint-node-port[data-port-type="data"] .Blueprint-node-port-label {
   color: #e0e0e0;
   color: #e0e0e0;
   font-size: 12px;
   font-size: 12px;
   font-weight: 500;
   font-weight: 500;
-  margin-left: 8px;
+  margin-left: 6px;
 }
 }
 
 
-/* True/False 输出端口标签样式 */
+/* true/false 输出端口标签样式 */
 .Blueprint-if-node .Blueprint-node-port.output.port-execution .Blueprint-node-port-label {
 .Blueprint-if-node .Blueprint-node-port.output.port-execution .Blueprint-node-port-label {
-  color: #ffffff;
+  color: #ffffff !important;
   font-size: 12px;
   font-size: 12px;
   font-weight: 600;
   font-weight: 600;
-  margin-right: 8px;
+  margin-right: 6px;
   text-shadow: 0 1px 2px rgba(0, 0, 0, 0.5);
   text-shadow: 0 1px 2px rgba(0, 0, 0, 0.5);
+  display: inline-block;
+  opacity: 1;
+  visibility: visible;
 }
 }
 
 
-/* 执行端口箭头样式优化 */
+/* 执行端口箭头样式 */
 .Blueprint-if-node .Blueprint-node-port-arrow {
 .Blueprint-if-node .Blueprint-node-port-arrow {
   filter: drop-shadow(0 0 4px rgba(255, 255, 255, 0.3));
   filter: drop-shadow(0 0 4px rgba(255, 255, 255, 0.3));
 }
 }
+
+/* 确保端口可见且可交互 */
+.Blueprint-if-node .Blueprint-node-port {
+  pointer-events: auto;
+  position: relative;
+  display: flex;
+  align-items: center;
+  gap: 4px;
+  z-index: 2;
+}
+
+.Blueprint-if-node .Blueprint-node-port.output {
+  flex-direction: row;
+}
+
+.Blueprint-if-node .Blueprint-node-port.input {
+  flex-direction: row;
+}

+ 32 - 4
src/pages/blueprint/node-renderer/if/if-node.js

@@ -16,16 +16,16 @@ export function createIfNodeData(x = 0, y = 0) {
   return {
   return {
     id: `if_${Date.now()}`,
     id: `if_${Date.now()}`,
     type: 'if',
     type: 'if',
-    label: 'Branch',
+    label: 'If',
     x,
     x,
     y,
     y,
     inputs: [
     inputs: [
       { id: 'input_0', label: '', type: 'execution' },
       { id: 'input_0', label: '', type: 'execution' },
-      { id: 'input_condition', label: 'Condition', type: 'data', paramType: 'boolean', value: false }
+      { id: 'input_condition', label: 'Condition', type: 'data', paramType: 'bool', value: false }
     ],
     ],
     outputs: [
     outputs: [
-      { id: 'output_true', label: 'True', type: 'execution' },
-      { id: 'output_false', label: 'False', type: 'execution' }
+      { id: 'output_true', label: '[True]', type: 'execution' },
+      { id: 'output_false', label: '[False]', type: 'execution' }
     ],
     ],
     data: {}
     data: {}
   };
   };
@@ -59,3 +59,31 @@ export function validateIfNode(node) {
     errors
     errors
   };
   };
 }
 }
+
+/**
+ * 获取 If 节点的端口
+ */
+export function getIfNodePorts(node) {
+  const executionInput = node.inputs?.find(p => p.type === 'execution');
+  const conditionInput = node.inputs?.find(p => p.id === 'input_condition');
+  const trueOutput = node.outputs?.find(p => p.id === 'output_true');
+  const falseOutput = node.outputs?.find(p => p.id === 'output_false');
+  
+  return {
+    executionInput,
+    conditionInput,
+    trueOutput,
+    falseOutput
+  };
+}
+
+/**
+ * 检查端口是否已连接
+ */
+export function isPortConnected(portId, nodeId, connections) {
+  if (!connections || !Array.isArray(connections)) return false;
+  return connections.some(conn => 
+    (conn.target === nodeId && conn.targetPort === portId) ||
+    (conn.source === nodeId && conn.sourcePort === portId)
+  );
+}

+ 85 - 6
src/pages/blueprint/node-renderer/if/if-node.jsx

@@ -2,24 +2,103 @@
  * IF 分支节点组件(模仿 Unreal Engine 的 Branch 节点)
  * IF 分支节点组件(模仿 Unreal Engine 的 Branch 节点)
  * 
  * 
  * 特点:
  * 特点:
- * - 紧凑的布局
- * - 红色 Condition 端口
- * - True/False 标签清晰显示
+ * - 自定义布局:True 在右上,False 在右下
+ * - 条件输入在左中,显示为简单的数据端口
+ * - 执行输入在左上
  */
  */
 
 
 import { NodeRenderer } from '../node-renderer.jsx';
 import { NodeRenderer } from '../node-renderer.jsx';
+import { getIfNodePorts, isPortConnected } from './if-node.js';
 import './if-node.css';
 import './if-node.css';
 
 
 export function IfNode(props) {
 export function IfNode(props) {
-  const { node } = props;
+  const { node, connections, onPortMouseDown, onPortMouseUp } = props;
+  
+  // 获取端口
+  const { executionInput, conditionInput, trueOutput, falseOutput } = getIfNodePorts(node);
+  
+  // 渲染端口
+  const renderPort = (port, isInput) => {
+    if (!port) return null;
+    
+    const connected = isPortConnected(port.id, node.id, connections);
+    const isExecution = port.type === 'execution';
+    const portClass = isExecution ? 'port-execution' : `port-${port.paramType || 'string'}`;
+    const direction = isInput ? 'input' : 'output';
+    const arrowClass = isExecution ? (isInput ? 'execution-input' : 'execution-output') : '';
+    
+    return (
+      <div
+        key={port.id}
+        className={`Blueprint-node-port ${direction} ${portClass} ${connected ? 'connected' : 'disconnected'}`}
+        data-port-type={port.type}
+        data-port-id={port.id}
+        onMouseDown={(e) => {
+          e.stopPropagation();
+          onPortMouseDown?.(node.id, port.id);
+        }}
+        onMouseUp={(e) => {
+          e.stopPropagation();
+          onPortMouseUp?.(node.id, port.id);
+        }}
+        title={port.label || ''}
+      >
+        {/* 输入端口:箭头/圆点在左,标签在右 */}
+        {isInput && isExecution && <div className={`Blueprint-node-port-arrow ${arrowClass}`}></div>}
+        {isInput && !isExecution && <div className="Blueprint-node-port-dot solid"></div>}
+        {isInput && port.label && <span className="Blueprint-node-port-label">{port.label}</span>}
+        
+        {/* 输出端口:标签在左,箭头/圆点在右 */}
+        {!isInput && port.label && (
+          <span className="Blueprint-node-port-label" style={{ color: '#fff', marginRight: '6px' }}>
+            {port.label}
+          </span>
+        )}
+        {!isInput && isExecution && <div className={`Blueprint-node-port-arrow ${arrowClass}`}></div>}
+        {!isInput && !isExecution && <div className="Blueprint-node-port-dot solid"></div>}
+      </div>
+    );
+  };
+  
+  // 自定义内容渲染
+  const renderCustomBody = () => (
+    <div className="Blueprint-if-node-body">
+      {/* 第一行:执行输入 和 True 输出 */}
+      <div className="Blueprint-if-node-top-row">
+        <div className="Blueprint-if-node-execution-input">
+          {renderPort(executionInput, true)}
+        </div>
+        <div className="Blueprint-if-node-true-output">
+          {renderPort(trueOutput, false)}
+        </div>
+      </div>
+      
+      {/* 第二行:条件输入区域 */}
+      <div className="Blueprint-if-node-middle-row">
+        <div className="Blueprint-if-node-condition">
+          {conditionInput ? renderPort(conditionInput, true) : (
+            <span className="Blueprint-if-condition-empty">No condition</span>
+          )}
+        </div>
+      </div>
+      
+      {/* 第三行:False 输出 */}
+      <div className="Blueprint-if-node-bottom-row">
+        <div className="Blueprint-if-node-false-output">
+          {renderPort(falseOutput, false)}
+        </div>
+      </div>
+    </div>
+  );
   
   
   return (
   return (
     <NodeRenderer
     <NodeRenderer
       {...props}
       {...props}
       className="Blueprint-if-node"
       className="Blueprint-if-node"
-      headerText="Branch"
+      headerText="If"
       showHeader={true}
       showHeader={true}
-      layoutType="standard"
+      layoutType="if-custom"
+      renderCustomBody={renderCustomBody}
     />
     />
   );
   );
 }
 }

+ 130 - 0
src/pages/blueprint/node-renderer/if/parse-condition.js

@@ -0,0 +1,130 @@
+/**
+ * 解析 If 节点的 condition 表达式
+ * 例如:"{turn}>=0" 解析为:
+ * [
+ *   { type: 'variable', content: 'turn' },
+ *   { type: 'operator', content: '>=' },
+ *   { type: 'value', content: '0' }
+ * ]
+ */
+
+/**
+ * 支持的比较操作符
+ */
+const OPERATORS = ['==', '!=', '>=', '<=', '>', '<'];
+
+/**
+ * 解析条件表达式
+ * @param {string} condition - 条件字符串,如 "{turn}>=0"
+ * @returns {Array} 解析后的片段数组
+ */
+export function parseCondition(condition) {
+  console.log(`🔍 [parseCondition] 解析 condition: "${condition}"`);
+  
+  if (!condition || typeof condition !== 'string') {
+    console.log(`  ⚠️ condition 为空或非字符串,返回空数组`);
+    return [];
+  }
+  
+  const segments = [];
+  
+  // 提取变量 {varName}
+  const varMatch = condition.match(/\{([^}]+)\}/);
+  if (!varMatch) {
+    console.log(`  ⚠️ 未找到变量,返回空数组`);
+    return [];
+  }
+  
+  const varName = varMatch[1].trim();
+  const varEndIndex = varMatch.index + varMatch[0].length;
+  
+  // 添加变量片段
+  segments.push({
+    type: 'variable',
+    content: varName
+  });
+  
+  // 查找操作符
+  const remainingStr = condition.substring(varEndIndex).trim();
+  let operator = null;
+  let operatorEndIndex = 0;
+  
+  for (const op of OPERATORS) {
+    if (remainingStr.startsWith(op)) {
+      operator = op;
+      operatorEndIndex = op.length;
+      break;
+    }
+  }
+  
+  if (!operator) {
+    console.log(`  ⚠️ 未找到操作符,返回只有变量的数组`);
+    return segments;
+  }
+  
+  // 添加操作符片段
+  segments.push({
+    type: 'operator',
+    content: operator
+  });
+  
+  // 提取值
+  const value = remainingStr.substring(operatorEndIndex).trim();
+  if (value) {
+    segments.push({
+      type: 'value',
+      content: value
+    });
+  }
+  
+  console.log(`  ✅ 解析结果: ${segments.length} 个片段`, segments);
+  return segments;
+}
+
+/**
+ * 创建 If 节点的条件输入端口
+ * @param {string} condition - 条件字符串
+ * @returns {Array} 输入端口数组
+ */
+export function createConditionPorts(condition) {
+  console.log(`🔌 [createConditionPorts] 创建条件端口, condition: "${condition}"`);
+  
+  const segments = parseCondition(condition);
+  const ports = [];
+  
+  segments.forEach((segment, index) => {
+    if (segment.type === 'variable') {
+      // 变量:实心圆连接点
+      ports.push({
+        id: `input_condition_var`,
+        label: segment.content,
+        type: 'data',
+        paramType: 'number',
+        paramName: 'condition_var',
+        segmentType: 'variable',
+        segmentIndex: index,
+        variableName: segment.content
+      });
+      console.log(`  🔌 添加变量端口: "${segment.content}"`);
+    } else if (segment.type === 'operator') {
+      // 操作符:纯显示,不创建端口
+      console.log(`  📝 操作符: "${segment.content}"`);
+    } else if (segment.type === 'value') {
+      // 数值:输入框端口
+      ports.push({
+        id: `input_condition_value`,
+        label: '比较值',
+        type: 'data',
+        paramType: 'number',
+        paramName: 'condition_value',
+        segmentType: 'value',
+        segmentIndex: index,
+        defaultValue: segment.content
+      });
+      console.log(`  📝 添加数值端口: "${segment.content}"`);
+    }
+  });
+  
+  console.log(`  ✅ 创建了 ${ports.length} 个条件端口`, ports);
+  return ports;
+}

+ 6 - 0
src/pages/blueprint/node-renderer/node-renderer.jsx

@@ -158,6 +158,12 @@ export function NodeRenderer({
           {/* 自定义内容区域(由子组件提供)*/}
           {/* 自定义内容区域(由子组件提供)*/}
           {renderCustomBody && renderCustomBody(node)}
           {renderCustomBody && renderCustomBody(node)}
         </>
         </>
+      ) : layoutType === 'if-custom' ? (
+        // If 自定义布局:完全自定义的端口位置
+        <>
+          {/* 自定义内容区域(由子组件提供)*/}
+          {renderCustomBody && renderCustomBody(node)}
+        </>
       ) : (
       ) : (
         // 标准流程节点布局:执行端口区域 + 数据端口区域
         // 标准流程节点布局:执行端口区域 + 数据端口区域
         <>
         <>

+ 23 - 5
src/pages/blueprint/utils/node-operations.js

@@ -244,6 +244,29 @@ export function getNodePortsFromType(nodeType, data) {
     return { inputs, outputs };
     return { inputs, outputs };
   }
   }
   
   
+  // If 节点特殊处理:简单的 Condition 数据端口
+  if (nodeType === 'if') {
+    console.log(`  🔀 If 节点`);
+    
+    // 执行输入
+    inputs.push({ id: 'input_0', label: '', type: 'execution' });
+    
+    // 条件输入端口(简单的布尔输入,颜色应该是红色)
+    inputs.push({ 
+      id: 'input_condition', 
+      label: 'Condition', 
+      type: 'data', 
+      paramType: 'bool' 
+    });
+    
+    // True 和 False 输出(带方括号)
+    outputs.push({ id: 'output_true', label: '[True]', type: 'execution' });
+    outputs.push({ id: 'output_false', label: '[False]', type: 'execution' });
+    
+    console.log(`  ✅ If 节点端口生成完成 - inputs: ${inputs.length}, outputs: ${outputs.length}`);
+    return { inputs, outputs };
+  }
+  
   // 添加执行流程端口
   // 添加执行流程端口
   inputs.push({ id: 'input_0', label: '', type: 'execution' });
   inputs.push({ id: 'input_0', label: '', type: 'execution' });
   outputs.push({ id: 'output_0', label: '', type: 'execution' });
   outputs.push({ id: 'output_0', label: '', type: 'execution' });
@@ -260,11 +283,6 @@ export function getNodePortsFromType(nodeType, data) {
     });
     });
   });
   });
   
   
-  // 特殊节点类型的执行输出端口(需要在数据端口之前添加,避免ID冲突)
-  if (nodeType === 'if') {
-    outputs.push({ id: 'output_exec_false', label: 'false', type: 'execution' });
-  }
-  
   // 根据类型添加数据输出端口
   // 根据类型添加数据输出端口
   if (nodeType === 'random') {
   if (nodeType === 'random') {
     // random 节点输出 number 类型
     // random 节点输出 number 类型

+ 3 - 3
static/processing/测试/bp.json

@@ -13,8 +13,8 @@
 			"y": 140
 			"y": 140
 		},
 		},
 		"node_2": {
 		"node_2": {
-			"x": 1380,
-			"y": 160
+			"x": 1400,
+			"y": 240
 		},
 		},
 		"var_get_mini": {
 		"var_get_mini": {
 			"x": 160,
 			"x": 160,
@@ -26,7 +26,7 @@
 		},
 		},
 		"var_get_turn": {
 		"var_get_turn": {
 			"x": 1060,
 			"x": 1060,
-			"y": 560
+			"y": 540
 		}
 		}
 	}
 	}
 }
 }

+ 17 - 7
static/processing/测试/processing.json

@@ -2,7 +2,7 @@
 	"variables": {
 	"variables": {
 		"mini": 1,
 		"mini": 1,
 		"swipeDirection": "",
 		"swipeDirection": "",
-		"turn":1
+		"aws":40
 	},
 	},
 	"execute": [
 	"execute": [
 		{
 		{
@@ -16,12 +16,22 @@
 			]
 			]
 		},
 		},
 		{
 		{
-			"type": "echo",
-			"value": "hello"
-		},
-		{
-			"type": "echo",
-			"value": "第{{turn}}轮"
+			"type":"if",
+			"condition": "{aws}>=0",
+			"true": 
+			[
+				{
+					"type": "echo",
+					"value": "hello"
+				}
+			],
+			"false": 
+			[
+				{
+					"type": "echo",
+					"value": "第{{aws}}轮"
+				}
+			]	
 		}
 		}
 	]
 	]
 }
 }