This commit is contained in:
dengqichen 2025-10-21 12:26:40 +08:00
parent 33c70b7894
commit e18874688e
51 changed files with 1622 additions and 6765 deletions

View File

@ -1,94 +0,0 @@
import React from 'react';
import { Modal, Form, Input, InputNumber, Radio } from 'antd';
import { Edge } from '@antv/x6';
import { EdgeCondition } from '../nodes/types';
interface ExpressionModalProps {
visible: boolean;
edge: Edge;
onOk: (condition: EdgeCondition) => void;
onCancel: () => void;
}
const ExpressionModal: React.FC<ExpressionModalProps> = ({
visible,
edge,
onOk,
onCancel
}) => {
const [form] = Form.useForm();
const currentCondition = edge.getProp('condition') as EdgeCondition;
const handleOk = async () => {
try {
const values = await form.validateFields();
onOk(values);
} catch (error) {
console.error('表单验证失败:', error);
}
};
return (
<Modal
title="编辑条件"
open={visible}
onOk={handleOk}
onCancel={onCancel}
width={600}
>
<Form
form={form}
layout="vertical"
initialValues={{
type: currentCondition?.type || 'EXPRESSION',
expression: currentCondition?.expression || '',
priority: currentCondition?.priority || 10
}}
>
<Form.Item
name="type"
label="条件类型"
rules={[{ required: true }]}
>
<Radio.Group>
<Radio value="EXPRESSION"></Radio>
<Radio value="DEFAULT"></Radio>
</Radio.Group>
</Form.Item>
<Form.Item
noStyle
shouldUpdate={(prevValues, currentValues) => prevValues.type !== currentValues.type}
>
{({ getFieldValue }) => {
const type = getFieldValue('type');
return type === 'EXPRESSION' ? (
<Form.Item
name="expression"
label="条件表达式"
rules={[{ required: true, message: '请输入条件表达式' }]}
extra="支持使用 ${变量名} 引用流程变量,例如:${amount} > 1000"
>
<Input.TextArea
placeholder="请输入条件表达式"
rows={4}
/>
</Form.Item>
) : null;
}}
</Form.Item>
<Form.Item
name="priority"
label="优先级"
rules={[{ required: true }]}
extra="数字越小优先级越高"
>
<InputNumber min={1} max={999} />
</Form.Item>
</Form>
</Modal>
);
};
export default ExpressionModal;

View File

@ -1,92 +1,247 @@
import React, { useEffect, useState } from 'react';
import { Tabs } from 'antd';
import { Cell } from '@antv/x6';
import type { WorkflowNodeDefinition, ConfigurableNodeDefinition } from "../nodes/types";
import {
Sheet,
SheetContent,
SheetHeader,
SheetTitle,
} from "@/components/ui/sheet";
import { Button } from "@/components/ui/button";
import { convertJsonSchemaToColumns } from '@/utils/jsonSchemaUtils';
import { BetaSchemaForm } from '@ant-design/pro-components';
import React, { useState, useEffect, useMemo } from 'react';
import { Drawer, Tabs, Button, Space, message } from 'antd';
import { SaveOutlined, ReloadOutlined, CloseOutlined } from '@ant-design/icons';
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: Cell | null;
nodeDefinition: WorkflowNodeDefinition | null;
onOk: (values: any) => void;
node: FlowNode | null;
onCancel: () => void;
}
interface FormData {
configs?: Record<string, any>;
inputMapping?: Record<string, any>;
outputMapping?: Record<string, any>;
onOk: (nodeId: string, updatedData: Partial<FlowNodeData>) => void;
}
const NodeConfigModal: React.FC<NodeConfigModalProps> = ({
visible,
node,
nodeDefinition,
onOk,
onCancel,
onOk
}) => {
const [formData, setFormData] = useState<FormData>({});
const [loading, setLoading] = useState(false);
const [activeTab, setActiveTab] = useState('config');
// 判断是否为可配置节点
const isConfigurableNode = (def: WorkflowNodeDefinition): def is ConfigurableNodeDefinition => {
return 'inputMappingSchema' in def || 'outputMappingSchema' in def;
};
// 获取节点定义
const nodeDefinition: WorkflowNodeDefinition | null = node?.data?.nodeDefinition || null;
// 创建Formily表单实例
const configForm = useMemo(() => createForm(), []);
const inputForm = useMemo(() => createForm(), []);
const outputForm = useMemo(() => createForm(), []);
// 初始化表单数据
useEffect(() => {
if (nodeDefinition && node) {
// 从节点数据中获取现有配置
const nodeData = node.getData() || {};
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({});
// 设置表单初始值
configForm.setInitialValues({ ...defaultConfig, ...(nodeData.configs || {}) });
configForm.reset();
if (isConfigurableNode(nodeDefinition)) {
inputForm.setInitialValues(nodeData.inputMapping || {});
inputForm.reset();
outputForm.setInitialValues(nodeData.outputMapping || {});
outputForm.reset();
}
}, [nodeDefinition, node]);
}
}, [visible, node, nodeDefinition, configForm, inputForm, outputForm]);
const handleSubmit = () => {
onOk(formData);
// 递归处理表单值将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 handleConfigChange = (values: Record<string, any>) => {
setFormData(prev => ({
...prev,
configs: values
}));
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: configs.nodeName || node.data.label,
configs,
inputMapping,
outputMapping,
};
const handleInputMappingChange = (values: Record<string, any>) => {
setFormData(prev => ({
...prev,
inputMapping: values
}));
onOk(node.id, updatedData);
message.success('节点配置保存成功');
onCancel();
} catch (error) {
if (error instanceof Error) {
message.error(error.message);
}
} finally {
setLoading(false);
}
};
const handleOutputMappingChange = (values: Record<string, any>) => {
setFormData(prev => ({
...prev,
outputMapping: values
}));
const handleReset = () => {
configForm.reset();
inputForm.reset();
outputForm.reset();
message.info('已重置为初始值');
};
// 将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%' } // 统一宽度
};
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"}'
};
// ✅ 关键修复将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%' } // 统一宽度
};
}
// 设置默认值非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) {
@ -98,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,
},
];
@ -119,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,
});
}
@ -138,66 +287,81 @@ 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,
});
}
}
return (
<Sheet open={visible} onOpenChange={(open) => !open && onCancel()}>
<SheetContent
style={{
width: '600px',
maxWidth: '90vw',
display: 'flex',
flexDirection: 'column'
}}
>
<SheetHeader>
<SheetTitle>
<Drawer
title={
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', paddingRight: '24px' }}>
<span style={{ fontSize: '16px', fontWeight: '600' }}>
- {nodeDefinition.nodeName}
</SheetTitle>
</SheetHeader>
<div style={{ flex: 1, overflow: 'hidden', display: 'flex', flexDirection: 'column' }}>
</span>
<Button
type="text"
icon={<CloseOutlined />}
onClick={onCancel}
size="small"
/>
</div>
}
placement="right"
width={720}
open={visible}
onClose={onCancel}
closeIcon={null}
styles={{
body: { padding: '0 24px 24px' },
header: { borderBottom: '1px solid #f0f0f0', padding: '16px 24px' }
}}
footer={
<div style={{
display: 'flex',
justifyContent: 'space-between',
padding: '12px 0',
borderTop: '1px solid #f0f0f0'
}}>
<Button
icon={<ReloadOutlined />}
onClick={handleReset}
>
</Button>
<Space>
<Button onClick={onCancel}>
</Button>
<Button
type="primary"
loading={loading}
icon={<SaveOutlined />}
onClick={handleSubmit}
>
</Button>
</Space>
</div>
}
>
<div style={{ paddingTop: '16px' }}>
<Tabs
activeKey={activeTab}
onChange={setActiveTab}
items={tabItems}
style={{ flex: 1, display: 'flex', flexDirection: 'column' }}
className="flex-tabs"
size="large"
/>
</div>
<div
style={{
borderTop: '1px solid #f0f0f0',
padding: '16px 0 0 0',
display: 'flex',
justifyContent: 'flex-end',
gap: '8px'
}}
>
<Button variant="outline" onClick={onCancel}>
</Button>
<Button onClick={handleSubmit}>
</Button>
</div>
</SheetContent>
</Sheet>
</Drawer>
);
};

View File

@ -1,91 +1,30 @@
import React, {useState, useEffect} from 'react';
import {Card, Collapse, Tooltip, message} from 'antd';
import type {NodeCategory} from '../nodes/types';
import {
PlayCircleOutlined,
StopOutlined,
UserOutlined,
ApiOutlined,
CodeOutlined,
NodeIndexOutlined,
SplitCellsOutlined,
AppstoreOutlined,
BranchesOutlined
} from '@ant-design/icons';
import {getNodeDefinitionList} from "../nodes/nodeService";
import type {WorkflowNodeDefinition} from "../nodes/types";
import React from 'react';
import { NodeCategory } from '../types';
import { NODE_DEFINITIONS } from '../nodes';
import type { WorkflowNodeDefinition } from '../nodes/types';
// 使用 Collapse 组件,不需要解构 Panel
// 图标映射配置
const iconMap: Record<string, any> = {
'play-circle': PlayCircleOutlined,
'stop': StopOutlined,
'user': UserOutlined,
'api': ApiOutlined,
'code': CodeOutlined,
'build': CodeOutlined, // 构建任务图标
'fork': NodeIndexOutlined,
'branches': SplitCellsOutlined,
'apartment': AppstoreOutlined
};
// 节点类型到图标的映射
const typeIconMap: Record<string, any> = {
'START_EVENT': PlayCircleOutlined,
'END_EVENT': StopOutlined,
'USER_TASK': UserOutlined,
'SERVICE_TASK': ApiOutlined,
'SCRIPT_TASK': CodeOutlined,
'DEPLOY_NODE': CodeOutlined, // 构建任务节点
'EXCLUSIVE_GATEWAY': NodeIndexOutlined,
'PARALLEL_GATEWAY': SplitCellsOutlined,
'SUB_PROCESS': AppstoreOutlined,
'CALL_ACTIVITY': BranchesOutlined
};
// 节点分类配置
const categoryConfig: Record<NodeCategory, {
label: string;
key: string;
}> = {
EVENT: {label: '事件节点', key: '1'},
TASK: {label: '任务节点', key: '2'},
GATEWAY: {label: '网关节点', key: '3'},
CONTAINER: {label: '容器节点', key: '4'},
// 图标映射函数
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 {
onNodeDragStart?: (node: WorkflowNodeDefinition, e: React.DragEvent) => void,
nodeDefinitions?: WorkflowNodeDefinition[]
className?: string;
}
const NodePanel: React.FC<NodePanelProps> = ({onNodeDragStart}) => {
const [loading, setLoading] = useState(false);
const [nodeDefinitions, setNodeDefinitions] = useState<WorkflowNodeDefinition[]>([]);
// 加载节点定义列表
const loadNodeDefinitions = async () => {
setLoading(true);
try {
const data = await getNodeDefinitionList();
console.log(data)
setNodeDefinitions(data);
} catch (error) {
if (error instanceof Error) {
message.error(error.message);
}
} finally {
setLoading(false);
}
};
useEffect(() => {
loadNodeDefinitions();
}, []);
// 按分类对节点进行分组
const groupedNodes = nodeDefinitions.reduce((acc, node) => {
const NodePanel: React.FC<NodePanelProps> = ({ className = '' }) => {
// 按分类分组节点
const nodesByCategory = NODE_DEFINITIONS.reduce((acc, node) => {
if (!acc[node.category]) {
acc[node.category] = [];
}
@ -93,145 +32,156 @@ const NodePanel: React.FC<NodePanelProps> = ({onNodeDragStart}) => {
return acc;
}, {} as Record<NodeCategory, WorkflowNodeDefinition[]>);
// 处理节点拖拽开始事件
const handleDragStart = (node: WorkflowNodeDefinition, e: React.DragEvent) => {
e.dataTransfer.setData('node', JSON.stringify(node));
onNodeDragStart?.(node, e);
// 拖拽开始处理
const handleDragStart = (event: React.DragEvent, nodeDefinition: WorkflowNodeDefinition) => {
event.dataTransfer.setData('application/reactflow', JSON.stringify({
nodeType: nodeDefinition.nodeType,
nodeDefinition
}));
event.dataTransfer.effectAllowed = 'move';
};
// 渲染节点图标
const renderNodeIcon = (node: WorkflowNodeDefinition) => {
const iconName = node.uiConfig?.style.icon;
// 首先尝试使用配置的图标
let IconComponent = iconMap[iconName];
// 渲染节点项
const renderNodeItem = (nodeDefinition: WorkflowNodeDefinition) => (
<div
key={nodeDefinition.nodeCode}
draggable
style={{
display: 'flex',
alignItems: 'center',
gap: '8px',
padding: '8px 12px',
borderRadius: '6px',
border: '1px solid #e5e7eb',
background: 'white',
cursor: 'grab',
transition: 'all 0.2s ease-in-out',
marginBottom: '6px'
}}
onMouseEnter={(e) => {
e.currentTarget.style.background = '#f9fafb';
e.currentTarget.style.borderColor = nodeDefinition.uiConfig.style.fill;
e.currentTarget.style.transform = 'translateX(2px)';
}}
onMouseLeave={(e) => {
e.currentTarget.style.background = 'white';
e.currentTarget.style.borderColor = '#e5e7eb';
e.currentTarget.style.transform = 'translateX(0)';
}}
onDragStart={(e) => {
e.currentTarget.style.cursor = 'grabbing';
handleDragStart(e, nodeDefinition);
}}
onDragEnd={(e) => {
e.currentTarget.style.cursor = 'grab';
}}
>
<div style={{
fontSize: '16px',
width: '20px',
textAlign: 'center',
color: nodeDefinition.uiConfig.style.fill
}}>
{getNodeIcon(nodeDefinition.uiConfig.style.icon)}
</div>
<div>
<div style={{
fontSize: '13px',
fontWeight: '500',
color: '#374151',
lineHeight: '1.2'
}}>
{nodeDefinition.nodeName}
</div>
<div style={{
fontSize: '11px',
color: '#6b7280',
lineHeight: '1.2',
marginTop: '2px'
}}>
{nodeDefinition.description}
</div>
</div>
</div>
);
// 如果没有找到对应的图标,使用节点类型对应的默认图标
if (!IconComponent) {
IconComponent = typeIconMap[node.nodeType] || AppstoreOutlined;
}
// 分类标题映射
const categoryTitles = {
[NodeCategory.EVENT]: '🎯 事件节点',
[NodeCategory.TASK]: '📋 任务节点',
[NodeCategory.GATEWAY]: '🔀 网关节点',
[NodeCategory.CONTAINER]: '📦 容器节点'
};
return (
<IconComponent
style={{
color: node.uiConfig?.style.iconColor || '#1890ff',
fontSize: '16px',
marginRight: '6px'
}}
/>
);
};
const getNodeItemStyle = (node: WorkflowNodeDefinition) => ({
width: '100%',
padding: '10px 12px',
border: `1px solid ${node.uiConfig?.style.stroke}`,
borderRadius: '6px',
cursor: 'move',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
gap: '10px',
background: node.uiConfig?.style.fill,
transition: 'all 0.3s',
boxShadow: '0 1px 2px rgba(0,0,0,0.05)',
'&:hover': {
transform: 'translateY(-1px)',
boxShadow: '0 3px 6px rgba(0,0,0,0.1)',
}
});
const tooltipStyle = {
maxWidth: '300px'
};
const tooltipOverlayInnerStyle = {
padding: '12px 16px'
};
// 构建折叠面板的 items只包含有节点的分类
const collapseItems = Object.entries(categoryConfig)
.filter(([category]) => groupedNodes[category as NodeCategory]?.length > 0) // 过滤掉没有节点的分类
.map(([category, {label, key}]) => ({
key,
label: <span style={{fontSize: '14px', fontWeight: 500}}>{label}</span>,
children: (
<div style={{
display: 'flex',
flexDirection: 'column',
gap: '12px',
padding: '8px 4px'
}}>
{groupedNodes[category as NodeCategory]?.map(node => (
<Tooltip
key={node.nodeCode}
title={
<div>
<div style={{fontSize: '14px', fontWeight: 500}}>{node.description}</div>
</div>
}
styles={{
root: tooltipStyle,
body: tooltipOverlayInnerStyle
}}
placement="right"
arrow={false}
>
<div
draggable
onDragStart={(e) => handleDragStart(node, e)}
style={getNodeItemStyle(node)}
>
<div style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
gap: '6px'
}}>
{renderNodeIcon(node)}
<span style={{
fontSize: '13px',
color: '#262626',
<div className={`node-panel ${className}`} style={{
width: '260px',
height: '100%',
background: '#f8fafc',
borderRight: '1px solid #e5e7eb',
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
}}>{node.nodeName}({node.nodeType})</span>
display: 'flex',
flexDirection: 'column'
}}>
<div style={{
padding: '16px',
background: 'white',
borderBottom: '1px solid #e5e7eb'
}}>
<h3 style={{
margin: 0,
fontSize: '14px',
fontWeight: '600',
color: '#374151'
}}>
</h3>
<p style={{
margin: '4px 0 0 0',
fontSize: '12px',
color: '#6b7280'
}}>
</p>
</div>
<div style={{
flex: '1',
overflow: 'auto',
padding: '12px'
}}>
{Object.entries(nodesByCategory).map(([category, nodes]) => (
<div key={category} style={{ marginBottom: '16px' }}>
<div style={{
fontSize: '12px',
fontWeight: '600',
color: '#4b5563',
marginBottom: '8px',
padding: '4px 0',
borderBottom: '1px solid #e5e7eb'
}}>
{categoryTitles[category as NodeCategory]}
</div>
{nodes.map(renderNodeItem)}
</div>
</Tooltip>
))}
</div>
)
}));
return (
<Card
title="流程节点"
size="small"
loading={loading}
styles={{
header: {
padding: '12px 16px',
fontSize: '15px',
fontWeight: 500,
borderBottom: '1px solid #f0f0f0',
},
body: {
{/* 使用提示 */}
<div style={{
padding: '12px',
height: 'calc(100vh - 250px)',
overflowY: 'auto',
overflowX: 'hidden'
}
}}
>
<Collapse
defaultActiveKey={['1', '2', '3', '4']}
ghost
items={collapseItems}
style={{
background: 'transparent',
}}
/>
</Card>
background: '#f1f5f9',
borderTop: '1px solid #e5e7eb',
fontSize: '11px',
color: '#64748b',
lineHeight: '1.4'
}}>
💡
<br />
<br />
<br />
</div>
</div>
);
};

View File

@ -1,48 +0,0 @@
import React from 'react';
import NodePanel from './NodePanel';
import type { WorkflowNodeDefinition } from "../nodes/nodeService";
interface WorkflowCanvasProps {
graphContainerRef: React.RefObject<HTMLDivElement>;
minimapContainerRef: React.RefObject<HTMLDivElement>;
nodeDefinitions: NodeDefinitionResponse[];
onNodeDragStart: (node: NodeDefinitionResponse, e: React.DragEvent) => void;
onDrop: (e: React.DragEvent) => void;
onDragOver: (e: React.DragEvent) => void;
}
/**
*
*/
const WorkflowCanvas: React.FC<WorkflowCanvasProps> = ({
graphContainerRef,
minimapContainerRef,
nodeDefinitions,
onNodeDragStart,
onDrop,
onDragOver
}) => {
return (
<div className="content">
<div className="sidebar">
<NodePanel
nodeDefinitions={nodeDefinitions}
onNodeDragStart={onNodeDragStart}
/>
</div>
<div className="main-area">
<div className="workflow-container">
<div
ref={graphContainerRef}
className="workflow-canvas"
onDrop={onDrop}
onDragOver={onDragOver}
/>
<div ref={minimapContainerRef} className="minimap-container"/>
</div>
</div>
</div>
);
};
export default WorkflowCanvas;

View File

@ -1,163 +1,168 @@
import React from 'react';
import { Button, Space, Tooltip, Modal } from 'antd';
import { Button, Divider, Tooltip } from 'antd';
import {
ArrowLeftOutlined,
SaveOutlined,
CopyOutlined,
DeleteOutlined,
UndoOutlined,
RedoOutlined,
ScissorOutlined,
SnippetsOutlined,
SelectOutlined,
ZoomInOutlined,
ZoomOutOutlined,
ExpandOutlined,
ArrowLeftOutlined
} from '@ant-design/icons';
interface WorkflowToolbarProps {
title: string;
scale: number;
canUndo: boolean;
canRedo: boolean;
onBack: () => void;
onSave: () => void;
onUndo: () => void;
onRedo: () => void;
onCopy: () => void;
onCut: () => void;
onPaste: () => void;
onZoomIn: () => void;
onZoomOut: () => void;
onSelectAll: () => void;
onDelete: () => void;
title?: string;
onSave?: () => void;
onUndo?: () => void;
onRedo?: () => void;
onZoomIn?: () => void;
onZoomOut?: () => void;
onFitView?: () => void;
onBack?: () => void;
canUndo?: boolean;
canRedo?: boolean;
zoom?: number;
className?: string;
}
/**
*
*/
const WorkflowToolbar: React.FC<WorkflowToolbarProps> = ({
title,
scale,
canUndo,
canRedo,
onBack,
title = '工作流设计器',
onSave,
onUndo,
onRedo,
onCopy,
onCut,
onPaste,
onZoomIn,
onZoomOut,
onSelectAll,
onDelete
onFitView,
onBack,
canUndo = false,
canRedo = false,
zoom = 1,
className = ''
}) => {
const handleDelete = () => {
Modal.confirm({
title: '确认删除',
content: '确定要删除选中的元素吗?',
onOk: onDelete
});
};
return (
<div className="header">
<Space>
<Tooltip title="返回">
<div
className={`workflow-toolbar ${className}`}
style={{
height: '56px',
background: 'white',
borderBottom: '1px solid #e5e7eb',
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
padding: '0 16px',
boxShadow: '0 1px 2px rgba(0, 0, 0, 0.05)'
}}
>
{/* 左侧:返回按钮和标题 */}
<div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
<Tooltip title="返回列表">
<Button
className="back-button"
icon={<ArrowLeftOutlined/>}
type="text"
icon={<ArrowLeftOutlined />}
onClick={onBack}
style={{ color: '#6b7280' }}
/>
</Tooltip>
<span>{title}</span>
</Space>
<div className="actions">
<Space>
<Space.Compact>
<Tooltip title="撤销">
<Divider type="vertical" />
<h2 style={{
margin: 0,
fontSize: '16px',
fontWeight: '600',
color: '#374151'
}}>
{title}
</h2>
</div>
{/* 右侧:操作按钮区域 */}
<div style={{ display: 'flex', alignItems: 'center', gap: '6px' }}>
{/* 撤销/重做 */}
<Tooltip title="撤销 (Ctrl+Z)">
<Button
icon={<UndoOutlined/>}
type="text"
icon={<UndoOutlined />}
onClick={onUndo}
disabled={!canUndo}
>
</Button>
size="middle"
style={{
color: canUndo ? '#374151' : '#d1d5db',
}}
/>
</Tooltip>
<Tooltip title="重做">
<Tooltip title="重做 (Ctrl+Shift+Z)">
<Button
icon={<RedoOutlined/>}
type="text"
icon={<RedoOutlined />}
onClick={onRedo}
disabled={!canRedo}
>
</Button>
</Tooltip>
</Space.Compact>
<Space.Compact>
<Tooltip title="剪切">
<Button
icon={<ScissorOutlined/>}
onClick={onCut}
size="middle"
style={{
color: canRedo ? '#374151' : '#d1d5db',
}}
/>
</Tooltip>
<Tooltip title="复制">
<Divider type="vertical" style={{ margin: '0 4px' }} />
{/* 视图操作 */}
<Tooltip title="放大 (+)">
<Button
icon={<CopyOutlined/>}
onClick={onCopy}
/>
</Tooltip>
<Tooltip title="粘贴">
<Button
icon={<SnippetsOutlined/>}
onClick={onPaste}
/>
</Tooltip>
</Space.Compact>
<Space.Compact>
<Tooltip title="放大">
<Button
icon={<ZoomInOutlined/>}
type="text"
icon={<ZoomInOutlined />}
onClick={onZoomIn}
disabled={scale >= 2}
>
</Button>
size="middle"
/>
</Tooltip>
<Tooltip title="缩小">
<Tooltip title="缩小 (-)">
<Button
icon={<ZoomOutOutlined/>}
type="text"
icon={<ZoomOutOutlined />}
onClick={onZoomOut}
disabled={scale <= 0.2}
>
</Button>
</Tooltip>
</Space.Compact>
<Space.Compact>
<Tooltip title="全选">
<Button
icon={<SelectOutlined/>}
onClick={onSelectAll}
size="middle"
/>
</Tooltip>
<Tooltip title="删除">
<Tooltip title="适应视图">
<Button
icon={<DeleteOutlined/>}
onClick={handleDelete}
type="text"
icon={<ExpandOutlined />}
onClick={onFitView}
size="middle"
/>
</Tooltip>
</Space.Compact>
<Tooltip title="保存">
{/* 缩放比例显示 */}
<div style={{
background: '#f8fafc',
padding: '6px 12px',
borderRadius: '6px',
fontSize: '12px',
color: '#475569',
fontFamily: 'ui-monospace, monospace',
minWidth: '60px',
textAlign: 'center',
fontWeight: '600',
border: '1px solid #e2e8f0',
marginLeft: '4px'
}}>
{Math.round(zoom * 100)}%
</div>
<Divider type="vertical" style={{ margin: '0 8px' }} />
{/* 保存按钮(最右侧) */}
<Button
type="primary"
icon={<SaveOutlined/>}
icon={<SaveOutlined />}
onClick={onSave}
size="middle"
style={{
borderRadius: '6px',
fontWeight: '500',
boxShadow: '0 2px 4px rgba(59, 130, 246, 0.2)',
}}
>
</Button>
</Tooltip>
</Space>
</div>
</div>
);

View File

@ -1,231 +0,0 @@
import { useState, useEffect, useCallback } from 'react';
import { message } from 'antd';
import { Graph } from '@antv/x6';
import { getDefinitionDetail, saveDefinition } from '../../Definition/service';
import { getNodeDefinitionList } from '../nodes/nodeService';
import { validateWorkflow } from '../utils/validator';
import { restoreNodeFromData } from '../utils/nodeUtils';
import type { WorkflowNodeDefinition } from '../nodes/types';
/**
* Hook
*/
export const useWorkflowData = () => {
const [nodeDefinitions, setNodeDefinitions] = useState<WorkflowNodeDefinition[]>([]);
const [isNodeDefinitionsLoaded, setIsNodeDefinitionsLoaded] = useState(false);
const [definitionData, setDefinitionData] = useState<any>(null);
const [title, setTitle] = useState<string>('工作流设计');
// 加载节点定义列表
useEffect(() => {
const loadNodeDefinitions = async () => {
try {
const data = await getNodeDefinitionList();
setNodeDefinitions(data);
setIsNodeDefinitionsLoaded(true);
} catch (error) {
console.error('加载节点定义失败:', error);
message.error('加载节点定义失败');
}
};
loadNodeDefinitions();
}, []);
// 加载工作流定义详情
const loadDefinitionDetail = useCallback(async (graphInstance: Graph, definitionId: string) => {
try {
console.log('正在加载工作流定义详情, ID:', definitionId);
const response = await getDefinitionDetail(Number(definitionId));
console.log('工作流定义详情加载成功:', response);
setTitle(`工作流设计 - ${response.name}`);
setDefinitionData(response);
if (!graphInstance) {
console.error('Graph instance is not initialized');
return;
}
// 确保节点定义已加载
if (!nodeDefinitions || nodeDefinitions.length === 0) {
console.error('节点定义未加载,无法还原工作流');
return;
}
// 清空画布
graphInstance.clearCells();
const nodeMap = new Map();
// 创建节点
response.graph?.nodes?.forEach((existingNode: any) => {
console.log('正在还原节点:', existingNode.nodeType, existingNode);
// 查找节点定义
const nodeDefinition = nodeDefinitions.find(def => def.nodeType === existingNode.nodeType);
if (!nodeDefinition) {
console.error('找不到节点定义:', existingNode.nodeType);
return;
}
// 恢复节点
const node = restoreNodeFromData(graphInstance, existingNode, nodeDefinition);
if (!node) {
console.error('节点创建失败:', existingNode);
return;
}
nodeMap.set(existingNode.id, node);
console.log('节点创建成功ID:', node.id, '映射 ID:', existingNode.id);
});
console.log('所有节点创建完成nodeMap:', nodeMap);
console.log('准备创建边:', response.graph?.edges);
// 创建边
response.graph?.edges?.forEach((edge: any) => {
const sourceNode = nodeMap.get(edge.from);
const targetNode = nodeMap.get(edge.to);
if (sourceNode && targetNode) {
// 根据节点类型获取正确的端口组
const getPortByGroup = (node: any, group: string) => {
const ports = node.getPorts();
const port = ports.find((p: any) => p.group === group);
return port?.id;
};
// 获取源节点的输出端口一定是out组
const sourcePort = getPortByGroup(sourceNode, 'out');
// 获取目标节点的输入端口一定是in组
const targetPort = getPortByGroup(targetNode, 'in');
if (!sourcePort || !targetPort) {
console.error('无法找到正确的端口:', edge);
return;
}
const newEdge = graphInstance.addEdge({
source: {
cell: sourceNode.id,
port: sourcePort,
},
target: {
cell: targetNode.id,
port: targetPort,
},
vertices: edge?.vertices || [], // 恢复顶点信息
attrs: {
line: {
stroke: '#5F95FF',
strokeWidth: 2,
targetMarker: {
name: 'classic',
size: 7,
},
},
},
labels: [{
attrs: {
label: {
text: edge.config?.condition?.expression || edge.name || ''
}
}
}]
});
// 设置边的条件属性
if (edge.config?.condition) {
newEdge.setProp('condition', edge.config.condition);
}
}
});
} catch (error) {
console.error('加载工作流定义失败:', error);
message.error('加载工作流定义失败');
}
}, [nodeDefinitions]);
// 保存工作流
const saveWorkflow = useCallback(async (graph: Graph) => {
if (!graph) {
console.error('Graph 实例为空');
message.error('图形实例不存在,无法保存');
return;
}
if (!definitionData) {
console.error('definitionData 为空 - 工作流定义数据未加载');
message.error('工作流定义数据未加载,请刷新页面重试');
return;
}
try {
// 校验流程图
const validationResult = validateWorkflow(graph);
if (!validationResult.valid) {
message.error(validationResult.message);
return;
}
// 获取所有节点和边的数据 - 只保存业务数据不保存UI配置
const nodes = graph.getNodes().map(node => {
const nodeData = node.getData();
const position = node.getPosition();
return {
id: node.id,
nodeCode: nodeData.nodeCode,
nodeType: nodeData.nodeType,
nodeName: nodeData.nodeName,
position, // 只保存位置信息
configs: nodeData.configs || {}, // 节点配置数据
inputMapping: nodeData.inputMapping || {}, // 输入映射数据
outputMapping: nodeData.outputMapping || {} // 输出映射数据
};
});
const edges = graph.getEdges().map(edge => {
const condition = edge.getProp('condition');
const vertices = edge.getVertices(); // 获取边的顶点信息
return {
id: edge.id,
from: edge.getSourceCellId(),
to: edge.getTargetCellId(),
name: edge.getLabels()?.[0]?.attrs?.label?.text || '',
config: {
type: 'sequence',
condition: condition || undefined
},
vertices: vertices // 保存顶点信息
};
});
// 构建保存数据 - 只保存图形结构不保存Schema配置
const saveData = {
...definitionData,
graph: {
nodes,
edges
}
};
// 调用保存接口
await saveDefinition(saveData);
message.success('保存成功');
} catch (error) {
console.error('保存流程失败:', error);
message.error('保存流程失败');
}
}, [nodeDefinitions, definitionData]);
return {
nodeDefinitions,
isNodeDefinitionsLoaded,
definitionData,
title,
loadDefinitionDetail,
saveWorkflow
};
};

View File

@ -1,47 +0,0 @@
import { Graph } from '@antv/x6';
import { message } from 'antd';
import { createNodeFromDefinition } from '../utils/nodeUtils';
import type { WorkflowNodeDefinition } from '../nodes/types';
/**
* Hook
*/
export const useWorkflowDragDrop = (
graph: Graph | null,
nodeDefinitions: WorkflowNodeDefinition[]
) => {
// 处理节点拖拽开始
const handleNodeDragStart = (node: WorkflowNodeDefinition, e: React.DragEvent) => {
e.dataTransfer.setData('node', JSON.stringify(node));
};
// 处理节点拖拽结束
const handleDrop = (e: React.DragEvent) => {
e.preventDefault();
if (!graph) return;
try {
const nodeData = e.dataTransfer.getData('node');
if (!nodeData) return;
const nodeDefinition = JSON.parse(nodeData);
const {clientX, clientY} = e;
const point = graph.clientToLocal({x: clientX, y: clientY});
createNodeFromDefinition(graph, nodeDefinition, point);
} catch (error) {
console.error('创建节点失败:', error);
message.error('创建节点失败');
}
};
// 处理拖拽过程中
const handleDragOver = (e: React.DragEvent) => {
e.preventDefault();
};
return {
handleNodeDragStart,
handleDrop,
handleDragOver
};
};

View File

@ -1,187 +0,0 @@
import { useState, useEffect, useRef } from 'react';
import { Graph, Edge } from '@antv/x6';
import { message } from 'antd';
import { GraphInitializer } from '../utils/graph/graphInitializer';
import { EventRegistrar } from '../utils/graph/eventRegistrar';
import type { WorkflowNodeDefinition } from '../nodes/types';
/**
* Hook
*/
export const useWorkflowGraph = (
nodeDefinitions: WorkflowNodeDefinition[],
isNodeDefinitionsLoaded: boolean,
onNodeEdit: (cell: any, nodeDefinition?: WorkflowNodeDefinition) => void,
onEdgeEdit: (edge: Edge) => void
) => {
const [graph, setGraph] = useState<Graph | null>(null);
const [scale, setScale] = useState(1);
const graphContainerRef = useRef<HTMLDivElement>(null);
const minimapContainerRef = useRef<HTMLDivElement>(null);
// 初始化图形
useEffect(() => {
if (!graphContainerRef.current || !minimapContainerRef.current || !isNodeDefinitionsLoaded) {
return;
}
const initializer = new GraphInitializer(
graphContainerRef.current,
minimapContainerRef.current
);
const newGraph = initializer.initializeGraph();
if (newGraph) {
// 注册事件
const eventRegistrar = new EventRegistrar(
newGraph,
nodeDefinitions,
onNodeEdit,
onEdgeEdit
);
eventRegistrar.registerAllEvents();
setGraph(newGraph);
}
return () => {
graph?.dispose();
};
}, [isNodeDefinitionsLoaded, nodeDefinitions, onNodeEdit, onEdgeEdit]);
// 图形操作方法
const graphOperations = {
// 撤销操作
undo: () => {
if (!graph) return;
const history = (graph as any).history;
if (!history) {
console.error('History plugin not initialized');
return;
}
if (history.canUndo()) {
history.undo();
message.success('已撤销');
} else {
message.info('没有可撤销的操作');
}
},
// 重做操作
redo: () => {
if (!graph) return;
const history = (graph as any).history;
if (!history) {
console.error('History plugin not initialized');
return;
}
if (history.canRedo()) {
history.redo();
message.success('已重做');
} else {
message.info('没有可重做的操作');
}
},
// 复制
copy: () => {
if (!graph) return;
const cells = graph.getSelectedCells();
if (cells.length === 0) {
message.info('请先选择要复制的节点');
return;
}
graph.copy(cells);
message.success('已复制');
},
// 剪切
cut: () => {
if (!graph) return;
const cells = graph.getSelectedCells();
if (cells.length === 0) {
message.info('请先选择要剪切的节点');
return;
}
graph.cut(cells);
message.success('已剪切');
},
// 粘贴
paste: () => {
if (!graph) return;
if (graph.isClipboardEmpty()) {
message.info('剪贴板为空');
return;
}
const cells = graph.paste({offset: 32});
graph.cleanSelection();
graph.select(cells);
message.success('已粘贴');
},
// 缩放
zoom: (delta: number) => {
if (!graph) return;
const currentScale = graph.scale();
const newScale = Math.max(0.2, Math.min(2, currentScale.sx + delta));
graph.scale(newScale, newScale);
setScale(newScale);
},
// 全选
selectAll: () => {
if (!graph) return;
const cells = graph.getCells();
if (cells.length === 0) {
message.info('当前没有可选择的元素');
return;
}
graph.resetSelection();
graph.select(cells);
// 为选中的元素添加高亮样式
cells.forEach(cell => {
if (cell.isNode()) {
cell.setAttrByPath('body/stroke', '#1890ff');
cell.setAttrByPath('body/strokeWidth', 3);
cell.setAttrByPath('body/strokeDasharray', '5 5');
} else if (cell.isEdge()) {
cell.setAttrByPath('line/stroke', '#1890ff');
cell.setAttrByPath('line/strokeWidth', 3);
cell.setAttrByPath('line/strokeDasharray', '5 5');
}
});
},
// 删除选中元素
deleteSelected: () => {
if (!graph) return;
const cells = graph.getSelectedCells();
if (cells.length === 0) {
message.info('请先选择要删除的元素');
return;
}
graph.removeCells(cells);
},
// 检查是否可以撤销/重做
canUndo: () => {
return (graph as any)?.history?.canUndo() || false;
},
canRedo: () => {
return (graph as any)?.history?.canRedo() || false;
}
};
return {
graph,
scale,
graphContainerRef,
minimapContainerRef,
graphOperations
};
};

View File

@ -1,117 +0,0 @@
import { useState, useCallback } from 'react';
import { Cell, Edge } from '@antv/x6';
import { message } from 'antd';
import type { WorkflowNodeDefinition } from '../nodes/types';
import type { EdgeCondition } from '../nodes/types';
/**
* Hook
*/
export const useWorkflowModals = () => {
const [selectedNode, setSelectedNode] = useState<Cell | null>(null);
const [selectedNodeDefinition, setSelectedNodeDefinition] = useState<WorkflowNodeDefinition | null>(null);
const [selectedEdge, setSelectedEdge] = useState<Edge | null>(null);
const [configModalVisible, setConfigModalVisible] = useState(false);
const [expressionModalVisible, setExpressionModalVisible] = useState(false);
// 处理节点编辑
const handleNodeEdit = useCallback((cell: Cell, nodeDefinition?: WorkflowNodeDefinition) => {
setSelectedNode(cell);
setSelectedNodeDefinition(nodeDefinition || null);
setConfigModalVisible(true);
}, []);
// 处理边编辑
const handleEdgeEdit = useCallback((edge: Edge) => {
setSelectedEdge(edge);
setExpressionModalVisible(true);
}, []);
// 处理节点配置更新
const handleNodeConfigUpdate = useCallback((formData: any) => {
if (!selectedNode) return;
// 更新节点数据
const nodeData = selectedNode.getData() || {};
const updatedData = {
...nodeData,
...formData
};
selectedNode.setData(updatedData);
// 更新节点显示名称如果配置中有nodeName
if (formData.configs?.nodeName) {
selectedNode.attr('label/text', formData.configs.nodeName);
}
setConfigModalVisible(false);
message.success('节点配置已更新');
}, [selectedNode]);
// 处理条件更新
const handleConditionUpdate = useCallback((condition: EdgeCondition) => {
if (!selectedEdge) return;
// 更新边的属性
selectedEdge.setProp('condition', condition);
// 更新边的标签显示
const labelText = condition.type === 'EXPRESSION'
? condition.expression
: '默认路径';
selectedEdge.setLabels([{
attrs: {
label: {
text: labelText,
fill: '#333',
fontSize: 12
},
rect: {
fill: '#fff',
stroke: '#ccc',
rx: 3,
ry: 3,
padding: 5
}
},
position: {
distance: 0.5,
offset: 0
}
}]);
setExpressionModalVisible(false);
setSelectedEdge(null);
}, [selectedEdge]);
// 关闭节点配置弹窗
const closeNodeConfigModal = useCallback(() => {
setConfigModalVisible(false);
setSelectedNode(null);
setSelectedNodeDefinition(null);
}, []);
// 关闭条件配置弹窗
const closeExpressionModal = useCallback(() => {
setExpressionModalVisible(false);
setSelectedEdge(null);
}, []);
return {
// 状态
selectedNode,
selectedNodeDefinition,
selectedEdge,
configModalVisible,
expressionModalVisible,
// 处理函数
handleNodeEdit,
handleEdgeEdit,
handleNodeConfigUpdate,
handleConditionUpdate,
closeNodeConfigModal,
closeExpressionModal
};
};

View File

@ -1,171 +1,267 @@
.workflow-design {
position: relative;
height: calc(100vh - 184px);
// 工作流设计器 - React Flow版本
// 变量定义
@header-height: 64px;
@toolbar-height: 56px;
@sidebar-width: 260px;
@container-padding: 24px;
// 主容器 - 覆盖父容器样式
.workflow-design-container {
// 使用负边距抵消父容器的padding
margin: -@container-padding;
// 精确计算高度:视口高度 - header高度
height: calc(100vh - @header-height);
// 补偿左右padding
width: calc(100% + @container-padding * 2);
display: flex;
flex-direction: column;
background: #f8fafc;
overflow: hidden;
background: #ffffff;
position: relative;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
.header {
padding: 16px 24px;
border-bottom: 1px solid #f0f0f0;
background: #ffffff;
display: flex;
justify-content: space-between;
align-items: center;
flex-shrink: 0;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
// React Flow 样式覆盖
.react-flow {
width: 100% !important;
height: 100% !important;
.back-button {
margin-right: 16px;
&__renderer {
width: 100% !important;
height: 100% !important;
}
.actions {
.ant-space-compact {
margin-right: 8px;
}
}
}
.content {
flex: 1;
min-height: 0;
display: flex;
padding: 16px;
gap: 16px;
overflow: hidden; // 防止外层出现滚动条
.sidebar {
width: 280px;
flex-shrink: 0;
border-radius: 8px;
background: #ffffff;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
border: 1px solid #f0f0f0;
display: flex;
flex-direction: column;
overflow: hidden;
:global {
.ant-collapse {
border: none;
background: transparent;
flex: 1;
overflow-y: auto; // 只在折叠面板内部显示滚动条
.ant-collapse-item {
border-radius: 0;
.ant-collapse-header {
padding: 8px 16px;
}
.ant-collapse-content-box {
padding: 0;
}
}
}
}
.node-item {
padding: 8px 16px;
margin: 4px 8px;
border: 1px solid #d9d9d9;
border-radius: 4px;
cursor: move;
transition: all 0.3s;
background: #ffffff;
// 自定义控制按钮样式
&__controls {
button {
background: rgba(255, 255, 255, 0.95);
border: 1px solid #e5e7eb;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
transition: all 0.2s ease-in-out;
&:hover {
background: #f5f5f5;
border-color: #1890ff;
background: #f9fafb;
border-color: #3b82f6;
}
}
}
.main-area {
// 小地图样式
&__minimap {
background: #f9fafb;
border: 1px solid #e5e7eb;
border-radius: 8px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
}
// 内容区域
.workflow-content-area {
flex: 1;
display: flex;
flex-direction: column;
min-width: 0;
overflow: hidden;
position: relative;
min-height: 0;
.workflow-container {
// 节点面板区域
.node-panel {
width: @sidebar-width;
background: #f8fafc;
border-right: 1px solid #e5e7eb;
overflow: hidden;
display: flex;
flex-direction: column;
box-shadow: 2px 0 4px rgba(0, 0, 0, 0.05);
// 面板头部
.panel-header {
padding: 16px;
background: #ffffff;
border-bottom: 1px solid #e5e7eb;
h3 {
margin: 0;
font-size: 14px;
font-weight: 600;
color: #374151;
}
p {
margin: 4px 0 0 0;
font-size: 12px;
color: #6b7280;
}
}
// 节点分组
.node-group {
margin-bottom: 16px;
.group-title {
font-size: 12px;
font-weight: 600;
color: #4b5563;
margin-bottom: 8px;
padding: 4px 0;
border-bottom: 1px solid #e5e7eb;
}
// 节点项
.node-item {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 12px;
border-radius: 6px;
border: 1px solid #e5e7eb;
background: #ffffff;
cursor: grab;
transition: all 0.2s ease-in-out;
margin-bottom: 6px;
&:hover {
background: #f9fafb;
border-color: var(--node-color, #3b82f6);
transform: translateX(2px);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
&:active {
cursor: grabbing;
}
.node-icon {
font-size: 16px;
width: 20px;
text-align: center;
}
.node-info {
.node-name {
font-size: 13px;
font-weight: 500;
color: #374151;
line-height: 1.2;
}
.node-desc {
font-size: 11px;
color: #6b7280;
line-height: 1.2;
margin-top: 2px;
}
}
}
}
// 使用提示
.panel-tips {
padding: 12px;
background: #f1f5f9;
border-top: 1px solid #e5e7eb;
font-size: 11px;
color: #64748b;
line-height: 1.4;
}
}
// 画布区域
.workflow-canvas-area {
flex: 1;
position: relative;
border: 1px solid #d9d9d9;
border-radius: 4px;
background: #ffffff;
overflow: hidden;
background: #f8fafc;
.workflow-canvas {
width: 100%;
height: 100%;
}
.minimap-container {
// 画布状态面板
.canvas-status {
position: absolute;
right: 20px;
bottom: 20px;
width: var(--minimap-width);
height: var(--minimap-height);
border: 1px solid #f0f0f0;
border-radius: 4px;
background: #fff;
top: 12px;
left: 12px;
background: rgba(255, 255, 255, 0.95);
padding: 8px 12px;
border-radius: 6px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
z-index: 1;
overflow: hidden;
}
font-size: 12px;
color: #6b7280;
z-index: 10;
backdrop-filter: blur(4px);
}
}
}
:global {
.node-selected {
> rect {
stroke: #1890ff;
stroke-width: 2px;
}
> path {
stroke: #1890ff;
stroke-width: 2px;
// 响应式适配
@media (max-width: 1024px) {
.workflow-content-area .node-panel {
width: 220px;
}
}
.x6-node-selected {
rect, ellipse {
stroke: #1890ff;
stroke-width: 2px;
}
}
@media (max-width: 768px) {
.workflow-content-area {
flex-direction: column;
.x6-edge-selected {
path {
stroke: #1890ff;
stroke-width: 2px !important;
}
}
// 右键菜单样式
.x6-context-menu {
background: #fff;
border-radius: 4px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
padding: 4px 0;
min-width: 120px;
&-item {
padding: 5px 16px;
cursor: pointer;
user-select: none;
transition: all 0.3s;
color: rgba(0, 0, 0, 0.85);
font-size: 14px;
&:hover {
background: #f5f5f5;
}
.node-panel {
width: 100%;
height: 200px;
border-right: none;
border-bottom: 1px solid #e5e7eb;
}
}
}
}
// 工具栏特殊样式
.workflow-toolbar {
height: @toolbar-height;
background: #ffffff;
border-bottom: 1px solid #e5e7eb;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 16px;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
flex-shrink: 0;
.toolbar-section {
display: flex;
align-items: center;
gap: 12px;
}
.toolbar-title {
h2 {
margin: 0;
font-size: 16px;
font-weight: 600;
color: #374151;
}
.subtitle {
font-size: 12px;
color: #6b7280;
margin-top: 2px;
}
}
.toolbar-status {
display: flex;
align-items: center;
gap: 12px;
font-size: 12px;
color: #6b7280;
.status-item {
background: #f3f4f6;
padding: 4px 8px;
border-radius: 4px;
font-family: 'Monaco', 'Menlo', monospace;
}
.react-flow-badge {
background: #ecfdf5;
color: #065f46;
padding: 4px 8px;
border-radius: 4px;
font-weight: 500;
}
}
}

View File

@ -1,148 +1,617 @@
import React, { useEffect } from 'react';
import React, { useState, useCallback, useRef, useEffect } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import { message } from 'antd';
import { ReactFlowProvider, useReactFlow } from '@xyflow/react';
import WorkflowToolbar from './components/WorkflowToolbar';
import WorkflowCanvas from './components/WorkflowCanvas';
import NodeConfigDrawer from './components/NodeConfigModal';
import ExpressionModal from './components/ExpressionModal';
import { useWorkflowGraph } from './hooks/useWorkflowGraph';
import { useWorkflowData } from './hooks/useWorkflowData';
import { useWorkflowModals } from './hooks/useWorkflowModals';
import { useWorkflowDragDrop } from './hooks/useWorkflowDragDrop';
import NodePanel from './components/NodePanel';
import FlowCanvas from './components/FlowCanvas';
import NodeConfigModal from './components/NodeConfigModal';
import EdgeConfigModal, { type EdgeCondition } from './components/EdgeConfigModal';
import type { FlowNode, FlowEdge, FlowNodeData } from './types';
import type { WorkflowNodeDefinition } from './nodes/types';
import { NodeType } from './types';
import { useWorkflowSave } from './hooks/useWorkflowSave';
import { useWorkflowLoad } from './hooks/useWorkflowLoad';
import { useHistory } from './hooks/useHistory';
// 样式
import '@xyflow/react/dist/style.css';
import './index.less';
/**
*
*/
const WorkflowDesign: React.FC = () => {
const WorkflowDesignInner: React.FC = () => {
const { id } = useParams<{ id: string }>();
const navigate = useNavigate();
// 数据管理
const {
nodeDefinitions,
isNodeDefinitionsLoaded,
title,
loadDefinitionDetail,
saveWorkflow
} = useWorkflowData();
getNodes,
setNodes,
getEdges,
setEdges,
screenToFlowPosition,
fitView,
zoomIn,
zoomOut,
getZoom
} = useReactFlow();
// 弹窗管理
const {
selectedNode,
selectedNodeDefinition,
selectedEdge,
configModalVisible,
expressionModalVisible,
handleNodeEdit,
handleEdgeEdit,
handleNodeConfigUpdate,
handleConditionUpdate,
closeNodeConfigModal,
closeExpressionModal
} = useWorkflowModals();
const [workflowTitle, setWorkflowTitle] = useState('新建工作流');
const [currentZoom, setCurrentZoom] = useState(1); // 当前缩放比例
const reactFlowWrapper = useRef<HTMLDivElement>(null);
// 图形管理
const {
graph,
scale,
graphContainerRef,
minimapContainerRef,
graphOperations
} = useWorkflowGraph(
nodeDefinitions,
isNodeDefinitionsLoaded,
handleNodeEdit,
handleEdgeEdit
);
// 当前工作流ID
const currentWorkflowId = id ? parseInt(id) : undefined;
// 拖拽管理
const {
handleNodeDragStart,
handleDrop,
handleDragOver
} = useWorkflowDragDrop(graph, nodeDefinitions);
// 节点配置模态框状态
const [configModalVisible, setConfigModalVisible] = useState(false);
const [configNode, setConfigNode] = useState<FlowNode | null>(null);
// 边配置模态框状态
const [edgeConfigModalVisible, setEdgeConfigModalVisible] = useState(false);
const [configEdge, setConfigEdge] = useState<FlowEdge | null>(null);
// 保存和加载hooks
const { hasUnsavedChanges, saveWorkflow, markUnsaved } = useWorkflowSave();
const { workflowDefinition, loadWorkflow } = useWorkflowLoad();
// 历史记录管理
const history = useHistory();
// 剪贴板(用于复制粘贴)
const clipboard = useRef<{ nodes: FlowNode[]; edges: FlowEdge[] } | null>(null);
// 加载工作流数据
useEffect(() => {
console.log('工作流数据加载检查:', {
hasGraph: !!graph,
hasId: !!id,
id,
isNodeDefinitionsLoaded
const loadData = async () => {
if (currentWorkflowId) {
const data = await loadWorkflow(currentWorkflowId);
if (data) {
setNodes(data.nodes);
setEdges(data.edges);
setWorkflowTitle(data.definition.name);
}
}
};
loadData();
}, [currentWorkflowId, loadWorkflow, setNodes, setEdges]);
// 初始化缩放比例
useEffect(() => {
setCurrentZoom(getZoom());
}, [getZoom]);
// 自动适应视图
useEffect(() => {
// 延迟执行fitView以确保节点已渲染
const timer = setTimeout(() => {
fitView({
padding: 0.1,
duration: 800,
minZoom: 1.0, // 最小缩放100%
maxZoom: 1.0 // 最大缩放100%确保默认100%
});
// 更新zoom显示
setTimeout(() => setCurrentZoom(getZoom()), 850);
}, 100);
if (!id) {
console.error('工作流ID缺失无法加载定义数据');
return;
}
return () => clearTimeout(timer);
}, [fitView, getZoom]);
if (graph && id && isNodeDefinitionsLoaded) {
console.log('开始加载工作流定义详情:', id);
loadDefinitionDetail(graph, id);
// 初始化示例节点 - 优化位置和布局
const initialNodes: FlowNode[] = [
{
id: '1',
type: 'START_EVENT',
position: { x: 250, y: 50 },
data: {
label: '开始',
nodeType: NodeType.START_EVENT,
category: 'EVENT' as any,
icon: '▶️',
color: '#10b981'
}
}, [graph, id, isNodeDefinitionsLoaded, loadDefinitionDetail]);
},
{
id: '2',
type: 'USER_TASK',
position: { x: 200, y: 150 },
data: {
label: '用户审批',
nodeType: NodeType.USER_TASK,
category: 'TASK' as any,
icon: '👤',
color: '#6366f1'
}
},
{
id: '3',
type: 'END_EVENT',
position: { x: 250, y: 250 },
data: {
label: '结束',
nodeType: NodeType.END_EVENT,
category: 'EVENT' as any,
icon: '⏹️',
color: '#ef4444'
}
}
];
const initialEdges: FlowEdge[] = [
{
id: 'e1-2',
source: '1',
target: '2',
type: 'default',
animated: true,
data: {
label: '提交',
condition: {
type: 'DEFAULT',
priority: 0
}
}
},
{
id: 'e2-3',
source: '2',
target: '3',
type: 'default',
animated: true,
data: {
label: '通过',
condition: {
type: 'DEFAULT',
priority: 0
}
}
}
];
// 工具栏事件处理
const handleBack = () => navigate('/workflow/definition');
const handleSave = () => {
if (!graph) {
console.error('Graph 实例不存在');
const handleSave = useCallback(async () => {
const nodes = getNodes() as FlowNode[];
const edges = getEdges() as FlowEdge[];
const success = await saveWorkflow({
nodes,
edges,
workflowId: currentWorkflowId,
name: workflowTitle,
description: workflowDefinition?.description || '',
definitionData: workflowDefinition // 传递原始定义数据
});
if (success) {
console.log('保存工作流成功:', { nodes, edges });
}
}, [getNodes, getEdges, saveWorkflow, currentWorkflowId, workflowTitle, workflowDefinition]);
const handleBack = useCallback(() => {
navigate('/workflow/definition');
}, [navigate]);
// 撤销操作
const handleUndo = useCallback(() => {
const state = history.undo();
if (state) {
history.pauseRecording();
setNodes(state.nodes);
setEdges(state.edges);
setTimeout(() => history.resumeRecording(), 100);
message.success('已撤销');
} else {
message.info('没有可撤销的操作');
}
}, [history, setNodes, setEdges]);
// 重做操作
const handleRedo = useCallback(() => {
const state = history.redo();
if (state) {
history.pauseRecording();
setNodes(state.nodes);
setEdges(state.edges);
setTimeout(() => history.resumeRecording(), 100);
message.success('已重做');
} else {
message.info('没有可重做的操作');
}
}, [history, setNodes, setEdges]);
// 复制选中的节点和边
const handleCopy = useCallback(() => {
const selectedNodes = getNodes().filter(node => node.selected);
const selectedEdges = getEdges().filter(edge => {
// 只复制两端都被选中的边
return selectedNodes.some(n => n.id === edge.source) &&
selectedNodes.some(n => n.id === edge.target);
});
if (selectedNodes.length > 0) {
clipboard.current = {
nodes: JSON.parse(JSON.stringify(selectedNodes)),
edges: JSON.parse(JSON.stringify(selectedEdges))
};
message.success(`已复制 ${selectedNodes.length} 个节点`);
} else {
message.warning('请先选择要复制的节点');
}
}, [getNodes, getEdges]);
// 粘贴节点
const handlePaste = useCallback(() => {
if (!clipboard.current || clipboard.current.nodes.length === 0) {
message.info('剪贴板为空');
return;
}
saveWorkflow(graph);
const { nodes: copiedNodes, edges: copiedEdges } = clipboard.current;
const offset = 50; // 粘贴偏移量
const idMap = new Map<string, string>();
// 创建新节点(带偏移)
const newNodes = copiedNodes.map(node => {
const newId = `${node.type}-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
idMap.set(node.id, newId);
return {
...node,
id: newId,
position: {
x: node.position.x + offset,
y: node.position.y + offset
},
selected: true
};
const handleZoomIn = () => graphOperations.zoom(0.1);
const handleZoomOut = () => graphOperations.zoom(-0.1);
});
// 创建新边更新source和target为新ID
const newEdges = copiedEdges.map(edge => {
const newSource = idMap.get(edge.source);
const newTarget = idMap.get(edge.target);
if (!newSource || !newTarget) return null;
return {
...edge,
id: `e${newSource}-${newTarget}`,
source: newSource,
target: newTarget,
selected: true
};
}).filter(edge => edge !== null) as FlowEdge[];
// 取消其他元素的选中状态
setNodes(nodes => [
...nodes.map(n => ({ ...n, selected: false })),
...newNodes
]);
setEdges(edges => [
...edges.map(e => ({ ...e, selected: false })),
...newEdges
]);
message.success(`已粘贴 ${newNodes.length} 个节点`);
}, [setNodes, setEdges]);
const handleDelete = useCallback(() => {
const selectedNodes = getNodes().filter(node => node.selected);
const selectedEdges = getEdges().filter(edge => edge.selected);
if (selectedNodes.length > 0 || selectedEdges.length > 0) {
setNodes(nodes => nodes.filter(node => !node.selected));
setEdges(edges => edges.filter(edge => !edge.selected));
message.success(`已删除 ${selectedNodes.length} 个节点和 ${selectedEdges.length} 条连接`);
markUnsaved();
} else {
message.warning('请先选择要删除的元素');
}
}, [getNodes, getEdges, setNodes, setEdges, markUnsaved]);
const handleSelectAll = useCallback(() => {
setNodes(nodes => nodes.map(node => ({ ...node, selected: true })));
setEdges(edges => edges.map(edge => ({ ...edge, selected: true })));
message.info('已全选所有元素');
}, [setNodes, setEdges]);
const handleFitView = useCallback(() => {
fitView({ padding: 0.2, duration: 800 });
// 延迟更新zoom值以获取最新的缩放比例
setTimeout(() => setCurrentZoom(getZoom()), 850);
}, [fitView, getZoom]);
const handleZoomIn = useCallback(() => {
zoomIn({ duration: 300 });
// 延迟更新zoom值以获取最新的缩放比例
setTimeout(() => setCurrentZoom(getZoom()), 350);
}, [zoomIn, getZoom]);
const handleZoomOut = useCallback(() => {
zoomOut({ duration: 300 });
// 延迟更新zoom值以获取最新的缩放比例
setTimeout(() => setCurrentZoom(getZoom()), 350);
}, [zoomOut, getZoom]);
// 处理节点拖拽放置 - 使用官方推荐的screenToFlowPosition方法
const handleDrop = useCallback((event: React.DragEvent) => {
event.preventDefault();
const dragData = event.dataTransfer.getData('application/reactflow');
if (!dragData) return;
try {
const { nodeType, nodeDefinition }: { nodeType: string; nodeDefinition: WorkflowNodeDefinition } = JSON.parse(dragData);
// 根据React Flow官方文档screenToFlowPosition会自动处理所有边界计算
// 不需要手动减去容器边界!
const position = screenToFlowPosition({
x: event.clientX,
y: event.clientY,
});
const newNode: FlowNode = {
id: `${nodeType}-${Date.now()}`,
type: nodeType,
position,
data: {
label: nodeDefinition.nodeName,
nodeType: nodeDefinition.nodeType,
category: nodeDefinition.category,
icon: nodeDefinition.uiConfig.style.icon,
color: nodeDefinition.uiConfig.style.fill,
// 保存原始节点定义引用,用于配置
nodeDefinition,
// 初始化配置数据
configs: {
nodeName: nodeDefinition.nodeName,
nodeCode: nodeDefinition.nodeCode,
description: nodeDefinition.description
},
inputMapping: {},
outputMapping: {}
}
};
setNodes(nodes => [...nodes, newNode]);
message.success(`已添加 ${nodeDefinition.nodeName} 节点`);
} catch (error) {
console.error('解析拖拽数据失败:', error);
message.error('添加节点失败');
}
}, [screenToFlowPosition, setNodes]);
// 处理节点双击 - 打开配置面板
const handleNodeClick = useCallback((event: React.MouseEvent, node: FlowNode) => {
// 只处理双击事件
if (event.detail === 2) {
console.log('双击节点,打开配置:', node);
setConfigNode(node);
setConfigModalVisible(true);
}
}, []);
// 处理边双击 - 打开条件配置弹窗
const handleEdgeClick = useCallback((event: React.MouseEvent, edge: FlowEdge) => {
if (event.detail === 2) {
console.log('双击边,打开配置:', edge);
setConfigEdge(edge);
setEdgeConfigModalVisible(true);
}
}, []);
// 处理节点配置更新
const handleNodeConfigUpdate = useCallback((nodeId: string, updatedData: Partial<FlowNodeData>) => {
setNodes((nodes) =>
nodes.map((node) =>
node.id === nodeId
? {
...node,
data: {
...node.data,
...updatedData,
},
}
: node
)
);
markUnsaved(); // 标记有未保存更改
}, [setNodes, markUnsaved]);
// 关闭节点配置模态框
const handleCloseConfigModal = useCallback(() => {
setConfigModalVisible(false);
setConfigNode(null);
}, []);
// 处理边条件更新
const handleEdgeConditionUpdate = useCallback((edgeId: string, condition: EdgeCondition) => {
setEdges((edges) =>
edges.map((edge) => {
if (edge.id === edgeId) {
// 根据条件类型生成显示文本
const label = condition.type === 'EXPRESSION'
? condition.expression
: '默认路径';
return {
...edge,
data: {
...edge.data,
condition,
},
label,
};
}
return edge;
})
);
markUnsaved(); // 标记有未保存更改
message.success('边条件配置已更新');
setEdgeConfigModalVisible(false);
setConfigEdge(null);
}, [setEdges, markUnsaved]);
// 关闭边配置模态框
const handleCloseEdgeConfigModal = useCallback(() => {
setEdgeConfigModalVisible(false);
setConfigEdge(null);
}, []);
// 监听视图变化(缩放、平移等)
const handleViewportChange = useCallback(() => {
const zoom = getZoom();
setCurrentZoom(zoom);
}, [getZoom]);
// 监听节点和边的变化,记录到历史
useEffect(() => {
const nodes = getNodes() as FlowNode[];
const edges = getEdges() as FlowEdge[];
// 只在有节点或边时记录(避免空状态)
if (nodes.length > 0 || edges.length > 0) {
history.record(nodes, edges);
}
}, [getNodes, getEdges, history]);
// 键盘快捷键支持
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 - 撤销(仅在画布区域)
if (ctrlKey && e.key === 'z' && !e.shiftKey) {
if (!shouldSkipShortcut) {
e.preventDefault();
handleUndo();
}
}
// Ctrl+Shift+Z / Cmd+Shift+Z - 重做(仅在画布区域)
else if (ctrlKey && e.key === 'z' && e.shiftKey) {
if (!shouldSkipShortcut) {
e.preventDefault();
handleRedo();
}
}
// Ctrl+C / Cmd+C - 复制节点(仅在画布区域)
else if (ctrlKey && e.key === 'c') {
if (!shouldSkipShortcut) {
e.preventDefault();
handleCopy();
}
}
// Ctrl+V / Cmd+V - 粘贴节点(仅在画布区域)
else if (ctrlKey && e.key === 'v') {
if (!shouldSkipShortcut) {
e.preventDefault();
handlePaste();
}
}
// Ctrl+A / Cmd+A - 全选节点(仅在画布区域)
else if (ctrlKey && e.key === 'a') {
if (!shouldSkipShortcut) {
e.preventDefault();
handleSelectAll();
}
}
// Delete / Backspace - 删除节点(仅在画布区域)
else if (e.key === 'Delete' || e.key === 'Backspace') {
if (!shouldSkipShortcut) {
e.preventDefault();
handleDelete();
}
}
};
window.addEventListener('keydown', handleKeyDown);
return () => window.removeEventListener('keydown', handleKeyDown);
}, [handleUndo, handleRedo, handleCopy, handlePaste, handleSelectAll, handleDelete]);
return (
<div className="workflow-design">
<div
className="workflow-design-container"
style={{
// 确保覆盖父容器的overflow设置
overflow: 'hidden'
}}
>
{/* 工具栏 */}
<WorkflowToolbar
title={title}
scale={scale}
canUndo={graphOperations.canUndo()}
canRedo={graphOperations.canRedo()}
onBack={handleBack}
title={`${workflowTitle}${hasUnsavedChanges ? ' *' : ''}`}
onSave={handleSave}
onUndo={graphOperations.undo}
onRedo={graphOperations.redo}
onCopy={graphOperations.copy}
onCut={graphOperations.cut}
onPaste={graphOperations.paste}
onBack={handleBack}
onUndo={handleUndo}
onRedo={handleRedo}
onZoomIn={handleZoomIn}
onZoomOut={handleZoomOut}
onSelectAll={graphOperations.selectAll}
onDelete={graphOperations.deleteSelected}
onFitView={handleFitView}
canUndo={history.canUndo}
canRedo={history.canRedo}
zoom={currentZoom}
/>
<WorkflowCanvas
graphContainerRef={graphContainerRef}
minimapContainerRef={minimapContainerRef}
nodeDefinitions={nodeDefinitions}
onNodeDragStart={handleNodeDragStart}
{/* 主要内容区域 */}
<div className="workflow-content-area">
{/* 节点面板 */}
<NodePanel />
{/* 画布区域 */}
<div
ref={reactFlowWrapper}
className="workflow-canvas-area"
>
<FlowCanvas
initialNodes={initialNodes}
initialEdges={initialEdges}
onNodeClick={handleNodeClick}
onEdgeClick={handleEdgeClick}
onDrop={handleDrop}
onDragOver={handleDragOver}
onViewportChange={handleViewportChange}
className="workflow-canvas"
/>
</div>
</div>
{configModalVisible && selectedNode && selectedNodeDefinition && (
<NodeConfigDrawer
{/* 节点配置弹窗 */}
<NodeConfigModal
visible={configModalVisible}
node={selectedNode}
nodeDefinition={selectedNodeDefinition}
onCancel={closeNodeConfigModal}
node={configNode}
onCancel={handleCloseConfigModal}
onOk={handleNodeConfigUpdate}
/>
)}
{selectedEdge && (
<ExpressionModal
visible={expressionModalVisible}
edge={selectedEdge}
onOk={handleConditionUpdate}
onCancel={closeExpressionModal}
{/* 边条件配置弹窗 */}
<EdgeConfigModal
visible={edgeConfigModalVisible}
edge={configEdge}
onOk={handleEdgeConditionUpdate}
onCancel={handleCloseEdgeConfigModal}
/>
)}
</div>
);
};
const WorkflowDesign: React.FC = () => {
return (
<ReactFlowProvider>
<WorkflowDesignInner />
</ReactFlowProvider>
);
};
export default WorkflowDesign;

View File

@ -1,234 +0,0 @@
import { ConfigurableNodeDefinition, NodeType, NodeCategory } from '../types';
/**
*
*
*/
export const DeployNode: ConfigurableNodeDefinition = {
nodeCode: "DEPLOY_NODE",
nodeName: "构建任务",
nodeType: NodeType.DEPLOY_NODE,
category: NodeCategory.TASK,
description: "执行应用构建和部署任务",
// UI 配置 - 节点在画布上的显示样式(现代化设计)
uiConfig: {
shape: 'rect',
size: {
width: 160,
height: 80
},
style: {
fill: '#1890ff',
stroke: '#0050b3',
strokeWidth: 2,
icon: 'build',
iconColor: '#fff'
},
ports: {
groups: {
// 输入端口 - 现代化样式
in: {
position: 'left',
attrs: {
circle: {
r: 7,
fill: '#ffffff',
stroke: '#3b82f6',
strokeWidth: 2.5,
// 现代化端口样式
filter: 'drop-shadow(0 2px 4px rgba(59, 130, 246, 0.3))',
// 端口悬浮效果
':hover': {
r: 8,
fill: '#dbeafe',
stroke: '#2563eb'
}
}
}
},
// 输出端口 - 现代化样式
out: {
position: 'right',
attrs: {
circle: {
r: 7,
fill: '#ffffff',
stroke: '#3b82f6',
strokeWidth: 2.5,
// 现代化端口样式
filter: 'drop-shadow(0 2px 4px rgba(59, 130, 246, 0.3))',
// 端口悬浮效果
':hover': {
r: 8,
fill: '#dbeafe',
stroke: '#2563eb'
}
}
}
}
}
}
},
// 基本配置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

@ -1,72 +0,0 @@
import { BaseNodeDefinition, NodeType, NodeCategory } from '../types';
/**
*
*
*/
export const EndEventNode: BaseNodeDefinition = {
nodeCode: "END_EVENT",
nodeName: "结束",
nodeType: NodeType.END_EVENT,
category: NodeCategory.EVENT,
description: "工作流的结束节点",
// UI 配置 - 节点在画布上的显示样式(现代化设计)
uiConfig: {
shape: 'ellipse',
size: {
width: 80,
height: 50
},
style: {
fill: '#f5222d',
stroke: '#cf1322',
strokeWidth: 2,
icon: 'stop',
iconColor: '#fff'
},
ports: {
groups: {
// 结束节点只有输入端口 - 现代化样式
in: {
position: 'left',
attrs: {
circle: {
r: 4,
fill: '#fff',
stroke: '#f5222d'
}
}
}
}
}
},
// 基本配置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

@ -1,271 +0,0 @@
import { ConfigurableNodeDefinition, NodeType, NodeCategory } from '../types';
/**
* Jenkins构建节点定义
*
*/
export const JenkinsBuildNode: ConfigurableNodeDefinition = {
nodeCode: "JENKINS_BUILD",
nodeName: "Jenkins构建",
nodeType: NodeType.JENKINS_BUILD,
category: NodeCategory.TASK,
description: "通过Jenkins执行构建任务",
// UI 配置 - 节点在画布上的显示样式
uiConfig: {
shape: 'rect',
size: {
width: 160,
height: 80
},
style: {
fill: '#52c41a',
stroke: '#389e08',
strokeWidth: 2,
icon: 'jenkins',
iconColor: '#fff'
},
ports: {
groups: {
// 输入端口
in: {
position: 'left',
attrs: {
circle: {
r: 4,
fill: '#fff',
stroke: '#52c41a'
}
}
},
// 输出端口
out: {
position: 'right',
attrs: {
circle: {
r: 4,
fill: '#fff',
stroke: '#52c41a'
}
}
}
}
}
},
// 基本配置Schema - 设计时用于生成表单保存时转为key/value包含基本信息和Jenkins配置
configSchema: {
type: "object",
title: "基本配置",
description: "节点的基本信息和Jenkins构建配置参数",
properties: {
nodeName: {
type: "string",
title: "节点名称",
description: "节点在流程图中显示的名称",
default: "Jenkins构建"
},
nodeCode: {
type: "string",
title: "节点编码",
description: "节点的唯一标识符",
default: "JENKINS_BUILD"
},
description: {
type: "string",
title: "节点描述",
description: "节点的详细说明",
default: "通过Jenkins执行构建任务"
},
jenkinsUrl: {
type: "string",
title: "Jenkins服务器地址",
description: "Jenkins服务器的完整URL地址",
default: "http://jenkins.example.com:8080",
format: "uri"
},
jobName: {
type: "string",
title: "Job名称",
description: "要执行的Jenkins Job名称",
default: ""
},
username: {
type: "string",
title: "用户名",
description: "Jenkins认证用户名",
default: ""
},
apiToken: {
type: "string",
title: "API Token",
description: "Jenkins API Token或密码",
default: "",
format: "password"
},
timeout: {
type: "number",
title: "超时时间(秒)",
description: "构建超时时间",
default: 600,
minimum: 60,
maximum: 7200
},
waitForCompletion: {
type: "boolean",
title: "等待构建完成",
description: "是否等待Jenkins构建完成",
default: true
},
retryCount: {
type: "number",
title: "重试次数",
description: "构建失败时的重试次数",
default: 1,
minimum: 0,
maximum: 5
},
branchName: {
type: "string",
title: "分支名称",
description: "要构建的Git分支名称",
default: "main"
}
},
required: ["nodeName", "nodeCode", "jenkinsUrl", "jobName", "username", "apiToken"]
},
// 输入映射Schema - 定义从上游节点接收的数据
inputMappingSchema: {
type: "object",
title: "输入映射",
description: "从上游节点接收的数据映射配置",
properties: {
sourceCodeUrl: {
type: "string",
title: "源代码仓库地址",
description: "Git仓库的URL地址",
default: "${upstream.gitUrl}"
},
buildParameters: {
type: "object",
title: "构建参数",
description: "传递给Jenkins Job的构建参数",
properties: {},
additionalProperties: true,
default: {}
},
environmentVariables: {
type: "object",
title: "环境变量",
description: "构建时的环境变量",
properties: {},
additionalProperties: true,
default: {}
},
commitId: {
type: "string",
title: "提交ID",
description: "要构建的Git提交ID",
default: "${upstream.commitId}"
},
projectName: {
type: "string",
title: "项目名称",
description: "项目的名称标识",
default: "${upstream.projectName}"
},
buildVersion: {
type: "string",
title: "构建版本",
description: "构建版本号",
default: "${upstream.version}"
}
},
required: ["sourceCodeUrl", "buildParameters"]
},
// 输出映射Schema - 定义传递给下游节点的数据
outputMappingSchema: {
type: "object",
title: "输出映射",
description: "传递给下游节点的数据映射配置",
properties: {
buildId: {
type: "string",
title: "构建ID",
description: "Jenkins构建的唯一标识符",
default: "${jenkins.buildNumber}"
},
buildStatus: {
type: "string",
title: "构建状态",
description: "构建完成状态",
enum: ["SUCCESS", "FAILURE", "UNSTABLE", "ABORTED", "NOT_BUILT"],
default: "SUCCESS"
},
buildUrl: {
type: "string",
title: "构建URL",
description: "Jenkins构建页面的URL",
default: "${jenkins.buildUrl}"
},
buildDuration: {
type: "number",
title: "构建耗时",
description: "构建耗时(毫秒)",
default: 0
},
artifactsUrl: {
type: "string",
title: "构建产物URL",
description: "构建产物的下载地址",
default: "${jenkins.artifactsUrl}"
},
consoleLog: {
type: "string",
title: "控制台日志",
description: "构建的控制台输出日志",
default: "${jenkins.consoleText}"
},
testResults: {
type: "object",
title: "测试结果",
description: "构建中的测试结果统计",
properties: {
totalCount: { type: "number", title: "总测试数", default: 0 },
passCount: { type: "number", title: "通过数", default: 0 },
failCount: { type: "number", title: "失败数", default: 0 },
skipCount: { type: "number", title: "跳过数", default: 0 }
},
default: {
totalCount: 0,
passCount: 0,
failCount: 0,
skipCount: 0
}
},
buildParameters: {
type: "object",
title: "构建参数",
description: "实际使用的构建参数",
properties: {},
additionalProperties: true,
default: {}
},
commitInfo: {
type: "object",
title: "提交信息",
description: "构建对应的Git提交信息",
properties: {
commitId: { type: "string", title: "提交ID" },
commitMessage: { type: "string", title: "提交消息" },
commitAuthor: { type: "string", title: "提交作者" },
commitTimestamp: { type: "string", title: "提交时间" }
},
default: {}
}
},
required: ["buildId", "buildStatus", "buildUrl"]
}
};

View File

@ -1,72 +0,0 @@
import {BaseNodeDefinition, NodeType, NodeCategory} from '../types';
/**
*
*
*/
export const StartEventNode: BaseNodeDefinition = {
nodeCode: "START_EVENT",
nodeName: "开始",
nodeType: NodeType.START_EVENT,
category: NodeCategory.EVENT,
description: "工作流的起始节点",
// UI 配置 - 节点在画布上的显示样式(现代化设计)
uiConfig: {
shape: 'ellipse',
size: {
width: 80,
height: 50
},
style: {
fill: '#52c41a',
stroke: '#389e08',
strokeWidth: 2,
icon: 'play-circle',
iconColor: '#fff'
},
ports: {
groups: {
// 开始节点只有输出端口 - 现代化样式
out: {
position: 'right',
attrs: {
circle: {
r: 4,
fill: '#fff',
stroke: '#52c41a'
}
}
}
}
}
},
// 基本配置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

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

View File

@ -1,17 +0,0 @@
/**
* - 访
*/
import {NODE_DEFINITIONS} from './definitions';
import {WorkflowNodeDefinition} from './types';
/**
*
*/
export const getNodeDefinitionList = async (): Promise<WorkflowNodeDefinition[]> => {
return new Promise((resolve) => {
setTimeout(() => {
resolve(NODE_DEFINITIONS);
}, 10);
});
};

View File

@ -1,5 +1,4 @@
// 节点分类 (从 ../types.ts 合并)
// 节点分类
export enum NodeCategory {
EVENT = 'EVENT',
TASK = 'TASK',
@ -7,100 +6,7 @@ export enum NodeCategory {
CONTAINER = 'CONTAINER'
}
// 条件类型
export type ConditionType = 'EXPRESSION' | 'SCRIPT' | 'DEFAULT';
// 边的条件配置
export interface EdgeCondition {
type: ConditionType;
expression?: string;
script?: string;
priority: number;
}
// 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 PortStyle {
r: number;
fill: string;
stroke: string;
strokeWidth?: number; // 新增:端口描边宽度
filter?: string; // 新增:端口滤镜效果
':hover'?: { // 新增:端口悬浮状态
r?: number;
fill?: string;
stroke?: string;
};
}
export interface PortAttributes {
circle: PortStyle;
}
export type PortPosition = 'left' | 'right' | 'top' | 'bottom';
export interface PortConfig {
attrs: PortAttributes;
position: PortPosition;
}
export interface PortGroups {
in?: PortConfig;
out?: PortConfig;
}
export type NodeShape = 'rect' | 'circle' | 'ellipse' | 'polygon';
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; // 新增:字体族
':hover'?: { // 新增:悬浮状态
fill?: string;
stroke?: string;
boxShadow?: string;
transform?: string;
};
':active'?: { // 新增:激活状态
transform?: string;
boxShadow?: string;
};
}
export interface UIConfig {
size: NodeSize;
ports: {
groups: PortGroups;
};
shape: NodeShape;
style: NodeStyle;
position?: { x: number; y: number };
}
// 节点类型和分类(保持原有的枚举格式)
// 节点类型(完整定义,包含所有可能的类型)
export enum NodeType {
START_EVENT = 'START_EVENT',
END_EVENT = 'END_EVENT',
@ -114,6 +20,58 @@ export enum NodeType {
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 定义
/**
* JSON Schema - 使 Formily ISchema
* @formily/react @formily/json-schema
*/
import type { ISchema } from '@formily/react';
export type JSONSchema = ISchema;
// 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 {
@ -150,5 +108,10 @@ export interface NodeInstanceData {
// UI位置信息
position: { x: number; y: number };
uiConfig: UIConfig; // 包含运行时可能更新的UI配置如位置
uiConfig: UIConfig;
}
// 判断是否为可配置节点
export const isConfigurableNode = (def: WorkflowNodeDefinition): def is ConfigurableNodeDefinition => {
return 'inputMappingSchema' in def || 'outputMappingSchema' in def;
};

View File

@ -1,358 +0,0 @@
import { Graph, Cell, Edge } from '@antv/x6';
import { message, Modal } from 'antd';
import { createRoot } from 'react-dom/client';
import { Dropdown } from 'antd';
import React from 'react';
import type { NodeDefinitionResponse } from "../../nodes/nodeService";
/**
*
*/
export class NodeStyleManager {
private static hoverStyle = {
strokeWidth: 2,
stroke: '#52c41a' // 绿色
};
// 保存节点的原始样式
static saveNodeOriginalStyle(node: any) {
const data = node.getData();
if (!data?.originalStyle) {
const originalStyle = {
stroke: node.getAttrByPath('body/stroke') || '#5F95FF',
strokeWidth: node.getAttrByPath('body/strokeWidth') || 1
};
node.setData({
...data,
originalStyle
});
}
}
// 获取节点的原始样式
static getNodeOriginalStyle(node: any) {
const data = node.getData();
return data?.originalStyle || {
stroke: '#5F95FF',
strokeWidth: 2
};
}
// 恢复节点的原始样式
static resetNodeStyle(node: any) {
const originalStyle = this.getNodeOriginalStyle(node);
node.setAttrByPath('body/stroke', originalStyle.stroke);
node.setAttrByPath('body/strokeWidth', originalStyle.strokeWidth);
}
// 应用悬停样式
static applyHoverStyle(node: any) {
this.saveNodeOriginalStyle(node);
node.setAttrByPath('body/stroke', this.hoverStyle.stroke);
node.setAttrByPath('body/strokeWidth', this.hoverStyle.strokeWidth);
}
}
/**
*
*/
export class PortManager {
static showPorts(nodeId: string) {
const ports = document.querySelectorAll(`[data-cell-id="${nodeId}"] .x6-port-body`);
ports.forEach((port) => {
port.setAttribute('style', 'visibility: visible; fill: #fff; stroke: #85ca6d;');
});
}
static hidePorts(nodeId: string) {
const ports = document.querySelectorAll(`[data-cell-id="${nodeId}"] .x6-port-body`);
ports.forEach((port) => {
port.setAttribute('style', 'visibility: hidden');
});
}
static hideAllPorts() {
const ports = document.querySelectorAll('.x6-port-body');
ports.forEach((port) => {
port.setAttribute('style', 'visibility: hidden');
});
}
static showAllPorts() {
const ports = document.querySelectorAll('.x6-port-body');
ports.forEach((port) => {
const portGroup = port.getAttribute('port-group');
if (portGroup !== 'top') {
port.setAttribute('style', 'visibility: visible; fill: #fff; stroke: #85ca6d;');
}
});
}
}
/**
*
*/
export class ContextMenuManager {
static createNodeContextMenu(
e: MouseEvent,
cell: Cell,
graph: Graph,
nodeDefinitions: NodeDefinitionResponse[],
onEdit: (cell: Cell, nodeDefinition?: NodeDefinitionResponse) => void
) {
e.preventDefault();
graph.cleanSelection();
graph.select(cell);
const dropdownContainer = document.createElement('div');
dropdownContainer.style.position = 'absolute';
dropdownContainer.style.left = `${e.clientX}px`;
dropdownContainer.style.top = `${e.clientY}px`;
document.body.appendChild(dropdownContainer);
const root = createRoot(dropdownContainer);
let isOpen = true;
const closeMenu = () => {
isOpen = false;
root.render(
React.createElement(Dropdown, {
menu: { items },
open: false,
onOpenChange: (open: boolean) => {
if (!open) {
setTimeout(() => {
root.unmount();
document.body.removeChild(dropdownContainer);
}, 100);
}
}
}, React.createElement('div'))
);
};
const items = [
{
key: '1',
label: '编辑',
onClick: () => {
closeMenu();
const nodeDefinition = nodeDefinitions.find(def => def.nodeType === cell.getProp('nodeType'));
onEdit(cell, nodeDefinition);
}
},
{
key: '2',
label: '删除',
onClick: () => {
closeMenu();
Modal.confirm({
title: '确认删除',
content: '确定要删除该节点吗?',
onOk: () => {
cell.remove();
}
});
}
}
];
root.render(
React.createElement(Dropdown, {
menu: { items },
open: isOpen
}, React.createElement('div'))
);
const handleClickOutside = (e: MouseEvent) => {
if (!dropdownContainer.contains(e.target as Node)) {
closeMenu();
document.removeEventListener('click', handleClickOutside);
}
};
document.addEventListener('click', handleClickOutside);
}
static createEdgeContextMenu(
e: MouseEvent,
edge: Edge,
graph: Graph,
onEditCondition: (edge: Edge) => void
) {
e.preventDefault();
graph.cleanSelection();
graph.select(edge);
const sourceNode = graph.getCellById(edge.getSourceCellId());
const isFromGateway = sourceNode.getProp('nodeType') === 'GATEWAY_NODE';
const dropdownContainer = document.createElement('div');
dropdownContainer.style.position = 'absolute';
dropdownContainer.style.left = `${e.clientX}px`;
dropdownContainer.style.top = `${e.clientY}px`;
document.body.appendChild(dropdownContainer);
const root = createRoot(dropdownContainer);
let isOpen = true;
const closeMenu = () => {
isOpen = false;
root.render(
React.createElement(Dropdown, {
menu: { items },
open: false,
onOpenChange: (open: boolean) => {
if (!open) {
setTimeout(() => {
root.unmount();
document.body.removeChild(dropdownContainer);
}, 100);
}
}
}, React.createElement('div'))
);
};
const items = [
// 只有网关节点的出线才显示编辑条件选项
...(isFromGateway ? [{
key: 'edit',
label: '编辑条件',
onClick: () => {
closeMenu();
onEditCondition(edge);
}
}] : []),
{
key: 'delete',
label: '删除',
danger: true,
onClick: () => {
closeMenu();
Modal.confirm({
title: '确认删除',
content: '确定要删除该连接线吗?',
onOk: () => {
edge.remove();
}
});
}
}
];
root.render(
React.createElement(Dropdown, {
menu: { items },
open: isOpen
}, React.createElement('div'))
);
const handleClickOutside = (e: MouseEvent) => {
if (!dropdownContainer.contains(e.target as Node)) {
closeMenu();
document.removeEventListener('click', handleClickOutside);
}
};
document.addEventListener('click', handleClickOutside);
}
}
/**
*
*/
export class EdgeStyleManager {
static readonly defaultEdgeAttrs = {
line: {
stroke: '#5F95FF',
strokeWidth: 2,
targetMarker: {
name: 'classic',
size: 7,
},
},
};
static readonly hoverEdgeAttrs = {
line: {
stroke: '#52c41a',
strokeWidth: 2,
},
};
static applyDefaultStyle(edge: Edge) {
edge.setAttrs(this.defaultEdgeAttrs);
}
static applyHoverStyle(edge: Edge) {
edge.setAttrs(this.hoverEdgeAttrs);
}
static addEdgeTools(edge: Edge) {
edge.addTools([
{
name: 'vertices', // 顶点工具
args: {
padding: 4,
attrs: {
fill: '#fff',
stroke: '#5F95FF',
strokeWidth: 2,
}
}
},
{
name: 'segments', // 线段工具
args: {
attrs: {
fill: '#fff',
stroke: '#5F95FF',
strokeWidth: 2,
}
}
}
]);
}
}
/**
*
*/
export class SelectionManager {
static applySelectionStyle(cell: Cell) {
if (cell.isNode()) {
cell.setAttrByPath('body/stroke', '#1890ff');
cell.setAttrByPath('body/strokeWidth', 2);
cell.setAttrByPath('body/strokeDasharray', '5 5');
} else if (cell.isEdge()) {
cell.setAttrByPath('line/stroke', '#1890ff');
cell.setAttrByPath('line/strokeWidth', 2);
cell.setAttrByPath('line/strokeDasharray', '5 5');
}
}
static removeSelectionStyle(cell: Cell) {
if (cell.isNode()) {
cell.setAttrByPath('body/stroke', '#5F95FF');
cell.setAttrByPath('body/strokeWidth', 2);
cell.setAttrByPath('body/strokeDasharray', null);
} else if (cell.isEdge()) {
cell.setAttrByPath('line/stroke', '#5F95FF');
cell.setAttrByPath('line/strokeWidth', 2);
cell.setAttrByPath('line/strokeDasharray', null);
}
}
static selectAll(graph: Graph) {
const cells = graph.getCells();
if (cells.length === 0) {
message.info('当前没有可选择的元素');
return;
}
graph.resetSelection();
graph.select(cells);
// 为选中的元素添加高亮样式
cells.forEach(cell => {
this.applySelectionStyle(cell);
});
}
}

View File

@ -1,216 +0,0 @@
import { Graph, Edge } from '@antv/x6';
import type { WorkflowNodeDefinition } from "../../nodes/types";
import { NodeStyleManager, PortManager, ContextMenuManager, EdgeStyleManager, SelectionManager } from './eventHandlers';
/**
* -
*/
export class EventRegistrar {
private graph: Graph;
private nodeDefinitions: WorkflowNodeDefinition[];
private onNodeEdit: (cell: any, nodeDefinition?: WorkflowNodeDefinition) => void;
private onEdgeEdit: (edge: Edge) => void;
constructor(
graph: Graph,
nodeDefinitions: WorkflowNodeDefinition[],
onNodeEdit: (cell: any, nodeDefinition?: WorkflowNodeDefinition) => void,
onEdgeEdit: (edge: Edge) => void
) {
this.graph = graph;
this.nodeDefinitions = nodeDefinitions;
this.onNodeEdit = onNodeEdit;
this.onEdgeEdit = onEdgeEdit;
}
/**
*
*/
registerAllEvents() {
this.registerNodeEvents();
this.registerEdgeEvents();
this.registerSelectionEvents();
this.registerCanvasEvents();
this.registerHistoryEvents();
}
/**
*
*/
private registerNodeEvents() {
// 节点悬停事件
this.graph.on('node:mouseenter', ({node}) => {
NodeStyleManager.applyHoverStyle(node);
PortManager.showPorts(node.id);
});
this.graph.on('node:mouseleave', ({node}) => {
NodeStyleManager.resetNodeStyle(node);
PortManager.hidePorts(node.id);
});
// 节点拖动事件
this.graph.on('node:drag:start', ({node}) => {
NodeStyleManager.saveNodeOriginalStyle(node);
});
this.graph.on('node:moved', ({node}) => {
NodeStyleManager.resetNodeStyle(node);
PortManager.hidePorts(node.id);
});
// 节点点击事件
this.graph.on('node:click', ({node}) => {
const selectedNode = this.graph.getSelectedCells()[0];
if (selectedNode && selectedNode.isNode() && selectedNode.id !== node.id) {
NodeStyleManager.resetNodeStyle(selectedNode);
}
this.graph.resetSelection();
this.graph.select(node);
});
// 节点双击事件
this.graph.on('node:dblclick', ({node}) => {
const nodeType = node.getProp('nodeType');
const nodeDefinition = this.nodeDefinitions.find(def => def.nodeType === nodeType);
if (nodeDefinition) {
this.onNodeEdit(node, nodeDefinition);
}
});
// 节点右键菜单
this.graph.on('node:contextmenu', ({cell, e}) => {
ContextMenuManager.createNodeContextMenu(
e as MouseEvent,
cell,
this.graph,
this.nodeDefinitions,
this.onNodeEdit
);
});
}
/**
*
*/
private registerEdgeEvents() {
// 边连接事件
this.graph.on('edge:connected', ({edge}) => {
EdgeStyleManager.applyDefaultStyle(edge);
});
// 边悬停事件
this.graph.on('edge:mouseenter', ({edge}) => {
EdgeStyleManager.applyHoverStyle(edge);
PortManager.showAllPorts();
});
this.graph.on('edge:mouseleave', ({edge}) => {
EdgeStyleManager.applyDefaultStyle(edge);
PortManager.hideAllPorts();
});
// 边选择事件
this.graph.on('edge:selected', ({edge}) => {
EdgeStyleManager.addEdgeTools(edge);
});
this.graph.on('edge:unselected', ({edge}) => {
edge.removeTools();
});
// 边移动事件
this.graph.on('edge:moved', ({edge, terminal}) => {
if (!edge || !terminal) return;
const isSource = terminal.type === 'source';
const source = isSource ? terminal : edge.getSource();
const target = isSource ? edge.getTarget() : terminal;
if (source && target) {
edge.remove();
this.graph.addEdge({
source: {
cell: source.cell,
port: source.port,
},
target: {
cell: target.cell,
port: target.port,
},
attrs: EdgeStyleManager.defaultEdgeAttrs,
});
}
});
// 边改变事件
this.graph.on('edge:change:source edge:change:target', ({edge}) => {
if (edge) {
EdgeStyleManager.applyDefaultStyle(edge);
}
});
// 边右键菜单
this.graph.on('edge:contextmenu', ({cell, e}) => {
ContextMenuManager.createEdgeContextMenu(
e as MouseEvent,
cell as Edge,
this.graph,
this.onEdgeEdit
);
});
}
/**
*
*/
private registerSelectionEvents() {
this.graph.on('selection:changed', ({selected, removed}) => {
// 处理新选中的元素
selected.forEach(cell => {
SelectionManager.applySelectionStyle(cell);
});
// 处理取消选中的元素
removed.forEach(cell => {
SelectionManager.removeSelectionStyle(cell);
});
});
}
/**
*
*/
private registerCanvasEvents() {
// 点击空白处事件
this.graph.on('blank:click', () => {
const selectedNode = this.graph.getSelectedCells()[0];
if (selectedNode && selectedNode.isNode()) {
NodeStyleManager.resetNodeStyle(selectedNode);
}
this.graph.resetSelection();
});
// 禁用默认右键菜单
this.graph.on('blank:contextmenu', ({e}) => {
e.preventDefault();
});
}
/**
*
*/
private registerHistoryEvents() {
this.graph.on('cell:added', () => {
// 可以在这里添加额外的逻辑
});
this.graph.on('cell:removed', () => {
// 可以在这里添加额外的逻辑
});
this.graph.on('cell:changed', () => {
// 可以在这里添加额外的逻辑
});
}
}

View File

@ -1,164 +0,0 @@
import { Graph } from '@antv/x6';
/**
* X6
*/
export const createGraphConfig = (container: HTMLElement): Graph.Options => ({
container,
grid: {
size: 10,
visible: true,
type: 'dot',
args: {
color: '#a0a0a0',
thickness: 1,
},
},
connecting: {
snap: true, // 连线时自动吸附
allowBlank: false, // 禁止连线到空白位置
allowLoop: false, // 禁止自环
allowNode: false, // 禁止连接到节点(只允许连接到连接桩)
allowEdge: false, // 禁止边连接到边
connector: {
name: 'rounded',
args: {
radius: 8
}
},
router: {
name: 'manhattan',
args: {
padding: 1
}
},
validateMagnet({magnet}) {
return magnet.getAttribute('port-group') !== 'top';
},
validateEdge() {
return true;
}
},
highlighting: {
magnetAvailable: {
name: 'stroke',
args: {
padding: 4,
attrs: {
strokeWidth: 4,
stroke: '#52c41a',
},
},
},
},
// clipboard: {
// enabled: true,
// },
selecting: {
enabled: true,
multiple: true,
rubberband: true,
movable: true,
showNodeSelectionBox: false, // 禁用节点选择框
showEdgeSelectionBox: false, // 禁用边选择框
selectNodeOnMoved: false,
selectEdgeOnMoved: false,
},
snapline: true,
keyboard: {
enabled: true,
global: false,
},
panning: {
enabled: true,
eventTypes: ['rightMouseDown'], // 右键按下时启用画布拖拽
},
mousewheel: {
enabled: true,
modifiers: ['ctrl', 'meta'],
minScale: 0.2,
maxScale: 2,
},
edgeMovable: true, // 允许边移动
edgeLabelMovable: true, // 允许边标签移动
vertexAddable: true, // 允许添加顶点
vertexMovable: true, // 允许顶点移动
vertexDeletable: true, // 允许删除顶点
});
/**
*
*/
export const createValidateConnection = (graph: Graph) => {
return ({sourceCell, targetCell, sourceMagnet, targetMagnet}: any) => {
if (sourceCell === targetCell) {
return false; // 禁止自连接
}
if (!sourceMagnet || !targetMagnet) {
return false; // 需要有效的连接点
}
// 获取源节点和目标节点的类型
const sourceNodeType = sourceCell.getProp('nodeType');
const targetNodeType = targetCell.getProp('nodeType');
// 如果源节点或目标节点是网关类型,允许多条连线
if (sourceNodeType === 'GATEWAY_NODE' || targetNodeType === 'GATEWAY_NODE') {
return true;
}
// 对于其他类型的节点,检查是否已存在连接
const edges = graph.getEdges();
const exists = edges.some(edge => {
const source = edge.getSource();
const target = edge.getTarget();
return (
(source as any).cell === sourceCell.id &&
(target as any).cell === targetCell.id &&
(source as any).port === sourceMagnet.getAttribute('port') &&
(target as any).port === targetMagnet.getAttribute('port')
);
});
return !exists;
};
};
/**
*
*/
export const createMiniMapConfig = (
container: HTMLElement,
width: number,
height: number,
scale: number
) => ({
container,
width,
height,
padding: 5,
scalable: false,
minScale: scale * 0.8,
maxScale: scale * 1.2,
graphOptions: {
connecting: {
connector: 'rounded',
connectionPoint: 'anchor',
router: {
name: 'manhattan',
},
},
async: true,
frozen: true,
interacting: false,
grid: false
},
viewport: {
padding: 0,
fitToContent: false,
initialPosition: {
x: 0,
y: 0
},
initialScale: scale
}
});

View File

@ -1,166 +0,0 @@
import { Graph } from '@antv/x6';
import { Selection } from '@antv/x6-plugin-selection';
import { MiniMap } from '@antv/x6-plugin-minimap';
import { Clipboard } from '@antv/x6-plugin-clipboard';
import { History } from '@antv/x6-plugin-history';
import { Transform } from '@antv/x6-plugin-transform';
import { createGraphConfig, createValidateConnection, createMiniMapConfig } from './graphConfig';
/**
* X6
*/
export class GraphInitializer {
private graphContainer: HTMLElement;
private minimapContainer: HTMLElement;
constructor(graphContainer: HTMLElement, minimapContainer: HTMLElement) {
this.graphContainer = graphContainer;
this.minimapContainer = minimapContainer;
}
/**
*
*/
initializeGraph(): Graph {
// 获取主画布容器尺寸
const containerWidth = this.graphContainer.clientWidth;
const containerHeight = this.graphContainer.clientHeight;
// 计算小地图尺寸
const MINIMAP_BASE_WIDTH = 200;
const minimapWidth = MINIMAP_BASE_WIDTH;
const minimapHeight = Math.round((MINIMAP_BASE_WIDTH * containerHeight) / containerWidth);
const scale = minimapWidth / containerWidth;
// 设置CSS变量
this.setCSSVariables(minimapWidth, minimapHeight);
// 创建图形配置
const config = createGraphConfig(this.graphContainer);
const graph = new Graph(config);
// 设置连接验证
(graph as any).options.connecting.validateConnection = createValidateConnection(graph);
// 注册插件
this.registerPlugins(graph, minimapWidth, minimapHeight, scale);
// 设置键盘事件
this.setupKeyboardEvents(graph);
return graph;
}
/**
* CSS变量
*/
private setCSSVariables(minimapWidth: number, minimapHeight: number) {
document.documentElement.style.setProperty('--minimap-width', `${minimapWidth}px`);
document.documentElement.style.setProperty('--minimap-height', `${minimapHeight}px`);
}
/**
*
*/
private registerPlugins(graph: Graph, minimapWidth: number, minimapHeight: number, scale: number) {
// History 插件
const history = new History({
enabled: true,
beforeAddCommand(event: any, args: any) {
return true;
},
afterExecuteCommand: () => {},
afterUndo: () => {},
afterRedo: () => {},
});
// Selection 插件
const selection = new Selection({
enabled: true,
multiple: true,
rubberband: true,
movable: true,
showNodeSelectionBox: false,
showEdgeSelectionBox: false,
selectNodeOnMoved: false,
selectEdgeOnMoved: false,
multipleSelectionModifiers: ['ctrl', 'meta'],
pointerEvents: 'auto'
});
// MiniMap 插件
const minimapConfig = createMiniMapConfig(this.minimapContainer, minimapWidth, minimapHeight, scale);
const minimap = new MiniMap(minimapConfig);
// Transform 插件
const transform = new Transform({
resizing: false,
rotating: false,
});
// 注册插件
graph.use(selection);
graph.use(minimap);
graph.use(new Clipboard());
graph.use(history);
graph.use(transform);
// 扩展 graph 对象,添加 history 属性
(graph as any).history = history;
// 设置变换事件监听
this.setupTransformListener(graph, minimap, scale);
}
/**
*
*/
private setupTransformListener(graph: Graph, minimap: MiniMap, scale: number) {
graph.on('transform', () => {
if (minimap) {
const mainViewport = graph.getView().getVisibleArea();
(minimap as any).viewport.scale = scale;
(minimap as any).viewport.center(mainViewport.center);
}
});
}
/**
*
*/
private setupKeyboardEvents(graph: Graph) {
this.graphContainer?.addEventListener('keydown', (e: KeyboardEvent) => {
// 只有当画布或其子元素被聚焦时才处理快捷键
if (!this.graphContainer?.contains(document.activeElement)) {
return;
}
// Ctrl+A 或 Command+A (Mac)
if ((e.ctrlKey || e.metaKey) && e.key === 'a') {
e.preventDefault(); // 阻止浏览器默认的全选行为
if (!graph) return;
const cells = graph.getCells();
if (cells.length > 0) {
graph.resetSelection();
graph.select(cells);
// 为选中的元素添加高亮样式
cells.forEach(cell => {
if (cell.isNode()) {
cell.setAttrByPath('body/stroke', '#1890ff');
cell.setAttrByPath('body/strokeWidth', 3);
cell.setAttrByPath('body/strokeDasharray', '5 5');
} else if (cell.isEdge()) {
cell.setAttrByPath('line/stroke', '#1890ff');
cell.setAttrByPath('line/strokeWidth', 3);
cell.setAttrByPath('line/strokeDasharray', '5 5');
}
});
}
}
});
// 确保画布可以接收键盘事件
this.graphContainer?.setAttribute('tabindex', '0');
}
}

View File

@ -1,125 +0,0 @@
import {Graph} from '@antv/x6';
import {WorkflowNodeDefinition, NodeInstanceData} from '../nodes/types';
/**
*
*/
export const createNodeFromDefinition = (
graph: Graph,
nodeDefinition: WorkflowNodeDefinition,
position: { x: number; y: number }
) => {
const {uiConfig} = nodeDefinition;
// 创建节点配置 - 直接使用X6原生形状名称
const nodeConfig = {
shape: uiConfig.shape,
x: position.x,
y: position.y,
width: uiConfig.size.width,
height: uiConfig.size.height,
attrs: {
body: {
...uiConfig.style,
...(uiConfig.shape === 'polygon' ? {
refPoints: '0,10 10,0 20,10 10,20',
} : {})
},
label: {
text: nodeDefinition.nodeName,
fontSize: 12,
fill: '#000'
},
},
ports: convertPortConfig(uiConfig.ports),
// 同时设置为props和data方便访问
nodeType: nodeDefinition.nodeType,
nodeCode: nodeDefinition.nodeCode,
data: {
nodeType: nodeDefinition.nodeType,
nodeCode: nodeDefinition.nodeCode,
nodeName: nodeDefinition.nodeName,
// 新创建的节点configs等数据为空
configs: {},
inputMapping: {},
outputMapping: {}
}
};
return graph.addNode(nodeConfig);
};
/**
*
*/
export const restoreNodeFromData = (
graph: Graph,
nodeData: NodeInstanceData,
nodeDefinition: WorkflowNodeDefinition
) => {
// 从节点定义中获取UI配置兼容旧数据中的uiConfig
const uiConfig = nodeData.uiConfig || nodeDefinition.uiConfig;
// 创建节点配置 - 直接使用X6原生形状名称
const nodeConfig = {
id: nodeData.nodeCode, // 使用保存的ID
shape: uiConfig.shape,
x: nodeData.position.x,
y: nodeData.position.y,
width: uiConfig.size.width,
height: uiConfig.size.height,
attrs: {
body: {
...uiConfig.style,
...(uiConfig.shape === 'polygon' ? {
refPoints: '0,10 10,0 20,10 10,20',
} : {})
},
label: {
text: nodeData.nodeName,
fontSize: 12,
fill: '#000'
},
},
ports: convertPortConfig(uiConfig.ports),
// 同时设置为props和data方便访问
nodeType: nodeData.nodeType,
nodeCode: nodeData.nodeCode,
data: {
nodeType: nodeData.nodeType,
nodeCode: nodeData.nodeCode,
nodeName: nodeData.nodeName,
// 统一使用configs字段名
configs: nodeData.configs || {},
inputMapping: nodeData.inputMapping || {},
outputMapping: nodeData.outputMapping || {}
}
};
return graph.addNode(nodeConfig);
};
/**
* X6格式
*/
const convertPortConfig = (ports: any) => {
if (!ports?.groups) return {items: []};
const groups: any = {};
const items: any[] = [];
Object.entries(ports.groups).forEach(([key, group]: [string, any]) => {
groups[key] = {
position: group.position,
attrs: {
circle: {
...group.attrs.circle,
magnet: true,
}
}
};
items.push({group: key});
});
return {groups, items};
};

View File

@ -1,72 +1,33 @@
import { Graph, Cell } from '@antv/x6';
import type { FlowNode, FlowEdge } from '../types';
import { NodeType } from '../types';
interface ValidationResult {
/**
*
*/
export interface ValidationResult {
valid: boolean;
message?: string;
}
/**
*
* @param node
* @param nodeDefinition
*
*/
const validateNodeConfig = (node: Cell, nodeDefinition: any): ValidationResult => {
const panelVariables = node.getProp('panelVariables');
const localVariables = node.getProp('localVariables');
// 校验面板变量
if (nodeDefinition?.panelVariablesSchema?.required) {
for (const field of nodeDefinition.panelVariablesSchema.required) {
if (!panelVariables?.[field]) {
const fieldTitle = nodeDefinition.panelVariablesSchema.properties[field]?.title || field;
return {
valid: false,
message: `节点 "${node.attr('label/text')}" 的面板变量 "${fieldTitle}" 是必填项`
};
}
}
}
// 校验环境变量
if (nodeDefinition?.localVariablesSchema?.required) {
for (const field of nodeDefinition.localVariablesSchema.required) {
if (!localVariables?.[field]) {
const fieldTitle = nodeDefinition.localVariablesSchema.properties[field]?.title || field;
return {
valid: false,
message: `节点 "${node.attr('label/text')}" 的环境变量 "${fieldTitle}" 是必填项`
};
}
}
}
return { valid: true };
};
/**
*
* @param graph
*/
const validateGraphNotEmpty = (graph: Graph): ValidationResult => {
const nodes = graph.getNodes();
const validateNotEmpty = (nodes: FlowNode[]): ValidationResult => {
if (nodes.length === 0) {
return {
valid: false,
message: '流程图中没有任何节点'
message: '流程图中没有任何节点,请至少添加一个节点'
};
}
return { valid: true };
};
/**
*
* @param graph
*
*/
const validateRequiredNodes = (graph: Graph): ValidationResult => {
const nodes = graph.getNodes();
const validateRequiredNodes = (nodes: FlowNode[]): ValidationResult => {
// 检查开始节点
const hasStartNode = nodes.some(node => node.getProp('nodeType') === 'START_EVENT');
const hasStartNode = nodes.some(node => node.data.nodeType === NodeType.START_EVENT);
if (!hasStartNode) {
return {
valid: false,
@ -75,7 +36,7 @@ const validateRequiredNodes = (graph: Graph): ValidationResult => {
}
// 检查结束节点
const hasEndNode = nodes.some(node => node.getProp('nodeType') === 'END_EVENT');
const hasEndNode = nodes.some(node => node.data.nodeType === NodeType.END_EVENT);
if (!hasEndNode) {
return {
valid: false,
@ -87,35 +48,100 @@ const validateRequiredNodes = (graph: Graph): ValidationResult => {
};
/**
*
* @param graph
*
*
*/
const validateNodeConnections = (graph: Graph): ValidationResult => {
const nodes = graph.getNodes();
const edges = graph.getEdges();
const validateNodeConnections = (nodes: FlowNode[], edges: FlowEdge[]): ValidationResult => {
if (nodes.length <= 1) {
return { valid: true }; // 单节点或空流程不需要验证连接
}
if (edges.length < nodes.length - 1) {
// 构建节点连接映射
const nodeConnections = new Map<string, { incoming: number; outgoing: number }>();
nodes.forEach(node => {
nodeConnections.set(node.id, { incoming: 0, outgoing: 0 });
});
edges.forEach(edge => {
const source = nodeConnections.get(edge.source);
const target = nodeConnections.get(edge.target);
if (source) source.outgoing++;
if (target) target.incoming++;
});
// 检查每个节点的连接情况
for (const [nodeId, connections] of nodeConnections.entries()) {
const node = nodes.find(n => n.id === nodeId);
if (!node) continue;
const nodeType = node.data.nodeType;
// 开始节点必须有出边
if (nodeType === NodeType.START_EVENT && connections.outgoing === 0) {
return {
valid: false,
message: '存在未连接的节点,请确保所有节点都已正确连接'
message: `开始节点"${node.data.label}"没有连接到任何其他节点`
};
}
// 结束节点必须有入边
if (nodeType === NodeType.END_EVENT && connections.incoming === 0) {
return {
valid: false,
message: `结束节点"${node.data.label}"没有被任何节点连接`
};
}
// 中间节点必须既有入边又有出边
if (nodeType !== NodeType.START_EVENT && nodeType !== NodeType.END_EVENT) {
if (connections.incoming === 0) {
return {
valid: false,
message: `节点"${node.data.label}"没有输入连接,请确保它被其他节点连接`
};
}
if (connections.outgoing === 0) {
return {
valid: false,
message: `节点"${node.data.label}"没有输出连接,请确保它连接到其他节点`
};
}
}
}
return { valid: true };
};
/**
*
* @param graph
*
*
*/
const validateAllNodesConfig = (graph: Graph): ValidationResult => {
const nodes = graph.getNodes();
const validateNodeConfigs = (nodes: FlowNode[]): ValidationResult => {
for (const node of nodes) {
const nodeDefinition = node.getProp('nodeDefinition');
const result = validateNodeConfig(node, nodeDefinition);
if (!result.valid) {
return result;
// 检查节点是否有配置数据
if (!node.data.configs) {
return {
valid: false,
message: `节点"${node.data.label}"缺少配置信息,请配置该节点`
};
}
// 检查节点名称
if (!node.data.configs.nodeName || node.data.configs.nodeName.trim() === '') {
return {
valid: false,
message: `节点"${node.data.label}"的名称不能为空`
};
}
// 检查节点编码
if (!node.data.configs.nodeCode || node.data.configs.nodeCode.trim() === '') {
return {
valid: false,
message: `节点"${node.data.label}"的编码不能为空`
};
}
}
@ -123,20 +149,54 @@ const validateAllNodesConfig = (graph: Graph): ValidationResult => {
};
/**
*
* @param graph
*
*
*/
export const validateWorkflow = (graph: Graph): ValidationResult => {
// 按顺序执行所有验证
const validateEdgeConditions = (edges: FlowEdge[]): ValidationResult => {
for (const edge of edges) {
const condition = edge.data?.condition;
if (condition) {
// 如果是表达式类型,检查表达式是否为空
if (condition.type === 'EXPRESSION') {
if (!condition.expression || condition.expression.trim() === '') {
return {
valid: false,
message: `边"${edge.id}"配置了表达式条件,但表达式为空`
};
}
}
// 检查优先级是否有效
if (condition.priority < 1 || condition.priority > 999) {
return {
valid: false,
message: `边"${edge.id}"的优先级必须在1-999之间`
};
}
}
}
return { valid: true };
};
/**
*
*
*/
export const validateWorkflow = (nodes: FlowNode[], edges: FlowEdge[]): ValidationResult => {
// 定义验证规则链
const validators = [
validateGraphNotEmpty,
validateRequiredNodes,
validateAllNodesConfig,
validateNodeConnections
() => validateNotEmpty(nodes),
() => validateRequiredNodes(nodes),
() => validateNodeConnections(nodes, edges),
() => validateNodeConfigs(nodes),
() => validateEdgeConditions(edges),
];
// 依次执行验证
for (const validator of validators) {
const result = validator(graph);
const result = validator();
if (!result.valid) {
return result;
}
@ -144,3 +204,4 @@ export const validateWorkflow = (graph: Graph): ValidationResult => {
return { valid: true };
};

View File

@ -1,164 +0,0 @@
import React, { useEffect } from 'react';
import { Modal, Form, Input, Select, message } from 'antd';
import * as service from '../service';
import type { WorkflowDefinition, WorkflowCategory } from '../types';
interface EditModalProps {
visible: boolean;
onClose: () => void;
onSuccess: () => void;
record?: WorkflowDefinition;
}
const EditModal: React.FC<EditModalProps> = ({
visible,
onClose,
onSuccess,
record
}) => {
const [form] = Form.useForm();
const [loading, setLoading] = React.useState(false);
const [categories, setCategories] = React.useState<WorkflowCategory[]>([]);
// 加载工作流分类
useEffect(() => {
const loadCategories = async () => {
try {
const data = await service.getWorkflowCategories();
setCategories(data);
} catch (error) {
console.error('加载工作流分类失败:', error);
}
};
if (visible) {
loadCategories();
}
}, [visible]);
// 设置表单初始值
useEffect(() => {
if (visible && record) {
form.setFieldsValue({
name: record.name,
key: record.key,
description: record.description,
category: record.category,
triggers: record.triggers || []
});
} else if (visible) {
form.resetFields();
}
}, [visible, record, form]);
const handleSubmit = async () => {
try {
const values = await form.validateFields();
setLoading(true);
if (record) {
await service.updateDefinition(record.id, values);
message.success('更新成功');
} else {
await service.createDefinition({
...values,
status: 'DRAFT',
graph: { nodes: [], edges: [] },
formConfig: { formItems: [] }
});
message.success('创建成功');
}
onSuccess();
onClose();
} catch (error) {
console.error('保存失败:', error);
if (error instanceof Error) {
message.error(error.message);
}
} finally {
setLoading(false);
}
};
return (
<Modal
title={record ? '编辑工作流' : '新建工作流'}
open={visible}
onCancel={onClose}
onOk={handleSubmit}
confirmLoading={loading}
width={600}
>
<Form
form={form}
layout="vertical"
initialValues={{
triggers: []
}}
>
<Form.Item
label="流程名称"
name="name"
rules={[{ required: true, message: '请输入流程名称' }]}
>
<Input placeholder="请输入流程名称" />
</Form.Item>
<Form.Item
label="流程标识"
name="key"
rules={[
{ required: true, message: '请输入流程标识' },
{ pattern: /^[a-zA-Z][a-zA-Z0-9_]*$/, message: '流程标识只能包含字母、数字和下划线,且以字母开头' }
]}
>
<Input placeholder="请输入流程标识" disabled={!!record} />
</Form.Item>
<Form.Item
label="流程分类"
name="category"
rules={[{ required: true, message: '请选择流程分类' }]}
>
<Select placeholder="请选择流程分类">
{categories.map(category => (
<Select.Option key={category.code} value={category.code}>
{category.label}
</Select.Option>
))}
</Select>
</Form.Item>
<Form.Item
label="触发方式"
name="triggers"
rules={[{ required: true, message: '请选择至少一种触发方式' }]}
>
<Select
mode="multiple"
placeholder="请选择触发方式"
options={categories.find(c => c.code === form.getFieldValue('category'))?.supportedTriggers?.map(trigger => ({
label: trigger.label,
value: trigger.code
})) || []}
/>
</Form.Item>
<Form.Item
label="描述"
name="description"
>
<Input.TextArea
placeholder="请输入流程描述"
rows={3}
maxLength={500}
showCount
/>
</Form.Item>
</Form>
</Modal>
);
};
export default EditModal;

View File

@ -1,247 +0,0 @@
import React, { useState, useEffect } from 'react';
import { Table, Card, Button, Space, Tag, message, Modal } from 'antd';
import { PlusOutlined, ExclamationCircleOutlined } from '@ant-design/icons';
import { useNavigate } from 'react-router-dom';
import * as service from './service';
import type { WorkflowDefinition, WorkflowDefinitionQuery, WorkflowCategory } from './types';
import { DEFAULT_PAGE_SIZE, DEFAULT_CURRENT } from '@/utils/page';
import EditModal from './components/EditModal';
const { confirm } = Modal;
const WorkflowDefinitionList: React.FC = () => {
const navigate = useNavigate();
const [loading, setLoading] = useState(false);
const [pageData, setPageData] = useState<{
content: WorkflowDefinition[];
totalElements: number;
size: number;
number: number;
} | null>(null);
const [modalVisible, setModalVisible] = useState(false);
const [currentRecord, setCurrentRecord] = useState<WorkflowDefinition>();
const [categories, setCategories] = useState<WorkflowCategory[]>([]);
const [query, setQuery] = useState<WorkflowDefinitionQuery>({
pageNum: DEFAULT_CURRENT - 1,
pageSize: DEFAULT_PAGE_SIZE
});
const loadData = async (params: WorkflowDefinitionQuery) => {
setLoading(true);
try {
const data = await service.getDefinitions(params);
setPageData(data);
} catch (error) {
if (error instanceof Error) {
message.error(error.message);
}
} finally {
setLoading(false);
}
};
const loadCategories = async () => {
try {
const data = await service.getWorkflowCategories();
setCategories(data);
} catch (error) {
console.error('加载工作流分类失败:', error);
message.error('加载工作流分类失败');
}
};
useEffect(() => {
loadData(query);
loadCategories();
}, [query]);
const handleCreateFlow = () => {
setCurrentRecord(undefined);
setModalVisible(true);
};
const handleEditFlow = (record: WorkflowDefinition) => {
setCurrentRecord(record);
setModalVisible(true);
};
const handleDesignFlow = (record: WorkflowDefinition) => {
// 新的React Flow设计器路径
navigate(`/workflow2/design/${record.id}`);
};
const handleModalClose = () => {
setModalVisible(false);
setCurrentRecord(undefined);
};
const handleDeploy = async (id: number) => {
confirm({
title: '确认发布',
icon: <ExclamationCircleOutlined />,
content: '确定要发布该流程定义吗?发布后将不能修改。',
onOk: async () => {
try {
await service.publishDefinition(id);
message.success('发布成功');
loadData(query);
} catch (error) {
if (error instanceof Error) {
message.error(error.message);
}
}
},
});
};
const handleDelete = async (id: number) => {
confirm({
title: '确认删除',
icon: <ExclamationCircleOutlined />,
content: '确定要删除该流程定义吗?删除后不可恢复。',
onOk: async () => {
try {
await service.deleteDefinition(id);
message.success('删除成功');
loadData(query);
} catch (error) {
if (error instanceof Error) {
message.error(error.message);
}
}
},
});
};
const handleStartFlow = async (record: WorkflowDefinition) => {
try {
await service.startWorkflowInstance(record.key, record.category);
message.success('流程启动成功');
} catch (error) {
if (error instanceof Error) {
message.error(error.message);
}
}
};
const columns = [
{
title: '流程名称',
dataIndex: 'name',
key: 'name',
},
{
title: '流程标识',
dataIndex: 'key',
key: 'key',
},
{
title: '流程分类',
dataIndex: 'category',
key: 'category',
render: (category: string) => {
const categoryInfo = categories.find(c => c.code === category);
return categoryInfo?.label || category;
}
},
{
title: '触发方式',
dataIndex: 'triggers',
key: 'triggers',
render: (triggers: string[], record: WorkflowDefinition) => {
const categoryInfo = categories.find(c => c.code === record.category);
return (triggers || [])?.map(triggerCode => {
const triggerInfo = categoryInfo?.supportedTriggers?.find(t => t.code === triggerCode);
return (
<Tag key={triggerCode}>
{triggerInfo?.label || triggerCode}
</Tag>
);
});
}
},
{
title: '版本',
dataIndex: 'flowVersion',
key: 'flowVersion',
},
{
title: '状态',
dataIndex: 'status',
key: 'status',
render: (status: string) => (
<Tag color={status === 'DRAFT' ? 'orange' : 'green'}>
{status === 'DRAFT' ? '草稿' : '已发布'}
</Tag>
),
},
{
title: '描述',
dataIndex: 'description',
key: 'description',
ellipsis: true,
},
{
title: '操作',
key: 'action',
fixed: 'right' as const,
width: 220,
render: (_: any, record: WorkflowDefinition) => (
<Space size="middle">
{record.status === 'DRAFT' && (
<>
<a onClick={() => handleEditFlow(record)}></a>
<a onClick={() => handleDesignFlow(record)}>(React Flow)</a>
</>
)}
{record.status === 'DRAFT' && (
<a onClick={() => handleDeploy(record.id)}></a>
)}
<a onClick={() => handleDelete(record.id)}></a>
{record.status !== 'DRAFT' && (
<a onClick={() => handleStartFlow(record)}></a>
)}
</Space>
),
},
];
return (
<Card
title={
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<span> (React Flow版本)</span>
<Button type="primary" icon={<PlusOutlined />} onClick={handleCreateFlow}>
</Button>
</div>
}
>
<Table
columns={columns}
dataSource={pageData?.content}
loading={loading}
rowKey="id"
scroll={{ x: 1400 }}
pagination={{
current: (query.pageNum || 0) + 1,
pageSize: query.pageSize,
total: pageData?.totalElements || 0,
onChange: (page, pageSize) => setQuery({
...query,
pageNum: page - 1,
pageSize
}),
}}
/>
<EditModal
visible={modalVisible}
onClose={handleModalClose}
onSuccess={() => loadData(query)}
record={currentRecord}
/>
</Card>
);
};
export default WorkflowDefinitionList;

View File

@ -1,81 +0,0 @@
import request from '@/utils/request';
import type { WorkflowDefinition, WorkflowDefinitionQuery, WorkflowCategory } from './types';
import type { Page } from '@/types/base';
// API 基础路径
const DEFINITION_URL = '/api/v1/workflow/definition';
/**
*
*/
export const getDefinitions = (params?: WorkflowDefinitionQuery) =>
request.get<Page<WorkflowDefinition>>(`${DEFINITION_URL}/page`, { params });
/**
*
*/
export const getDefinitionDetail = (id: number) =>
request.get<WorkflowDefinition>(`${DEFINITION_URL}/${id}`);
/**
*
*/
export const getPublishedDefinitions = () =>
request.get<WorkflowDefinition[]>(`${DEFINITION_URL}/published`);
/**
*
*/
export const saveDefinition = (data: WorkflowDefinition) =>
request.post<WorkflowDefinition>(DEFINITION_URL, data);
/**
*
*/
export const updateDefinition = (id: number, data: Partial<WorkflowDefinition>) =>
request.put<WorkflowDefinition>(`${DEFINITION_URL}/${id}`, data);
/**
*
*/
export const deleteDefinition = (id: number) =>
request.delete(`${DEFINITION_URL}/${id}`);
/**
*
*/
export const publishDefinition = (id: number) =>
request.post<void>(`${DEFINITION_URL}/${id}/published`);
/**
*
*/
export const deployDefinition = (id: number) =>
request.post<void>(`${DEFINITION_URL}/${id}/deploy`);
/**
*
*/
export const startWorkflowInstance = (processKey: string, categoryCode: string) =>
request.post<void>(`/api/v1/workflow/instance/start`, {
processKey,
businessKey: `${categoryCode}_${Date.now()}`,
});
/**
*
*/
export const getWorkflowCategories = () =>
request.get<WorkflowCategory[]>(`${DEFINITION_URL}/categories`);
/**
*
*/
export const saveWorkflowGraph = (id: number, graphData: any) =>
request.put<void>(`${DEFINITION_URL}/${id}/graph`, graphData);
/**
*
*/
export const getWorkflowGraph = (id: number) =>
request.get<any>(`${DEFINITION_URL}/${id}/graph`);

View File

@ -1,93 +0,0 @@
import { BaseResponse, BaseQuery } from '@/types/base';
// 复用现有的工作流定义类型保持API兼容性
export interface WorkflowDefinition {
id: number;
createTime: string | null;
createBy: string | null;
updateTime: string | null;
updateBy: string | null;
version: number;
deleted: boolean;
extraData: any;
name: string;
key: string;
description: string;
category: string;
triggers: string[];
flowVersion: number;
bpmnXml: string | null;
status: string;
graph: {
nodes: WorkflowDefinitionNode[];
edges: WorkflowDefinitionEdge[];
};
formVariablesSchema?: {
type: string;
required?: string[];
properties: {
[key: string]: {
type: string;
title: string;
description?: string;
dataSource?: {
url: string;
type: string;
params?: Record<string, any>;
dependsOn?: string[];
labelField?: string;
valueField?: string;
};
};
};
};
}
export interface WorkflowDefinitionNode {
id: string;
nodeCode: string;
nodeType: string;
nodeName: string;
position: {
x: number;
y: number;
};
configs: Record<string, any>;
inputMapping: Record<string, any>;
outputMapping: Record<string, any>;
}
export interface WorkflowDefinitionEdge {
id: string;
from: string;
to: string;
name: string;
config: {
type: string;
};
vertices: any[];
}
export interface WorkflowDefinitionQuery extends BaseQuery {
name?: string;
key?: string;
status?: string;
}
/**
*
*/
export interface WorkflowTrigger {
code: string;
label: string;
}
/**
*
*/
export interface WorkflowCategory {
code: string;
label: string;
description: string;
supportedTriggers: WorkflowTrigger[];
}

View File

@ -1,368 +0,0 @@
import React, { useState, useEffect, useMemo } from 'react';
import { Drawer, Tabs, Button, Space, message } from 'antd';
import { SaveOutlined, ReloadOutlined, CloseOutlined } from '@ant-design/icons';
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;
onCancel: () => void;
onOk: (nodeId: string, updatedData: Partial<FlowNodeData>) => void;
}
const NodeConfigModal: React.FC<NodeConfigModalProps> = ({
visible,
node,
onCancel,
onOk
}) => {
const [loading, setLoading] = useState(false);
const [activeTab, setActiveTab] = useState('config');
// 获取节点定义
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
};
// 设置表单初始值
configForm.setInitialValues({ ...defaultConfig, ...(nodeData.configs || {}) });
configForm.reset();
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: configs.nodeName || node.data.label,
configs,
inputMapping,
outputMapping,
};
onOk(node.id, updatedData);
message.success('节点配置保存成功');
onCancel();
} catch (error) {
if (error instanceof Error) {
message.error(error.message);
}
} finally {
setLoading(false);
}
};
const handleReset = () => {
configForm.reset();
inputForm.reset();
outputForm.reset();
message.info('已重置为初始值');
};
// 将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%' } // 统一宽度
};
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"}'
};
// ✅ 关键修复将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%' } // 统一宽度
};
}
// 设置默认值非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) {
return null;
}
// 构建Tabs配置
const tabItems = [
{
key: 'config',
label: '基本配置',
children: activeTab === 'config' ? (
<div style={{ padding: '16px 0' }}>
<FormProvider form={configForm}>
<FormLayout labelCol={6} wrapperCol={18} labelAlign="right" colon={false}>
<SchemaField schema={convertToFormilySchema(nodeDefinition.configSchema)} />
</FormLayout>
</FormProvider>
</div>
) : null,
},
];
// 如果是可配置节点添加输入和输出映射TAB
if (isConfigurableNode(nodeDefinition)) {
if (nodeDefinition.inputMappingSchema) {
tabItems.push({
key: 'input',
label: '输入映射',
children: activeTab === 'input' ? (
<div style={{ padding: '16px 0' }}>
<FormProvider form={inputForm}>
<FormLayout labelCol={6} wrapperCol={18} labelAlign="right" colon={false}>
<SchemaField schema={convertToFormilySchema(nodeDefinition.inputMappingSchema)} />
</FormLayout>
</FormProvider>
</div>
) : null,
});
}
if (nodeDefinition.outputMappingSchema) {
tabItems.push({
key: 'output',
label: '输出映射',
children: activeTab === 'output' ? (
<div style={{ padding: '16px 0' }}>
<FormProvider form={outputForm}>
<FormLayout labelCol={6} wrapperCol={18} labelAlign="right" colon={false}>
<SchemaField schema={convertToFormilySchema(nodeDefinition.outputMappingSchema)} />
</FormLayout>
</FormProvider>
</div>
) : null,
});
}
}
return (
<Drawer
title={
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', paddingRight: '24px' }}>
<span style={{ fontSize: '16px', fontWeight: '600' }}>
- {nodeDefinition.nodeName}
</span>
<Button
type="text"
icon={<CloseOutlined />}
onClick={onCancel}
size="small"
/>
</div>
}
placement="right"
width={720}
open={visible}
onClose={onCancel}
closeIcon={null}
styles={{
body: { padding: '0 24px 24px' },
header: { borderBottom: '1px solid #f0f0f0', padding: '16px 24px' }
}}
footer={
<div style={{
display: 'flex',
justifyContent: 'space-between',
padding: '12px 0',
borderTop: '1px solid #f0f0f0'
}}>
<Button
icon={<ReloadOutlined />}
onClick={handleReset}
>
</Button>
<Space>
<Button onClick={onCancel}>
</Button>
<Button
type="primary"
loading={loading}
icon={<SaveOutlined />}
onClick={handleSubmit}
>
</Button>
</Space>
</div>
}
>
<div style={{ paddingTop: '16px' }}>
<Tabs
activeKey={activeTab}
onChange={setActiveTab}
items={tabItems}
size="large"
/>
</div>
</Drawer>
);
};
export default NodeConfigModal;

View File

@ -1,188 +0,0 @@
import React from 'react';
import { NodeCategory } from '../types';
import { NODE_DEFINITIONS } from '../nodes';
import type { WorkflowNodeDefinition } from '../nodes/types';
// 图标映射函数
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;
}
const NodePanel: React.FC<NodePanelProps> = ({ className = '' }) => {
// 按分类分组节点
const nodesByCategory = NODE_DEFINITIONS.reduce((acc, node) => {
if (!acc[node.category]) {
acc[node.category] = [];
}
acc[node.category].push(node);
return acc;
}, {} as Record<NodeCategory, WorkflowNodeDefinition[]>);
// 拖拽开始处理
const handleDragStart = (event: React.DragEvent, nodeDefinition: WorkflowNodeDefinition) => {
event.dataTransfer.setData('application/reactflow', JSON.stringify({
nodeType: nodeDefinition.nodeType,
nodeDefinition
}));
event.dataTransfer.effectAllowed = 'move';
};
// 渲染节点项
const renderNodeItem = (nodeDefinition: WorkflowNodeDefinition) => (
<div
key={nodeDefinition.nodeCode}
draggable
style={{
display: 'flex',
alignItems: 'center',
gap: '8px',
padding: '8px 12px',
borderRadius: '6px',
border: '1px solid #e5e7eb',
background: 'white',
cursor: 'grab',
transition: 'all 0.2s ease-in-out',
marginBottom: '6px'
}}
onMouseEnter={(e) => {
e.currentTarget.style.background = '#f9fafb';
e.currentTarget.style.borderColor = nodeDefinition.uiConfig.style.fill;
e.currentTarget.style.transform = 'translateX(2px)';
}}
onMouseLeave={(e) => {
e.currentTarget.style.background = 'white';
e.currentTarget.style.borderColor = '#e5e7eb';
e.currentTarget.style.transform = 'translateX(0)';
}}
onDragStart={(e) => {
e.currentTarget.style.cursor = 'grabbing';
handleDragStart(e, nodeDefinition);
}}
onDragEnd={(e) => {
e.currentTarget.style.cursor = 'grab';
}}
>
<div style={{
fontSize: '16px',
width: '20px',
textAlign: 'center',
color: nodeDefinition.uiConfig.style.fill
}}>
{getNodeIcon(nodeDefinition.uiConfig.style.icon)}
</div>
<div>
<div style={{
fontSize: '13px',
fontWeight: '500',
color: '#374151',
lineHeight: '1.2'
}}>
{nodeDefinition.nodeName}
</div>
<div style={{
fontSize: '11px',
color: '#6b7280',
lineHeight: '1.2',
marginTop: '2px'
}}>
{nodeDefinition.description}
</div>
</div>
</div>
);
// 分类标题映射
const categoryTitles = {
[NodeCategory.EVENT]: '🎯 事件节点',
[NodeCategory.TASK]: '📋 任务节点',
[NodeCategory.GATEWAY]: '🔀 网关节点',
[NodeCategory.CONTAINER]: '📦 容器节点'
};
return (
<div className={`node-panel ${className}`} style={{
width: '260px',
height: '100%',
background: '#f8fafc',
borderRight: '1px solid #e5e7eb',
overflow: 'hidden',
display: 'flex',
flexDirection: 'column'
}}>
<div style={{
padding: '16px',
background: 'white',
borderBottom: '1px solid #e5e7eb'
}}>
<h3 style={{
margin: 0,
fontSize: '14px',
fontWeight: '600',
color: '#374151'
}}>
</h3>
<p style={{
margin: '4px 0 0 0',
fontSize: '12px',
color: '#6b7280'
}}>
</p>
</div>
<div style={{
flex: '1',
overflow: 'auto',
padding: '12px'
}}>
{Object.entries(nodesByCategory).map(([category, nodes]) => (
<div key={category} style={{ marginBottom: '16px' }}>
<div style={{
fontSize: '12px',
fontWeight: '600',
color: '#4b5563',
marginBottom: '8px',
padding: '4px 0',
borderBottom: '1px solid #e5e7eb'
}}>
{categoryTitles[category as NodeCategory]}
</div>
{nodes.map(renderNodeItem)}
</div>
))}
</div>
{/* 使用提示 */}
<div style={{
padding: '12px',
background: '#f1f5f9',
borderTop: '1px solid #e5e7eb',
fontSize: '11px',
color: '#64748b',
lineHeight: '1.4'
}}>
💡
<br />
<br />
<br />
</div>
</div>
);
};
export default NodePanel;

View File

@ -1,171 +0,0 @@
import React from 'react';
import { Button, Divider, Tooltip } from 'antd';
import {
SaveOutlined,
UndoOutlined,
RedoOutlined,
ZoomInOutlined,
ZoomOutOutlined,
ExpandOutlined,
ArrowLeftOutlined
} from '@ant-design/icons';
interface WorkflowToolbarProps {
title?: string;
onSave?: () => void;
onUndo?: () => void;
onRedo?: () => void;
onZoomIn?: () => void;
onZoomOut?: () => void;
onFitView?: () => void;
onBack?: () => void;
canUndo?: boolean;
canRedo?: boolean;
zoom?: number;
className?: string;
}
const WorkflowToolbar: React.FC<WorkflowToolbarProps> = ({
title = '工作流设计器',
onSave,
onUndo,
onRedo,
onZoomIn,
onZoomOut,
onFitView,
onBack,
canUndo = false,
canRedo = false,
zoom = 1,
className = ''
}) => {
return (
<div
className={`workflow-toolbar ${className}`}
style={{
height: '56px',
background: 'white',
borderBottom: '1px solid #e5e7eb',
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
padding: '0 16px',
boxShadow: '0 1px 2px rgba(0, 0, 0, 0.05)'
}}
>
{/* 左侧:返回按钮和标题 */}
<div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
<Tooltip title="返回列表">
<Button
type="text"
icon={<ArrowLeftOutlined />}
onClick={onBack}
style={{ color: '#6b7280' }}
/>
</Tooltip>
<Divider type="vertical" />
<h2 style={{
margin: 0,
fontSize: '16px',
fontWeight: '600',
color: '#374151'
}}>
{title}
</h2>
</div>
{/* 右侧:操作按钮区域 */}
<div style={{ display: 'flex', alignItems: 'center', gap: '6px' }}>
{/* 撤销/重做 */}
<Tooltip title="撤销 (Ctrl+Z)">
<Button
type="text"
icon={<UndoOutlined />}
onClick={onUndo}
disabled={!canUndo}
size="middle"
style={{
color: canUndo ? '#374151' : '#d1d5db',
}}
/>
</Tooltip>
<Tooltip title="重做 (Ctrl+Shift+Z)">
<Button
type="text"
icon={<RedoOutlined />}
onClick={onRedo}
disabled={!canRedo}
size="middle"
style={{
color: canRedo ? '#374151' : '#d1d5db',
}}
/>
</Tooltip>
<Divider type="vertical" style={{ margin: '0 4px' }} />
{/* 视图操作 */}
<Tooltip title="放大 (+)">
<Button
type="text"
icon={<ZoomInOutlined />}
onClick={onZoomIn}
size="middle"
/>
</Tooltip>
<Tooltip title="缩小 (-)">
<Button
type="text"
icon={<ZoomOutOutlined />}
onClick={onZoomOut}
size="middle"
/>
</Tooltip>
<Tooltip title="适应视图">
<Button
type="text"
icon={<ExpandOutlined />}
onClick={onFitView}
size="middle"
/>
</Tooltip>
{/* 缩放比例显示 */}
<div style={{
background: '#f8fafc',
padding: '6px 12px',
borderRadius: '6px',
fontSize: '12px',
color: '#475569',
fontFamily: 'ui-monospace, monospace',
minWidth: '60px',
textAlign: 'center',
fontWeight: '600',
border: '1px solid #e2e8f0',
marginLeft: '4px'
}}>
{Math.round(zoom * 100)}%
</div>
<Divider type="vertical" style={{ margin: '0 8px' }} />
{/* 保存按钮(最右侧) */}
<Button
type="primary"
icon={<SaveOutlined />}
onClick={onSave}
size="middle"
style={{
borderRadius: '6px',
fontWeight: '500',
boxShadow: '0 2px 4px rgba(59, 130, 246, 0.2)',
}}
>
</Button>
</div>
</div>
);
};
export default WorkflowToolbar;

View File

@ -1,267 +0,0 @@
// 工作流设计器 - React Flow版本
// 变量定义
@header-height: 64px;
@toolbar-height: 56px;
@sidebar-width: 260px;
@container-padding: 24px;
// 主容器 - 覆盖父容器样式
.workflow-design-container {
// 使用负边距抵消父容器的padding
margin: -@container-padding;
// 精确计算高度:视口高度 - header高度
height: calc(100vh - @header-height);
// 补偿左右padding
width: calc(100% + @container-padding * 2);
display: flex;
flex-direction: column;
overflow: hidden;
background: #ffffff;
position: relative;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
// React Flow 样式覆盖
.react-flow {
width: 100% !important;
height: 100% !important;
&__renderer {
width: 100% !important;
height: 100% !important;
}
// 自定义控制按钮样式
&__controls {
button {
background: rgba(255, 255, 255, 0.95);
border: 1px solid #e5e7eb;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
transition: all 0.2s ease-in-out;
&:hover {
background: #f9fafb;
border-color: #3b82f6;
}
}
}
// 小地图样式
&__minimap {
background: #f9fafb;
border: 1px solid #e5e7eb;
border-radius: 8px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
}
// 内容区域
.workflow-content-area {
flex: 1;
display: flex;
overflow: hidden;
position: relative;
min-height: 0;
// 节点面板区域
.node-panel {
width: @sidebar-width;
background: #f8fafc;
border-right: 1px solid #e5e7eb;
overflow: hidden;
display: flex;
flex-direction: column;
box-shadow: 2px 0 4px rgba(0, 0, 0, 0.05);
// 面板头部
.panel-header {
padding: 16px;
background: #ffffff;
border-bottom: 1px solid #e5e7eb;
h3 {
margin: 0;
font-size: 14px;
font-weight: 600;
color: #374151;
}
p {
margin: 4px 0 0 0;
font-size: 12px;
color: #6b7280;
}
}
// 节点分组
.node-group {
margin-bottom: 16px;
.group-title {
font-size: 12px;
font-weight: 600;
color: #4b5563;
margin-bottom: 8px;
padding: 4px 0;
border-bottom: 1px solid #e5e7eb;
}
// 节点项
.node-item {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 12px;
border-radius: 6px;
border: 1px solid #e5e7eb;
background: #ffffff;
cursor: grab;
transition: all 0.2s ease-in-out;
margin-bottom: 6px;
&:hover {
background: #f9fafb;
border-color: var(--node-color, #3b82f6);
transform: translateX(2px);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
&:active {
cursor: grabbing;
}
.node-icon {
font-size: 16px;
width: 20px;
text-align: center;
}
.node-info {
.node-name {
font-size: 13px;
font-weight: 500;
color: #374151;
line-height: 1.2;
}
.node-desc {
font-size: 11px;
color: #6b7280;
line-height: 1.2;
margin-top: 2px;
}
}
}
}
// 使用提示
.panel-tips {
padding: 12px;
background: #f1f5f9;
border-top: 1px solid #e5e7eb;
font-size: 11px;
color: #64748b;
line-height: 1.4;
}
}
// 画布区域
.workflow-canvas-area {
flex: 1;
position: relative;
overflow: hidden;
background: #f8fafc;
// 画布状态面板
.canvas-status {
position: absolute;
top: 12px;
left: 12px;
background: rgba(255, 255, 255, 0.95);
padding: 8px 12px;
border-radius: 6px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
font-size: 12px;
color: #6b7280;
z-index: 10;
backdrop-filter: blur(4px);
}
}
}
// 响应式适配
@media (max-width: 1024px) {
.workflow-content-area .node-panel {
width: 220px;
}
}
@media (max-width: 768px) {
.workflow-content-area {
flex-direction: column;
.node-panel {
width: 100%;
height: 200px;
border-right: none;
border-bottom: 1px solid #e5e7eb;
}
}
}
}
// 工具栏特殊样式
.workflow-toolbar {
height: @toolbar-height;
background: #ffffff;
border-bottom: 1px solid #e5e7eb;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 16px;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
flex-shrink: 0;
.toolbar-section {
display: flex;
align-items: center;
gap: 12px;
}
.toolbar-title {
h2 {
margin: 0;
font-size: 16px;
font-weight: 600;
color: #374151;
}
.subtitle {
font-size: 12px;
color: #6b7280;
margin-top: 2px;
}
}
.toolbar-status {
display: flex;
align-items: center;
gap: 12px;
font-size: 12px;
color: #6b7280;
.status-item {
background: #f3f4f6;
padding: 4px 8px;
border-radius: 4px;
font-family: 'Monaco', 'Menlo', monospace;
}
.react-flow-badge {
background: #ecfdf5;
color: #065f46;
padding: 4px 8px;
border-radius: 4px;
font-weight: 500;
}
}
}

View File

@ -1,617 +0,0 @@
import React, { useState, useCallback, useRef, useEffect } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import { message } from 'antd';
import { ReactFlowProvider, useReactFlow } from '@xyflow/react';
import WorkflowToolbar from './components/WorkflowToolbar';
import NodePanel from './components/NodePanel';
import FlowCanvas from './components/FlowCanvas';
import NodeConfigModal from './components/NodeConfigModal';
import EdgeConfigModal, { type EdgeCondition } from './components/EdgeConfigModal';
import type { FlowNode, FlowEdge, FlowNodeData } from './types';
import type { WorkflowNodeDefinition } from './nodes/types';
import { NodeType } from './types';
import { useWorkflowSave } from './hooks/useWorkflowSave';
import { useWorkflowLoad } from './hooks/useWorkflowLoad';
import { useHistory } from './hooks/useHistory';
// 样式
import '@xyflow/react/dist/style.css';
import './index.less';
const WorkflowDesignInner: React.FC = () => {
const { id } = useParams<{ id: string }>();
const navigate = useNavigate();
const {
getNodes,
setNodes,
getEdges,
setEdges,
screenToFlowPosition,
fitView,
zoomIn,
zoomOut,
getZoom
} = useReactFlow();
const [workflowTitle, setWorkflowTitle] = useState('新建工作流');
const [currentZoom, setCurrentZoom] = useState(1); // 当前缩放比例
const reactFlowWrapper = useRef<HTMLDivElement>(null);
// 当前工作流ID
const currentWorkflowId = id ? parseInt(id) : undefined;
// 节点配置模态框状态
const [configModalVisible, setConfigModalVisible] = useState(false);
const [configNode, setConfigNode] = useState<FlowNode | null>(null);
// 边配置模态框状态
const [edgeConfigModalVisible, setEdgeConfigModalVisible] = useState(false);
const [configEdge, setConfigEdge] = useState<FlowEdge | null>(null);
// 保存和加载hooks
const { hasUnsavedChanges, saveWorkflow, markUnsaved } = useWorkflowSave();
const { workflowDefinition, loadWorkflow } = useWorkflowLoad();
// 历史记录管理
const history = useHistory();
// 剪贴板(用于复制粘贴)
const clipboard = useRef<{ nodes: FlowNode[]; edges: FlowEdge[] } | null>(null);
// 加载工作流数据
useEffect(() => {
const loadData = async () => {
if (currentWorkflowId) {
const data = await loadWorkflow(currentWorkflowId);
if (data) {
setNodes(data.nodes);
setEdges(data.edges);
setWorkflowTitle(data.definition.name);
}
}
};
loadData();
}, [currentWorkflowId, loadWorkflow, setNodes, setEdges]);
// 初始化缩放比例
useEffect(() => {
setCurrentZoom(getZoom());
}, [getZoom]);
// 自动适应视图
useEffect(() => {
// 延迟执行fitView以确保节点已渲染
const timer = setTimeout(() => {
fitView({
padding: 0.1,
duration: 800,
minZoom: 1.0, // 最小缩放100%
maxZoom: 1.0 // 最大缩放100%确保默认100%
});
// 更新zoom显示
setTimeout(() => setCurrentZoom(getZoom()), 850);
}, 100);
return () => clearTimeout(timer);
}, [fitView, getZoom]);
// 初始化示例节点 - 优化位置和布局
const initialNodes: FlowNode[] = [
{
id: '1',
type: 'START_EVENT',
position: { x: 250, y: 50 },
data: {
label: '开始',
nodeType: NodeType.START_EVENT,
category: 'EVENT' as any,
icon: '▶️',
color: '#10b981'
}
},
{
id: '2',
type: 'USER_TASK',
position: { x: 200, y: 150 },
data: {
label: '用户审批',
nodeType: NodeType.USER_TASK,
category: 'TASK' as any,
icon: '👤',
color: '#6366f1'
}
},
{
id: '3',
type: 'END_EVENT',
position: { x: 250, y: 250 },
data: {
label: '结束',
nodeType: NodeType.END_EVENT,
category: 'EVENT' as any,
icon: '⏹️',
color: '#ef4444'
}
}
];
const initialEdges: FlowEdge[] = [
{
id: 'e1-2',
source: '1',
target: '2',
type: 'default',
animated: true,
data: {
label: '提交',
condition: {
type: 'DEFAULT',
priority: 0
}
}
},
{
id: 'e2-3',
source: '2',
target: '3',
type: 'default',
animated: true,
data: {
label: '通过',
condition: {
type: 'DEFAULT',
priority: 0
}
}
}
];
// 工具栏事件处理
const handleSave = useCallback(async () => {
const nodes = getNodes() as FlowNode[];
const edges = getEdges() as FlowEdge[];
const success = await saveWorkflow({
nodes,
edges,
workflowId: currentWorkflowId,
name: workflowTitle,
description: workflowDefinition?.description || '',
definitionData: workflowDefinition // 传递原始定义数据
});
if (success) {
console.log('保存工作流成功:', { nodes, edges });
}
}, [getNodes, getEdges, saveWorkflow, currentWorkflowId, workflowTitle, workflowDefinition]);
const handleBack = useCallback(() => {
navigate('/workflow2/definition');
}, [navigate]);
// 撤销操作
const handleUndo = useCallback(() => {
const state = history.undo();
if (state) {
history.pauseRecording();
setNodes(state.nodes);
setEdges(state.edges);
setTimeout(() => history.resumeRecording(), 100);
message.success('已撤销');
} else {
message.info('没有可撤销的操作');
}
}, [history, setNodes, setEdges]);
// 重做操作
const handleRedo = useCallback(() => {
const state = history.redo();
if (state) {
history.pauseRecording();
setNodes(state.nodes);
setEdges(state.edges);
setTimeout(() => history.resumeRecording(), 100);
message.success('已重做');
} else {
message.info('没有可重做的操作');
}
}, [history, setNodes, setEdges]);
// 复制选中的节点和边
const handleCopy = useCallback(() => {
const selectedNodes = getNodes().filter(node => node.selected);
const selectedEdges = getEdges().filter(edge => {
// 只复制两端都被选中的边
return selectedNodes.some(n => n.id === edge.source) &&
selectedNodes.some(n => n.id === edge.target);
});
if (selectedNodes.length > 0) {
clipboard.current = {
nodes: JSON.parse(JSON.stringify(selectedNodes)),
edges: JSON.parse(JSON.stringify(selectedEdges))
};
message.success(`已复制 ${selectedNodes.length} 个节点`);
} else {
message.warning('请先选择要复制的节点');
}
}, [getNodes, getEdges]);
// 粘贴节点
const handlePaste = useCallback(() => {
if (!clipboard.current || clipboard.current.nodes.length === 0) {
message.info('剪贴板为空');
return;
}
const { nodes: copiedNodes, edges: copiedEdges } = clipboard.current;
const offset = 50; // 粘贴偏移量
const idMap = new Map<string, string>();
// 创建新节点(带偏移)
const newNodes = copiedNodes.map(node => {
const newId = `${node.type}-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
idMap.set(node.id, newId);
return {
...node,
id: newId,
position: {
x: node.position.x + offset,
y: node.position.y + offset
},
selected: true
};
});
// 创建新边更新source和target为新ID
const newEdges = copiedEdges.map(edge => {
const newSource = idMap.get(edge.source);
const newTarget = idMap.get(edge.target);
if (!newSource || !newTarget) return null;
return {
...edge,
id: `e${newSource}-${newTarget}`,
source: newSource,
target: newTarget,
selected: true
};
}).filter(edge => edge !== null) as FlowEdge[];
// 取消其他元素的选中状态
setNodes(nodes => [
...nodes.map(n => ({ ...n, selected: false })),
...newNodes
]);
setEdges(edges => [
...edges.map(e => ({ ...e, selected: false })),
...newEdges
]);
message.success(`已粘贴 ${newNodes.length} 个节点`);
}, [setNodes, setEdges]);
const handleDelete = useCallback(() => {
const selectedNodes = getNodes().filter(node => node.selected);
const selectedEdges = getEdges().filter(edge => edge.selected);
if (selectedNodes.length > 0 || selectedEdges.length > 0) {
setNodes(nodes => nodes.filter(node => !node.selected));
setEdges(edges => edges.filter(edge => !edge.selected));
message.success(`已删除 ${selectedNodes.length} 个节点和 ${selectedEdges.length} 条连接`);
markUnsaved();
} else {
message.warning('请先选择要删除的元素');
}
}, [getNodes, getEdges, setNodes, setEdges, markUnsaved]);
const handleSelectAll = useCallback(() => {
setNodes(nodes => nodes.map(node => ({ ...node, selected: true })));
setEdges(edges => edges.map(edge => ({ ...edge, selected: true })));
message.info('已全选所有元素');
}, [setNodes, setEdges]);
const handleFitView = useCallback(() => {
fitView({ padding: 0.2, duration: 800 });
// 延迟更新zoom值以获取最新的缩放比例
setTimeout(() => setCurrentZoom(getZoom()), 850);
}, [fitView, getZoom]);
const handleZoomIn = useCallback(() => {
zoomIn({ duration: 300 });
// 延迟更新zoom值以获取最新的缩放比例
setTimeout(() => setCurrentZoom(getZoom()), 350);
}, [zoomIn, getZoom]);
const handleZoomOut = useCallback(() => {
zoomOut({ duration: 300 });
// 延迟更新zoom值以获取最新的缩放比例
setTimeout(() => setCurrentZoom(getZoom()), 350);
}, [zoomOut, getZoom]);
// 处理节点拖拽放置 - 使用官方推荐的screenToFlowPosition方法
const handleDrop = useCallback((event: React.DragEvent) => {
event.preventDefault();
const dragData = event.dataTransfer.getData('application/reactflow');
if (!dragData) return;
try {
const { nodeType, nodeDefinition }: { nodeType: string; nodeDefinition: WorkflowNodeDefinition } = JSON.parse(dragData);
// 根据React Flow官方文档screenToFlowPosition会自动处理所有边界计算
// 不需要手动减去容器边界!
const position = screenToFlowPosition({
x: event.clientX,
y: event.clientY,
});
const newNode: FlowNode = {
id: `${nodeType}-${Date.now()}`,
type: nodeType,
position,
data: {
label: nodeDefinition.nodeName,
nodeType: nodeDefinition.nodeType,
category: nodeDefinition.category,
icon: nodeDefinition.uiConfig.style.icon,
color: nodeDefinition.uiConfig.style.fill,
// 保存原始节点定义引用,用于配置
nodeDefinition,
// 初始化配置数据
configs: {
nodeName: nodeDefinition.nodeName,
nodeCode: nodeDefinition.nodeCode,
description: nodeDefinition.description
},
inputMapping: {},
outputMapping: {}
}
};
setNodes(nodes => [...nodes, newNode]);
message.success(`已添加 ${nodeDefinition.nodeName} 节点`);
} catch (error) {
console.error('解析拖拽数据失败:', error);
message.error('添加节点失败');
}
}, [screenToFlowPosition, setNodes]);
// 处理节点双击 - 打开配置面板
const handleNodeClick = useCallback((event: React.MouseEvent, node: FlowNode) => {
// 只处理双击事件
if (event.detail === 2) {
console.log('双击节点,打开配置:', node);
setConfigNode(node);
setConfigModalVisible(true);
}
}, []);
// 处理边双击 - 打开条件配置弹窗
const handleEdgeClick = useCallback((event: React.MouseEvent, edge: FlowEdge) => {
if (event.detail === 2) {
console.log('双击边,打开配置:', edge);
setConfigEdge(edge);
setEdgeConfigModalVisible(true);
}
}, []);
// 处理节点配置更新
const handleNodeConfigUpdate = useCallback((nodeId: string, updatedData: Partial<FlowNodeData>) => {
setNodes((nodes) =>
nodes.map((node) =>
node.id === nodeId
? {
...node,
data: {
...node.data,
...updatedData,
},
}
: node
)
);
markUnsaved(); // 标记有未保存更改
}, [setNodes, markUnsaved]);
// 关闭节点配置模态框
const handleCloseConfigModal = useCallback(() => {
setConfigModalVisible(false);
setConfigNode(null);
}, []);
// 处理边条件更新
const handleEdgeConditionUpdate = useCallback((edgeId: string, condition: EdgeCondition) => {
setEdges((edges) =>
edges.map((edge) => {
if (edge.id === edgeId) {
// 根据条件类型生成显示文本
const label = condition.type === 'EXPRESSION'
? condition.expression
: '默认路径';
return {
...edge,
data: {
...edge.data,
condition,
},
label,
};
}
return edge;
})
);
markUnsaved(); // 标记有未保存更改
message.success('边条件配置已更新');
setEdgeConfigModalVisible(false);
setConfigEdge(null);
}, [setEdges, markUnsaved]);
// 关闭边配置模态框
const handleCloseEdgeConfigModal = useCallback(() => {
setEdgeConfigModalVisible(false);
setConfigEdge(null);
}, []);
// 监听视图变化(缩放、平移等)
const handleViewportChange = useCallback(() => {
const zoom = getZoom();
setCurrentZoom(zoom);
}, [getZoom]);
// 监听节点和边的变化,记录到历史
useEffect(() => {
const nodes = getNodes() as FlowNode[];
const edges = getEdges() as FlowEdge[];
// 只在有节点或边时记录(避免空状态)
if (nodes.length > 0 || edges.length > 0) {
history.record(nodes, edges);
}
}, [getNodes, getEdges, history]);
// 键盘快捷键支持
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 - 撤销(仅在画布区域)
if (ctrlKey && e.key === 'z' && !e.shiftKey) {
if (!shouldSkipShortcut) {
e.preventDefault();
handleUndo();
}
}
// Ctrl+Shift+Z / Cmd+Shift+Z - 重做(仅在画布区域)
else if (ctrlKey && e.key === 'z' && e.shiftKey) {
if (!shouldSkipShortcut) {
e.preventDefault();
handleRedo();
}
}
// Ctrl+C / Cmd+C - 复制节点(仅在画布区域)
else if (ctrlKey && e.key === 'c') {
if (!shouldSkipShortcut) {
e.preventDefault();
handleCopy();
}
}
// Ctrl+V / Cmd+V - 粘贴节点(仅在画布区域)
else if (ctrlKey && e.key === 'v') {
if (!shouldSkipShortcut) {
e.preventDefault();
handlePaste();
}
}
// Ctrl+A / Cmd+A - 全选节点(仅在画布区域)
else if (ctrlKey && e.key === 'a') {
if (!shouldSkipShortcut) {
e.preventDefault();
handleSelectAll();
}
}
// Delete / Backspace - 删除节点(仅在画布区域)
else if (e.key === 'Delete' || e.key === 'Backspace') {
if (!shouldSkipShortcut) {
e.preventDefault();
handleDelete();
}
}
};
window.addEventListener('keydown', handleKeyDown);
return () => window.removeEventListener('keydown', handleKeyDown);
}, [handleUndo, handleRedo, handleCopy, handlePaste, handleSelectAll, handleDelete]);
return (
<div
className="workflow-design-container"
style={{
// 确保覆盖父容器的overflow设置
overflow: 'hidden'
}}
>
{/* 工具栏 */}
<WorkflowToolbar
title={`${workflowTitle}${hasUnsavedChanges ? ' *' : ''}`}
onSave={handleSave}
onBack={handleBack}
onUndo={handleUndo}
onRedo={handleRedo}
onZoomIn={handleZoomIn}
onZoomOut={handleZoomOut}
onFitView={handleFitView}
canUndo={history.canUndo}
canRedo={history.canRedo}
zoom={currentZoom}
/>
{/* 主要内容区域 */}
<div className="workflow-content-area">
{/* 节点面板 */}
<NodePanel />
{/* 画布区域 */}
<div
ref={reactFlowWrapper}
className="workflow-canvas-area"
>
<FlowCanvas
initialNodes={initialNodes}
initialEdges={initialEdges}
onNodeClick={handleNodeClick}
onEdgeClick={handleEdgeClick}
onDrop={handleDrop}
onViewportChange={handleViewportChange}
className="workflow-canvas"
/>
</div>
</div>
{/* 节点配置弹窗 */}
<NodeConfigModal
visible={configModalVisible}
node={configNode}
onCancel={handleCloseConfigModal}
onOk={handleNodeConfigUpdate}
/>
{/* 边条件配置弹窗 */}
<EdgeConfigModal
visible={edgeConfigModalVisible}
edge={configEdge}
onOk={handleEdgeConditionUpdate}
onCancel={handleCloseEdgeConfigModal}
/>
</div>
);
};
const WorkflowDesign: React.FC = () => {
return (
<ReactFlowProvider>
<WorkflowDesignInner />
</ReactFlowProvider>
);
};
export default WorkflowDesign;

View File

@ -1,117 +0,0 @@
// 节点分类
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'
}
// 节点类型到分类的映射
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 定义
/**
* JSON Schema - 使 Formily ISchema
* @formily/react @formily/json-schema
*/
import type { ISchema } from '@formily/react';
export type JSONSchema = ISchema;
// 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

@ -1,207 +0,0 @@
import type { FlowNode, FlowEdge } from '../types';
import { NodeType } from '../types';
/**
*
*/
export interface ValidationResult {
valid: boolean;
message?: string;
}
/**
*
*/
const validateNotEmpty = (nodes: FlowNode[]): ValidationResult => {
if (nodes.length === 0) {
return {
valid: false,
message: '流程图中没有任何节点,请至少添加一个节点'
};
}
return { valid: true };
};
/**
*
*/
const validateRequiredNodes = (nodes: FlowNode[]): ValidationResult => {
// 检查开始节点
const hasStartNode = nodes.some(node => node.data.nodeType === NodeType.START_EVENT);
if (!hasStartNode) {
return {
valid: false,
message: '流程图中必须包含开始节点'
};
}
// 检查结束节点
const hasEndNode = nodes.some(node => node.data.nodeType === NodeType.END_EVENT);
if (!hasEndNode) {
return {
valid: false,
message: '流程图中必须包含结束节点'
};
}
return { valid: true };
};
/**
*
*
*/
const validateNodeConnections = (nodes: FlowNode[], edges: FlowEdge[]): ValidationResult => {
if (nodes.length <= 1) {
return { valid: true }; // 单节点或空流程不需要验证连接
}
// 构建节点连接映射
const nodeConnections = new Map<string, { incoming: number; outgoing: number }>();
nodes.forEach(node => {
nodeConnections.set(node.id, { incoming: 0, outgoing: 0 });
});
edges.forEach(edge => {
const source = nodeConnections.get(edge.source);
const target = nodeConnections.get(edge.target);
if (source) source.outgoing++;
if (target) target.incoming++;
});
// 检查每个节点的连接情况
for (const [nodeId, connections] of nodeConnections.entries()) {
const node = nodes.find(n => n.id === nodeId);
if (!node) continue;
const nodeType = node.data.nodeType;
// 开始节点必须有出边
if (nodeType === NodeType.START_EVENT && connections.outgoing === 0) {
return {
valid: false,
message: `开始节点"${node.data.label}"没有连接到任何其他节点`
};
}
// 结束节点必须有入边
if (nodeType === NodeType.END_EVENT && connections.incoming === 0) {
return {
valid: false,
message: `结束节点"${node.data.label}"没有被任何节点连接`
};
}
// 中间节点必须既有入边又有出边
if (nodeType !== NodeType.START_EVENT && nodeType !== NodeType.END_EVENT) {
if (connections.incoming === 0) {
return {
valid: false,
message: `节点"${node.data.label}"没有输入连接,请确保它被其他节点连接`
};
}
if (connections.outgoing === 0) {
return {
valid: false,
message: `节点"${node.data.label}"没有输出连接,请确保它连接到其他节点`
};
}
}
}
return { valid: true };
};
/**
*
*
*/
const validateNodeConfigs = (nodes: FlowNode[]): ValidationResult => {
for (const node of nodes) {
// 检查节点是否有配置数据
if (!node.data.configs) {
return {
valid: false,
message: `节点"${node.data.label}"缺少配置信息,请配置该节点`
};
}
// 检查节点名称
if (!node.data.configs.nodeName || node.data.configs.nodeName.trim() === '') {
return {
valid: false,
message: `节点"${node.data.label}"的名称不能为空`
};
}
// 检查节点编码
if (!node.data.configs.nodeCode || node.data.configs.nodeCode.trim() === '') {
return {
valid: false,
message: `节点"${node.data.label}"的编码不能为空`
};
}
}
return { valid: true };
};
/**
*
*
*/
const validateEdgeConditions = (edges: FlowEdge[]): ValidationResult => {
for (const edge of edges) {
const condition = edge.data?.condition;
if (condition) {
// 如果是表达式类型,检查表达式是否为空
if (condition.type === 'EXPRESSION') {
if (!condition.expression || condition.expression.trim() === '') {
return {
valid: false,
message: `边"${edge.id}"配置了表达式条件,但表达式为空`
};
}
}
// 检查优先级是否有效
if (condition.priority < 1 || condition.priority > 999) {
return {
valid: false,
message: `边"${edge.id}"的优先级必须在1-999之间`
};
}
}
}
return { valid: true };
};
/**
*
*
*/
export const validateWorkflow = (nodes: FlowNode[], edges: FlowEdge[]): ValidationResult => {
// 定义验证规则链
const validators = [
() => validateNotEmpty(nodes),
() => validateRequiredNodes(nodes),
() => validateNodeConnections(nodes, edges),
() => validateNodeConfigs(nodes),
() => validateEdgeConditions(edges),
];
// 依次执行验证
for (const validator of validators) {
const result = validator();
if (!result.valid) {
return result;
}
}
return { valid: true };
};

View File

@ -1,369 +0,0 @@
import React, { useState, useEffect } from 'react';
import {
Table,
Card,
Button,
Space,
Tag,
message,
Modal,
Row,
Col,
Statistic
} from 'antd';
import {
PlayCircleOutlined,
PauseCircleOutlined,
StopOutlined,
DeleteOutlined,
ReloadOutlined,
EyeOutlined,
BarChartOutlined
} from '@ant-design/icons';
import { useNavigate } from 'react-router-dom';
import dayjs from 'dayjs';
import * as service from './service';
import type {
WorkflowInstance,
WorkflowInstanceQuery,
WorkflowInstanceStatus,
InstanceStatistics
} from './types';
import { DEFAULT_PAGE_SIZE, DEFAULT_CURRENT } from '@/utils/page';
const { confirm } = Modal;
const WorkflowInstanceList: React.FC = () => {
const navigate = useNavigate();
const [loading, setLoading] = useState(false);
const [pageData, setPageData] = useState<{
content: WorkflowInstance[];
totalElements: number;
size: number;
number: number;
} | null>(null);
const [statistics, setStatistics] = useState<InstanceStatistics | null>(null);
const [query, setQuery] = useState<WorkflowInstanceQuery>({
pageNum: DEFAULT_CURRENT - 1,
pageSize: DEFAULT_PAGE_SIZE
});
// 加载数据
const loadData = async (params: WorkflowInstanceQuery) => {
setLoading(true);
try {
const [instanceData, statsData] = await Promise.all([
service.getInstances(params),
service.getInstanceStatistics()
]);
setPageData(instanceData);
setStatistics(statsData);
} catch (error) {
if (error instanceof Error) {
message.error(error.message);
}
} finally {
setLoading(false);
}
};
useEffect(() => {
loadData(query);
}, [query]);
// 状态标签渲染
const renderStatus = (status: WorkflowInstanceStatus) => {
const statusConfig = {
RUNNING: { color: 'processing', text: '运行中' },
COMPLETED: { color: 'success', text: '已完成' },
FAILED: { color: 'error', text: '失败' },
SUSPENDED: { color: 'warning', text: '已挂起' },
TERMINATED: { color: 'default', text: '已终止' }
};
const config = statusConfig[status] || { color: 'default', text: status };
return <Tag color={config.color}>{config.text}</Tag>;
};
// 操作处理
const handleSuspend = async (instance: WorkflowInstance) => {
confirm({
title: '确认挂起',
content: `确定要挂起实例 "${instance.workflowName}" 吗?`,
onOk: async () => {
try {
await service.suspendInstance(instance.id);
message.success('实例已挂起');
loadData(query);
} catch (error) {
if (error instanceof Error) {
message.error(error.message);
}
}
}
});
};
const handleActivate = async (instance: WorkflowInstance) => {
try {
await service.activateInstance(instance.id);
message.success('实例已激活');
loadData(query);
} catch (error) {
if (error instanceof Error) {
message.error(error.message);
}
}
};
const handleTerminate = async (instance: WorkflowInstance) => {
confirm({
title: '确认终止',
content: `确定要终止实例 "${instance.workflowName}" 吗?此操作不可逆。`,
onOk: async () => {
try {
await service.terminateInstance(instance.id);
message.success('实例已终止');
loadData(query);
} catch (error) {
if (error instanceof Error) {
message.error(error.message);
}
}
}
});
};
const handleDelete = async (instance: WorkflowInstance) => {
confirm({
title: '确认删除',
content: `确定要删除实例 "${instance.workflowName}" 吗?此操作不可逆。`,
onOk: async () => {
try {
await service.deleteInstance(instance.id);
message.success('实例已删除');
loadData(query);
} catch (error) {
if (error instanceof Error) {
message.error(error.message);
}
}
}
});
};
// 查看详情
const handleViewDetail = (instance: WorkflowInstance) => {
navigate(`/workflow2/instance/${instance.id}`);
};
// 表格列定义
const columns = [
{
title: '实例ID',
dataIndex: 'id',
key: 'id',
width: 80,
},
{
title: '工作流名称',
dataIndex: 'workflowName',
key: 'workflowName',
},
{
title: '业务键',
dataIndex: 'businessKey',
key: 'businessKey',
ellipsis: true,
},
{
title: '状态',
dataIndex: 'status',
key: 'status',
render: renderStatus,
},
{
title: '启动人',
dataIndex: 'startUserName',
key: 'startUserName',
},
{
title: '开始时间',
dataIndex: 'startTime',
key: 'startTime',
render: (time: string) => dayjs(time).format('YYYY-MM-DD HH:mm:ss'),
},
{
title: '耗时',
dataIndex: 'duration',
key: 'duration',
render: (duration: number) => {
if (!duration) return '-';
const hours = Math.floor(duration / 3600000);
const minutes = Math.floor((duration % 3600000) / 60000);
const seconds = Math.floor((duration % 60000) / 1000);
return `${hours}h ${minutes}m ${seconds}s`;
},
},
{
title: '操作',
key: 'action',
fixed: 'right',
width: 200,
render: (_: any, record: WorkflowInstance) => (
<Space size="small">
<Button
type="link"
size="small"
icon={<EyeOutlined />}
onClick={() => handleViewDetail(record)}
>
</Button>
{record.status === 'RUNNING' && (
<Button
type="link"
size="small"
icon={<PauseCircleOutlined />}
onClick={() => handleSuspend(record)}
>
</Button>
)}
{record.status === 'SUSPENDED' && (
<Button
type="link"
size="small"
icon={<PlayCircleOutlined />}
onClick={() => handleActivate(record)}
>
</Button>
)}
{(record.status === 'RUNNING' || record.status === 'SUSPENDED') && (
<Button
type="link"
size="small"
danger
icon={<StopOutlined />}
onClick={() => handleTerminate(record)}
>
</Button>
)}
{(record.status === 'COMPLETED' || record.status === 'FAILED' || record.status === 'TERMINATED') && (
<Button
type="link"
size="small"
danger
icon={<DeleteOutlined />}
onClick={() => handleDelete(record)}
>
</Button>
)}
</Space>
),
},
];
return (
<div>
{/* 统计信息 */}
{statistics && (
<Row gutter={16} style={{ marginBottom: 16 }}>
<Col span={4}>
<Card>
<Statistic
title="总实例数"
value={statistics.total}
prefix={<BarChartOutlined />}
/>
</Card>
</Col>
<Col span={4}>
<Card>
<Statistic
title="运行中"
value={statistics.running}
valueStyle={{ color: '#1890ff' }}
/>
</Card>
</Col>
<Col span={4}>
<Card>
<Statistic
title="已完成"
value={statistics.completed}
valueStyle={{ color: '#52c41a' }}
/>
</Card>
</Col>
<Col span={4}>
<Card>
<Statistic
title="失败"
value={statistics.failed}
valueStyle={{ color: '#ff4d4f' }}
/>
</Card>
</Col>
<Col span={4}>
<Card>
<Statistic
title="今日启动"
value={statistics.todayStarted}
valueStyle={{ color: '#722ed1' }}
/>
</Card>
</Col>
<Col span={4}>
<Card>
<Statistic
title="平均耗时"
value={Math.round(statistics.avgDuration / 1000)}
suffix="秒"
valueStyle={{ color: '#fa8c16' }}
/>
</Card>
</Col>
</Row>
)}
{/* 主表格 */}
<Card
title="工作流实例列表"
extra={
<Space>
<Button
icon={<ReloadOutlined />}
onClick={() => loadData(query)}
>
</Button>
</Space>
}
>
<Table
columns={columns}
dataSource={pageData?.content}
loading={loading}
rowKey="id"
scroll={{ x: 1200 }}
pagination={{
current: (query.pageNum || 0) + 1,
pageSize: query.pageSize,
total: pageData?.totalElements || 0,
onChange: (page, pageSize) => setQuery({
...query,
pageNum: page - 1,
pageSize
}),
}}
/>
</Card>
</div>
);
};
export default WorkflowInstanceList;

View File

@ -1,90 +0,0 @@
import request from '@/utils/request';
import type { Page } from '@/types/base';
import type {
WorkflowInstance,
WorkflowInstanceQuery,
TaskInstance,
TaskQuery,
InstanceStatistics
} from './types';
// 获取工作流实例列表
export async function getInstances(params: WorkflowInstanceQuery): Promise<Page<WorkflowInstance>> {
return request.get('/api/v1/workflow/instances/page', { params });
}
// 获取单个工作流实例详情
export async function getInstanceDetail(id: number): Promise<WorkflowInstance> {
return request.get(`/api/v1/workflow/instances/${id}`);
}
// 启动工作流实例
export async function startInstance(data: {
workflowKey: string;
businessKey?: string;
variables?: Record<string, any>;
}): Promise<WorkflowInstance> {
return request.post('/api/v1/workflow/instances/start', data);
}
// 终止工作流实例
export async function terminateInstance(id: number, reason?: string): Promise<void> {
return request.post(`/api/v1/workflow/instances/${id}/terminate`, { reason });
}
// 挂起工作流实例
export async function suspendInstance(id: number, reason?: string): Promise<void> {
return request.post(`/api/v1/workflow/instances/${id}/suspend`, { reason });
}
// 激活工作流实例
export async function activateInstance(id: number): Promise<void> {
return request.post(`/api/v1/workflow/instances/${id}/activate`);
}
// 删除工作流实例
export async function deleteInstance(id: number): Promise<void> {
return request.delete(`/api/v1/workflow/instances/${id}`);
}
// 获取工作流实例的任务列表
export async function getInstanceTasks(instanceId: number): Promise<TaskInstance[]> {
return request.get(`/api/v1/workflow/instances/${instanceId}/tasks`);
}
// 获取任务列表
export async function getTasks(params: TaskQuery): Promise<Page<TaskInstance>> {
return request.get('/api/v1/workflow/tasks/page', { params });
}
// 完成任务
export async function completeTask(taskId: number, variables?: Record<string, any>): Promise<void> {
return request.post(`/api/v1/workflow/tasks/${taskId}/complete`, { variables });
}
// 委派任务
export async function delegateTask(taskId: number, assignee: string): Promise<void> {
return request.post(`/api/v1/workflow/tasks/${taskId}/delegate`, { assignee });
}
// 认领任务
export async function claimTask(taskId: number): Promise<void> {
return request.post(`/api/v1/workflow/tasks/${taskId}/claim`);
}
// 获取实例统计信息
export async function getInstanceStatistics(): Promise<InstanceStatistics> {
return request.get('/api/v1/workflow/instances/statistics');
}
// 获取实例执行历史
export async function getInstanceHistory(instanceId: number): Promise<any[]> {
return request.get(`/api/v1/workflow/instances/${instanceId}/history`);
}
// 获取实例流程图
export async function getInstanceDiagram(instanceId: number): Promise<string> {
return request.get(`/api/v1/workflow/instances/${instanceId}/diagram`, {
responseType: 'text'
});
}

View File

@ -1,88 +0,0 @@
import { BaseResponse, BaseQuery } from '@/types/base';
// 工作流实例状态枚举
export enum WorkflowInstanceStatus {
RUNNING = 'RUNNING',
COMPLETED = 'COMPLETED',
FAILED = 'FAILED',
SUSPENDED = 'SUSPENDED',
TERMINATED = 'TERMINATED'
}
// 任务状态枚举
export enum TaskStatus {
PENDING = 'PENDING',
RUNNING = 'RUNNING',
COMPLETED = 'COMPLETED',
FAILED = 'FAILED',
SKIPPED = 'SKIPPED'
}
// 工作流实例
export interface WorkflowInstance extends BaseResponse {
id: number;
workflowKey: string;
workflowName: string;
workflowVersion: number;
businessKey?: string;
status: WorkflowInstanceStatus;
startTime: string;
endTime?: string;
duration?: number;
startUserId?: number;
startUserName?: string;
currentTasks: TaskInstance[];
variables: Record<string, any>;
processDefinitionId: string;
}
// 任务实例
export interface TaskInstance extends BaseResponse {
id: number;
taskName: string;
taskType: string;
assignee?: string;
assigneeName?: string;
status: TaskStatus;
startTime: string;
endTime?: string;
duration?: number;
workflowInstanceId: number;
nodeId: string;
description?: string;
formKey?: string;
priority: number;
}
// 工作流实例查询条件
export interface WorkflowInstanceQuery extends BaseQuery {
workflowKey?: string;
workflowName?: string;
status?: WorkflowInstanceStatus;
businessKey?: string;
startUserId?: number;
startTimeFrom?: string;
startTimeTo?: string;
}
// 任务查询条件
export interface TaskQuery extends BaseQuery {
taskName?: string;
assignee?: string;
status?: TaskStatus;
workflowInstanceId?: number;
taskType?: string;
}
// 实例统计信息
export interface InstanceStatistics {
total: number;
running: number;
completed: number;
failed: number;
suspended: number;
terminated: number;
avgDuration: number;
todayStarted: number;
todayCompleted: number;
}

View File

@ -1,298 +0,0 @@
import React, { useState, useEffect } from 'react';
import {
Card,
Row,
Col,
Statistic,
Progress,
Table,
Tag,
Button,
Space,
message
} from 'antd';
import {
SyncOutlined,
CheckCircleOutlined,
CloseCircleOutlined,
PauseCircleOutlined,
BarChartOutlined,
LineChartOutlined,
ReloadOutlined
} from '@ant-design/icons';
import dayjs from 'dayjs';
import * as instanceService from '../Instance/service';
import type {
InstanceStatistics,
WorkflowInstance,
WorkflowInstanceStatus
} from '../Instance/types';
const WorkflowMonitor: React.FC = () => {
const [loading, setLoading] = useState(false);
const [statistics, setStatistics] = useState<InstanceStatistics | null>(null);
const [recentInstances, setRecentInstances] = useState<WorkflowInstance[]>([]);
// 加载监控数据
const loadMonitorData = async () => {
setLoading(true);
try {
const [statsData, instancesData] = await Promise.all([
instanceService.getInstanceStatistics(),
instanceService.getInstances({ pageNum: 0, pageSize: 10 })
]);
setStatistics(statsData);
setRecentInstances(instancesData.content);
} catch (error) {
if (error instanceof Error) {
message.error(error.message);
}
} finally {
setLoading(false);
}
};
useEffect(() => {
loadMonitorData();
// 定时刷新数据
const interval = setInterval(loadMonitorData, 30000); // 30秒刷新一次
return () => clearInterval(interval);
}, []);
// 状态渲染
const renderStatus = (status: WorkflowInstanceStatus) => {
const statusConfig = {
RUNNING: { color: 'processing', text: '运行中', icon: <SyncOutlined spin /> },
COMPLETED: { color: 'success', text: '已完成', icon: <CheckCircleOutlined /> },
FAILED: { color: 'error', text: '失败', icon: <CloseCircleOutlined /> },
SUSPENDED: { color: 'warning', text: '已挂起', icon: <PauseCircleOutlined /> },
TERMINATED: { color: 'default', text: '已终止', icon: <CloseCircleOutlined /> }
};
const config = statusConfig[status] || { color: 'default', text: status, icon: null };
return (
<Tag color={config.color} icon={config.icon}>
{config.text}
</Tag>
);
};
// 计算健康度
const getHealthScore = () => {
if (!statistics || statistics.total === 0) return 0;
const successRate = (statistics.completed / statistics.total) * 100;
return Math.round(successRate);
};
const columns = [
{
title: '实例ID',
dataIndex: 'id',
key: 'id',
width: 80,
},
{
title: '工作流名称',
dataIndex: 'workflowName',
key: 'workflowName',
ellipsis: true,
},
{
title: '状态',
dataIndex: 'status',
key: 'status',
render: renderStatus,
},
{
title: '开始时间',
dataIndex: 'startTime',
key: 'startTime',
render: (time: string) => dayjs(time).format('MM-DD HH:mm'),
},
{
title: '耗时',
dataIndex: 'duration',
key: 'duration',
render: (duration: number) => {
if (!duration) return '-';
return `${Math.round(duration / 1000)}s`;
},
},
];
return (
<div>
{/* 顶部操作栏 */}
<Card style={{ marginBottom: 16 }}>
<Row justify="space-between" align="middle">
<Col>
<Space>
<h2 style={{ margin: 0 }}></h2>
<Tag color="blue"></Tag>
</Space>
</Col>
<Col>
<Space>
<Button
icon={<ReloadOutlined />}
onClick={loadMonitorData}
loading={loading}
>
</Button>
</Space>
</Col>
</Row>
</Card>
{/* 统计卡片 */}
<Row gutter={16} style={{ marginBottom: 16 }}>
<Col span={6}>
<Card>
<Statistic
title="总实例数"
value={statistics?.total || 0}
prefix={<BarChartOutlined />}
loading={loading}
/>
</Card>
</Col>
<Col span={6}>
<Card>
<Statistic
title="今日启动"
value={statistics?.todayStarted || 0}
prefix={<LineChartOutlined />}
valueStyle={{ color: '#1890ff' }}
loading={loading}
/>
</Card>
</Col>
<Col span={6}>
<Card>
<Statistic
title="今日完成"
value={statistics?.todayCompleted || 0}
prefix={<CheckCircleOutlined />}
valueStyle={{ color: '#52c41a' }}
loading={loading}
/>
</Card>
</Col>
<Col span={6}>
<Card>
<div style={{ textAlign: 'center' }}>
<div style={{ fontSize: '14px', color: '#8c8c8c', marginBottom: '8px' }}>
</div>
<Progress
type="circle"
percent={getHealthScore()}
size={80}
strokeColor={{
'0%': '#108ee9',
'100%': '#87d068',
}}
/>
</div>
</Card>
</Col>
</Row>
{/* 图表和列表 */}
<Row gutter={16}>
<Col span={12}>
<Card title="实例运行状态" loading={loading}>
<Row gutter={16}>
<Col span={12}>
<Statistic
title="运行中"
value={statistics?.running || 0}
valueStyle={{ color: '#1890ff' }}
prefix={<SyncOutlined />}
/>
</Col>
<Col span={12}>
<Statistic
title="已完成"
value={statistics?.completed || 0}
valueStyle={{ color: '#52c41a' }}
prefix={<CheckCircleOutlined />}
/>
</Col>
</Row>
<Row gutter={16} style={{ marginTop: 24 }}>
<Col span={12}>
<Statistic
title="失败"
value={statistics?.failed || 0}
valueStyle={{ color: '#ff4d4f' }}
prefix={<CloseCircleOutlined />}
/>
</Col>
<Col span={12}>
<Statistic
title="已挂起"
value={statistics?.suspended || 0}
valueStyle={{ color: '#faad14' }}
prefix={<PauseCircleOutlined />}
/>
</Col>
</Row>
</Card>
</Col>
<Col span={12}>
<Card title="性能指标" loading={loading}>
<Row gutter={16}>
<Col span={24}>
<div style={{ textAlign: 'center', marginBottom: '16px' }}>
<div style={{ fontSize: '16px', fontWeight: 'bold', color: '#1890ff' }}>
</div>
<div style={{ fontSize: '24px', color: '#fa8c16', marginTop: '8px' }}>
{Math.round((statistics?.avgDuration || 0) / 1000)}
</div>
</div>
</Col>
</Row>
<Row gutter={16}>
<Col span={12}>
<div style={{ textAlign: 'center' }}>
<div style={{ fontSize: '14px' }}></div>
<div style={{ fontSize: '20px', color: '#52c41a' }}>
{getHealthScore()}%
</div>
</div>
</Col>
<Col span={12}>
<div style={{ textAlign: 'center' }}>
<div style={{ fontSize: '14px' }}></div>
<div style={{ fontSize: '20px', color: '#1890ff' }}>
{(statistics?.running || 0) + (statistics?.suspended || 0)}
</div>
</div>
</Col>
</Row>
</Card>
</Col>
</Row>
{/* 最近实例 */}
<Card
title="最近实例"
style={{ marginTop: 16 }}
loading={loading}
>
<Table
columns={columns}
dataSource={recentInstances}
rowKey="id"
size="small"
pagination={false}
/>
</Card>
</div>
);
};
export default WorkflowMonitor;

View File

@ -45,11 +45,7 @@ const JenkinsManagerList = lazy(() => import('../pages/Deploy/JenkinsManager/Lis
const GitManagerList = lazy(() => import('../pages/Deploy/GitManager/List'));
const External = lazy(() => import('../pages/Deploy/External'));
// Workflow2 React Flow 版本
const Workflow2DefinitionList = lazy(() => import('../pages/Workflow2/Definition'));
const Workflow2Design = lazy(() => import('../pages/Workflow2/Design'));
const Workflow2InstanceList = lazy(() => import('../pages/Workflow2/Instance'));
const Workflow2Monitor = lazy(() => import('../pages/Workflow2/Monitor'));
// Workflow2 相关路由已迁移到 Workflow删除旧路由
// 创建路由
const router = createBrowserRouter([
@ -230,43 +226,6 @@ const router = createBrowserRouter([
}
]
},
{
path: 'workflow2',
children: [
{
path: 'definition',
element: (
<Suspense fallback={<LoadingComponent/>}>
<Workflow2DefinitionList/>
</Suspense>
)
},
{
path: 'design/:id',
element: (
<Suspense fallback={<LoadingComponent/>}>
<Workflow2Design/>
</Suspense>
)
},
{
path: 'instance',
element: (
<Suspense fallback={<LoadingComponent/>}>
<Workflow2InstanceList/>
</Suspense>
)
},
{
path: 'monitor',
element: (
<Suspense fallback={<LoadingComponent/>}>
<Workflow2Monitor/>
</Suspense>
)
}
]
},
{
path: '*',
element: <Navigate to="/dashboard"/>