This commit is contained in:
dengqichen 2025-10-21 12:16:37 +08:00
parent 7bead2a989
commit 33c70b7894
5 changed files with 413 additions and 197 deletions

View File

@ -1,12 +1,26 @@
import React, { useState, useEffect } from 'react';
import React, { useState, useEffect, useMemo } from 'react';
import { Drawer, Tabs, Button, Space, message } from 'antd';
import { SaveOutlined, ReloadOutlined, CloseOutlined } from '@ant-design/icons';
import { BetaSchemaForm } from '@ant-design/pro-components';
import { convertJsonSchemaToColumns } from '@/utils/jsonSchemaUtils';
import { FormItem, Input, NumberPicker, Select, FormLayout, Switch } from '@formily/antd-v5';
import { createForm } from '@formily/core';
import { createSchemaField, FormProvider, ISchema } from '@formily/react';
import type { FlowNode, FlowNodeData } from '../types';
import type { WorkflowNodeDefinition } from '../nodes/types';
import { isConfigurableNode } from '../nodes/types';
// 创建Schema组件
const SchemaField = createSchemaField({
components: {
FormItem,
Input,
NumberPicker,
Select,
FormLayout,
Switch,
'Input.TextArea': Input.TextArea,
},
});
interface NodeConfigModalProps {
visible: boolean;
node: FlowNode | null;
@ -14,12 +28,6 @@ interface NodeConfigModalProps {
onOk: (nodeId: string, updatedData: Partial<FlowNodeData>) => void;
}
interface FormData {
configs?: Record<string, any>;
inputMapping?: Record<string, any>;
outputMapping?: Record<string, any>;
}
const NodeConfigModal: React.FC<NodeConfigModalProps> = ({
visible,
node,
@ -28,46 +36,85 @@ const NodeConfigModal: React.FC<NodeConfigModalProps> = ({
}) => {
const [loading, setLoading] = useState(false);
const [activeTab, setActiveTab] = useState('config');
const [formData, setFormData] = useState<FormData>({});
// 获取节点定义
const nodeDefinition: WorkflowNodeDefinition | null = node?.data?.nodeDefinition || null;
// 创建Formily表单实例
const configForm = useMemo(() => createForm(), []);
const inputForm = useMemo(() => createForm(), []);
const outputForm = useMemo(() => createForm(), []);
// 初始化表单数据
useEffect(() => {
if (visible && node && nodeDefinition) {
// 从节点数据中获取现有配置
const nodeData = node.data || {};
// 准备默认的基本信息配置
// 准备默认配置
const defaultConfig = {
nodeName: nodeDefinition.nodeName, // 默认节点名称
nodeCode: nodeDefinition.nodeCode, // 默认节点编码
description: nodeDefinition.description // 默认节点描述
nodeName: nodeDefinition.nodeName,
nodeCode: nodeDefinition.nodeCode,
description: nodeDefinition.description
};
// 合并默认值和已保存的配置
setFormData({
configs: { ...defaultConfig, ...(nodeData.configs || {}) },
inputMapping: nodeData.inputMapping || {},
outputMapping: nodeData.outputMapping || {},
});
} else {
setFormData({});
}
}, [visible, node, nodeDefinition]);
// 设置表单初始值
configForm.setInitialValues({ ...defaultConfig, ...(nodeData.configs || {}) });
configForm.reset();
const handleSubmit = () => {
if (!node) return;
if (isConfigurableNode(nodeDefinition)) {
inputForm.setInitialValues(nodeData.inputMapping || {});
inputForm.reset();
outputForm.setInitialValues(nodeData.outputMapping || {});
outputForm.reset();
}
}
}, [visible, node, nodeDefinition, configForm, inputForm, outputForm]);
// 递归处理表单值将JSON字符串转换为对象
const processFormValues = (values: Record<string, any>, schema: ISchema | undefined): Record<string, any> => {
const result: Record<string, any> = {};
if (!schema?.properties || typeof schema.properties !== 'object') return values;
Object.entries(values).forEach(([key, value]) => {
const propSchema = (schema.properties as Record<string, any>)?.[key];
// 如果是object类型且值是字符串尝试解析
if (propSchema?.type === 'object' && typeof value === 'string') {
try {
result[key] = JSON.parse(value);
} catch {
result[key] = value; // 解析失败保持原值
}
} else {
result[key] = value;
}
});
return result;
};
const handleSubmit = async () => {
if (!node || !nodeDefinition) return;
try {
setLoading(true);
// 获取表单值并转换
const configs = processFormValues(configForm.values, nodeDefinition.configSchema);
const inputMapping = isConfigurableNode(nodeDefinition)
? processFormValues(inputForm.values, nodeDefinition.inputMappingSchema)
: {};
const outputMapping = isConfigurableNode(nodeDefinition)
? processFormValues(outputForm.values, nodeDefinition.outputMappingSchema)
: {};
const updatedData: Partial<FlowNodeData> = {
// 更新节点标签为配置中的节点名称
label: formData.configs?.nodeName || node.data.label,
configs: formData.configs,
inputMapping: formData.inputMapping,
outputMapping: formData.outputMapping,
label: configs.nodeName || node.data.label,
configs,
inputMapping,
outputMapping,
};
onOk(node.id, updatedData);
@ -83,40 +130,118 @@ const NodeConfigModal: React.FC<NodeConfigModalProps> = ({
};
const handleReset = () => {
if (nodeDefinition) {
const defaultConfig = {
nodeName: nodeDefinition.nodeName,
nodeCode: nodeDefinition.nodeCode,
description: nodeDefinition.description
configForm.reset();
inputForm.reset();
outputForm.reset();
message.info('已重置为初始值');
};
setFormData({
configs: defaultConfig,
inputMapping: {},
outputMapping: {},
});
// 将JSON Schema转换为Formily Schema扩展配置
const convertToFormilySchema = (jsonSchema: ISchema): ISchema => {
const schema: ISchema = {
type: 'object',
properties: {}
};
if (!jsonSchema.properties || typeof jsonSchema.properties !== 'object') return schema;
Object.entries(jsonSchema.properties as Record<string, any>).forEach(([key, prop]: [string, any]) => {
const field: any = {
title: prop.title || key,
description: prop.description,
'x-decorator': 'FormItem',
'x-decorator-props': {
tooltip: prop.description,
labelCol: 6, // 标签占6列
wrapperCol: 18, // 内容占18列剩余空间
},
};
// 根据类型设置组件
switch (prop.type) {
case 'string':
if (prop.enum) {
field['x-component'] = 'Select';
field['x-component-props'] = {
style: { width: '100%' }, // 统一宽度
options: prop.enum.map((v: any, i: number) => ({
label: prop.enumNames?.[i] || v,
value: v
}))
};
} else if (prop.format === 'password') {
field['x-component'] = 'Input';
field['x-component-props'] = {
type: 'password',
style: { width: '100%' } // 统一宽度
};
} else {
field['x-component'] = 'Input';
field['x-component-props'] = {
style: { width: '100%' } // 统一宽度
};
}
break;
case 'number':
case 'integer':
field['x-component'] = 'NumberPicker';
field['x-component-props'] = {
min: prop.minimum,
max: prop.maximum,
style: { width: '100%' } // 统一宽度
};
const handleConfigChange = (values: Record<string, any>) => {
setFormData(prev => ({
...prev,
configs: values
}));
break;
case 'boolean':
field['x-component'] = 'Switch';
break;
case 'object':
field['x-component'] = 'Input.TextArea';
field['x-component-props'] = {
rows: 4,
style: { width: '100%' }, // 统一宽度
placeholder: '请输入JSON格式例如{"key": "value"}'
};
const handleInputMappingChange = (values: Record<string, any>) => {
setFormData(prev => ({
...prev,
inputMapping: values
}));
// ✅ 关键修复将object类型的default值转换为JSON字符串
if (prop.default !== undefined && typeof prop.default === 'object') {
field.default = JSON.stringify(prop.default, null, 2);
} else {
field.default = prop.default;
}
// Formily会自动处理object的序列化
field['x-validator'] = (value: any) => {
if (!value) return true;
if (typeof value === 'string') {
try {
JSON.parse(value);
return true;
} catch {
return '请输入有效的JSON格式';
}
}
return true;
};
break;
default:
field['x-component'] = 'Input';
field['x-component-props'] = {
style: { width: '100%' } // 统一宽度
};
}
const handleOutputMappingChange = (values: Record<string, any>) => {
setFormData(prev => ({
...prev,
outputMapping: values
}));
// 设置默认值非object类型
if (prop.type !== 'object' && prop.default !== undefined) {
field.default = prop.default;
}
// 设置必填
if (Array.isArray(jsonSchema.required) && jsonSchema.required.includes(key)) {
field.required = true;
}
(schema.properties as Record<string, any>)[key] = field;
});
return schema;
};
if (!nodeDefinition) {
@ -128,18 +253,15 @@ const NodeConfigModal: React.FC<NodeConfigModalProps> = ({
{
key: 'config',
label: '基本配置',
children: (
children: activeTab === 'config' ? (
<div style={{ padding: '16px 0' }}>
<BetaSchemaForm
key={`configs-${node?.id}-${JSON.stringify(formData.configs)}`}
layoutType="Form"
columns={convertJsonSchemaToColumns(nodeDefinition.configSchema as any)}
initialValues={formData.configs}
onValuesChange={(_, allValues) => handleConfigChange(allValues)}
submitter={false}
/>
<FormProvider form={configForm}>
<FormLayout labelCol={6} wrapperCol={18} labelAlign="right" colon={false}>
<SchemaField schema={convertToFormilySchema(nodeDefinition.configSchema)} />
</FormLayout>
</FormProvider>
</div>
),
) : null,
},
];
@ -149,18 +271,15 @@ const NodeConfigModal: React.FC<NodeConfigModalProps> = ({
tabItems.push({
key: 'input',
label: '输入映射',
children: (
children: activeTab === 'input' ? (
<div style={{ padding: '16px 0' }}>
<BetaSchemaForm
key={`input-${node?.id}-${JSON.stringify(formData.inputMapping)}`}
layoutType="Form"
columns={convertJsonSchemaToColumns(nodeDefinition.inputMappingSchema as any)}
initialValues={formData.inputMapping}
onValuesChange={(_, allValues) => handleInputMappingChange(allValues)}
submitter={false}
/>
<FormProvider form={inputForm}>
<FormLayout labelCol={6} wrapperCol={18} labelAlign="right" colon={false}>
<SchemaField schema={convertToFormilySchema(nodeDefinition.inputMappingSchema)} />
</FormLayout>
</FormProvider>
</div>
),
) : null,
});
}
@ -168,18 +287,15 @@ const NodeConfigModal: React.FC<NodeConfigModalProps> = ({
tabItems.push({
key: 'output',
label: '输出映射',
children: (
children: activeTab === 'output' ? (
<div style={{ padding: '16px 0' }}>
<BetaSchemaForm
key={`output-${node?.id}-${JSON.stringify(formData.outputMapping)}`}
layoutType="Form"
columns={convertJsonSchemaToColumns(nodeDefinition.outputMappingSchema as any)}
initialValues={formData.outputMapping}
onValuesChange={(_, allValues) => handleOutputMappingChange(allValues)}
submitter={false}
/>
<FormProvider form={outputForm}>
<FormLayout labelCol={6} wrapperCol={18} labelAlign="right" colon={false}>
<SchemaField schema={convertToFormilySchema(nodeDefinition.outputMappingSchema)} />
</FormLayout>
</FormProvider>
</div>
),
) : null,
});
}
}

View File

@ -3,7 +3,7 @@ import { message } from 'antd';
import * as definitionService from '../../Definition/service';
import type { FlowNode, FlowEdge } from '../types';
import type { WorkflowDefinition } from '../../Definition/types';
import { NODE_DEFINITIONS } from '../nodes';
import { NODE_DEFINITIONS, NodeType, getNodeCategory } from '../nodes';
interface LoadedWorkflowData {
nodes: FlowNode[];
@ -15,9 +15,25 @@ export const useWorkflowLoad = () => {
const [loading, setLoading] = useState(false);
const [workflowDefinition, setWorkflowDefinition] = useState<WorkflowDefinition | null>(null);
// 获取已注册的节点类型
const getRegisteredNodeTypes = useCallback((): NodeType[] => {
return NODE_DEFINITIONS.map(def => def.nodeType);
}, []);
// 从后端数据转换为React Flow格式
const convertToFlowFormat = useCallback((definition: WorkflowDefinition): LoadedWorkflowData => {
const nodes: FlowNode[] = (definition.graph?.nodes || []).map(node => {
const registeredTypes = getRegisteredNodeTypes();
const nodes: FlowNode[] = (definition.graph?.nodes || [])
.filter(node => {
// 过滤掉不支持的节点类型
const isSupported = registeredTypes.includes(node.nodeType as any);
if (!isSupported) {
console.warn(`节点类型 "${node.nodeType}" 不支持,已跳过`, node);
}
return isSupported;
})
.map(node => {
// 根据nodeType查找对应的节点定义
const nodeDefinition = NODE_DEFINITIONS.find(def => def.nodeType === node.nodeType);
@ -42,20 +58,45 @@ export const useWorkflowLoad = () => {
};
});
const edges: FlowEdge[] = (definition.graph?.edges || []).map(edge => ({
// 创建节点ID集合用于过滤边
const nodeIds = new Set(nodes.map(n => n.id));
const edges: FlowEdge[] = (definition.graph?.edges || [])
.filter(edge => {
// 只保留两端节点都存在的边
const sourceExists = nodeIds.has(edge.from);
const targetExists = nodeIds.has(edge.to);
if (!sourceExists || !targetExists) {
console.warn(`边的节点不存在,已跳过:`, edge);
}
return sourceExists && targetExists;
})
.map(edge => {
const edgeConfig: any = edge.config || {};
const condition = edgeConfig.condition || { type: 'DEFAULT', priority: 10 };
const label = condition.expression || edge.name || '';
return {
id: edge.id,
source: edge.from, // 后端使用from字段作为source
target: edge.to, // 后端使用to字段作为target
type: 'default',
type: 'smoothstep',
animated: true,
style: {
stroke: '#94a3b8',
strokeWidth: 2,
},
markerEnd: {
type: 'arrowclosed' as const,
color: '#94a3b8',
},
label,
data: {
label: edge.name || '连接',
condition: {
type: 'DEFAULT',
priority: 0
label,
condition
}
}
}));
};
});
return {
nodes,
@ -64,36 +105,19 @@ export const useWorkflowLoad = () => {
};
}, []);
// 根据节点类型获取分类
const getNodeCategory = (nodeType: string): string => {
const categoryMap: Record<string, string> = {
'START_EVENT': 'EVENT',
'END_EVENT': 'EVENT',
'USER_TASK': 'TASK',
'SERVICE_TASK': 'TASK',
'SCRIPT_TASK': 'TASK',
'DEPLOY_NODE': 'TASK',
'JENKINS_BUILD': 'TASK',
'GATEWAY_NODE': 'GATEWAY',
'SUB_PROCESS': 'CONTAINER',
'CALL_ACTIVITY': 'CONTAINER'
};
return categoryMap[nodeType] || 'TASK';
};
// 根据节点类型获取图标(图标名称格式)
const getNodeIcon = (nodeType: string): string => {
const iconMap: Record<string, string> = {
'START_EVENT': 'play-circle',
'END_EVENT': 'stop-circle',
'USER_TASK': 'user',
'SERVICE_TASK': 'api',
'SCRIPT_TASK': 'code',
'DEPLOY_NODE': 'build',
'JENKINS_BUILD': 'jenkins',
'GATEWAY_NODE': 'gateway',
'SUB_PROCESS': 'container',
'CALL_ACTIVITY': 'phone'
[NodeType.START_EVENT]: 'play-circle',
[NodeType.END_EVENT]: 'stop-circle',
[NodeType.USER_TASK]: 'user',
[NodeType.SERVICE_TASK]: 'api',
[NodeType.SCRIPT_TASK]: 'code',
[NodeType.DEPLOY_NODE]: 'build',
[NodeType.JENKINS_BUILD]: 'jenkins',
[NodeType.GATEWAY_NODE]: 'gateway',
[NodeType.SUB_PROCESS]: 'container',
[NodeType.CALL_ACTIVITY]: 'phone'
};
return iconMap[nodeType] || 'api';
};
@ -101,16 +125,16 @@ export const useWorkflowLoad = () => {
// 根据节点类型获取颜色
const getNodeColor = (nodeType: string): string => {
const colorMap: Record<string, string> = {
'START_EVENT': '#52c41a',
'END_EVENT': '#ff4d4f',
'USER_TASK': '#722ed1',
'SERVICE_TASK': '#fa8c16',
'SCRIPT_TASK': '#8b5cf6',
'DEPLOY_NODE': '#1890ff',
'JENKINS_BUILD': '#f97316',
'GATEWAY_NODE': '#84cc16',
'SUB_PROCESS': '#ec4899',
'CALL_ACTIVITY': '#14b8a6'
[NodeType.START_EVENT]: '#52c41a',
[NodeType.END_EVENT]: '#ff4d4f',
[NodeType.USER_TASK]: '#722ed1',
[NodeType.SERVICE_TASK]: '#fa8c16',
[NodeType.SCRIPT_TASK]: '#8b5cf6',
[NodeType.DEPLOY_NODE]: '#1890ff',
[NodeType.JENKINS_BUILD]: '#52c41a',
[NodeType.GATEWAY_NODE]: '#84cc16',
[NodeType.SUB_PROCESS]: '#ec4899',
[NodeType.CALL_ACTIVITY]: '#14b8a6'
};
return colorMap[nodeType] || '#6b7280';
};
@ -125,7 +149,15 @@ export const useWorkflowLoad = () => {
const flowData = convertToFlowFormat(definition);
// 提示不支持的节点
const totalNodes = definition.graph?.nodes?.length || 0;
const loadedNodes = flowData.nodes.length;
if (totalNodes > loadedNodes) {
message.warning(`工作流加载成功,但有 ${totalNodes - loadedNodes} 个节点类型不支持,已跳过`);
} else {
message.success(`工作流 "${definition.name}" 加载成功`);
}
return flowData;
} catch (error) {
console.error('加载工作流失败:', error);

View File

@ -478,39 +478,64 @@ const WorkflowDesignInner: React.FC = () => {
// 键盘快捷键支持
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
// 检查焦点是否在输入框、文本域或可编辑元素内
const target = e.target as HTMLElement;
const tagName = target.tagName?.toUpperCase();
const isInputElement = tagName === 'INPUT' ||
tagName === 'TEXTAREA' ||
target.isContentEditable ||
target.getAttribute('contenteditable') === 'true';
const isInDrawer = target.closest('.ant-drawer-body') !== null;
const isInModal = target.closest('.ant-modal') !== null;
// 在抽屉或模态框内,且在输入元素中时,允许原生行为
const shouldSkipShortcut = isInputElement || isInDrawer || isInModal;
const isMac = navigator.platform.toUpperCase().indexOf('MAC') >= 0;
const ctrlKey = isMac ? e.metaKey : e.ctrlKey;
// Ctrl+Z / Cmd+Z - 撤销
// Ctrl+Z / Cmd+Z - 撤销(仅在画布区域)
if (ctrlKey && e.key === 'z' && !e.shiftKey) {
if (!shouldSkipShortcut) {
e.preventDefault();
handleUndo();
}
// Ctrl+Shift+Z / Cmd+Shift+Z - 重做
}
// Ctrl+Shift+Z / Cmd+Shift+Z - 重做(仅在画布区域)
else if (ctrlKey && e.key === 'z' && e.shiftKey) {
if (!shouldSkipShortcut) {
e.preventDefault();
handleRedo();
}
// Ctrl+C / Cmd+C - 复制
}
// Ctrl+C / Cmd+C - 复制节点(仅在画布区域)
else if (ctrlKey && e.key === 'c') {
if (!shouldSkipShortcut) {
e.preventDefault();
handleCopy();
}
// Ctrl+V / Cmd+V - 粘贴
}
// Ctrl+V / Cmd+V - 粘贴节点(仅在画布区域)
else if (ctrlKey && e.key === 'v') {
if (!shouldSkipShortcut) {
e.preventDefault();
handlePaste();
}
// Ctrl+A / Cmd+A - 全选
}
// Ctrl+A / Cmd+A - 全选节点(仅在画布区域)
else if (ctrlKey && e.key === 'a') {
if (!shouldSkipShortcut) {
e.preventDefault();
handleSelectAll();
}
// Delete / Backspace - 删除
}
// Delete / Backspace - 删除节点(仅在画布区域)
else if (e.key === 'Delete' || e.key === 'Backspace') {
if (!shouldSkipShortcut) {
e.preventDefault();
handleDelete();
}
}
};
window.addEventListener('keydown', handleKeyDown);

View File

@ -6,23 +6,46 @@ export enum NodeCategory {
CONTAINER = 'CONTAINER'
}
// 节点类型
// 节点类型(完整定义,包含所有可能的类型)
export enum NodeType {
START_EVENT = 'START_EVENT',
END_EVENT = 'END_EVENT',
USER_TASK = 'USER_TASK',
SERVICE_TASK = 'SERVICE_TASK',
SCRIPT_TASK = 'SCRIPT_TASK',
DEPLOY_NODE = 'DEPLOY_NODE',
JENKINS_BUILD = 'JENKINS_BUILD',
GATEWAY_NODE = 'GATEWAY_NODE',
SUB_PROCESS = 'SUB_PROCESS',
CALL_ACTIVITY = 'CALL_ACTIVITY'
}
// 节点类型到分类的映射
export const NODE_CATEGORY_MAP: Record<NodeType, NodeCategory> = {
[NodeType.START_EVENT]: NodeCategory.EVENT,
[NodeType.END_EVENT]: NodeCategory.EVENT,
[NodeType.USER_TASK]: NodeCategory.TASK,
[NodeType.SERVICE_TASK]: NodeCategory.TASK,
[NodeType.SCRIPT_TASK]: NodeCategory.TASK,
[NodeType.DEPLOY_NODE]: NodeCategory.TASK,
[NodeType.JENKINS_BUILD]: NodeCategory.TASK,
[NodeType.GATEWAY_NODE]: NodeCategory.GATEWAY,
[NodeType.SUB_PROCESS]: NodeCategory.CONTAINER,
[NodeType.CALL_ACTIVITY]: NodeCategory.CONTAINER,
};
// 获取节点分类的工具函数
export const getNodeCategory = (nodeType: NodeType | string): NodeCategory => {
return NODE_CATEGORY_MAP[nodeType as NodeType] || NodeCategory.TASK;
};
// JSON Schema 定义
export interface JSONSchema {
type: string;
properties?: Record<string, any>;
required?: string[];
title?: string;
description?: string;
}
/**
* JSON Schema - 使 Formily ISchema
* @formily/react @formily/json-schema
*/
import type { ISchema } from '@formily/react';
export type JSONSchema = ISchema;
// UI 配置
export interface NodeSize {

View File

@ -117,7 +117,7 @@ export const convertJsonSchemaToColumns = (schema: JsonSchema): ProFormColumnsTy
formItemProps: {
required: schema.required?.includes(key),
},
initialValue: value.default,
// 不在这里设置initialValue在表单层面设置
readonly: value.readOnly,
};
@ -289,10 +289,30 @@ export const convertJsonSchemaToColumns = (schema: JsonSchema): ProFormColumnsTy
case 'object':
return {
...baseConfig,
valueType: 'jsonCode',
valueType: 'textarea',
fieldProps: {
placeholder: `请输入${value.title || key}`,
placeholder: `请输入JSON格式数据例如{"key": "value"}`,
disabled: value.readOnly,
rows: 4,
autoSize: { minRows: 4, maxRows: 8 },
},
// 转换值object → JSON string显示
transform: (value: any) => {
if (typeof value === 'object' && value !== null) {
return JSON.stringify(value, null, 2);
}
return value;
},
// 转换值JSON string → object保存
convertValue: (val: any) => {
if (typeof val === 'string') {
try {
return JSON.parse(val);
} catch {
return value.default || {};
}
}
return val;
},
};