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 { export interface WorkflowDefinitionNode {
id: number; id: number | string; // 支持数字或UUID字符串
nodeCode: string; nodeCode: string;
nodeType: string; nodeType: string;
nodeName: string; nodeName: string;
uiVariables: JSON; position?: { x: number; y: number }; // 节点位置
panelVariables: JSON; configs?: Record<string, any>; // 基本配置
localVariables: JSON; 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 { export interface WorkflowDefinitionQuery extends BaseQuery {

View File

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

View File

@ -8,7 +8,7 @@ import { NODE_DEFINITIONS, NodeType, getNodeCategory } from '../nodes';
interface LoadedWorkflowData { interface LoadedWorkflowData {
nodes: FlowNode[]; nodes: FlowNode[];
edges: FlowEdge[]; edges: FlowEdge[];
definition: WorkflowDefinition; definition: WorkflowDefinition | null;
} }
export const useWorkflowLoad = () => { export const useWorkflowLoad = () => {
@ -49,7 +49,7 @@ export const useWorkflowLoad = () => {
color: nodeDefinition?.renderConfig.theme.primary || getNodeColor(node.nodeType), color: nodeDefinition?.renderConfig.theme.primary || getNodeColor(node.nodeType),
configs: node.configs || {}, configs: node.configs || {},
inputMapping: node.inputMapping || {}, inputMapping: node.inputMapping || {},
outputMapping: node.outputMapping || {}, outputs: node.outputs || [], // ✅ 从后端加载的输出能力定义
// 添加节点定义引用,用于配置弹窗 // 添加节点定义引用,用于配置弹窗
nodeDefinition nodeDefinition
}, },
@ -168,40 +168,6 @@ export const useWorkflowLoad = () => {
} }
}, [convertToFlowFormat]); }, [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(() => { const resetLoad = useCallback(() => {
setWorkflowDefinition(null); setWorkflowDefinition(null);
@ -212,7 +178,6 @@ export const useWorkflowLoad = () => {
loading, loading,
workflowDefinition, workflowDefinition,
loadWorkflow, loadWorkflow,
createNewWorkflow,
resetLoad resetLoad
}; };
}; };

View File

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

View File

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

View File

@ -71,40 +71,58 @@ export const JenkinsBuildNodeDefinition: ConfigurableNodeDefinition = {
}, },
required: ["nodeName", "nodeCode", "jenkinsUrl"] required: ["nodeName", "nodeCode", "jenkinsUrl"]
}, },
// 输出映射Schema // ✅ 输出能力定义(只读展示,传递给后端)
outputMappingSchema: { outputs: [
type: "object", {
title: "输出映射", name: "buildNumber",
description: "传递给下游节点的数据映射配置", title: "构建编号",
properties: { type: "number",
buildId: { description: "Jenkins构建的唯一编号",
type: "string", example: 123,
title: "构建ID", required: true
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: {
type: "number",
title: "构建耗时",
description: "构建耗时(毫秒)",
default: 0
}
}, },
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 即可 // ✅ 不再需要单独的渲染组件,使用 BaseNode 即可

View File

@ -47,6 +47,22 @@ export const getNodeCategory = (nodeType: NodeType | string): NodeCategory => {
import type { ISchema } from '@formily/react'; import type { ISchema } from '@formily/react';
export type JSONSchema = ISchema; 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包含基本信息+节点配置) configSchema: JSONSchema; // 基本配置Schema包含基本信息+节点配置)
} }
// 可配置节点定义有3个TAB基本配置、输入、输出 // 可配置节点定义有3个面板:基本配置、输入映射、输出能力
export interface ConfigurableNodeDefinition extends BaseNodeDefinition { export interface ConfigurableNodeDefinition extends BaseNodeDefinition {
inputMappingSchema?: JSONSchema; // 输入映射的Schema定义 inputMappingSchema?: JSONSchema; // 输入映射的Schema定义(用户配置)
outputMappingSchema?: JSONSchema; // 输出映射的Schema定义 outputs?: OutputField[]; // 输出能力定义(只读展示)
} }
// 工作流节点定义(联合类型) // 工作流节点定义(联合类型)
@ -124,10 +140,10 @@ export interface NodeInstanceData {
category: NodeCategory; category: NodeCategory;
description?: string; description?: string;
// 运行时数据key/value格式 // 运行时数据
configs?: Record<string, any>; // 基本配置数据(包含基本信息+节点配置) configs?: Record<string, any>; // 基本配置数据(包含基本信息+节点配置)
inputMapping?: Record<string, any>; inputMapping?: Record<string, any>; // 输入映射(用户配置)
outputMapping?: Record<string, any>; outputs?: OutputField[]; // 输出能力定义(从节点定义中获取)
// UI位置信息 // UI位置信息
position: { x: number; y: number }; position: { x: number; y: number };
@ -135,5 +151,5 @@ export interface NodeInstanceData {
// 判断是否为可配置节点 // 判断是否为可配置节点
export const isConfigurableNode = (def: WorkflowNodeDefinition): def is ConfigurableNodeDefinition => { 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; icon: string;
color: string; color: string;
// 运行时数据(与原系统兼容的格式) // 运行时数据
configs?: Record<string, any>; // 基本配置数据(包含基本信息+节点配置) configs?: Record<string, any>; // 基本配置数据(包含基本信息+节点配置)
inputMapping?: Record<string, any>; // 输入参数映射 inputMapping?: Record<string, any>; // 输入映射(用户配置)
outputMapping?: Record<string, any>; // 输出参数映射 outputs?: import('./nodes/types').OutputField[]; // 输出能力定义(从节点定义中获取)
// 原始节点定义(使用新的节点定义接口) // 原始节点定义(使用新的节点定义接口)
nodeDefinition?: import('./nodes/types').WorkflowNodeDefinition; nodeDefinition?: import('./nodes/types').WorkflowNodeDefinition;