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

View File

@ -1,82 +1,22 @@
import React from 'react'; import React from 'react';
import { Card, Divider } from 'antd'; import { NodeCategory } from '../types';
import { NodeType, NodeCategory, type NodeDefinitionData } from '../types'; import { NODE_DEFINITIONS } from '../nodes/definitions';
import type { WorkflowNodeDefinition } from '../nodes/types';
// 节点定义数据 // 图标映射函数
const nodeDefinitions: NodeDefinitionData[] = [ const getNodeIcon = (iconName: string): string => {
{ const iconMap: Record<string, string> = {
nodeCode: 'start_event', 'play-circle': '▶️',
nodeName: '开始事件', 'stop-circle': '⏹️',
nodeType: NodeType.START_EVENT, 'user': '👤',
category: NodeCategory.EVENT, 'api': '⚙️',
description: '工作流开始节点', 'code': '📜',
icon: '▶️', 'build': '🚀',
color: '#10b981' 'jenkins': '🔨',
}, 'gateway': '💎'
{ };
nodeCode: 'end_event', return iconMap[iconName] || '📋';
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'
}
];
interface NodePanelProps { interface NodePanelProps {
className?: string; className?: string;
@ -84,16 +24,16 @@ interface NodePanelProps {
const NodePanel: React.FC<NodePanelProps> = ({ className = '' }) => { const NodePanel: React.FC<NodePanelProps> = ({ className = '' }) => {
// 按分类分组节点 // 按分类分组节点
const nodesByCategory = nodeDefinitions.reduce((acc, node) => { const nodesByCategory = NODE_DEFINITIONS.reduce((acc, node) => {
if (!acc[node.category]) { if (!acc[node.category]) {
acc[node.category] = []; acc[node.category] = [];
} }
acc[node.category].push(node); acc[node.category].push(node);
return acc; 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({ event.dataTransfer.setData('application/reactflow', JSON.stringify({
nodeType: nodeDefinition.nodeType, nodeType: nodeDefinition.nodeType,
nodeDefinition nodeDefinition
@ -102,11 +42,10 @@ const NodePanel: React.FC<NodePanelProps> = ({ className = '' }) => {
}; };
// 渲染节点项 // 渲染节点项
const renderNodeItem = (nodeDefinition: NodeDefinitionData) => ( const renderNodeItem = (nodeDefinition: WorkflowNodeDefinition) => (
<div <div
key={nodeDefinition.nodeCode} key={nodeDefinition.nodeCode}
draggable draggable
onDragStart={(e) => handleDragStart(e, nodeDefinition)}
style={{ style={{
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'center',
@ -121,7 +60,7 @@ const NodePanel: React.FC<NodePanelProps> = ({ className = '' }) => {
}} }}
onMouseEnter={(e) => { onMouseEnter={(e) => {
e.currentTarget.style.background = '#f9fafb'; 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)'; e.currentTarget.style.transform = 'translateX(2px)';
}} }}
onMouseLeave={(e) => { onMouseLeave={(e) => {
@ -140,9 +79,10 @@ const NodePanel: React.FC<NodePanelProps> = ({ className = '' }) => {
<div style={{ <div style={{
fontSize: '16px', fontSize: '16px',
width: '20px', width: '20px',
textAlign: 'center' textAlign: 'center',
color: nodeDefinition.uiConfig.style.fill
}}> }}>
{nodeDefinition.icon} {getNodeIcon(nodeDefinition.uiConfig.style.icon)}
</div> </div>
<div> <div>
<div style={{ <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 NodePanel from './components/NodePanel';
import FlowCanvas from './components/FlowCanvas'; import FlowCanvas from './components/FlowCanvas';
import NodeConfigModal from './components/NodeConfigModal'; import NodeConfigModal from './components/NodeConfigModal';
import PropertyPanel from './components/PropertyPanel'; import type { FlowNode, FlowEdge, FlowNodeData } from './types';
import type { FlowNode, FlowEdge, DragNodeData, FlowNodeData } from './types'; import type { WorkflowNodeDefinition } from './nodes/types';
import { NodeType } from './types'; import { NodeType } from './types';
import { useWorkflowSave } from './hooks/useWorkflowSave'; import { useWorkflowSave } from './hooks/useWorkflowSave';
import { useWorkflowLoad } from './hooks/useWorkflowLoad'; import { useWorkflowLoad } from './hooks/useWorkflowLoad';
@ -40,12 +40,11 @@ const WorkflowDesignInner: React.FC = () => {
// 节点配置模态框状态 // 节点配置模态框状态
const [configModalVisible, setConfigModalVisible] = useState(false); const [configModalVisible, setConfigModalVisible] = useState(false);
const [selectedNode, setSelectedNode] = useState<FlowNode | null>(null); const [configNode, setConfigNode] = useState<FlowNode | null>(null);
const [selectedEdge, setSelectedEdge] = useState<FlowEdge | null>(null);
// 保存和加载hooks // 保存和加载hooks
const { saving, hasUnsavedChanges, saveWorkflow, markUnsaved } = useWorkflowSave(); const { hasUnsavedChanges, saveWorkflow, markUnsaved } = useWorkflowSave();
const { loading: loadingWorkflow, workflowDefinition, loadWorkflow } = useWorkflowLoad(); const { workflowDefinition, loadWorkflow } = useWorkflowLoad();
// 加载工作流数据 // 加载工作流数据
useEffect(() => { useEffect(() => {
@ -230,7 +229,7 @@ const WorkflowDesignInner: React.FC = () => {
if (!dragData) return; if (!dragData) return;
try { try {
const { nodeType, nodeDefinition }: DragNodeData = JSON.parse(dragData); const { nodeType, nodeDefinition }: { nodeType: string; nodeDefinition: WorkflowNodeDefinition } = JSON.parse(dragData);
// 根据React Flow官方文档screenToFlowPosition会自动处理所有边界计算 // 根据React Flow官方文档screenToFlowPosition会自动处理所有边界计算
// 不需要手动减去容器边界! // 不需要手动减去容器边界!
@ -245,11 +244,20 @@ const WorkflowDesignInner: React.FC = () => {
position, position,
data: { data: {
label: nodeDefinition.nodeName, label: nodeDefinition.nodeName,
nodeType, nodeType: nodeDefinition.nodeType,
category: nodeDefinition.category, category: nodeDefinition.category,
icon: nodeDefinition.icon, icon: nodeDefinition.uiConfig.style.icon,
color: nodeDefinition.color, color: nodeDefinition.uiConfig.style.fill,
nodeDefinition // 保存原始节点定义引用,用于配置
nodeDefinition,
// 初始化配置数据
configs: {
nodeName: nodeDefinition.nodeName,
nodeCode: nodeDefinition.nodeCode,
description: nodeDefinition.description
},
inputMapping: {},
outputMapping: {}
} }
}; };
@ -261,31 +269,22 @@ const WorkflowDesignInner: React.FC = () => {
} }
}, [screenToFlowPosition, setNodes]); }, [screenToFlowPosition, setNodes]);
// 处理节点点击 - 单击选择,双击打开配置面板 // 处理节点双击 - 打开配置面板
const handleNodeClick = useCallback((event: React.MouseEvent, node: FlowNode) => { const handleNodeClick = useCallback((event: React.MouseEvent, node: FlowNode) => {
console.log('节点点击:', node); // 只处理双击事件
// 清除边选择
setSelectedEdge(null);
// 检查是否是双击
if (event.detail === 2) { if (event.detail === 2) {
setSelectedNode(node); console.log('双击节点,打开配置:', node);
setConfigNode(node);
setConfigModalVisible(true); setConfigModalVisible(true);
} else {
setSelectedNode(node);
message.info(`选择了节点: ${node.data.label}`);
} }
}, []); }, []);
// 处理边点击 // 处理边双击 - 暂时只记录日志
const handleEdgeClick = useCallback((event: React.MouseEvent, edge: FlowEdge) => { const handleEdgeClick = useCallback((event: React.MouseEvent, edge: FlowEdge) => {
console.log('边点击:', edge); if (event.detail === 2) {
console.log('双击边:', edge);
// 清除节点选择 message.info(`双击了连接: ${edge.data?.label || '连接'},配置功能待实现`);
setSelectedNode(null); }
setSelectedEdge(edge);
message.info(`选择了连接: ${edge.data?.label || '连接'}`);
}, []); }, []);
// 处理节点配置更新 // 处理节点配置更新
@ -306,28 +305,10 @@ const WorkflowDesignInner: React.FC = () => {
markUnsaved(); // 标记有未保存更改 markUnsaved(); // 标记有未保存更改
}, [setNodes, 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(() => { const handleCloseConfigModal = useCallback(() => {
setConfigModalVisible(false); setConfigModalVisible(false);
setSelectedNode(null); setConfigNode(null);
}, []); }, []);
return ( return (
@ -343,7 +324,6 @@ const WorkflowDesignInner: React.FC = () => {
title={`${workflowTitle}${hasUnsavedChanges ? ' *' : ''}`} title={`${workflowTitle}${hasUnsavedChanges ? ' *' : ''}`}
onSave={handleSave} onSave={handleSave}
onBack={handleBack} onBack={handleBack}
saving={saving}
onUndo={handleUndo} onUndo={handleUndo}
onRedo={handleRedo} onRedo={handleRedo}
onCopy={handleCopy} onCopy={handleCopy}
@ -376,20 +356,12 @@ const WorkflowDesignInner: React.FC = () => {
className="workflow-canvas" className="workflow-canvas"
/> />
</div> </div>
{/* 属性面板 */}
<PropertyPanel
selectedNode={selectedNode}
selectedEdge={selectedEdge}
onNodeUpdate={handleNodeConfigUpdate}
onEdgeUpdate={handleEdgeConfigUpdate}
/>
</div> </div>
{/* 节点配置弹窗 */} {/* 节点配置弹窗 */}
<NodeConfigModal <NodeConfigModal
visible={configModalVisible} visible={configModalVisible}
node={selectedNode} node={configNode}
onCancel={handleCloseConfigModal} onCancel={handleCloseConfigModal}
onOk={handleNodeConfigUpdate} 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; category: NodeCategory;
icon: string; icon: string;
color: string; color: string;
// 节点配置数据
configs?: { // 运行时数据(与原系统兼容的格式)
timeout?: number; // 超时时间(秒) configs?: Record<string, any>; // 基本配置数据(包含基本信息+节点配置)
retryCount?: number; // 重试次数
priority?: string; // 优先级
[key: string]: any; // 其他自定义配置
};
inputMapping?: Record<string, any>; // 输入参数映射 inputMapping?: Record<string, any>; // 输入参数映射
outputMapping?: Record<string, any>; // 输出参数映射 outputMapping?: Record<string, any>; // 输出参数映射
// 原始节点定义
nodeDefinition?: NodeDefinitionData; // 原始节点定义(使用新的节点定义接口)
nodeDefinition?: import('./nodes/types').WorkflowNodeDefinition;
} }
// React Flow 边数据 - 添加索引签名以满足React Flow的类型约束 // React Flow 边数据 - 添加索引签名以满足React Flow的类型约束