This commit is contained in:
dengqichen 2025-10-20 21:58:31 +08:00
parent 20c4184d45
commit 3315114522
12 changed files with 968 additions and 562 deletions

View File

@ -1,7 +1,11 @@
import React, { useState, useEffect } from 'react';
import { Modal, Form, Input, Tabs, Card, Button, Space, message } from 'antd';
import { Modal, Tabs, Button, Space, message } from 'antd';
import { SaveOutlined, ReloadOutlined } from '@ant-design/icons';
import { BetaSchemaForm } from '@ant-design/pro-components';
import { convertJsonSchemaToColumns } from '@/utils/jsonSchemaUtils';
import type { FlowNode, FlowNodeData } from '../types';
import type { WorkflowNodeDefinition } from '../nodes/types';
import { isConfigurableNode } from '../nodes/types';
interface NodeConfigModalProps {
visible: boolean;
@ -10,7 +14,11 @@ interface NodeConfigModalProps {
onOk: (nodeId: string, updatedData: Partial<FlowNodeData>) => void;
}
const { TextArea } = Input;
interface FormData {
configs?: Record<string, any>;
inputMapping?: Record<string, any>;
outputMapping?: Record<string, any>;
}
const NodeConfigModal: React.FC<NodeConfigModalProps> = ({
visible,
@ -18,187 +26,176 @@ const NodeConfigModal: React.FC<NodeConfigModalProps> = ({
onCancel,
onOk
}) => {
const [form] = Form.useForm();
const [loading, setLoading] = useState(false);
const [activeTab, setActiveTab] = useState('basic');
const [activeTab, setActiveTab] = useState('config');
const [formData, setFormData] = useState<FormData>({});
// 获取节点定义
const nodeDefinition: WorkflowNodeDefinition | null = node?.data?.nodeDefinition || null;
// 重置表单数据
useEffect(() => {
if (visible && node) {
form.setFieldsValue({
label: node.data.label,
description: node.data.nodeDefinition?.description || '',
// 基础配置
timeout: node.data.configs?.timeout || 3600,
retryCount: node.data.configs?.retryCount || 0,
priority: node.data.configs?.priority || 'normal',
// 输入映射
inputMapping: JSON.stringify(node.data.inputMapping || {}, null, 2),
// 输出映射
outputMapping: JSON.stringify(node.data.outputMapping || {}, null, 2),
if (visible && node && nodeDefinition) {
// 从节点数据中获取现有配置
const nodeData = node.data || {};
// 准备默认的基本信息配置
const defaultConfig = {
nodeName: nodeDefinition.nodeName, // 默认节点名称
nodeCode: nodeDefinition.nodeCode, // 默认节点编码
description: nodeDefinition.description // 默认节点描述
};
// 合并默认值和已保存的配置
setFormData({
configs: { ...defaultConfig, ...(nodeData.configs || {}) },
inputMapping: nodeData.inputMapping || {},
outputMapping: nodeData.outputMapping || {},
});
} else {
form.resetFields();
setFormData({});
}
}, [visible, node, form]);
}, [visible, node, nodeDefinition]);
const handleSubmit = async () => {
const handleSubmit = () => {
if (!node) return;
try {
const values = await form.validateFields();
setLoading(true);
// 解析JSON字符串
let inputMapping = {};
let outputMapping = {};
try {
inputMapping = values.inputMapping ? JSON.parse(values.inputMapping) : {};
} catch (error) {
message.error('输入映射JSON格式错误');
return;
}
try {
outputMapping = values.outputMapping ? JSON.parse(values.outputMapping) : {};
} catch (error) {
message.error('输出映射JSON格式错误');
return;
}
const updatedData: Partial<FlowNodeData> = {
label: values.label,
configs: {
...node.data.configs,
timeout: values.timeout,
retryCount: values.retryCount,
priority: values.priority,
},
inputMapping,
outputMapping,
// 更新节点标签为配置中的节点名称
label: formData.configs?.nodeName || node.data.label,
configs: formData.configs,
inputMapping: formData.inputMapping,
outputMapping: formData.outputMapping,
};
onOk(node.id, updatedData);
message.success('节点配置保存成功');
onCancel();
} catch (error) {
console.error('保存节点配置失败:', error);
if (error instanceof Error) {
message.error(error.message);
}
} finally {
setLoading(false);
}
};
const handleReset = () => {
form.resetFields();
if (node) {
form.setFieldsValue({
label: node.data.label,
description: node.data.nodeDefinition?.description || '',
timeout: 3600,
retryCount: 0,
priority: 'normal',
inputMapping: '{}',
outputMapping: '{}',
if (nodeDefinition) {
const defaultConfig = {
nodeName: nodeDefinition.nodeName,
nodeCode: nodeDefinition.nodeCode,
description: nodeDefinition.description
};
setFormData({
configs: defaultConfig,
inputMapping: {},
outputMapping: {},
});
}
};
const renderBasicConfig = () => (
<Card size="small" title="基本信息">
<Form.Item
label="节点名称"
name="label"
rules={[{ required: true, message: '请输入节点名称' }]}
>
<Input placeholder="请输入节点名称" />
</Form.Item>
const handleConfigChange = (values: Record<string, any>) => {
setFormData(prev => ({
...prev,
configs: values
}));
};
<Form.Item label="节点描述" name="description">
<TextArea
placeholder="请输入节点描述"
rows={3}
maxLength={500}
showCount
/>
</Form.Item>
const handleInputMappingChange = (values: Record<string, any>) => {
setFormData(prev => ({
...prev,
inputMapping: values
}));
};
<Form.Item
label="超时时间(秒)"
name="timeout"
rules={[{ required: true, message: '请输入超时时间' }]}
>
<Input type="number" min={1} placeholder="3600" />
</Form.Item>
const handleOutputMappingChange = (values: Record<string, any>) => {
setFormData(prev => ({
...prev,
outputMapping: values
}));
};
<Form.Item
label="重试次数"
name="retryCount"
>
<Input type="number" min={0} max={10} placeholder="0" />
</Form.Item>
if (!nodeDefinition) {
return null;
}
<Form.Item
label="优先级"
name="priority"
>
<Input placeholder="normal" />
</Form.Item>
</Card>
);
const renderInputMapping = () => (
<Card size="small" title="输入参数映射">
<Form.Item
label="输入映射配置"
name="inputMapping"
extra="请输入有效的JSON格式用于配置节点的输入参数映射"
>
<TextArea
placeholder={`示例:
// 构建Tabs配置
const tabItems = [
{
"param1": "\${workflow.variable1}",
"param2": "固定值",
"param3": "\${previous.output.result}"
}`}
rows={12}
style={{ fontFamily: 'Monaco, Consolas, monospace' }}
key: 'config',
label: '基本配置',
children: (
<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}
/>
</Form.Item>
</Card>
);
</div>
),
},
];
const renderOutputMapping = () => (
<Card size="small" title="输出参数映射">
<Form.Item
label="输出映射配置"
name="outputMapping"
extra="请输入有效的JSON格式用于配置节点的输出参数映射"
>
<TextArea
placeholder={`示例:
{
"result": "\${task.output.result}",
"status": "\${task.status}",
"message": "\${task.output.message}"
}`}
rows={12}
style={{ fontFamily: 'Monaco, Consolas, monospace' }}
// 如果是可配置节点添加输入和输出映射TAB
if (isConfigurableNode(nodeDefinition)) {
if (nodeDefinition.inputMappingSchema) {
tabItems.push({
key: 'input',
label: '输入映射',
children: (
<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}
/>
</Form.Item>
</Card>
);
</div>
),
});
}
if (nodeDefinition.outputMappingSchema) {
tabItems.push({
key: 'output',
label: '输出映射',
children: (
<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}
/>
</div>
),
});
}
}
return (
<Modal
title={`配置节点: ${node?.data?.label || '未知节点'}`}
title={`编辑节点 - ${nodeDefinition.nodeName}`}
open={visible}
onCancel={onCancel}
width={800}
style={{ top: 20 }}
footer={
<Space>
<Button onClick={onCancel}></Button>
<Button onClick={onCancel}>
</Button>
<Button
icon={<ReloadOutlined />}
onClick={handleReset}
@ -216,39 +213,13 @@ const NodeConfigModal: React.FC<NodeConfigModalProps> = ({
</Space>
}
>
<Form
form={form}
layout="vertical"
initialValues={{
timeout: 3600,
retryCount: 0,
priority: 'normal',
inputMapping: '{}',
outputMapping: '{}'
}}
>
<div style={{ maxHeight: '70vh', overflow: 'auto' }}>
<Tabs
activeKey={activeTab}
onChange={setActiveTab}
items={[
{
key: 'basic',
label: '基本配置',
children: renderBasicConfig()
},
{
key: 'input',
label: '输入映射',
children: renderInputMapping()
},
{
key: 'output',
label: '输出映射',
children: renderOutputMapping()
}
]}
items={tabItems}
/>
</Form>
</div>
</Modal>
);
};

View File

@ -1,82 +1,22 @@
import React from 'react';
import { Card, Divider } from 'antd';
import { NodeType, NodeCategory, type NodeDefinitionData } from '../types';
import { NodeCategory } from '../types';
import { NODE_DEFINITIONS } from '../nodes/definitions';
import type { WorkflowNodeDefinition } from '../nodes/types';
// 节点定义数据
const nodeDefinitions: NodeDefinitionData[] = [
{
nodeCode: 'start_event',
nodeName: '开始事件',
nodeType: NodeType.START_EVENT,
category: NodeCategory.EVENT,
description: '工作流开始节点',
icon: '▶️',
color: '#10b981'
},
{
nodeCode: 'end_event',
nodeName: '结束事件',
nodeType: NodeType.END_EVENT,
category: NodeCategory.EVENT,
description: '工作流结束节点',
icon: '⏹️',
color: '#ef4444'
},
{
nodeCode: 'user_task',
nodeName: '用户任务',
nodeType: NodeType.USER_TASK,
category: NodeCategory.TASK,
description: '需要用户手动处理的任务',
icon: '👤',
color: '#6366f1'
},
{
nodeCode: 'service_task',
nodeName: '服务任务',
nodeType: NodeType.SERVICE_TASK,
category: NodeCategory.TASK,
description: '自动执行的服务任务',
icon: '⚙️',
color: '#f59e0b'
},
{
nodeCode: 'script_task',
nodeName: '脚本任务',
nodeType: NodeType.SCRIPT_TASK,
category: NodeCategory.TASK,
description: '执行脚本代码的任务',
icon: '📜',
color: '#8b5cf6'
},
{
nodeCode: 'deploy_node',
nodeName: '部署节点',
nodeType: NodeType.DEPLOY_NODE,
category: NodeCategory.TASK,
description: '应用部署任务',
icon: '🚀',
color: '#06b6d4'
},
{
nodeCode: 'jenkins_build',
nodeName: 'Jenkins构建',
nodeType: NodeType.JENKINS_BUILD,
category: NodeCategory.TASK,
description: 'Jenkins构建任务',
icon: '🔨',
color: '#dc2626'
},
{
nodeCode: 'gateway_node',
nodeName: '网关节点',
nodeType: NodeType.GATEWAY_NODE,
category: NodeCategory.GATEWAY,
description: '条件分支网关',
icon: '💎',
color: '#7c3aed'
}
];
// 图标映射函数
const getNodeIcon = (iconName: string): string => {
const iconMap: Record<string, string> = {
'play-circle': '▶️',
'stop-circle': '⏹️',
'user': '👤',
'api': '⚙️',
'code': '📜',
'build': '🚀',
'jenkins': '🔨',
'gateway': '💎'
};
return iconMap[iconName] || '📋';
};
interface NodePanelProps {
className?: string;
@ -84,16 +24,16 @@ interface NodePanelProps {
const NodePanel: React.FC<NodePanelProps> = ({ className = '' }) => {
// 按分类分组节点
const nodesByCategory = nodeDefinitions.reduce((acc, node) => {
const nodesByCategory = NODE_DEFINITIONS.reduce((acc, node) => {
if (!acc[node.category]) {
acc[node.category] = [];
}
acc[node.category].push(node);
return acc;
}, {} as Record<NodeCategory, NodeDefinitionData[]>);
}, {} as Record<NodeCategory, WorkflowNodeDefinition[]>);
// 拖拽开始处理
const handleDragStart = (event: React.DragEvent, nodeDefinition: NodeDefinitionData) => {
const handleDragStart = (event: React.DragEvent, nodeDefinition: WorkflowNodeDefinition) => {
event.dataTransfer.setData('application/reactflow', JSON.stringify({
nodeType: nodeDefinition.nodeType,
nodeDefinition
@ -102,11 +42,10 @@ const NodePanel: React.FC<NodePanelProps> = ({ className = '' }) => {
};
// 渲染节点项
const renderNodeItem = (nodeDefinition: NodeDefinitionData) => (
const renderNodeItem = (nodeDefinition: WorkflowNodeDefinition) => (
<div
key={nodeDefinition.nodeCode}
draggable
onDragStart={(e) => handleDragStart(e, nodeDefinition)}
style={{
display: 'flex',
alignItems: 'center',
@ -121,7 +60,7 @@ const NodePanel: React.FC<NodePanelProps> = ({ className = '' }) => {
}}
onMouseEnter={(e) => {
e.currentTarget.style.background = '#f9fafb';
e.currentTarget.style.borderColor = nodeDefinition.color;
e.currentTarget.style.borderColor = nodeDefinition.uiConfig.style.fill;
e.currentTarget.style.transform = 'translateX(2px)';
}}
onMouseLeave={(e) => {
@ -140,9 +79,10 @@ const NodePanel: React.FC<NodePanelProps> = ({ className = '' }) => {
<div style={{
fontSize: '16px',
width: '20px',
textAlign: 'center'
textAlign: 'center',
color: nodeDefinition.uiConfig.style.fill
}}>
{nodeDefinition.icon}
{getNodeIcon(nodeDefinition.uiConfig.style.icon)}
</div>
<div>
<div style={{

View File

@ -1,238 +0,0 @@
import React, { useState, useEffect } from 'react';
import { Card, Form, Input, Select, Button, Space, message, Divider } from 'antd';
import { SaveOutlined, ClearOutlined } from '@ant-design/icons';
import type { FlowNode, FlowEdge } from '../types';
interface PropertyPanelProps {
selectedNode?: FlowNode | null;
selectedEdge?: FlowEdge | null;
onNodeUpdate?: (nodeId: string, updates: any) => void;
onEdgeUpdate?: (edgeId: string, updates: any) => void;
className?: string;
}
const PropertyPanel: React.FC<PropertyPanelProps> = ({
selectedNode,
selectedEdge,
onNodeUpdate,
onEdgeUpdate,
className = ''
}) => {
const [form] = Form.useForm();
const [hasChanges, setHasChanges] = useState(false);
// 重置表单
useEffect(() => {
if (selectedNode) {
form.setFieldsValue({
label: selectedNode.data.label,
timeout: selectedNode.data.configs?.timeout || 3600,
retryCount: selectedNode.data.configs?.retryCount || 0,
priority: selectedNode.data.configs?.priority || 'normal',
});
} else if (selectedEdge) {
form.setFieldsValue({
label: selectedEdge.data?.label || '',
conditionType: selectedEdge.data?.condition?.type || 'DEFAULT',
expression: selectedEdge.data?.condition?.expression || '',
});
} else {
form.resetFields();
}
setHasChanges(false);
}, [selectedNode, selectedEdge, form]);
// 监听表单变化
const handleFormChange = () => {
setHasChanges(true);
};
// 保存配置
const handleSave = async () => {
try {
const values = await form.validateFields();
if (selectedNode && onNodeUpdate) {
const updates = {
label: values.label,
configs: {
...selectedNode.data.configs,
timeout: values.timeout,
retryCount: values.retryCount,
priority: values.priority,
},
};
onNodeUpdate(selectedNode.id, updates);
message.success('节点配置已保存');
} else if (selectedEdge && onEdgeUpdate) {
const updates = {
label: values.label,
condition: {
type: values.conditionType,
expression: values.expression,
priority: 0,
},
};
onEdgeUpdate(selectedEdge.id, updates);
message.success('连接配置已保存');
}
setHasChanges(false);
} catch (error) {
message.error('保存失败');
}
};
// 清空选择
const handleClear = () => {
form.resetFields();
setHasChanges(false);
};
// 渲染节点属性
const renderNodeProperties = () => {
if (!selectedNode) return null;
return (
<Card size="small" title={`节点属性: ${selectedNode.data.nodeType}`}>
<Form.Item label="节点名称" name="label" rules={[{ required: true }]}>
<Input placeholder="请输入节点名称" />
</Form.Item>
<Form.Item label="超时时间(秒)" name="timeout">
<Input type="number" min={1} placeholder="3600" />
</Form.Item>
<Form.Item label="重试次数" name="retryCount">
<Input type="number" min={0} max={10} placeholder="0" />
</Form.Item>
<Form.Item label="优先级" name="priority">
<Select placeholder="选择优先级">
<Select.Option value="low"></Select.Option>
<Select.Option value="normal"></Select.Option>
<Select.Option value="high"></Select.Option>
<Select.Option value="urgent"></Select.Option>
</Select>
</Form.Item>
</Card>
);
};
// 渲染边属性
const renderEdgeProperties = () => {
if (!selectedEdge) return null;
return (
<Card size="small" title="连接属性">
<Form.Item label="连接名称" name="label">
<Input placeholder="请输入连接名称" />
</Form.Item>
<Form.Item label="条件类型" name="conditionType">
<Select placeholder="选择条件类型">
<Select.Option value="DEFAULT"></Select.Option>
<Select.Option value="EXPRESSION"></Select.Option>
<Select.Option value="SCRIPT"></Select.Option>
</Select>
</Form.Item>
<Form.Item label="条件表达式" name="expression">
<Input.TextArea
placeholder="请输入条件表达式"
rows={4}
/>
</Form.Item>
</Card>
);
};
return (
<div className={`property-panel ${className}`} style={{
width: '300px',
height: '100%',
background: '#f8fafc',
borderLeft: '1px solid #e5e7eb',
display: 'flex',
flexDirection: 'column',
overflow: 'hidden'
}}>
<div style={{
padding: '16px',
background: '#ffffff',
borderBottom: '1px solid #e5e7eb',
flexShrink: 0
}}>
<h3 style={{
margin: 0,
fontSize: '14px',
fontWeight: '600',
color: '#374151'
}}>
</h3>
<p style={{
margin: '4px 0 0 0',
fontSize: '12px',
color: '#6b7280'
}}>
{selectedNode ? '节点配置' : selectedEdge ? '连接配置' : '请选择节点或连接'}
</p>
</div>
<div style={{ flex: 1, overflow: 'auto', padding: '16px' }}>
{!selectedNode && !selectedEdge && (
<div style={{
textAlign: 'center',
color: '#9ca3af',
fontSize: '14px',
marginTop: '40px'
}}>
<div style={{ fontSize: '48px', marginBottom: '16px' }}>📋</div>
<p></p>
<p style={{ fontSize: '12px' }}></p>
</div>
)}
<Form
form={form}
layout="vertical"
onValuesChange={handleFormChange}
>
{renderNodeProperties()}
{renderEdgeProperties()}
</Form>
</div>
{(selectedNode || selectedEdge) && (
<div style={{
padding: '16px',
background: '#ffffff',
borderTop: '1px solid #e5e7eb',
flexShrink: 0
}}>
<Space style={{ width: '100%', justifyContent: 'space-between' }}>
<Button
icon={<ClearOutlined />}
onClick={handleClear}
size="small"
>
</Button>
<Button
type="primary"
icon={<SaveOutlined />}
onClick={handleSave}
disabled={!hasChanges}
size="small"
>
</Button>
</Space>
</div>
)}
</div>
);
};
export default PropertyPanel;

View File

@ -7,8 +7,8 @@ import WorkflowToolbar from './components/WorkflowToolbar';
import NodePanel from './components/NodePanel';
import FlowCanvas from './components/FlowCanvas';
import NodeConfigModal from './components/NodeConfigModal';
import PropertyPanel from './components/PropertyPanel';
import type { FlowNode, FlowEdge, DragNodeData, FlowNodeData } from './types';
import type { FlowNode, FlowEdge, FlowNodeData } from './types';
import type { WorkflowNodeDefinition } from './nodes/types';
import { NodeType } from './types';
import { useWorkflowSave } from './hooks/useWorkflowSave';
import { useWorkflowLoad } from './hooks/useWorkflowLoad';
@ -40,12 +40,11 @@ const WorkflowDesignInner: React.FC = () => {
// 节点配置模态框状态
const [configModalVisible, setConfigModalVisible] = useState(false);
const [selectedNode, setSelectedNode] = useState<FlowNode | null>(null);
const [selectedEdge, setSelectedEdge] = useState<FlowEdge | null>(null);
const [configNode, setConfigNode] = useState<FlowNode | null>(null);
// 保存和加载hooks
const { saving, hasUnsavedChanges, saveWorkflow, markUnsaved } = useWorkflowSave();
const { loading: loadingWorkflow, workflowDefinition, loadWorkflow } = useWorkflowLoad();
const { hasUnsavedChanges, saveWorkflow, markUnsaved } = useWorkflowSave();
const { workflowDefinition, loadWorkflow } = useWorkflowLoad();
// 加载工作流数据
useEffect(() => {
@ -230,7 +229,7 @@ const WorkflowDesignInner: React.FC = () => {
if (!dragData) return;
try {
const { nodeType, nodeDefinition }: DragNodeData = JSON.parse(dragData);
const { nodeType, nodeDefinition }: { nodeType: string; nodeDefinition: WorkflowNodeDefinition } = JSON.parse(dragData);
// 根据React Flow官方文档screenToFlowPosition会自动处理所有边界计算
// 不需要手动减去容器边界!
@ -245,11 +244,20 @@ const WorkflowDesignInner: React.FC = () => {
position,
data: {
label: nodeDefinition.nodeName,
nodeType,
nodeType: nodeDefinition.nodeType,
category: nodeDefinition.category,
icon: nodeDefinition.icon,
color: nodeDefinition.color,
nodeDefinition
icon: nodeDefinition.uiConfig.style.icon,
color: nodeDefinition.uiConfig.style.fill,
// 保存原始节点定义引用,用于配置
nodeDefinition,
// 初始化配置数据
configs: {
nodeName: nodeDefinition.nodeName,
nodeCode: nodeDefinition.nodeCode,
description: nodeDefinition.description
},
inputMapping: {},
outputMapping: {}
}
};
@ -261,31 +269,22 @@ const WorkflowDesignInner: React.FC = () => {
}
}, [screenToFlowPosition, setNodes]);
// 处理节点点击 - 单击选择,双击打开配置面板
// 处理节点双击 - 打开配置面板
const handleNodeClick = useCallback((event: React.MouseEvent, node: FlowNode) => {
console.log('节点点击:', node);
// 清除边选择
setSelectedEdge(null);
// 检查是否是双击
// 只处理双击事件
if (event.detail === 2) {
setSelectedNode(node);
console.log('双击节点,打开配置:', node);
setConfigNode(node);
setConfigModalVisible(true);
} else {
setSelectedNode(node);
message.info(`选择了节点: ${node.data.label}`);
}
}, []);
// 处理边点击
// 处理边双击 - 暂时只记录日志
const handleEdgeClick = useCallback((event: React.MouseEvent, edge: FlowEdge) => {
console.log('边点击:', edge);
// 清除节点选择
setSelectedNode(null);
setSelectedEdge(edge);
message.info(`选择了连接: ${edge.data?.label || '连接'}`);
if (event.detail === 2) {
console.log('双击边:', edge);
message.info(`双击了连接: ${edge.data?.label || '连接'},配置功能待实现`);
}
}, []);
// 处理节点配置更新
@ -306,28 +305,10 @@ const WorkflowDesignInner: React.FC = () => {
markUnsaved(); // 标记有未保存更改
}, [setNodes, markUnsaved]);
// 处理边配置更新
const handleEdgeConfigUpdate = useCallback((edgeId: string, updatedData: any) => {
setEdges((edges) =>
edges.map((edge) =>
edge.id === edgeId
? {
...edge,
data: {
...edge.data,
...updatedData,
},
}
: edge
)
);
markUnsaved(); // 标记有未保存更改
}, [setEdges, markUnsaved]);
// 关闭配置模态框
const handleCloseConfigModal = useCallback(() => {
setConfigModalVisible(false);
setSelectedNode(null);
setConfigNode(null);
}, []);
return (
@ -343,7 +324,6 @@ const WorkflowDesignInner: React.FC = () => {
title={`${workflowTitle}${hasUnsavedChanges ? ' *' : ''}`}
onSave={handleSave}
onBack={handleBack}
saving={saving}
onUndo={handleUndo}
onRedo={handleRedo}
onCopy={handleCopy}
@ -376,20 +356,12 @@ const WorkflowDesignInner: React.FC = () => {
className="workflow-canvas"
/>
</div>
{/* 属性面板 */}
<PropertyPanel
selectedNode={selectedNode}
selectedEdge={selectedEdge}
onNodeUpdate={handleNodeConfigUpdate}
onEdgeUpdate={handleEdgeConfigUpdate}
/>
</div>
{/* 节点配置弹窗 */}
<NodeConfigModal
visible={configModalVisible}
node={selectedNode}
node={configNode}
onCancel={handleCloseConfigModal}
onOk={handleNodeConfigUpdate}
/>

View File

@ -0,0 +1,189 @@
import { ConfigurableNodeDefinition, NodeType, NodeCategory } from '../types';
/**
*
*
*/
export const DeployNode: ConfigurableNodeDefinition = {
nodeCode: "DEPLOY_NODE",
nodeName: "构建任务",
nodeType: NodeType.DEPLOY_NODE,
category: NodeCategory.TASK,
description: "执行应用构建和部署任务",
// UI 配置
uiConfig: {
size: {
width: 120,
height: 60
},
style: {
fill: '#1890ff',
stroke: '#0050b3',
strokeWidth: 2,
icon: 'build',
iconColor: '#fff'
}
},
// 基本配置Schema - 设计时用于生成表单保存时转为key/value包含基本信息+节点配置)
configSchema: {
type: "object",
title: "基本配置",
description: "节点的基本信息和构建任务的配置参数",
properties: {
// 基本信息
nodeName: {
type: "string",
title: "节点名称",
description: "节点在流程图中显示的名称",
default: "构建任务"
},
nodeCode: {
type: "string",
title: "节点编码",
description: "节点的唯一标识符",
default: "DEPLOY_NODE"
},
description: {
type: "string",
title: "节点描述",
description: "节点的详细说明",
default: "执行应用构建和部署任务"
},
// 节点配置
buildCommand: {
type: "string",
title: "构建命令",
description: "执行构建的命令",
default: "npm run build"
},
timeout: {
type: "number",
title: "超时时间(秒)",
description: "构建超时时间",
default: 300,
minimum: 30,
maximum: 3600
},
retryCount: {
type: "number",
title: "重试次数",
description: "构建失败时的重试次数",
default: 2,
minimum: 0,
maximum: 5
},
environment: {
type: "string",
title: "运行环境",
description: "构建运行的环境",
enum: ["development", "staging", "production"],
default: "production"
},
dockerImage: {
type: "string",
title: "Docker镜像",
description: "构建使用的Docker镜像",
default: "node:18-alpine"
},
workingDirectory: {
type: "string",
title: "工作目录",
description: "构建的工作目录",
default: "/app"
}
},
required: ["nodeName", "nodeCode", "buildCommand", "timeout"]
},
// 输入映射Schema - 定义从上游节点接收的数据
inputMappingSchema: {
type: "object",
title: "输入映射",
description: "从上游节点接收的数据映射配置",
properties: {
sourceCodePath: {
type: "string",
title: "源代码路径",
description: "源代码在存储中的路径",
default: "${upstream.outputPath}"
},
buildArgs: {
type: "array",
title: "构建参数",
description: "额外的构建参数列表",
items: {
type: "string"
},
default: []
},
envVariables: {
type: "object",
title: "环境变量",
description: "构建时的环境变量",
properties: {},
additionalProperties: true
},
dependencies: {
type: "string",
title: "依赖文件",
description: "依赖配置文件路径",
default: "${upstream.dependenciesFile}"
}
},
required: ["sourceCodePath"]
},
// 输出映射Schema - 定义传递给下游节点的数据
outputMappingSchema: {
type: "object",
title: "输出映射",
description: "传递给下游节点的数据映射配置",
properties: {
buildArtifactPath: {
type: "string",
title: "构建产物路径",
description: "构建完成后的产物存储路径",
default: "/artifacts/${buildId}"
},
buildLog: {
type: "string",
title: "构建日志",
description: "构建过程的日志文件路径",
default: "/logs/build-${buildId}.log"
},
buildStatus: {
type: "string",
title: "构建状态",
description: "构建完成状态",
enum: ["SUCCESS", "FAILED", "TIMEOUT"],
default: "SUCCESS"
},
buildTime: {
type: "number",
title: "构建耗时",
description: "构建耗时(秒)",
default: 0
},
dockerImageTag: {
type: "string",
title: "Docker镜像标签",
description: "构建生成的Docker镜像标签",
default: "${imageRegistry}/${projectName}:${buildId}"
},
metadata: {
type: "object",
title: "构建元数据",
description: "构建过程中的元数据信息",
properties: {
buildId: { type: "string" },
buildTime: { type: "string" },
gitCommit: { type: "string" },
gitBranch: { type: "string" }
}
}
},
required: ["buildArtifactPath", "buildStatus"]
}
};

View File

@ -0,0 +1,56 @@
import {BaseNodeDefinition, NodeType, NodeCategory} from '../types';
/**
*
*
*/
export const EndEventNode: BaseNodeDefinition = {
nodeCode: "END_EVENT",
nodeName: "结束",
nodeType: NodeType.END_EVENT,
category: NodeCategory.EVENT,
description: "工作流的结束节点",
// UI 配置
uiConfig: {
size: {
width: 80,
height: 50
},
style: {
fill: '#ff4d4f',
stroke: '#cf1322',
strokeWidth: 2,
icon: 'stop-circle',
iconColor: '#fff'
}
},
// 基本配置Schema - 用于生成"基本配置"TAB包含基本信息
configSchema: {
type: "object",
title: "基本配置",
description: "节点的基本配置信息",
properties: {
nodeName: {
type: "string",
title: "节点名称",
description: "节点在流程图中显示的名称",
default: "结束"
},
nodeCode: {
type: "string",
title: "节点编码",
description: "节点的唯一标识符",
default: "END_EVENT"
},
description: {
type: "string",
title: "节点描述",
description: "节点的详细说明",
default: "工作流的结束节点"
}
},
required: ["nodeName", "nodeCode"]
}
};

View File

@ -0,0 +1,180 @@
import { ConfigurableNodeDefinition, NodeType, NodeCategory } from '../types';
/**
*
*
*/
export const ServiceTaskNode: ConfigurableNodeDefinition = {
nodeCode: "SERVICE_TASK",
nodeName: "服务任务",
nodeType: NodeType.SERVICE_TASK,
category: NodeCategory.TASK,
description: "自动执行的服务任务",
// UI 配置
uiConfig: {
size: {
width: 120,
height: 60
},
style: {
fill: '#fa8c16',
stroke: '#d46b08',
strokeWidth: 2,
icon: 'api',
iconColor: '#fff'
}
},
// 基本配置Schema
configSchema: {
type: "object",
title: "基本配置",
description: "服务任务的基本配置信息",
properties: {
// 基本信息
nodeName: {
type: "string",
title: "节点名称",
description: "节点在流程图中显示的名称",
default: "服务任务"
},
nodeCode: {
type: "string",
title: "节点编码",
description: "节点的唯一标识符",
default: "SERVICE_TASK"
},
description: {
type: "string",
title: "节点描述",
description: "节点的详细说明",
default: "自动执行的服务任务"
},
// 节点配置
serviceUrl: {
type: "string",
title: "服务URL",
description: "调用的服务接口地址",
default: "https://api.example.com/service"
},
httpMethod: {
type: "string",
title: "HTTP方法",
description: "HTTP请求方法",
enum: ["GET", "POST", "PUT", "DELETE", "PATCH"],
default: "POST"
},
timeout: {
type: "number",
title: "超时时间(秒)",
description: "服务调用超时时间",
default: 30,
minimum: 1,
maximum: 300
},
retryCount: {
type: "number",
title: "重试次数",
description: "失败时的重试次数",
default: 3,
minimum: 0,
maximum: 10
},
headers: {
type: "object",
title: "请求头",
description: "HTTP请求头配置",
default: {
"Content-Type": "application/json"
}
},
async: {
type: "boolean",
title: "异步执行",
description: "是否异步执行服务调用",
default: false
}
},
required: ["nodeName", "nodeCode", "serviceUrl", "httpMethod"]
},
// 输入映射Schema
inputMappingSchema: {
type: "object",
title: "输入映射",
description: "从上游节点接收的数据映射配置",
properties: {
requestBody: {
type: "object",
title: "请求体",
description: "发送给服务的请求数据",
default: {}
},
queryParams: {
type: "object",
title: "查询参数",
description: "URL查询参数",
default: {}
},
pathParams: {
type: "object",
title: "路径参数",
description: "URL路径参数",
default: {}
},
authToken: {
type: "string",
title: "认证令牌",
description: "服务调用的认证令牌",
default: "${upstream.token}"
}
}
},
// 输出映射Schema
outputMappingSchema: {
type: "object",
title: "输出映射",
description: "传递给下游节点的数据映射配置",
properties: {
responseData: {
type: "object",
title: "响应数据",
description: "服务返回的响应数据",
default: {}
},
statusCode: {
type: "number",
title: "状态码",
description: "HTTP响应状态码",
default: 200
},
responseHeaders: {
type: "object",
title: "响应头",
description: "HTTP响应头信息",
default: {}
},
executionTime: {
type: "number",
title: "执行时间",
description: "服务调用耗时(毫秒)",
default: 0
},
success: {
type: "boolean",
title: "执行成功",
description: "服务调用是否成功",
default: true
},
errorMessage: {
type: "string",
title: "错误信息",
description: "失败时的错误信息",
default: ""
}
},
required: ["responseData", "statusCode", "success"]
}
};

View File

@ -0,0 +1,56 @@
import {BaseNodeDefinition, NodeType, NodeCategory} from '../types';
/**
*
*
*/
export const StartEventNode: BaseNodeDefinition = {
nodeCode: "START_EVENT",
nodeName: "开始",
nodeType: NodeType.START_EVENT,
category: NodeCategory.EVENT,
description: "工作流的起始节点",
// UI 配置
uiConfig: {
size: {
width: 80,
height: 50
},
style: {
fill: '#52c41a',
stroke: '#389e08',
strokeWidth: 2,
icon: 'play-circle',
iconColor: '#fff'
}
},
// 基本配置Schema - 用于生成"基本配置"TAB包含基本信息
configSchema: {
type: "object",
title: "基本配置",
description: "节点的基本配置信息",
properties: {
nodeName: {
type: "string",
title: "节点名称",
description: "节点在流程图中显示的名称",
default: "开始"
},
nodeCode: {
type: "string",
title: "节点编码",
description: "节点的唯一标识符",
default: "START_EVENT"
},
description: {
type: "string",
title: "节点描述",
description: "节点的详细说明",
default: "工作流的起始节点"
}
},
required: ["nodeName", "nodeCode"]
}
};

View File

@ -0,0 +1,155 @@
import { ConfigurableNodeDefinition, NodeType, NodeCategory } from '../types';
/**
*
*
*/
export const UserTaskNode: ConfigurableNodeDefinition = {
nodeCode: "USER_TASK",
nodeName: "用户任务",
nodeType: NodeType.USER_TASK,
category: NodeCategory.TASK,
description: "需要用户手动执行的任务",
// UI 配置
uiConfig: {
size: {
width: 120,
height: 60
},
style: {
fill: '#722ed1',
stroke: '#531dab',
strokeWidth: 2,
icon: 'user',
iconColor: '#fff'
}
},
// 基本配置Schema
configSchema: {
type: "object",
title: "基本配置",
description: "用户任务的基本配置信息",
properties: {
// 基本信息
nodeName: {
type: "string",
title: "节点名称",
description: "节点在流程图中显示的名称",
default: "用户任务"
},
nodeCode: {
type: "string",
title: "节点编码",
description: "节点的唯一标识符",
default: "USER_TASK"
},
description: {
type: "string",
title: "节点描述",
description: "节点的详细说明",
default: "需要用户手动执行的任务"
},
// 节点配置
assignee: {
type: "string",
title: "任务分配人",
description: "任务分配给的用户",
default: ""
},
candidateGroups: {
type: "array",
title: "候选组",
description: "可以执行此任务的用户组",
items: {
type: "string"
},
default: []
},
dueDate: {
type: "string",
title: "截止时间",
description: "任务的截止时间",
format: "date-time"
},
priority: {
type: "string",
title: "优先级",
description: "任务优先级",
enum: ["low", "normal", "high", "urgent"],
default: "normal"
},
formKey: {
type: "string",
title: "表单键",
description: "关联的表单标识",
default: ""
}
},
required: ["nodeName", "nodeCode"]
},
// 输入映射Schema
inputMappingSchema: {
type: "object",
title: "输入映射",
description: "从上游节点接收的数据映射配置",
properties: {
taskData: {
type: "object",
title: "任务数据",
description: "传递给用户任务的数据",
default: {}
},
assigneeExpression: {
type: "string",
title: "分配人表达式",
description: "动态计算分配人的表达式",
default: "${upstream.userId}"
},
formVariables: {
type: "object",
title: "表单变量",
description: "表单初始化变量",
default: {}
}
}
},
// 输出映射Schema
outputMappingSchema: {
type: "object",
title: "输出映射",
description: "传递给下游节点的数据映射配置",
properties: {
taskResult: {
type: "object",
title: "任务结果",
description: "用户任务的执行结果",
default: {}
},
completedBy: {
type: "string",
title: "完成人",
description: "实际完成任务的用户",
default: "${task.assignee}"
},
completedAt: {
type: "string",
title: "完成时间",
description: "任务完成的时间",
format: "date-time",
default: "${task.endTime}"
},
decision: {
type: "string",
title: "决策结果",
description: "用户的决策结果",
enum: ["approved", "rejected", "pending"],
default: "approved"
}
},
required: ["taskResult", "completedBy"]
}
};

View File

@ -0,0 +1,29 @@
import {WorkflowNodeDefinition} from '../types';
import {DeployNode} from './DeployNode';
import {StartEventNode} from './StartEventNode';
import {EndEventNode} from './EndEventNode';
import {UserTaskNode} from './UserTaskNode';
import {ServiceTaskNode} from './ServiceTaskNode';
/**
*
*/
export const NODE_DEFINITIONS: WorkflowNodeDefinition[] = [
StartEventNode,
EndEventNode,
UserTaskNode,
ServiceTaskNode,
DeployNode,
// 在这里添加更多节点定义
];
/**
*
*/
export {
StartEventNode,
EndEventNode,
UserTaskNode,
ServiceTaskNode,
DeployNode
};

View File

@ -0,0 +1,99 @@
// 节点分类
export enum NodeCategory {
EVENT = 'EVENT',
TASK = 'TASK',
GATEWAY = 'GATEWAY',
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'
}
// JSON Schema 定义
export interface JSONSchema {
type: string;
properties?: Record<string, any>;
required?: string[];
title?: string;
description?: string;
}
// UI 配置
export interface NodeSize {
width: number;
height: number;
}
export interface NodeStyle {
fill: string;
icon: string;
stroke: string;
iconColor: string;
strokeWidth: number;
iconSize?: number;
borderRadius?: string;
boxShadow?: string;
transition?: string;
fontSize?: string;
fontWeight?: string;
fontFamily?: string;
}
export interface UIConfig {
size: NodeSize;
style: NodeStyle;
}
// 基础节点定义(只有基本配置)
export interface BaseNodeDefinition {
nodeCode: string;
nodeName: string;
nodeType: NodeType;
category: NodeCategory;
description: string;
uiConfig: UIConfig;
configSchema: JSONSchema; // 基本配置Schema包含基本信息+节点配置)
}
// 可配置节点定义有3个TAB基本配置、输入、输出
export interface ConfigurableNodeDefinition extends BaseNodeDefinition {
inputMappingSchema?: JSONSchema; // 输入映射的Schema定义
outputMappingSchema?: JSONSchema; // 输出映射的Schema定义
}
// 工作流节点定义(联合类型)
export type WorkflowNodeDefinition = BaseNodeDefinition | ConfigurableNodeDefinition;
// 节点实例数据(运行时)
export interface NodeInstanceData {
nodeCode: string;
nodeName: string;
nodeType: NodeType;
category: NodeCategory;
description?: string;
// 运行时数据key/value格式
configs?: Record<string, any>; // 基本配置数据(包含基本信息+节点配置)
inputMapping?: Record<string, any>;
outputMapping?: Record<string, any>;
// UI位置信息
position: { x: number; y: number };
uiConfig: UIConfig;
}
// 判断是否为可配置节点
export const isConfigurableNode = (def: WorkflowNodeDefinition): def is ConfigurableNodeDefinition => {
return 'inputMappingSchema' in def || 'outputMappingSchema' in def;
};

View File

@ -40,17 +40,14 @@ export interface FlowNodeData extends Record<string, unknown> {
category: NodeCategory;
icon: string;
color: string;
// 节点配置数据
configs?: {
timeout?: number; // 超时时间(秒)
retryCount?: number; // 重试次数
priority?: string; // 优先级
[key: string]: any; // 其他自定义配置
};
// 运行时数据(与原系统兼容的格式)
configs?: Record<string, any>; // 基本配置数据(包含基本信息+节点配置)
inputMapping?: Record<string, any>; // 输入参数映射
outputMapping?: Record<string, any>; // 输出参数映射
// 原始节点定义
nodeDefinition?: NodeDefinitionData;
// 原始节点定义(使用新的节点定义接口)
nodeDefinition?: import('./nodes/types').WorkflowNodeDefinition;
}
// React Flow 边数据 - 添加索引签名以满足React Flow的类型约束