فهرست منبع

箭头位置正确了

yichael 4 ماه پیش
والد
کامیت
e0c7a9ec58

+ 48 - 9
src/pages/blueprint/blueprint-core.js

@@ -3,7 +3,7 @@
  */
 
 import { useState, useRef, useEffect, useCallback } from 'react';
-import { workflowToBlueprint, blueprintToWorkflow, createVariableConnections } from './utils/workflow-converter.js';
+import { workflowToBlueprint, blueprintToWorkflow, createVariableConnections, analyzeVariableReferences } from './utils/workflow-converter.js';
 import { ConnectionManager } from './utils/connection-manager.js';
 import { createNode, copyNode, copyNodes, getNodePortsFromType, createVariableNode } from './utils/node-operations.js';
 import { snapToGrid } from './utils/canvas-controller.js';
@@ -79,15 +79,40 @@ export function useBlueprint(workflowName = null) {
       const blueprint = workflowToBlueprint(processingData);
       console.log('转换后的 blueprint:', blueprint);
       
-      // 创建变量节点
-      const variableNodes = [];
+      // 分析需要创建哪些变量节点(Get/Set)
       const variables = processingData.variables || {};
       const varNames = Object.keys(variables);
-      varNames.forEach((varName, index) => {
+      
+      // 收集所有被引用的变量及其引用类型
+      const varReferences = analyzeVariableReferences(blueprint.nodes);
+      
+      // 创建变量节点(根据引用类型创建 Get 或 Set 节点)
+      const variableNodes = [];
+      let yOffset = 200;
+      varNames.forEach((varName) => {
         const varValue = variables[varName];
-        // 变量节点放在左侧,垂直排列
-        const varNode = createVariableNode(varName, varValue, 50, 200 + index * 120);
-        variableNodes.push(varNode);
+        const refs = varReferences.get(varName) || { asInput: false, asOutput: false };
+        
+        // 如果变量被用作输入参数,创建 Get 节点
+        if (refs.asInput) {
+          const getNode = createVariableNode(varName, varValue, 50, yOffset, 'get');
+          variableNodes.push(getNode);
+          yOffset += 80;
+        }
+        
+        // 如果变量被用作输出参数,创建 Set 节点
+        if (refs.asOutput) {
+          const setNode = createVariableNode(varName, varValue, 50, yOffset, 'set');
+          variableNodes.push(setNode);
+          yOffset += 80;
+        }
+        
+        // 如果变量没有被引用,默认创建一个 Get 节点
+        if (!refs.asInput && !refs.asOutput) {
+          const getNode = createVariableNode(varName, varValue, 50, yOffset, 'get');
+          variableNodes.push(getNode);
+          yOffset += 80;
+        }
       });
       console.log('创建的变量节点数量:', variableNodes.length);
       
@@ -307,8 +332,22 @@ export function useBlueprint(workflowName = null) {
    * 添加节点
    */
   const addNode = (nodeType, x, y) => {
-    const nodeData = nodeType.method ? { method: nodeType.method } : {};
-    const node = createNode(nodeType.type, x, y, nodeData);
+    let node;
+    
+    // 检查是否是变量节点(Get/Set)
+    if (nodeType.varName !== undefined && nodeType.varMode !== undefined) {
+      node = createVariableNode(
+        nodeType.varName,
+        nodeType.varValue,
+        x,
+        y,
+        nodeType.varMode
+      );
+    } else {
+      const nodeData = nodeType.method ? { method: nodeType.method } : {};
+      node = createNode(nodeType.type, x, y, nodeData);
+    }
+    
     setNodes(prev => [...prev, node]);
     setIsDirty(true); // 标记为已修改
     return node;

+ 15 - 1
src/pages/blueprint/blueprint.js

@@ -188,6 +188,19 @@ export function useBlueprintLogic(propWorkflowName) {
   const handleCloseContextMenu = () => {
     setContextMenu({ visible: false, x: 0, y: 0, canvasX: 0, canvasY: 0 });
   };
+  
+  /**
+   * 添加变量节点(从拖放创建)
+   */
+  const addVariableNode = (varName, varValue, canvasX, canvasY, mode) => {
+    const nodeType = {
+      type: 'variable',
+      varName,
+      varValue,
+      varMode: mode
+    };
+    blueprintState.addNode(nodeType, canvasX, canvasY);
+  };
 
   return {
     ...blueprintState,
@@ -211,6 +224,7 @@ export function useBlueprintLogic(propWorkflowName) {
     handleNodeSelect,
     getProgressStyle,
     formatProgress,
-    handleCloseContextMenu
+    handleCloseContextMenu,
+    addVariableNode
   };
 }

+ 46 - 1
src/pages/blueprint/blueprint.jsx

@@ -2,12 +2,14 @@
  * 蓝图编辑器主页面
  */
 
+import { useState } from 'react';
 import './blueprint.css';
 import { useBlueprintLogic } from './blueprint.js';
 import { BlueprintToolbar } from './toolbar/blueprint-toolbar.jsx';
 import { BlueprintCanvas } from './canvas/blueprint-canvas.jsx';
 import { BlueprintVariablePanel } from './variable-panel/variable-panel.jsx';
 import { BlueprintRightClickMenu } from './canvas/right-click-menu/right-click-menu.jsx';
+import { VariableNodeDialog } from './variable-node-dialog/variable-node-dialog.jsx';
 
 function Blueprint({ workflowName: propWorkflowName = null }) {
   const {
@@ -39,9 +41,42 @@ function Blueprint({ workflowName: propWorkflowName = null }) {
     handleNodeSelect,
     getProgressStyle,
     formatProgress,
-    handleCloseContextMenu
+    handleCloseContextMenu,
+    addVariableNode
   } = useBlueprintLogic(propWorkflowName);
   
+  // 变量节点类型选择对话框状态
+  const [variableDialog, setVariableDialog] = useState({
+    visible: false,
+    varName: '',
+    varValue: null,
+    canvasX: 0,
+    canvasY: 0
+  });
+  
+  // 处理变量拖放到画布
+  const handleVariableDrop = (varName, varValue, canvasX, canvasY) => {
+    setVariableDialog({
+      visible: true,
+      varName,
+      varValue,
+      canvasX,
+      canvasY
+    });
+  };
+  
+  // 处理变量节点类型选择
+  const handleVariableNodeSelect = (mode) => {
+    const { varName, varValue, canvasX, canvasY } = variableDialog;
+    addVariableNode(varName, varValue, canvasX, canvasY, mode);
+    setVariableDialog({ visible: false, varName: '', varValue: null, canvasX: 0, canvasY: 0 });
+  };
+  
+  // 关闭变量节点对话框
+  const handleVariableDialogCancel = () => {
+    setVariableDialog({ visible: false, varName: '', varValue: null, canvasX: 0, canvasY: 0 });
+  };
+  
   return (
     <div 
       className="Blueprint-container" 
@@ -77,6 +112,7 @@ function Blueprint({ workflowName: propWorkflowName = null }) {
         onConnectionDelete={deleteConnection}
         onCanvasContextMenu={handleCanvasContextMenu}
         onPortValueChange={updatePortValue}
+        onVariableDrop={handleVariableDrop}
         canvasControllerRef={canvasControllerRef}
       />
       
@@ -94,6 +130,15 @@ function Blueprint({ workflowName: propWorkflowName = null }) {
         canDelete={selectedNodeIds.length > 0}
         canCopy={selectedNodeIds.length > 0}
         canPaste={true}
+        variables={variables}
+      />
+      
+      {/* 变量节点类型选择对话框 */}
+      <VariableNodeDialog
+        visible={variableDialog.visible}
+        varName={variableDialog.varName}
+        onSelect={handleVariableNodeSelect}
+        onCancel={handleVariableDialogCancel}
       />
       
       {/* 加载提示 */}

+ 13 - 1
src/pages/blueprint/canvas/blueprint-canvas.jsx

@@ -4,7 +4,7 @@
 
 import './canvas.css';
 import { NodeRenderer } from '../node-renderer/node-renderer.jsx';
-import { useCanvasLogic, useCanvasEventHandlers, createEventHandlers, getConnectionClass, calculateGridStyle } from './canvas.js';
+import { useCanvasLogic, useCanvasEventHandlers, createEventHandlers, getConnectionClass, calculateGridStyle, handleVariableDrop, handleVariableDragOver } from './canvas.js';
 import { createBezierPath } from '../utils/connection-manager.js';
 
 export function BlueprintCanvas({ 
@@ -19,6 +19,7 @@ export function BlueprintCanvas({
   onConnectionDelete,
   onCanvasContextMenu,
   onPortValueChange,
+  onVariableDrop,
   canvasControllerRef: externalControllerRef
 }) {
   const {
@@ -62,9 +63,20 @@ export function BlueprintCanvas({
   
   const gridStyle = calculateGridStyle(transform);
   
+  // 拖放事件处理
+  const onDrop = (e) => {
+    handleVariableDrop(e, transform, canvasRef.current, onVariableDrop);
+  };
+  
+  const onDragOver = (e) => {
+    handleVariableDragOver(e);
+  };
+  
   return (
     <div 
       className="Blueprint-canvas-area"
+      onDrop={onDrop}
+      onDragOver={onDragOver}
     >
       {/* 背景网格层 */}
       <div 

+ 97 - 41
src/pages/blueprint/canvas/canvas.js

@@ -338,67 +338,82 @@ export function useCanvasLogic(connections, externalControllerRef, nodes = []) {
       if (isVariable) {
         // 变量节点:端口垂直居中(top: 50%)
         const portY = node.y + nodeDims.height / 2;
+        // 端口容器宽度16px(min-width: 16px),圆点在容器内居中(justify-content: center)
+        const portContainerWidth = 16;
+        const portContainerHalfWidth = portContainerWidth / 2; // 8px
+        
         if (isInput) {
-          // 变量节点的输入端口:在节点内部(left: -8px,端口容器宽度16px,中心在节点左边缘)
-          // 变量节点的输入端口使用 left: -8px,端口中心在节点左边缘
-          return { x: node.x, y: portY };
+          // Set 变量节点的输入端口(左侧)
+          // CSS: left: -8px,端口容器左边缘在 node.x - 8
+          // 圆点在容器内居中,圆点中心 = node.x - 8 + 8 = node.x
+          return { x: node.x - 8 + portContainerHalfWidth, y: portY };
         } else {
-          // 变量节点的输出端口:在节点内部(right: -8px,端口容器宽度16px,中心在节点右边缘)
-          // 变量节点的输出端口使用 right: -8px,端口中心在节点右边缘
-          return { x: node.x + nodeDims.width, y: portY };
+          // Get 变量节点的输出端口(右侧)
+          // CSS: right: -8px,端口容器右边缘在 node.x + width + 8
+          // 圆点在容器内居中,圆点中心 = node.x + width + 8 - 8 = node.x + width
+          return { x: node.x + nodeDims.width + 8 - portContainerHalfWidth, y: portY };
         }
       } else {
-        // 流程节点:使用标准端口位置计算
-        const headerHeight = 40;
-        const bodyPadding = 24;
-        const portHeight = 56;
+        // 流程节点:三区域结构(header + execution + params)
+        const headerHeight = 40;      // 区域1:标题区域高度
+        const executionHeight = 40;   // 区域2:执行箭头区域高度(固定)
+        const paramsTopPadding = 12;  // 区域3:参数区域顶部padding
+        const portGap = 8;            // 参数之间的间距(gap: 8px)
+        const portRowHeight = 20;     // 每个参数行的高度(约20px,包含圆点和标签)
         
         // 分离执行端口和数据端口
-        const executionPorts = isInput 
-          ? (node.inputs || []).filter(p => p.type === 'execution')
-          : (node.outputs || []).filter(p => p.type === 'execution');
         const dataPorts = isInput
           ? (node.inputs || []).filter(p => p.type !== 'execution')
           : (node.outputs || []).filter(p => p.type !== 'execution');
         
-        const portIndex = isInput 
-          ? node.inputs?.findIndex(p => p.id === port.id) ?? -1
-          : node.outputs?.findIndex(p => p.id === port.id) ?? -1;
-        
-        if (portIndex === -1) {
-          return null;
-        }
-        
         const isExecutionPort = port.type === 'execution';
         let portCenterY;
+        let portCenterX;
         
         if (isExecutionPort) {
-          // 执行端口:固定在最上方,紧贴header下方
-          portCenterY = node.y + headerHeight + 12;
-        } else {
-          // 数据端口:在执行端口下方
-          const executionPortOffset = executionPorts.length > 0 ? 32 : 0;
-          const dataPortIndex = dataPorts.findIndex(p => p.id === port.id);
-          portCenterY = node.y + headerHeight + bodyPadding + executionPortOffset + dataPortIndex * portHeight + 8; // 8px是端口高度16px的一半
-        }
-        
-        if (isInput) {
-          if (isExecutionPort) {
-            // 执行输入端口:在节点左边缘(left: -8px,端口中心在节点左边缘)
-            return { x: node.x, y: portCenterY };
+          // 执行端口:在执行区域内垂直居中
+          // 执行区域起始Y = node.y + headerHeight
+          // 执行区域高度 = executionHeight
+          // 端口垂直居中 = node.y + headerHeight + executionHeight / 2
+          portCenterY = node.y + headerHeight + executionHeight / 2;
+          
+          if (isInput) {
+            // 执行输入端口:在节点左侧
+            portCenterX = node.x + 8; // padding: 0 8px
           } else {
-            // 数据输入端口:在节点内部(left: 8px,端口容器宽度16px,中心在 left + 8px = node.x + 16px)
-            return { x: node.x + 8 + 8, y: portCenterY }; // 8px是left位置,8px是容器宽度的一半
+            // 执行输出端口:在节点右侧
+            portCenterX = node.x + nodeDims.width - 8;
           }
         } else {
-          if (isExecutionPort) {
-            // 执行输出端口:在节点右边缘(right: -8px,端口中心在节点右边缘)
-            return { x: node.x + nodeDims.width, y: portCenterY };
+          // 数据端口:在参数区域内
+          // 参数区域起始Y = node.y + headerHeight + executionHeight + paramsTopPadding
+          const dataPortIndex = dataPorts.findIndex(p => p.id === port.id);
+          if (dataPortIndex === -1) {
+            return null;
+          }
+          
+          // 每个端口的Y位置 = 参数区域起始Y + 索引 * (行高 + 间距) + 行高/2
+          const paramsStartY = node.y + headerHeight + executionHeight + paramsTopPadding;
+          portCenterY = paramsStartY + dataPortIndex * (portRowHeight + portGap) + portRowHeight / 2;
+          
+          // 圆点直径12px,半径6px
+          const dotRadius = 6;
+          
+          if (isInput) {
+            // 数据输入端口:圆点在最左侧
+            // CSS: .Blueprint-node-params padding: 12px 16px,端口 position: relative
+            // 圆点在端口容器的最左边
+            portCenterX = node.x + 16 + dotRadius; // 16px是左padding,dotRadius是圆点中心
           } else {
-            // 数据输出端口:在节点内部(right: 8px,端口容器宽度16px,中心在 right - 8px = node.x + width - 16px)
-            return { x: node.x + nodeDims.width - 8 - 8, y: portCenterY }; // 8px是right位置,8px是容器宽度的一半
+            // 数据输出端口:
+            // - align-self: flex-end 让端口容器靠右
+            // - flex-direction: row-reverse 让圆点在视觉上靠右
+            // 圆点中心 = 节点右边缘 - 右padding(16px) - 圆点半径
+            portCenterX = node.x + nodeDims.width - 16 - dotRadius;
           }
         }
+        
+        return { x: portCenterX, y: portCenterY };
       }
     };
     
@@ -571,3 +586,44 @@ export function calculateGridStyle(transform, gridSize = 20) {
     backgroundSize: `${scaledGridSize}px ${scaledGridSize}px`
   };
 }
+
+/**
+ * 处理变量拖放到画布
+ * @param {DragEvent} e - 拖放事件
+ * @param {Object} transform - 画布变换
+ * @param {HTMLElement} canvasRef - 画布元素引用
+ * @param {Function} onVariableDrop - 变量拖放回调
+ */
+export function handleVariableDrop(e, transform, canvasRef, onVariableDrop) {
+  e.preventDefault();
+  
+  try {
+    const jsonData = e.dataTransfer.getData('application/json');
+    if (!jsonData) return;
+    
+    const dragData = JSON.parse(jsonData);
+    if (dragData.type !== 'variable') return;
+    
+    // 计算画布坐标
+    const rect = canvasRef.getBoundingClientRect();
+    const screenX = e.clientX - rect.left;
+    const screenY = e.clientY - rect.top;
+    
+    // 转换为画布坐标
+    const canvasX = (screenX - transform.translateX) / transform.scale;
+    const canvasY = (screenY - transform.translateY) / transform.scale;
+    
+    onVariableDrop?.(dragData.varName, dragData.varValue, canvasX, canvasY);
+  } catch (err) {
+    console.error('解析拖放数据失败:', err);
+  }
+}
+
+/**
+ * 处理拖放悬停
+ * @param {DragEvent} e - 拖放事件
+ */
+export function handleVariableDragOver(e) {
+  e.preventDefault();
+  e.dataTransfer.dropEffect = 'copy';
+}

+ 53 - 3
src/pages/blueprint/canvas/right-click-menu/right-click-menu.js

@@ -16,10 +16,45 @@ export function getMenuStyle(x, y) {
   };
 }
 
+/**
+ * 根据变量列表生成变量节点类型
+ * @param {Object} variables - 变量对象 {varName: value, ...}
+ * @returns {Array} 变量节点类型数组
+ */
+export function getVariableNodeTypes(variables) {
+  if (!variables || Object.keys(variables).length === 0) {
+    return [];
+  }
+  
+  const varTypes = [];
+  
+  Object.entries(variables).forEach(([varName, varValue]) => {
+    // Get 节点
+    varTypes.push({
+      type: 'variable',
+      label: `Get ${varName}`,
+      varName: varName,
+      varValue: varValue,
+      varMode: 'get'
+    });
+    
+    // Set 节点
+    varTypes.push({
+      type: 'variable',
+      label: `Set ${varName}`,
+      varName: varName,
+      varValue: varValue,
+      varMode: 'set'
+    });
+  });
+  
+  return varTypes;
+}
+
 /**
  * 右键菜单逻辑钩子
  */
-export function useRightClickMenuLogic(visible) {
+export function useRightClickMenuLogic(visible, variables = {}) {
   const [searchTerm, setSearchTerm] = useState('');
   
   // 当菜单显示时重置搜索词
@@ -31,10 +66,25 @@ export function useRightClickMenuLogic(visible) {
   
   const nodeTypes = getAvailableNodeTypes();
   
+  // 添加变量节点类型
+  const allNodeTypes = useMemo(() => {
+    const varTypes = getVariableNodeTypes(variables);
+    if (varTypes.length > 0) {
+      return [
+        ...nodeTypes,
+        {
+          category: '变量',
+          types: varTypes
+        }
+      ];
+    }
+    return nodeTypes;
+  }, [nodeTypes, variables]);
+  
   // 过滤节点类型
   const filteredCategories = useMemo(() => {
-    return filterNodeTypes(nodeTypes, searchTerm);
-  }, [nodeTypes, searchTerm]);
+    return filterNodeTypes(allNodeTypes, searchTerm);
+  }, [allNodeTypes, searchTerm]);
   
   return {
     searchTerm,

+ 8 - 3
src/pages/blueprint/canvas/right-click-menu/right-click-menu.jsx

@@ -18,9 +18,10 @@ export function BlueprintRightClickMenu({
   onPaste, 
   canDelete, 
   canCopy, 
-  canPaste 
+  canPaste,
+  variables = {} // 工作流中的变量列表
 }) {
-  const { searchTerm, setSearchTerm, filteredCategories } = useRightClickMenuLogic(visible);
+  const { searchTerm, setSearchTerm, filteredCategories } = useRightClickMenuLogic(visible, variables);
   
   if (!visible) return null;
   
@@ -65,12 +66,16 @@ export function BlueprintRightClickMenu({
             const nodeType = typeof type === 'string' ? type : type.type;
             const label = typeof type === 'string' ? type : (type.label || type.type);
             const method = typeof type === 'object' ? type.method : null;
+            // 变量节点的额外参数
+            const varName = typeof type === 'object' ? type.varName : null;
+            const varValue = typeof type === 'object' ? type.varValue : null;
+            const varMode = typeof type === 'object' ? type.varMode : null;
             
             return (
               <div
                 key={typeIndex}
                 className="Right-click-menu-item"
-                onClick={() => handleNodeTypeClickWrapper({ type: nodeType, method })}
+                onClick={() => handleNodeTypeClickWrapper({ type: nodeType, method, varName, varValue, varMode })}
                 title={label}
               >
                 {label}

+ 1 - 0
src/pages/blueprint/node-renderer/node-renderer.css

@@ -169,6 +169,7 @@
 .Blueprint-node-execution .Blueprint-node-port.output.port-execution {
   right: auto;
   top: auto;
+  margin-left: auto; /* 当只有输出端口时,推到右边 */
 }
 
 /* 参数区域内的端口样式 */

+ 29 - 10
src/pages/blueprint/node-renderer/node-renderer.jsx

@@ -38,7 +38,7 @@ export function NodeRenderer({ node, isSelected, connections = [], onPortMouseDo
     >
       {node.type === 'variable' ? (
         <div className="Blueprint-variable-node-label">
-          {node.label || node.varName || node.type}
+          {node.varMode === 'set' ? `Set ${node.varName || node.label}` : (node.label || node.varName || node.type)}
         </div>
       ) : (
         <div className="Blueprint-node-header">
@@ -46,20 +46,19 @@ export function NodeRenderer({ node, isSelected, connections = [], onPortMouseDo
         </div>
       )}
       {node.type === 'variable' ? (
-        /* 变量节点:有输入端口(左侧)和输出端口(右侧) */
+        /* 变量节点:根据 varMode 决定端口位置 */
+        /* Get 节点:只有右侧输出端口 */
+        /* Set 节点:只有左侧输入端口 */
         <>
           <div className="Blueprint-variable-node-body">
-            {/* 输入端口(左侧)- 只在被连接时显示 */}
-            {node.inputs && node.inputs.length > 0 && node.inputs.map((input, index) => {
+            {/* Set 节点的输入端口(左侧) */}
+            {node.varMode === 'set' && node.inputs && node.inputs.length > 0 && node.inputs.map((input, index) => {
               const portTypeClass = getPortTypeClass(input);
               const isConnected = isPortConnected(node.id, input.id, connections);
-              if (!isConnected) {
-                return null;
-              }
               return (
                 <div
                   key={input.id}
-                  className={`Blueprint-node-port input ${portTypeClass} connected`}
+                  className={`Blueprint-node-port input ${portTypeClass} ${isConnected ? 'connected' : 'disconnected'}`}
                   style={{ left: '-8px', top: '50%', transform: 'translateY(-50%)' }}
                   data-port-type={input.type}
                   data-param-type={input.paramType || ''}
@@ -67,11 +66,31 @@ export function NodeRenderer({ node, isSelected, connections = [], onPortMouseDo
                   onMouseUp={(e) => handlePortMouseUp(e, onPortMouseUp, node.id, input.id)}
                   title={input.label || input.id}
                 >
-                  <div className="Blueprint-node-port-dot solid"></div>
+                  <div className={`Blueprint-node-port-dot ${isConnected ? 'solid' : 'hollow'}`}></div>
+                </div>
+              );
+            })}
+            {/* Get 节点的输出端口(右侧) */}
+            {node.varMode === 'get' && node.outputs && node.outputs.length > 0 && node.outputs.map((output, index) => {
+              const portTypeClass = getPortTypeClass(output);
+              const isConnected = isPortConnected(node.id, output.id, connections);
+              return (
+                <div
+                  key={output.id}
+                  className={`Blueprint-node-port output ${portTypeClass} ${isConnected ? 'connected' : 'disconnected'}`}
+                  style={{ right: '-8px', top: '50%', transform: 'translateY(-50%)' }}
+                  data-port-type={output.type}
+                  data-param-type={output.paramType || ''}
+                  onMouseDown={(e) => handlePortMouseDown(e, onPortMouseDown, node.id, output.id)}
+                  onMouseUp={(e) => handlePortMouseUp(e, onPortMouseUp, node.id, output.id)}
+                  title={output.label || output.id}
+                >
+                  <div className={`Blueprint-node-port-dot ${isConnected ? 'solid' : 'hollow'}`}></div>
                 </div>
               );
             })}
-            {node.outputs && node.outputs.length > 0 && node.outputs.map((output, index) => {
+            {/* 兼容旧的变量节点(没有 varMode 的情况,默认为 get) */}
+            {!node.varMode && node.outputs && node.outputs.length > 0 && node.outputs.map((output, index) => {
               const portTypeClass = getPortTypeClass(output);
               const isConnected = isPortConnected(node.id, output.id, connections);
               return (

+ 25 - 16
src/pages/blueprint/utils/node-operations.js

@@ -281,30 +281,38 @@ function getNodeLabelFromType(nodeType, data = {}) {
  * @param {*} varValue - 变量值
  * @param {number} x - X 坐标
  * @param {number} y - Y 坐标
+ * @param {string} mode - 模式:'get'(只有输出端口)或 'set'(只有输入端口)
  * @returns {Object} 变量节点对象
  */
-export function createVariableNode(varName, varValue, x, y) {
-  const nodeId = `var_${varName}_${Date.now()}`;
+export function createVariableNode(varName, varValue, x, y, mode = 'get') {
+  const nodeId = `var_${mode}_${varName}_${Date.now()}`;
   const varType = typeof varValue === 'number' ? 'int' : 'string';
   
-  // 变量节点有输入端口(用于接收值)和输出端口(用于输出值)
-  const inputs = [{
-    id: 'input_0',
-    label: '',
-    type: 'data',
-    paramType: varType
-  }];
+  let inputs = [];
+  let outputs = [];
   
-  const outputs = [{
-    id: 'output_0',
-    label: varName,
-    type: 'data',
-    paramType: varType
-  }];
+  if (mode === 'get') {
+    // Get 节点:只有右侧输出端口
+    outputs = [{
+      id: 'output_0',
+      label: varName,
+      type: 'data',
+      paramType: varType
+    }];
+  } else if (mode === 'set') {
+    // Set 节点:只有左侧输入端口
+    inputs = [{
+      id: 'input_0',
+      label: '',
+      type: 'data',
+      paramType: varType
+    }];
+  }
   
   return {
     id: nodeId,
     type: 'variable',
+    varMode: mode, // 'get' 或 'set'
     label: varName,
     varName: varName,
     varType: varType,
@@ -316,7 +324,8 @@ export function createVariableNode(varName, varValue, x, y) {
     data: {
       varName,
       varValue,
-      varType
+      varType,
+      varMode: mode
     }
   };
 }

+ 109 - 63
src/pages/blueprint/utils/workflow-converter.js

@@ -184,111 +184,157 @@ export function workflowToBlueprint(workflow) {
   return { nodes, connections };
 }
 
+/**
+ * 分析流程节点中的变量引用,确定每个变量是作为输入还是输出使用
+ * @param {Array} processNodes - 流程节点数组
+ * @returns {Map} 变量引用映射 Map<varName, {asInput: boolean, asOutput: boolean}>
+ */
+export function analyzeVariableReferences(processNodes) {
+  const varReferences = new Map();
+  
+  // 辅助函数:提取变量名
+  const extractVarName = (value) => {
+    if (typeof value === 'string') {
+      // 去掉引号和大括号
+      const cleaned = value.replace(/^["']|["']$/g, '').replace(/^{|}$/g, '');
+      return cleaned || null;
+    }
+    return null;
+  };
+  
+  // 遍历所有流程节点
+  processNodes.forEach(processNode => {
+    if (processNode.type === 'begin' || !processNode.data) {
+      return;
+    }
+    
+    // 分析输入变量引用(inVars)
+    if (processNode.data.inVars && Array.isArray(processNode.data.inVars)) {
+      processNode.data.inVars.forEach(value => {
+        const varName = extractVarName(value);
+        if (varName) {
+          if (!varReferences.has(varName)) {
+            varReferences.set(varName, { asInput: false, asOutput: false });
+          }
+          varReferences.get(varName).asInput = true;
+        }
+      });
+    }
+    
+    // 分析输出变量引用(outVars)
+    if (processNode.data.outVars && Array.isArray(processNode.data.outVars)) {
+      processNode.data.outVars.forEach(value => {
+        const varName = extractVarName(value);
+        if (varName) {
+          if (!varReferences.has(varName)) {
+            varReferences.set(varName, { asInput: false, asOutput: false });
+          }
+          varReferences.get(varName).asOutput = true;
+        }
+      });
+    }
+  });
+  
+  return varReferences;
+}
+
 /**
  * 根据 inVars 和 outVars 中的变量引用创建变量节点与流程节点的连接
  * @param {Array} processNodes - 流程节点数组
- * @param {Array} variableNodes - 变量节点数组
+ * @param {Array} variableNodes - 变量节点数组(包含 Get 和 Set 节点)
  * @param {Object} workflow - 工作流对象(包含 variables)
  * @returns {Array} 连接数组
  */
 export function createVariableConnections(processNodes, variableNodes, workflow) {
   const connections = [];
-  const variableNodeMap = new Map();
   
-  // 创建变量节点映射(变量名 -> 变量节点)
+  // 创建变量节点映射(按变量名和模式分类)
+  // getNodes: Map<varName, varNode> - Get 节点(用于输入)
+  // setNodes: Map<varName, varNode> - Set 节点(用于输出)
+  const getNodes = new Map();
+  const setNodes = new Map();
+  
   variableNodes.forEach(varNode => {
     const varName = varNode.varName || varNode.label;
-    if (varName) {
-      variableNodeMap.set(varName, varNode);
+    if (!varName) return;
+    
+    if (varNode.varMode === 'set') {
+      setNodes.set(varName, varNode);
+    } else {
+      // 默认为 get 模式
+      getNodes.set(varName, varNode);
     }
   });
   
+  // 辅助函数:提取变量名
+  const extractVarName = (value) => {
+    if (typeof value === 'string') {
+      const cleaned = value.replace(/^["']|["']$/g, '').replace(/^{|}$/g, '');
+      return cleaned || null;
+    }
+    return null;
+  };
+  
   // 遍历所有流程节点
   processNodes.forEach(processNode => {
     if (processNode.type === 'begin' || !processNode.data) {
       return;
     }
     
-    // 处理输入变量连接(从变量节点到流程节点)
+    // 处理输入变量连接(从 Get 变量节点到流程节点)
     if (processNode.data.inVars && Array.isArray(processNode.data.inVars)) {
       const inVars = processNode.data.inVars;
       const dataInputPorts = processNode.inputs?.filter(input => input.type === 'data') || [];
       
       inVars.forEach((value, index) => {
-        if (index >= dataInputPorts.length) {
-          return;
-        }
+        if (index >= dataInputPorts.length) return;
         
         const inputPort = dataInputPorts[index];
-        if (!inputPort) {
-          return;
-        }
+        if (!inputPort) return;
         
-        // 检查值是否是变量引用(格式:"{varName}" 或 varName)
-        let varName = null;
-        if (typeof value === 'string') {
-          // 去掉引号和大括号
-          const cleaned = value.replace(/^["']|["']$/g, '').replace(/^{|}$/g, '');
-          if (cleaned && variableNodeMap.has(cleaned)) {
-            varName = cleaned;
-          }
-        }
+        const varName = extractVarName(value);
+        if (!varName) return;
         
-        // 如果找到变量引用,创建从变量节点到流程节点的连接
-        if (varName) {
-          const varNode = variableNodeMap.get(varName);
-          if (varNode && varNode.outputs && varNode.outputs.length > 0) {
-            const varOutputPort = varNode.outputs[0]; // 变量节点只有一个输出端口
-            connections.push({
-              id: `conn_var_in_${processNode.id}_${inputPort.id}_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
-              source: varNode.id,
-              target: processNode.id,
-              sourcePort: varOutputPort.id,
-              targetPort: inputPort.id
-            });
-          }
+        // 从 Get 节点连接到流程节点的输入端口
+        const getNode = getNodes.get(varName);
+        if (getNode && getNode.outputs && getNode.outputs.length > 0) {
+          const varOutputPort = getNode.outputs[0];
+          connections.push({
+            id: `conn_var_in_${processNode.id}_${inputPort.id}_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
+            source: getNode.id,
+            target: processNode.id,
+            sourcePort: varOutputPort.id,
+            targetPort: inputPort.id
+          });
         }
       });
     }
     
-    // 处理输出变量连接(从流程节点到变量节点)
+    // 处理输出变量连接(从流程节点到 Set 变量节点)
     if (processNode.data.outVars && Array.isArray(processNode.data.outVars)) {
       const outVars = processNode.data.outVars;
       const dataOutputPorts = processNode.outputs?.filter(output => output.type === 'data') || [];
       
       outVars.forEach((value, index) => {
-        if (index >= dataOutputPorts.length) {
-          return;
-        }
+        if (index >= dataOutputPorts.length) return;
         
         const outputPort = dataOutputPorts[index];
-        if (!outputPort) {
-          return;
-        }
+        if (!outputPort) return;
         
-        // 检查值是否是变量引用(格式:"{varName}" 或 varName)
-        let varName = null;
-        if (typeof value === 'string') {
-          // 去掉引号和大括号
-          const cleaned = value.replace(/^["']|["']$/g, '').replace(/^{|}$/g, '');
-          if (cleaned && variableNodeMap.has(cleaned)) {
-            varName = cleaned;
-          }
-        }
+        const varName = extractVarName(value);
+        if (!varName) return;
         
-        // 如果找到变量引用,创建从流程节点到变量节点的连接
-        if (varName) {
-          const varNode = variableNodeMap.get(varName);
-          if (varNode && varNode.inputs && varNode.inputs.length > 0) {
-            const varInputPort = varNode.inputs[0]; // 变量节点只有一个输入端口
-            connections.push({
-              id: `conn_var_out_${processNode.id}_${outputPort.id}_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
-              source: processNode.id,
-              target: varNode.id,
-              sourcePort: outputPort.id,
-              targetPort: varInputPort.id
-            });
-          }
+        // 从流程节点的输出端口连接到 Set 节点
+        const setNode = setNodes.get(varName);
+        if (setNode && setNode.inputs && setNode.inputs.length > 0) {
+          const varInputPort = setNode.inputs[0];
+          connections.push({
+            id: `conn_var_out_${processNode.id}_${outputPort.id}_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
+            source: processNode.id,
+            target: setNode.id,
+            sourcePort: outputPort.id,
+            targetPort: varInputPort.id
+          });
         }
       });
     }

+ 107 - 0
src/pages/blueprint/variable-node-dialog/variable-node-dialog.css

@@ -0,0 +1,107 @@
+/**
+ * 变量节点类型选择对话框样式
+ */
+
+.Variable-node-dialog-overlay {
+  position: fixed;
+  top: 0;
+  left: 0;
+  right: 0;
+  bottom: 0;
+  background-color: rgba(0, 0, 0, 0.5);
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  z-index: 1000;
+}
+
+.Variable-node-dialog {
+  background-color: #2d2d2d;
+  border-radius: 8px;
+  padding: 20px;
+  min-width: 280px;
+  box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
+  border: 1px solid #444;
+}
+
+.Variable-node-dialog-title {
+  font-size: 16px;
+  font-weight: bold;
+  color: #fff;
+  margin-bottom: 8px;
+  text-align: center;
+}
+
+.Variable-node-dialog-subtitle {
+  font-size: 13px;
+  color: #aaa;
+  margin-bottom: 20px;
+  text-align: center;
+}
+
+.Variable-node-dialog-buttons {
+  display: flex;
+  gap: 12px;
+  justify-content: center;
+}
+
+.Variable-node-dialog-button {
+  flex: 1;
+  padding: 12px 20px;
+  border: none;
+  border-radius: 6px;
+  font-size: 14px;
+  font-weight: 500;
+  cursor: pointer;
+  transition: all 0.2s ease;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  gap: 4px;
+}
+
+.Variable-node-dialog-button.get {
+  background-color: #2e7d32;
+  color: #fff;
+}
+
+.Variable-node-dialog-button.get:hover {
+  background-color: #388e3c;
+}
+
+.Variable-node-dialog-button.set {
+  background-color: #1565c0;
+  color: #fff;
+}
+
+.Variable-node-dialog-button.set:hover {
+  background-color: #1976d2;
+}
+
+.Variable-node-dialog-button-label {
+  font-size: 14px;
+  font-weight: bold;
+}
+
+.Variable-node-dialog-button-desc {
+  font-size: 11px;
+  opacity: 0.8;
+}
+
+.Variable-node-dialog-cancel {
+  margin-top: 12px;
+  text-align: center;
+}
+
+.Variable-node-dialog-cancel-button {
+  background: none;
+  border: none;
+  color: #888;
+  font-size: 13px;
+  cursor: pointer;
+  padding: 8px 16px;
+}
+
+.Variable-node-dialog-cancel-button:hover {
+  color: #fff;
+}

+ 68 - 0
src/pages/blueprint/variable-node-dialog/variable-node-dialog.jsx

@@ -0,0 +1,68 @@
+/**
+ * 变量节点类型选择对话框组件
+ */
+
+import './variable-node-dialog.css';
+
+export function VariableNodeDialog({ 
+  visible, 
+  varName, 
+  onSelect, 
+  onCancel 
+}) {
+  if (!visible) return null;
+  
+  const handleOverlayClick = (e) => {
+    if (e.target === e.currentTarget) {
+      onCancel?.();
+    }
+  };
+  
+  const handleGetClick = () => {
+    onSelect?.('get');
+  };
+  
+  const handleSetClick = () => {
+    onSelect?.('set');
+  };
+  
+  return (
+    <div 
+      className="Variable-node-dialog-overlay"
+      onClick={handleOverlayClick}
+    >
+      <div className="Variable-node-dialog">
+        <div className="Variable-node-dialog-title">
+          创建变量节点
+        </div>
+        <div className="Variable-node-dialog-subtitle">
+          变量: {varName}
+        </div>
+        <div className="Variable-node-dialog-buttons">
+          <button 
+            className="Variable-node-dialog-button get"
+            onClick={handleGetClick}
+          >
+            <span className="Variable-node-dialog-button-label">Get</span>
+            <span className="Variable-node-dialog-button-desc">获取变量值</span>
+          </button>
+          <button 
+            className="Variable-node-dialog-button set"
+            onClick={handleSetClick}
+          >
+            <span className="Variable-node-dialog-button-label">Set</span>
+            <span className="Variable-node-dialog-button-desc">设置变量值</span>
+          </button>
+        </div>
+        <div className="Variable-node-dialog-cancel">
+          <button 
+            className="Variable-node-dialog-cancel-button"
+            onClick={onCancel}
+          >
+            取消
+          </button>
+        </div>
+      </div>
+    </div>
+  );
+}

+ 17 - 0
src/pages/blueprint/variable-panel/variable-panel.js

@@ -51,6 +51,23 @@ export function getVariableType(value) {
   return 'string';
 }
 
+/**
+ * 处理变量拖拽开始
+ * @param {DragEvent} e - 拖拽事件
+ * @param {string} varName - 变量名
+ * @param {*} varValue - 变量值
+ */
+export function handleDragStart(e, varName, varValue) {
+  // 设置拖拽数据
+  const dragData = {
+    type: 'variable',
+    varName: varName,
+    varValue: varValue
+  };
+  e.dataTransfer.setData('application/json', JSON.stringify(dragData));
+  e.dataTransfer.effectAllowed = 'copy';
+}
+
 /**
  * 添加新变量
  */

+ 7 - 2
src/pages/blueprint/variable-panel/variable-panel.jsx

@@ -3,7 +3,7 @@
  */
 
 import './variable-panel.css';
-import { useVariablePanelLogic, getVariableType } from './variable-panel.js';
+import { useVariablePanelLogic, getVariableType, handleDragStart } from './variable-panel.js';
 
 export function BlueprintVariablePanel({ variables = {}, onVariablesChange }) {
   const {
@@ -44,7 +44,12 @@ export function BlueprintVariablePanel({ variables = {}, onVariablesChange }) {
             const isEditing = editingVar === varName;
             
             return (
-              <div key={varName} className="Variable-panel-item">
+              <div 
+                key={varName} 
+                className="Variable-panel-item"
+                draggable={!isEditing}
+                onDragStart={(e) => handleDragStart(e, varName, varValue)}
+              >
                 <div className="Variable-panel-item-name-row">
                   {isEditing ? (
                     <input

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

@@ -1,20 +1,20 @@
 {
 	"nodePositions": {
-		"node_begin_1768716093486": {
-			"x": 100,
-			"y": 100
+		"node_begin_1768719615224": {
+			"x": 80,
+			"y": 160
 		},
 		"node_0": {
-			"x": 420,
-			"y": 100
+			"x": 360,
+			"y": 180
 		},
-		"var_mini_1768716093486": {
-			"x": 100,
-			"y": 280
+		"var_get_mini_1768719615225": {
+			"x": 80,
+			"y": 420
 		},
-		"var_swipeDirection_1768716093486": {
-			"x": 500,
-			"y": 520
+		"var_set_swipeDirection_1768719615225": {
+			"x": 700,
+			"y": 420
 		}
 	}
 }