This commit is contained in:
dengqichen 2025-10-21 16:25:51 +08:00
parent 3b67370c2e
commit 329cb955d8
9 changed files with 706 additions and 1112 deletions

File diff suppressed because it is too large Load Diff

View File

@ -39,13 +39,22 @@ export interface WorkflowDefinition extends BaseResponse {
}
export interface WorkflowDefinitionNode {
id: number;
id: number | string; // 支持数字或UUID字符串
nodeCode: string;
nodeType: string;
nodeName: string;
uiVariables: JSON;
panelVariables: JSON;
localVariables: JSON;
position?: { x: number; y: number }; // 节点位置
configs?: Record<string, any>; // 基本配置
inputMapping?: Record<string, any>; // 输入映射
outputs?: Array<{ // 输出能力定义
name: string;
title: string;
type: 'string' | 'number' | 'boolean' | 'object' | 'array'; // 与 OutputField 保持一致
description: string;
enum?: string[];
example?: any;
required?: boolean;
}>;
}
export interface WorkflowDefinitionQuery extends BaseQuery {

View File

@ -46,7 +46,6 @@ const NodeConfigModal: React.FC<NodeConfigModalProps> = ({
// 创建Formily表单实例
const configForm = useMemo(() => createForm(), []);
const inputForm = useMemo(() => createForm(), []);
const outputForm = useMemo(() => createForm(), []);
// 初始化表单数据
useEffect(() => {
@ -67,12 +66,9 @@ const NodeConfigModal: React.FC<NodeConfigModalProps> = ({
if (isConfigurableNode(nodeDefinition)) {
inputForm.setInitialValues(nodeData.inputMapping || {});
inputForm.reset();
outputForm.setInitialValues(nodeData.outputMapping || {});
outputForm.reset();
}
}
}, [visible, node, nodeDefinition, configForm, inputForm, outputForm]);
}, [visible, node, nodeDefinition, configForm, inputForm]);
// 递归处理表单值将JSON字符串转换为对象
const processFormValues = (values: Record<string, any>, schema: ISchema | undefined): Record<string, any> => {
@ -109,15 +105,13 @@ const NodeConfigModal: React.FC<NodeConfigModalProps> = ({
const inputMapping = isConfigurableNode(nodeDefinition)
? processFormValues(inputForm.values, nodeDefinition.inputMappingSchema)
: {};
const outputMapping = isConfigurableNode(nodeDefinition)
? processFormValues(outputForm.values, nodeDefinition.outputMappingSchema)
: {};
const updatedData: Partial<FlowNodeData> = {
label: configs.nodeName || node.data.label,
configs,
inputMapping,
outputMapping,
// outputs 保留原值(不修改,因为它是只读的输出能力定义)
outputs: node.data.outputs || [],
};
onOk(node.id, updatedData);
@ -142,7 +136,6 @@ const NodeConfigModal: React.FC<NodeConfigModalProps> = ({
const handleReset = () => {
configForm.reset();
inputForm.reset();
outputForm.reset();
toast({
title: "已重置",
description: "表单已重置为初始值",
@ -313,18 +306,55 @@ const NodeConfigModal: React.FC<NodeConfigModalProps> = ({
</AccordionItem>
)}
{/* 输出映射 - 条件显示 */}
{isConfigurableNode(nodeDefinition) && nodeDefinition.outputMappingSchema && (
{/* 输出能力 - 只读展示 */}
{isConfigurableNode(nodeDefinition) && nodeDefinition.outputs && nodeDefinition.outputs.length > 0 && (
<AccordionItem value="output" className="border-b">
<AccordionTrigger className="text-base font-semibold flex-row-reverse justify-end gap-2 hover:no-underline">
</AccordionTrigger>
<AccordionContent className="px-1">
<FormProvider form={outputForm}>
<FormLayout layout="vertical" colon={false}>
<SchemaField schema={convertToFormilySchema(nodeDefinition.outputMappingSchema)} />
</FormLayout>
</FormProvider>
<div className="text-sm text-muted-foreground mb-4 p-3 bg-muted/50 rounded-md">
💡 <code className="px-1 py-0.5 bg-background rounded">{'${upstream.字段名}'}</code>
</div>
<div className="space-y-3">
{nodeDefinition.outputs.map((output) => (
<div key={output.name} className="p-3 border rounded-md bg-background">
<div className="flex items-start justify-between mb-2">
<div className="flex-1">
<div className="flex items-center gap-2">
<span className="font-medium text-foreground">{output.title}</span>
<code className="px-1.5 py-0.5 text-xs bg-muted rounded">{output.name}</code>
<span className="text-xs text-muted-foreground border border-border px-1.5 py-0.5 rounded">
{output.type}
</span>
{output.required && (
<span className="text-xs text-red-500">*</span>
)}
</div>
<p className="text-sm text-muted-foreground mt-1">
{output.description}
</p>
</div>
</div>
{output.enum && (
<div className="mt-2 text-xs">
<span className="text-muted-foreground"></span>
<span className="ml-1 text-foreground">{output.enum.join(', ')}</span>
</div>
)}
{output.example !== undefined && (
<div className="mt-2 text-xs">
<span className="text-muted-foreground"></span>
<code className="ml-1 px-1.5 py-0.5 bg-muted rounded text-foreground">
{typeof output.example === 'object'
? JSON.stringify(output.example)
: String(output.example)}
</code>
</div>
)}
</div>
))}
</div>
</AccordionContent>
</AccordionItem>
)}

View File

@ -8,7 +8,7 @@ import { NODE_DEFINITIONS, NodeType, getNodeCategory } from '../nodes';
interface LoadedWorkflowData {
nodes: FlowNode[];
edges: FlowEdge[];
definition: WorkflowDefinition;
definition: WorkflowDefinition | null;
}
export const useWorkflowLoad = () => {
@ -49,7 +49,7 @@ export const useWorkflowLoad = () => {
color: nodeDefinition?.renderConfig.theme.primary || getNodeColor(node.nodeType),
configs: node.configs || {},
inputMapping: node.inputMapping || {},
outputMapping: node.outputMapping || {},
outputs: node.outputs || [], // ✅ 从后端加载的输出能力定义
// 添加节点定义引用,用于配置弹窗
nodeDefinition
},
@ -168,40 +168,6 @@ export const useWorkflowLoad = () => {
}
}, [convertToFlowFormat]);
// 创建新的空白工作流
const createNewWorkflow = useCallback((): LoadedWorkflowData => {
const emptyDefinition: WorkflowDefinition = {
id: 0,
createTime: null,
createBy: null,
updateTime: null,
updateBy: null,
version: 1,
deleted: false,
extraData: null,
name: '新建工作流',
key: `workflow_${Date.now()}`,
description: '',
category: 'GENERAL',
triggers: [],
flowVersion: 1,
bpmnXml: null,
status: 'DRAFT',
graph: {
nodes: [],
edges: []
}
};
setWorkflowDefinition(emptyDefinition);
return {
nodes: [],
edges: [],
definition: emptyDefinition
};
}, []);
// 重置加载状态
const resetLoad = useCallback(() => {
setWorkflowDefinition(null);
@ -212,7 +178,6 @@ export const useWorkflowLoad = () => {
loading,
workflowDefinition,
loadWorkflow,
createNewWorkflow,
resetLoad
};
};

View File

@ -3,6 +3,7 @@ import { message } from 'antd';
import * as definitionService from '../../Definition/service';
import type { FlowNode, FlowEdge } from '../types';
import { NodeType } from '../types';
import { isConfigurableNode } from '../nodes/types';
interface WorkflowSaveData {
nodes: FlowNode[];
@ -58,8 +59,8 @@ export const useWorkflowSave = () => {
// 转换节点和边数据为后端格式
const graph = {
nodes: data.nodes.map(node => ({
id: node.id, // 使用React Flow的节点ID
nodeCode: node.id, // nodeCode与ID相同
id: node.id, // 使用React Flow的节点IDUUID
nodeCode: node.data.configs?.nodeCode || node.data.nodeType, // ✅ 使用预定义的 nodeCode如 "START_EVENT"
nodeType: node.data.nodeType,
nodeName: node.data.label,
position: {
@ -68,7 +69,9 @@ export const useWorkflowSave = () => {
},
configs: node.data.configs || {},
inputMapping: node.data.inputMapping || {},
outputMapping: node.data.outputMapping || {}
outputs: (node.data.nodeDefinition && isConfigurableNode(node.data.nodeDefinition))
? node.data.nodeDefinition.outputs || []
: [] // ✅ 输出能力定义(直接从节点定义中获取)
})),
edges: data.edges.map(edge => ({
id: edge.id,
@ -83,28 +86,14 @@ export const useWorkflowSave = () => {
}))
};
// 构建保存数据 - 完全模仿原始系统的逻辑
const workflowData = data.definitionData
? {
// 如果有原始定义数据,展开它并覆盖 graph
...data.definitionData,
graph
}
: {
// 如果没有原始定义数据,创建新的工作流
name: data.name || '新建工作流',
description: data.description || '',
key: `workflow_${Date.now()}`,
category: 'GENERAL',
triggers: [],
flowVersion: 1,
bpmnXml: null,
status: 'DRAFT',
graph
// ✅ 透传模式:只传递业务字段,更新 graph
const workflowData = {
...data.definitionData, // 透传原始数据
graph // 只覆盖 graph 字段
};
// 无论新建还是更新,都调用 saveDefinition (POST 请求)
await definitionService.saveDefinition(workflowData as any);
// 只有更新操作(外层列表已创建草稿,这里只更新)
await definitionService.updateDefinition(data.workflowId!, workflowData as any);
message.success('工作流保存成功');
setLastSaved(new Date());

View File

@ -11,6 +11,7 @@ import NodeConfigModal from './components/NodeConfigModal';
import EdgeConfigModal, { type EdgeCondition } from './components/EdgeConfigModal';
import type { FlowNode, FlowEdge, FlowNodeData } from './types';
import type { WorkflowNodeDefinition } from './nodes/types';
import { isConfigurableNode } from './nodes/types';
import { useWorkflowSave } from './hooks/useWorkflowSave';
import { useWorkflowLoad } from './hooks/useWorkflowLoad';
import { useHistory } from './hooks/useHistory';
@ -302,7 +303,7 @@ const WorkflowDesignInner: React.FC = () => {
description: nodeDefinition.description
},
inputMapping: {},
outputMapping: {}
outputs: isConfigurableNode(nodeDefinition) ? nodeDefinition.outputs || [] : [] // ✅ 从节点定义中获取输出能力
}
};

View File

@ -71,40 +71,58 @@ export const JenkinsBuildNodeDefinition: ConfigurableNodeDefinition = {
},
required: ["nodeName", "nodeCode", "jenkinsUrl"]
},
// 输出映射Schema
outputMappingSchema: {
type: "object",
title: "输出映射",
description: "传递给下游节点的数据映射配置",
properties: {
buildId: {
type: "string",
title: "构建ID",
description: "Jenkins构建的唯一标识符",
default: "${jenkins.buildNumber}"
},
buildStatus: {
type: "string",
title: "构建状态",
description: "构建完成状态",
enum: ["SUCCESS", "FAILURE", "UNSTABLE", "ABORTED", "NOT_BUILT"],
default: "SUCCESS"
},
buildUrl: {
type: "string",
title: "构建URL",
description: "Jenkins构建页面的URL",
default: "${jenkins.buildUrl}"
},
buildDuration: {
// ✅ 输出能力定义(只读展示,传递给后端)
outputs: [
{
name: "buildNumber",
title: "构建编号",
type: "number",
title: "构建耗时",
description: "构建耗时(毫秒)",
default: 0
}
description: "Jenkins构建的唯一编号",
example: 123,
required: true
},
required: ["buildId", "buildStatus", "buildUrl"]
{
name: "buildStatus",
title: "构建状态",
type: "string",
enum: ["SUCCESS", "FAILURE", "UNSTABLE", "ABORTED"],
description: "构建执行的结果状态",
example: "SUCCESS",
required: true
},
{
name: "buildUrl",
title: "构建URL",
type: "string",
description: "Jenkins构建页面的访问地址",
example: "http://jenkins.example.com/job/app/123/",
required: true
},
{
name: "artifactUrl",
title: "构建产物地址",
type: "string",
description: "构建生成的jar/war包下载地址",
example: "http://jenkins.example.com/job/app/123/artifact/target/app-1.0.0.jar",
required: false
},
{
name: "gitCommitId",
title: "Git提交ID",
type: "string",
description: "本次构建使用的Git提交哈希值",
example: "a3f5e8d2c4b1a5e9f2d3e7b8c9d1a2f3e4b5c6d7",
required: true
},
{
name: "buildDuration",
title: "构建时长",
type: "number",
description: "构建执行的时长(秒)",
example: 120,
required: true
}
]
};
// ✅ 不再需要单独的渲染组件,使用 BaseNode 即可

View File

@ -47,6 +47,22 @@ export const getNodeCategory = (nodeType: NodeType | string): NodeCategory => {
import type { ISchema } from '@formily/react';
export type JSONSchema = ISchema;
// ========== 输出字段定义 ==========
/**
* -
*
*/
export interface OutputField {
name: string; // 字段名(用于表达式引用 ${upstream.buildNumber}
title: string; // 显示名称
description: string; // 详细说明
type: 'string' | 'number' | 'boolean' | 'object' | 'array'; // 数据类型
enum?: string[]; // 枚举值(可选)
example?: any; // 示例值
required?: boolean; // 是否必定产生(默认 true
}
// ========== 渲染配置(配置驱动节点渲染) ==========
// 节点尺寸
@ -107,10 +123,10 @@ export interface BaseNodeDefinition {
configSchema: JSONSchema; // 基本配置Schema包含基本信息+节点配置)
}
// 可配置节点定义有3个TAB基本配置、输入、输出
// 可配置节点定义有3个面板:基本配置、输入映射、输出能力
export interface ConfigurableNodeDefinition extends BaseNodeDefinition {
inputMappingSchema?: JSONSchema; // 输入映射的Schema定义
outputMappingSchema?: JSONSchema; // 输出映射的Schema定义
inputMappingSchema?: JSONSchema; // 输入映射的Schema定义(用户配置)
outputs?: OutputField[]; // 输出能力定义(只读展示)
}
// 工作流节点定义(联合类型)
@ -124,10 +140,10 @@ export interface NodeInstanceData {
category: NodeCategory;
description?: string;
// 运行时数据key/value格式
// 运行时数据
configs?: Record<string, any>; // 基本配置数据(包含基本信息+节点配置)
inputMapping?: Record<string, any>;
outputMapping?: Record<string, any>;
inputMapping?: Record<string, any>; // 输入映射(用户配置)
outputs?: OutputField[]; // 输出能力定义(从节点定义中获取)
// UI位置信息
position: { x: number; y: number };
@ -135,5 +151,5 @@ export interface NodeInstanceData {
// 判断是否为可配置节点
export const isConfigurableNode = (def: WorkflowNodeDefinition): def is ConfigurableNodeDefinition => {
return 'inputMappingSchema' in def || 'outputMappingSchema' in def;
return 'inputMappingSchema' in def || 'outputs' in def;
};

View File

@ -41,10 +41,10 @@ export interface FlowNodeData extends Record<string, unknown> {
icon: string;
color: string;
// 运行时数据(与原系统兼容的格式)
// 运行时数据
configs?: Record<string, any>; // 基本配置数据(包含基本信息+节点配置)
inputMapping?: Record<string, any>; // 输入参数映射
outputMapping?: Record<string, any>; // 输出参数映射
inputMapping?: Record<string, any>; // 输入映射(用户配置)
outputs?: import('./nodes/types').OutputField[]; // 输出能力定义(从节点定义中获取)
// 原始节点定义(使用新的节点定义接口)
nodeDefinition?: import('./nodes/types').WorkflowNodeDefinition;