From e340e3106d50ebfe531cc7e8bca88e7cf2bb4d5c Mon Sep 17 00:00:00 2001 From: dengqichen Date: Sat, 25 Oct 2025 01:31:20 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E5=AE=A1=E6=89=B9=E7=BB=84?= =?UTF-8?q?=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/components/VariableInput/index.tsx | 20 ++++++-- .../src/components/VariableInput/types.ts | 20 ++++++++ .../Design/components/EdgeConfigModal.tsx | 8 +++- .../Design/components/NodeConfigModal.tsx | 4 ++ frontend/src/pages/Workflow/Design/index.tsx | 48 +++++++++++++++++-- .../src/utils/workflow/variableConversion.ts | 10 ++++ 6 files changed, 101 insertions(+), 9 deletions(-) diff --git a/frontend/src/components/VariableInput/index.tsx b/frontend/src/components/VariableInput/index.tsx index cb9003bf..8b05ac46 100644 --- a/frontend/src/components/VariableInput/index.tsx +++ b/frontend/src/components/VariableInput/index.tsx @@ -18,6 +18,7 @@ const VariableInput: React.FC = ({ allNodes, allEdges, currentNodeId, + formFields = [], variant = 'input', placeholder, disabled = false, @@ -32,10 +33,23 @@ const VariableInput: React.FC = ({ const dropdownRef = useRef(null); const highlightRef = useRef(null); - // 收集可用变量 + // 将表单字段转换为变量格式(匹配 NodeVariable 结构) + const formVariables = useMemo(() => { + return formFields.map(field => ({ + nodeId: 'form', + nodeName: '启动表单', + fieldName: field.name, + fieldType: field.type, + displayText: `\${form.${field.name}}`, + fullText: `form.${field.name}`, + })); + }, [formFields]); + + // 收集可用变量(节点变量 + 表单变量) const allVariables = useMemo(() => { - return collectNodeVariables(currentNodeId, allNodes, allEdges); - }, [currentNodeId, allNodes, allEdges]); + const nodeVariables = collectNodeVariables(currentNodeId, allNodes, allEdges); + return [...formVariables, ...nodeVariables]; + }, [currentNodeId, allNodes, allEdges, formVariables]); // 根据搜索文本过滤变量 const filteredVariables = useMemo(() => { diff --git a/frontend/src/components/VariableInput/types.ts b/frontend/src/components/VariableInput/types.ts index 9aa120fd..e2baacc5 100644 --- a/frontend/src/components/VariableInput/types.ts +++ b/frontend/src/components/VariableInput/types.ts @@ -1,5 +1,22 @@ import type { FlowNode, FlowEdge } from '@/pages/Workflow/Design/types'; +/** + * 表单字段信息(用于变量引用) + */ +export interface FormField { + /** 字段名称(变量名)*/ + name: string; + + /** 字段标签(显示名称)*/ + label: string; + + /** 字段类型 */ + type: string; + + /** 字段描述 */ + description?: string; +} + /** * 变量输入组件的 Props */ @@ -19,6 +36,9 @@ export interface VariableInputProps { /** 当前节点ID(用于过滤前序节点)*/ currentNodeId: string; + /** 表单字段列表(用于支持 ${form.xxx} 变量)*/ + formFields?: FormField[]; + /** 渲染类型 */ variant?: 'input' | 'textarea'; diff --git a/frontend/src/pages/Workflow/Design/components/EdgeConfigModal.tsx b/frontend/src/pages/Workflow/Design/components/EdgeConfigModal.tsx index 1f81b635..ab79119c 100644 --- a/frontend/src/pages/Workflow/Design/components/EdgeConfigModal.tsx +++ b/frontend/src/pages/Workflow/Design/components/EdgeConfigModal.tsx @@ -11,12 +11,14 @@ import { useToast } from '@/components/ui/use-toast'; import type { FlowEdge, FlowNode } from '../types'; import { convertToUUID, convertToDisplayName } from '@/utils/workflow/variableConversion'; import VariableInput from '@/components/VariableInput'; +import type { FormField as FormFieldType } from '@/components/VariableInput/types'; interface EdgeConfigModalProps { visible: boolean; edge: FlowEdge | null; allNodes: FlowNode[]; allEdges: FlowEdge[]; + formFields?: FormFieldType[]; onOk: (edgeId: string, condition: EdgeCondition) => void; onCancel: () => void; } @@ -58,6 +60,7 @@ const EdgeConfigModal: React.FC = ({ edge, allNodes, allEdges, + formFields = [], onOk, onCancel }) => { @@ -200,9 +203,10 @@ const EdgeConfigModal: React.FC = ({ allNodes={allNodes} allEdges={allEdges} currentNodeId={edge?.target || ''} + formFields={formFields} variant="textarea" - placeholder="请输入条件表达式,如:${Jenkins构建.buildStatus == 'SUCCESS'}" - rows={4} + placeholder="请输入条件表达式,如:${form.环境选择} 或 ${Jenkins构建.buildStatus == 'SUCCESS'}" + rows={4} /> diff --git a/frontend/src/pages/Workflow/Design/components/NodeConfigModal.tsx b/frontend/src/pages/Workflow/Design/components/NodeConfigModal.tsx index 6041662b..c281ac5f 100644 --- a/frontend/src/pages/Workflow/Design/components/NodeConfigModal.tsx +++ b/frontend/src/pages/Workflow/Design/components/NodeConfigModal.tsx @@ -22,12 +22,14 @@ import { convertJsonSchemaToZod, extractDataSourceTypes } from '../utils/schemaC import { loadDataSource, DataSourceType, type DataSourceOption } from '@/domain/dataSource'; import { convertObjectToUUID, convertObjectToDisplayName } from '@/utils/workflow/variableConversion'; import VariableInput from '@/components/VariableInput'; +import type { FormField as FormFieldType } from '@/components/VariableInput/types'; interface NodeConfigModalProps { visible: boolean; node: FlowNode | null; allNodes: FlowNode[]; allEdges: FlowEdge[]; + formFields?: FormFieldType[]; onCancel: () => void; onOk: (nodeId: string, updatedData: Partial) => void; } @@ -125,6 +127,7 @@ const NodeConfigModal: React.FC = ({ node, allNodes, allEdges, + formFields = [], onCancel, onOk }) => { @@ -500,6 +503,7 @@ const NodeConfigModal: React.FC = ({ allNodes={allNodesRef.current} allEdges={allEdgesRef.current} currentNodeId={node?.id || ''} + formFields={formFields} variant={isTextarea ? 'textarea' : 'input'} placeholder={prop.description || `请输入${prop.title || ''}`} disabled={loading} diff --git a/frontend/src/pages/Workflow/Design/index.tsx b/frontend/src/pages/Workflow/Design/index.tsx index e0fb18e5..e47b28a8 100644 --- a/frontend/src/pages/Workflow/Design/index.tsx +++ b/frontend/src/pages/Workflow/Design/index.tsx @@ -1,4 +1,4 @@ -import React, { useState, useCallback, useRef, useEffect } from 'react'; +import React, { useState, useCallback, useRef, useEffect, useMemo } from 'react'; import { useParams, useNavigate } from 'react-router-dom'; import { message } from 'antd'; import { ReactFlowProvider, useReactFlow } from '@xyflow/react'; @@ -57,6 +57,44 @@ const WorkflowDesignInner: React.FC = () => { const [formDefinition, setFormDefinition] = useState(null); const [formPreviewVisible, setFormPreviewVisible] = useState(false); + // 提取表单字段(用于变量引用) + const formFields = useMemo(() => { + if (!formDefinition?.schema?.fields) return []; + + const extractFields = (fields: any[]): any[] => { + const result: any[] = []; + + for (const field of fields) { + // 跳过布局组件 + if (['text', 'divider'].includes(field.type)) { + continue; + } + + // 栅格布局:递归提取子字段 + if (field.type === 'grid' && field.children) { + for (const column of field.children) { + if (Array.isArray(column)) { + result.push(...extractFields(column)); + } + } + continue; + } + + // 普通字段 + result.push({ + name: field.name, + label: field.label || field.name, + type: field.type, + description: field.description, + }); + } + + return result; + }; + + return extractFields(formDefinition.schema.fields); + }, [formDefinition]); + // 保存和加载hooks const { hasUnsavedChanges, saveWorkflow, markUnsaved } = useWorkflowSave(); const { workflowDefinition, loadWorkflow } = useWorkflowLoad(); @@ -560,13 +598,14 @@ const WorkflowDesignInner: React.FC = () => { {/* 节点配置弹窗 */} + onOk={handleNodeConfigUpdate} + /> {/* 边条件配置弹窗 */} { edge={configEdge} allNodes={getNodes() as FlowNode[]} allEdges={getEdges() as FlowEdge[]} + formFields={formFields} onOk={handleEdgeConditionUpdate} onCancel={handleCloseEdgeConfigModal} /> diff --git a/frontend/src/utils/workflow/variableConversion.ts b/frontend/src/utils/workflow/variableConversion.ts index 65cf56d6..fc295b5a 100644 --- a/frontend/src/utils/workflow/variableConversion.ts +++ b/frontend/src/utils/workflow/variableConversion.ts @@ -29,6 +29,11 @@ export const convertToUUID = ( } return displayText.replace(VARIABLE_PATTERN, (match, nodeNameOrId, fieldName) => { + // 特殊处理:表单字段 + if (nodeNameOrId === '启动表单' || nodeNameOrId === 'form') { + return `\${form.${fieldName}}`; + } + // 尝试通过名称查找节点 const nodeByName = allNodes.find(n => { const nodeName = n.data?.label || n.data?.nodeDefinition?.nodeName; @@ -67,6 +72,11 @@ export const convertToDisplayName = ( } return uuidText.replace(VARIABLE_PATTERN, (match, nodeIdOrName, fieldName) => { + // 特殊处理:表单字段 + if (nodeIdOrName === 'form' || nodeIdOrName === '启动表单') { + return `\${启动表单.${fieldName}}`; + } + // 尝试通过UUID查找节点 const nodeById = allNodes.find(n => n.id === nodeIdOrName);