From 85bebb7fc38a6f59a0a5b3e1dd9e65db3fa5da9a Mon Sep 17 00:00:00 2001 From: dengqichen Date: Thu, 20 Nov 2025 13:52:03 +0800 Subject: [PATCH] =?UTF-8?q?=E9=87=8D=E6=9E=84=E6=B6=88=E6=81=AF=E9=80=9A?= =?UTF-8?q?=E7=9F=A5=E5=BC=B9=E7=AA=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Design/components/KeyValueEditor.tsx | 88 +++++++++++++++++ .../Design/components/NodeConfigModal.tsx | 26 ++++- .../Workflow/Design/hooks/useWorkflowSave.ts | 21 +++- .../src/pages/Workflow/Design/nodes/index.ts | 4 + .../src/pages/Workflow/Design/nodes/types.ts | 2 + frontend/src/services/maintenanceDetector.ts | 6 +- .../src/utils/workflow/variableConversion.ts | 99 +++++++++++-------- 7 files changed, 200 insertions(+), 46 deletions(-) create mode 100644 frontend/src/pages/Workflow/Design/components/KeyValueEditor.tsx diff --git a/frontend/src/pages/Workflow/Design/components/KeyValueEditor.tsx b/frontend/src/pages/Workflow/Design/components/KeyValueEditor.tsx new file mode 100644 index 00000000..7378c3a9 --- /dev/null +++ b/frontend/src/pages/Workflow/Design/components/KeyValueEditor.tsx @@ -0,0 +1,88 @@ +import React from 'react'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { Plus, Trash2 } from 'lucide-react'; +import { useFieldArray, Control, Controller } from 'react-hook-form'; + +/** + * 键值对编辑器组件 + * 通过 x-component: "KeyValueEditor" 声明使用 + * 用于编辑数组类型的键值对数据(如 HTTP 请求头、查询参数) + */ +interface KeyValueEditorProps { + control: Control; + name: string; + keyPlaceholder?: string; + valuePlaceholder?: string; + disabled?: boolean; +} + +export const KeyValueEditor: React.FC = ({ + control, + name, + keyPlaceholder = 'Key', + valuePlaceholder = 'Value', + disabled = false, +}) => { + const { fields, append, remove } = useFieldArray({ + control, + name, + }); + + return ( +
+ {fields.map((field, index) => ( +
+
+ ( + + )} + /> + ( + + )} + /> +
+ +
+ ))} + + +
+ ); +}; diff --git a/frontend/src/pages/Workflow/Design/components/NodeConfigModal.tsx b/frontend/src/pages/Workflow/Design/components/NodeConfigModal.tsx index 8b07bf7d..619ad32c 100644 --- a/frontend/src/pages/Workflow/Design/components/NodeConfigModal.tsx +++ b/frontend/src/pages/Workflow/Design/components/NodeConfigModal.tsx @@ -24,6 +24,7 @@ import { convertObjectToUUID, convertObjectToDisplayName } from '@/utils/workflo import CodeMirrorVariableInput from '@/components/CodeMirrorVariableInput'; import SelectOrVariableInput from '@/components/SelectOrVariableInput'; import type { FormField as FormFieldType } from '@/components/CodeMirrorVariableInput/types'; +import { KeyValueEditor } from './KeyValueEditor'; interface NodeConfigModalProps { visible: boolean; @@ -221,7 +222,7 @@ const NodeConfigModal: React.FC = ({ // ✅ 初始化表单数据 useEffect(() => { if (visible && node && nodeDefinition) { - const nodeData = node.data || {}; + const nodeData = (node.data || {}) as FlowNodeData; // 设置固定节点信息(从 configs 或 nodeDefinition 获取) setNodeName(nodeData.configs?.nodeName || nodeDefinition.nodeName); @@ -410,6 +411,27 @@ const NodeConfigModal: React.FC = ({ // ✅ 渲染字段控件 const renderFieldControl = useCallback((prop: any, field: any, key: string) => { + // ✅ 优先检查自定义组件(x-component) + if (prop['x-component']) { + switch (prop['x-component']) { + case 'KeyValueEditor': + return ( + + ); + // 未来可以添加更多自定义组件 + default: + console.warn(`Unknown x-component: ${prop['x-component']}`); + break; + } + } + // 动态数据源 if (prop['x-dataSource']) { const options = dataSourceCache[prop['x-dataSource']] || []; @@ -607,7 +629,7 @@ const NodeConfigModal: React.FC = ({ /> ); } - }, [dataSourceCache, loadingDataSources, loading, node, renderSelect]); + }, [dataSourceCache, loadingDataSources, loading, node, renderSelect, inputForm.control]); // ✅ 渲染表单字段(不使用 useCallback 以支持 watch 的响应式更新) const renderFormFields = ( diff --git a/frontend/src/pages/Workflow/Design/hooks/useWorkflowSave.ts b/frontend/src/pages/Workflow/Design/hooks/useWorkflowSave.ts index dd08dec9..eaefe0f9 100644 --- a/frontend/src/pages/Workflow/Design/hooks/useWorkflowSave.ts +++ b/frontend/src/pages/Workflow/Design/hooks/useWorkflowSave.ts @@ -4,6 +4,7 @@ import * as definitionService from '../../Definition/List/service'; import type { FlowNode, FlowEdge } from '../types'; import { NodeType, isConfigurableNode } from '../nodes/types'; import { getGatewayErrors, getGatewayWarnings } from '../utils/gatewayValidation'; +import { convertToUUID } from '@/utils/workflow/variableConversion'; interface WorkflowSaveData { nodes: FlowNode[]; @@ -95,17 +96,33 @@ export const useWorkflowSave = () => { const isFromParallelGateway = sourceNode?.data?.nodeType === 'GATEWAY_NODE' && sourceNode?.data?.inputMapping?.gatewayType === 'parallelGateway'; + // ⚠️⚠️⚠️ 关键:兜底转换边的表达式(可能从旧数据加载,从未编辑过) + const originalCondition = edge.data?.condition; + let finalCondition = originalCondition; + + if (originalCondition && originalCondition.type === 'EXPRESSION' && originalCondition.expression) { + // 转换条件表达式中的节点名称为 UUID + finalCondition = { + ...originalCondition, + expression: convertToUUID(originalCondition.expression, data.nodes) + }; + } + + // 转换边的 name(通常就是 condition.expression) + const originalName = edge.data?.label || ""; + const finalName = originalName ? convertToUUID(originalName, data.nodes) : ""; + return { id: edge.id, from: edge.source, // 后端使用from字段 to: edge.target, // 后端使用to字段 - name: edge.data?.label || "", // 边的名称 + name: finalName, // ⚠️ 边的名称(已转换) sourceHandle: edge.sourceHandle, // 保存源连接点 targetHandle: edge.targetHandle, // 保存目标连接点 config: { type: "sequence", // 固定为sequence类型 // ⚠️ 并行网关的边线不传递条件配置(BPMN 规范不允许) - condition: isFromParallelGateway ? undefined : edge.data?.condition + condition: isFromParallelGateway ? undefined : finalCondition }, // 优先保存多个拐点;若无则回退到单个控制点;否则为空 vertices: Array.isArray((edge as any)?.data?.vertices) && (edge as any).data.vertices.length > 0 diff --git a/frontend/src/pages/Workflow/Design/nodes/index.ts b/frontend/src/pages/Workflow/Design/nodes/index.ts index c4dc9c41..7db40301 100644 --- a/frontend/src/pages/Workflow/Design/nodes/index.ts +++ b/frontend/src/pages/Workflow/Design/nodes/index.ts @@ -10,6 +10,7 @@ import { EndEventNodeDefinition } from './EndEventNode'; import { JenkinsBuildNodeDefinition } from './JenkinsBuildNode'; import { NotificationNodeDefinition } from './NotificationNode'; import { ApprovalNodeDefinition } from './ApprovalNode'; +import { HttpRequestNodeDefinition } from './HttpRequestNode'; import { GatewayNodeDefinition } from './GatewayNode'; import type { WorkflowNodeDefinition } from './types'; @@ -22,6 +23,7 @@ export const NODE_DEFINITIONS: WorkflowNodeDefinition[] = [ JenkinsBuildNodeDefinition, NotificationNodeDefinition, ApprovalNodeDefinition, + HttpRequestNodeDefinition, GatewayNodeDefinition, ]; @@ -35,6 +37,7 @@ export const nodeTypes = { JENKINS_BUILD: BaseNode, NOTIFICATION: BaseNode, APPROVAL: BaseNode, + HTTP_REQUEST: BaseNode, GATEWAY_NODE: BaseNode, }; @@ -51,6 +54,7 @@ export { JenkinsBuildNodeDefinition, NotificationNodeDefinition, ApprovalNodeDefinition, + HttpRequestNodeDefinition, GatewayNodeDefinition, }; diff --git a/frontend/src/pages/Workflow/Design/nodes/types.ts b/frontend/src/pages/Workflow/Design/nodes/types.ts index ad4ef2f1..315f9971 100644 --- a/frontend/src/pages/Workflow/Design/nodes/types.ts +++ b/frontend/src/pages/Workflow/Design/nodes/types.ts @@ -17,6 +17,7 @@ export enum NodeType { JENKINS_BUILD = 'JENKINS_BUILD', NOTIFICATION = 'NOTIFICATION', APPROVAL = 'APPROVAL', + HTTP_REQUEST = 'HTTP_REQUEST', GATEWAY_NODE = 'GATEWAY_NODE', SUB_PROCESS = 'SUB_PROCESS', CALL_ACTIVITY = 'CALL_ACTIVITY' @@ -33,6 +34,7 @@ export const NODE_CATEGORY_MAP: Record = { [NodeType.JENKINS_BUILD]: NodeCategory.TASK, [NodeType.NOTIFICATION]: NodeCategory.TASK, [NodeType.APPROVAL]: NodeCategory.TASK, + [NodeType.HTTP_REQUEST]: NodeCategory.TASK, [NodeType.GATEWAY_NODE]: NodeCategory.GATEWAY, [NodeType.SUB_PROCESS]: NodeCategory.CONTAINER, [NodeType.CALL_ACTIVITY]: NodeCategory.CONTAINER, diff --git a/frontend/src/services/maintenanceDetector.ts b/frontend/src/services/maintenanceDetector.ts index 5a58b4f4..eab4c8fa 100644 --- a/frontend/src/services/maintenanceDetector.ts +++ b/frontend/src/services/maintenanceDetector.ts @@ -96,9 +96,9 @@ class MaintenanceDetector { this.recoveryCheckTimer = setInterval(async () => { try { - // 尝试请求一个轻量级接口 + // 尝试请求 Spring Boot Actuator 健康检查端点(无需认证) // 使用原生fetch避免触发axios拦截器 - const response = await fetch('/api/health', { + const response = await fetch('/actuator/health', { method: 'GET', cache: 'no-cache', signal: AbortSignal.timeout(3000) @@ -147,7 +147,7 @@ class MaintenanceDetector { */ async checkRecovery(): Promise { try { - const response = await fetch('/api/health', { + const response = await fetch('/actuator/health', { method: 'GET', cache: 'no-cache', signal: AbortSignal.timeout(3000) diff --git a/frontend/src/utils/workflow/variableConversion.ts b/frontend/src/utils/workflow/variableConversion.ts index fc295b5a..d75c3b86 100644 --- a/frontend/src/utils/workflow/variableConversion.ts +++ b/frontend/src/utils/workflow/variableConversion.ts @@ -1,12 +1,15 @@ import type { FlowNode } from '@/pages/Workflow/Design/types'; /** - * 变量表达式的正则模式 - * 匹配 ${xxx.yyy} 格式 - * xxx: 节点名称或UUID - * yyy: 字段名 + * 正则1:匹配 ${...} 块 */ -const VARIABLE_PATTERN = /\$\{([^.}]+)\.([^}]+)\}/g; +const BLOCK_PATTERN = /\$\{([^}]+)\}/g; + +/** + * 正则2:在表达式内匹配 节点名.字段 引用 + * 支持中文节点名、UUID格式节点ID + */ +const NODE_REF_PATTERN = /([a-zA-Z0-9_\u4e00-\u9fa5\-]+)\.([a-zA-Z0-9_.]+)/g; /** * 将显示名称格式转换为UUID格式 @@ -28,26 +31,39 @@ export const convertToUUID = ( return displayText; } - return displayText.replace(VARIABLE_PATTERN, (match, nodeNameOrId, fieldName) => { - // 特殊处理:表单字段 - if (nodeNameOrId === '启动表单' || nodeNameOrId === 'form') { - return `\${form.${fieldName}}`; - } + console.log('🔍 convertToUUID - 输入:', displayText); + console.log('🔍 可用节点:', allNodes.map(n => ({ id: n.id, label: n.data?.label }))); + + // 阶段1:处理 ${...} 块 + return displayText.replace(BLOCK_PATTERN, (blockMatch, blockContent) => { + console.log('🔍 找到 ${} 块:', blockContent); - // 尝试通过名称查找节点 - const nodeByName = allNodes.find(n => { - const nodeName = n.data?.label || n.data?.nodeDefinition?.nodeName; - return nodeName === nodeNameOrId; + // 阶段2:在块内容中查找并替换所有 节点名.字段 引用 + const transformedContent = blockContent.replace(NODE_REF_PATTERN, (refMatch: string, nodeNameOrId: string, fieldPath: string) => { + console.log('🔍 找到节点引用:', { refMatch, nodeNameOrId, fieldPath }); + + // 特殊处理:表单字段 + if (nodeNameOrId === '启动表单' || nodeNameOrId === 'form' || nodeNameOrId === 'notification') { + return `${nodeNameOrId}.${fieldPath}`; + } + + // 尝试通过名称查找节点 + const nodeByName = allNodes.find(n => { + const nodeName = n.data?.label || n.data?.nodeDefinition?.nodeName; + return nodeName === nodeNameOrId; + }); + + if (nodeByName) { + console.log('✅ 找到节点,转换为UUID:', nodeByName.id); + return `${nodeByName.id}.${fieldPath}`; + } + + // 可能已经是UUID格式,保持原样 + console.log('⚠️ 未找到节点(可能已是UUID):', nodeNameOrId); + return refMatch; }); - if (nodeByName) { - // 找到了,替换为UUID - return `\${${nodeByName.id}.${fieldName}}`; - } - - // 可能已经是UUID格式,或者节点已被删除 - // 保持原样 - return match; + return `\${${transformedContent}}`; }); }; @@ -71,24 +87,29 @@ export const convertToDisplayName = ( return uuidText; } - return uuidText.replace(VARIABLE_PATTERN, (match, nodeIdOrName, fieldName) => { - // 特殊处理:表单字段 - if (nodeIdOrName === 'form' || nodeIdOrName === '启动表单') { - return `\${启动表单.${fieldName}}`; - } + // 阶段1:处理 ${...} 块 + return uuidText.replace(BLOCK_PATTERN, (blockMatch, blockContent) => { + // 阶段2:在块内容中查找并替换所有 UUID.字段 引用为 节点名.字段 + const transformedContent = blockContent.replace(NODE_REF_PATTERN, (refMatch: string, nodeIdOrName: string, fieldPath: string) => { + // 特殊处理:表单字段 + if (nodeIdOrName === 'form' || nodeIdOrName === '启动表单' || nodeIdOrName === 'notification') { + return `${nodeIdOrName}.${fieldPath}`; + } + + // 尝试通过UUID查找节点 + const nodeById = allNodes.find(n => n.id === nodeIdOrName); + + if (nodeById) { + // 找到了,替换为显示名称 + const nodeName = nodeById.data?.label || nodeById.data?.nodeDefinition?.nodeName || nodeIdOrName; + return `${nodeName}.${fieldPath}`; + } + + // 可能已经是显示名称格式,保持原样 + return refMatch; + }); - // 尝试通过UUID查找节点 - const nodeById = allNodes.find(n => n.id === nodeIdOrName); - - if (nodeById) { - // 找到了,替换为显示名称 - const nodeName = nodeById.data?.label || nodeById.data?.nodeDefinition?.nodeName || nodeIdOrName; - return `\${${nodeName}.${fieldName}}`; - } - - // 可能已经是显示名称格式,或者节点已被删除 - // 保持原样 - return match; + return `\${${transformedContent}}`; }); };