1
This commit is contained in:
parent
33c70b7894
commit
e18874688e
@ -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;
|
|
||||||
@ -1,92 +1,247 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useState, useEffect, useMemo } from 'react';
|
||||||
import { Tabs } from 'antd';
|
import { Drawer, Tabs, Button, Space, message } from 'antd';
|
||||||
import { Cell } from '@antv/x6';
|
import { SaveOutlined, ReloadOutlined, CloseOutlined } from '@ant-design/icons';
|
||||||
import type { WorkflowNodeDefinition, ConfigurableNodeDefinition } from "../nodes/types";
|
import { FormItem, Input, NumberPicker, Select, FormLayout, Switch } from '@formily/antd-v5';
|
||||||
import {
|
import { createForm } from '@formily/core';
|
||||||
Sheet,
|
import { createSchemaField, FormProvider, ISchema } from '@formily/react';
|
||||||
SheetContent,
|
import type { FlowNode, FlowNodeData } from '../types';
|
||||||
SheetHeader,
|
import type { WorkflowNodeDefinition } from '../nodes/types';
|
||||||
SheetTitle,
|
import { isConfigurableNode } from '../nodes/types';
|
||||||
} from "@/components/ui/sheet";
|
|
||||||
import { Button } from "@/components/ui/button";
|
// 创建Schema组件
|
||||||
import { convertJsonSchemaToColumns } from '@/utils/jsonSchemaUtils';
|
const SchemaField = createSchemaField({
|
||||||
import { BetaSchemaForm } from '@ant-design/pro-components';
|
components: {
|
||||||
|
FormItem,
|
||||||
|
Input,
|
||||||
|
NumberPicker,
|
||||||
|
Select,
|
||||||
|
FormLayout,
|
||||||
|
Switch,
|
||||||
|
'Input.TextArea': Input.TextArea,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
interface NodeConfigModalProps {
|
interface NodeConfigModalProps {
|
||||||
visible: boolean;
|
visible: boolean;
|
||||||
node: Cell | null;
|
node: FlowNode | null;
|
||||||
nodeDefinition: WorkflowNodeDefinition | null;
|
|
||||||
onOk: (values: any) => void;
|
|
||||||
onCancel: () => void;
|
onCancel: () => void;
|
||||||
}
|
onOk: (nodeId: string, updatedData: Partial<FlowNodeData>) => void;
|
||||||
|
|
||||||
interface FormData {
|
|
||||||
configs?: Record<string, any>;
|
|
||||||
inputMapping?: Record<string, any>;
|
|
||||||
outputMapping?: Record<string, any>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const NodeConfigModal: React.FC<NodeConfigModalProps> = ({
|
const NodeConfigModal: React.FC<NodeConfigModalProps> = ({
|
||||||
visible,
|
visible,
|
||||||
node,
|
node,
|
||||||
nodeDefinition,
|
|
||||||
onOk,
|
|
||||||
onCancel,
|
onCancel,
|
||||||
|
onOk
|
||||||
}) => {
|
}) => {
|
||||||
const [formData, setFormData] = useState<FormData>({});
|
const [loading, setLoading] = useState(false);
|
||||||
const [activeTab, setActiveTab] = useState('config');
|
const [activeTab, setActiveTab] = useState('config');
|
||||||
|
|
||||||
// 判断是否为可配置节点
|
// 获取节点定义
|
||||||
const isConfigurableNode = (def: WorkflowNodeDefinition): def is ConfigurableNodeDefinition => {
|
const nodeDefinition: WorkflowNodeDefinition | null = node?.data?.nodeDefinition || null;
|
||||||
return 'inputMappingSchema' in def || 'outputMappingSchema' in def;
|
|
||||||
};
|
|
||||||
|
|
||||||
|
// 创建Formily表单实例
|
||||||
|
const configForm = useMemo(() => createForm(), []);
|
||||||
|
const inputForm = useMemo(() => createForm(), []);
|
||||||
|
const outputForm = useMemo(() => createForm(), []);
|
||||||
|
|
||||||
|
// 初始化表单数据
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (nodeDefinition && node) {
|
if (visible && node && nodeDefinition) {
|
||||||
// 从节点数据中获取现有配置
|
const nodeData = node.data || {};
|
||||||
const nodeData = node.getData() || {};
|
|
||||||
|
|
||||||
// 准备默认的基本信息配置
|
// 准备默认配置
|
||||||
const defaultConfig = {
|
const defaultConfig = {
|
||||||
nodeName: nodeDefinition.nodeName, // 默认节点名称
|
nodeName: nodeDefinition.nodeName,
|
||||||
nodeCode: nodeDefinition.nodeCode, // 默认节点编码
|
nodeCode: nodeDefinition.nodeCode,
|
||||||
description: nodeDefinition.description // 默认节点描述
|
description: nodeDefinition.description
|
||||||
};
|
};
|
||||||
|
|
||||||
// 合并默认值和已保存的配置
|
// 设置表单初始值
|
||||||
setFormData({
|
configForm.setInitialValues({ ...defaultConfig, ...(nodeData.configs || {}) });
|
||||||
configs: { ...defaultConfig, ...(nodeData.configs || {}) },
|
configForm.reset();
|
||||||
inputMapping: nodeData.inputMapping || {},
|
|
||||||
outputMapping: nodeData.outputMapping || {},
|
if (isConfigurableNode(nodeDefinition)) {
|
||||||
});
|
inputForm.setInitialValues(nodeData.inputMapping || {});
|
||||||
} else {
|
inputForm.reset();
|
||||||
setFormData({});
|
|
||||||
|
outputForm.setInitialValues(nodeData.outputMapping || {});
|
||||||
|
outputForm.reset();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}, [nodeDefinition, node]);
|
}, [visible, node, nodeDefinition, configForm, inputForm, outputForm]);
|
||||||
|
|
||||||
const handleSubmit = () => {
|
// 递归处理表单值,将JSON字符串转换为对象
|
||||||
onOk(formData);
|
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>) => {
|
const handleSubmit = async () => {
|
||||||
setFormData(prev => ({
|
if (!node || !nodeDefinition) return;
|
||||||
...prev,
|
|
||||||
configs: values
|
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 handleInputMappingChange = (values: Record<string, any>) => {
|
const handleReset = () => {
|
||||||
setFormData(prev => ({
|
configForm.reset();
|
||||||
...prev,
|
inputForm.reset();
|
||||||
inputMapping: values
|
outputForm.reset();
|
||||||
}));
|
message.info('已重置为初始值');
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleOutputMappingChange = (values: Record<string, any>) => {
|
// 将JSON Schema转换为Formily Schema(扩展配置)
|
||||||
setFormData(prev => ({
|
const convertToFormilySchema = (jsonSchema: ISchema): ISchema => {
|
||||||
...prev,
|
const schema: ISchema = {
|
||||||
outputMapping: values
|
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) {
|
if (!nodeDefinition) {
|
||||||
@ -98,18 +253,15 @@ const NodeConfigModal: React.FC<NodeConfigModalProps> = ({
|
|||||||
{
|
{
|
||||||
key: 'config',
|
key: 'config',
|
||||||
label: '基本配置',
|
label: '基本配置',
|
||||||
children: (
|
children: activeTab === 'config' ? (
|
||||||
<div style={{ padding: '16px 0' }}>
|
<div style={{ padding: '16px 0' }}>
|
||||||
<BetaSchemaForm
|
<FormProvider form={configForm}>
|
||||||
key={`configs-${node?.id}-${JSON.stringify(formData.configs)}`}
|
<FormLayout labelCol={6} wrapperCol={18} labelAlign="right" colon={false}>
|
||||||
layoutType="Form"
|
<SchemaField schema={convertToFormilySchema(nodeDefinition.configSchema)} />
|
||||||
columns={convertJsonSchemaToColumns(nodeDefinition.configSchema as any)}
|
</FormLayout>
|
||||||
initialValues={formData.configs}
|
</FormProvider>
|
||||||
onValuesChange={(_, allValues) => handleConfigChange(allValues)}
|
|
||||||
submitter={false}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
),
|
) : null,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
@ -119,18 +271,15 @@ const NodeConfigModal: React.FC<NodeConfigModalProps> = ({
|
|||||||
tabItems.push({
|
tabItems.push({
|
||||||
key: 'input',
|
key: 'input',
|
||||||
label: '输入映射',
|
label: '输入映射',
|
||||||
children: (
|
children: activeTab === 'input' ? (
|
||||||
<div style={{ padding: '16px 0' }}>
|
<div style={{ padding: '16px 0' }}>
|
||||||
<BetaSchemaForm
|
<FormProvider form={inputForm}>
|
||||||
key={`input-${node?.id}-${JSON.stringify(formData.inputMapping)}`}
|
<FormLayout labelCol={6} wrapperCol={18} labelAlign="right" colon={false}>
|
||||||
layoutType="Form"
|
<SchemaField schema={convertToFormilySchema(nodeDefinition.inputMappingSchema)} />
|
||||||
columns={convertJsonSchemaToColumns(nodeDefinition.inputMappingSchema as any)}
|
</FormLayout>
|
||||||
initialValues={formData.inputMapping}
|
</FormProvider>
|
||||||
onValuesChange={(_, allValues) => handleInputMappingChange(allValues)}
|
|
||||||
submitter={false}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
),
|
) : null,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -138,66 +287,81 @@ const NodeConfigModal: React.FC<NodeConfigModalProps> = ({
|
|||||||
tabItems.push({
|
tabItems.push({
|
||||||
key: 'output',
|
key: 'output',
|
||||||
label: '输出映射',
|
label: '输出映射',
|
||||||
children: (
|
children: activeTab === 'output' ? (
|
||||||
<div style={{ padding: '16px 0' }}>
|
<div style={{ padding: '16px 0' }}>
|
||||||
<BetaSchemaForm
|
<FormProvider form={outputForm}>
|
||||||
key={`output-${node?.id}-${JSON.stringify(formData.outputMapping)}`}
|
<FormLayout labelCol={6} wrapperCol={18} labelAlign="right" colon={false}>
|
||||||
layoutType="Form"
|
<SchemaField schema={convertToFormilySchema(nodeDefinition.outputMappingSchema)} />
|
||||||
columns={convertJsonSchemaToColumns(nodeDefinition.outputMappingSchema as any)}
|
</FormLayout>
|
||||||
initialValues={formData.outputMapping}
|
</FormProvider>
|
||||||
onValuesChange={(_, allValues) => handleOutputMappingChange(allValues)}
|
|
||||||
submitter={false}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
),
|
) : null,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Sheet open={visible} onOpenChange={(open) => !open && onCancel()}>
|
<Drawer
|
||||||
<SheetContent
|
title={
|
||||||
style={{
|
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', paddingRight: '24px' }}>
|
||||||
width: '600px',
|
<span style={{ fontSize: '16px', fontWeight: '600' }}>
|
||||||
maxWidth: '90vw',
|
|
||||||
display: 'flex',
|
|
||||||
flexDirection: 'column'
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<SheetHeader>
|
|
||||||
<SheetTitle>
|
|
||||||
编辑节点 - {nodeDefinition.nodeName}
|
编辑节点 - {nodeDefinition.nodeName}
|
||||||
</SheetTitle>
|
</span>
|
||||||
</SheetHeader>
|
<Button
|
||||||
|
type="text"
|
||||||
<div style={{ flex: 1, overflow: 'hidden', display: 'flex', flexDirection: 'column' }}>
|
icon={<CloseOutlined />}
|
||||||
<Tabs
|
onClick={onCancel}
|
||||||
activeKey={activeTab}
|
size="small"
|
||||||
onChange={setActiveTab}
|
|
||||||
items={tabItems}
|
|
||||||
style={{ flex: 1, display: 'flex', flexDirection: 'column' }}
|
|
||||||
className="flex-tabs"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
}
|
||||||
<div
|
placement="right"
|
||||||
style={{
|
width={720}
|
||||||
borderTop: '1px solid #f0f0f0',
|
open={visible}
|
||||||
padding: '16px 0 0 0',
|
onClose={onCancel}
|
||||||
display: 'flex',
|
closeIcon={null}
|
||||||
justifyContent: 'flex-end',
|
styles={{
|
||||||
gap: '8px'
|
body: { padding: '0 24px 24px' },
|
||||||
}}
|
header: { borderBottom: '1px solid #f0f0f0', padding: '16px 24px' }
|
||||||
>
|
}}
|
||||||
<Button variant="outline" onClick={onCancel}>
|
footer={
|
||||||
取消
|
<div style={{
|
||||||
</Button>
|
display: 'flex',
|
||||||
<Button onClick={handleSubmit}>
|
justifyContent: 'space-between',
|
||||||
确定
|
padding: '12px 0',
|
||||||
|
borderTop: '1px solid #f0f0f0'
|
||||||
|
}}>
|
||||||
|
<Button
|
||||||
|
icon={<ReloadOutlined />}
|
||||||
|
onClick={handleReset}
|
||||||
|
>
|
||||||
|
重置
|
||||||
</Button>
|
</Button>
|
||||||
|
<Space>
|
||||||
|
<Button onClick={onCancel}>
|
||||||
|
取消
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
loading={loading}
|
||||||
|
icon={<SaveOutlined />}
|
||||||
|
onClick={handleSubmit}
|
||||||
|
>
|
||||||
|
保存配置
|
||||||
|
</Button>
|
||||||
|
</Space>
|
||||||
</div>
|
</div>
|
||||||
</SheetContent>
|
}
|
||||||
</Sheet>
|
>
|
||||||
|
<div style={{ paddingTop: '16px' }}>
|
||||||
|
<Tabs
|
||||||
|
activeKey={activeTab}
|
||||||
|
onChange={setActiveTab}
|
||||||
|
items={tabItems}
|
||||||
|
size="large"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Drawer>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -1,91 +1,30 @@
|
|||||||
import React, {useState, useEffect} from 'react';
|
import React from 'react';
|
||||||
import {Card, Collapse, Tooltip, message} from 'antd';
|
import { NodeCategory } from '../types';
|
||||||
import type {NodeCategory} from '../nodes/types';
|
import { NODE_DEFINITIONS } from '../nodes';
|
||||||
import {
|
import type { WorkflowNodeDefinition } from '../nodes/types';
|
||||||
PlayCircleOutlined,
|
|
||||||
StopOutlined,
|
|
||||||
UserOutlined,
|
|
||||||
ApiOutlined,
|
|
||||||
CodeOutlined,
|
|
||||||
NodeIndexOutlined,
|
|
||||||
SplitCellsOutlined,
|
|
||||||
AppstoreOutlined,
|
|
||||||
BranchesOutlined
|
|
||||||
} from '@ant-design/icons';
|
|
||||||
import {getNodeDefinitionList} from "../nodes/nodeService";
|
|
||||||
import type {WorkflowNodeDefinition} from "../nodes/types";
|
|
||||||
|
|
||||||
// 使用 Collapse 组件,不需要解构 Panel
|
// 图标映射函数
|
||||||
|
const getNodeIcon = (iconName: string): string => {
|
||||||
// 图标映射配置
|
const iconMap: Record<string, string> = {
|
||||||
const iconMap: Record<string, any> = {
|
'play-circle': '▶️',
|
||||||
'play-circle': PlayCircleOutlined,
|
'stop-circle': '⏹️',
|
||||||
'stop': StopOutlined,
|
'user': '👤',
|
||||||
'user': UserOutlined,
|
'api': '⚙️',
|
||||||
'api': ApiOutlined,
|
'code': '📜',
|
||||||
'code': CodeOutlined,
|
'build': '🚀',
|
||||||
'build': CodeOutlined, // 构建任务图标
|
'jenkins': '🔨',
|
||||||
'fork': NodeIndexOutlined,
|
'gateway': '💎'
|
||||||
'branches': SplitCellsOutlined,
|
};
|
||||||
'apartment': AppstoreOutlined
|
return iconMap[iconName] || '📋';
|
||||||
};
|
|
||||||
|
|
||||||
// 节点类型到图标的映射
|
|
||||||
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'},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
interface NodePanelProps {
|
interface NodePanelProps {
|
||||||
onNodeDragStart?: (node: WorkflowNodeDefinition, e: React.DragEvent) => void,
|
className?: string;
|
||||||
nodeDefinitions?: WorkflowNodeDefinition[]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const NodePanel: React.FC<NodePanelProps> = ({onNodeDragStart}) => {
|
const NodePanel: React.FC<NodePanelProps> = ({ className = '' }) => {
|
||||||
const [loading, setLoading] = useState(false);
|
// 按分类分组节点
|
||||||
const [nodeDefinitions, setNodeDefinitions] = useState<WorkflowNodeDefinition[]>([]);
|
const nodesByCategory = NODE_DEFINITIONS.reduce((acc, node) => {
|
||||||
|
|
||||||
// 加载节点定义列表
|
|
||||||
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) => {
|
|
||||||
if (!acc[node.category]) {
|
if (!acc[node.category]) {
|
||||||
acc[node.category] = [];
|
acc[node.category] = [];
|
||||||
}
|
}
|
||||||
@ -93,145 +32,156 @@ const NodePanel: React.FC<NodePanelProps> = ({onNodeDragStart}) => {
|
|||||||
return acc;
|
return acc;
|
||||||
}, {} as Record<NodeCategory, WorkflowNodeDefinition[]>);
|
}, {} as Record<NodeCategory, WorkflowNodeDefinition[]>);
|
||||||
|
|
||||||
// 处理节点拖拽开始事件
|
// 拖拽开始处理
|
||||||
const handleDragStart = (node: WorkflowNodeDefinition, e: React.DragEvent) => {
|
const handleDragStart = (event: React.DragEvent, nodeDefinition: WorkflowNodeDefinition) => {
|
||||||
e.dataTransfer.setData('node', JSON.stringify(node));
|
event.dataTransfer.setData('application/reactflow', JSON.stringify({
|
||||||
onNodeDragStart?.(node, e);
|
nodeType: nodeDefinition.nodeType,
|
||||||
};
|
nodeDefinition
|
||||||
|
|
||||||
// 渲染节点图标
|
|
||||||
const renderNodeIcon = (node: WorkflowNodeDefinition) => {
|
|
||||||
const iconName = node.uiConfig?.style.icon;
|
|
||||||
// 首先尝试使用配置的图标
|
|
||||||
let IconComponent = iconMap[iconName];
|
|
||||||
|
|
||||||
// 如果没有找到对应的图标,使用节点类型对应的默认图标
|
|
||||||
if (!IconComponent) {
|
|
||||||
IconComponent = typeIconMap[node.nodeType] || AppstoreOutlined;
|
|
||||||
}
|
|
||||||
|
|
||||||
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',
|
|
||||||
overflow: 'hidden',
|
|
||||||
textOverflow: 'ellipsis',
|
|
||||||
whiteSpace: 'nowrap',
|
|
||||||
}}>{node.nodeName}({node.nodeType})</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Tooltip>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}));
|
}));
|
||||||
|
event.dataTransfer.effectAllowed = 'move';
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
// 渲染节点项
|
||||||
<Card
|
const renderNodeItem = (nodeDefinition: WorkflowNodeDefinition) => (
|
||||||
title="流程节点"
|
<div
|
||||||
size="small"
|
key={nodeDefinition.nodeCode}
|
||||||
loading={loading}
|
draggable
|
||||||
styles={{
|
style={{
|
||||||
header: {
|
display: 'flex',
|
||||||
padding: '12px 16px',
|
alignItems: 'center',
|
||||||
fontSize: '15px',
|
gap: '8px',
|
||||||
fontWeight: 500,
|
padding: '8px 12px',
|
||||||
borderBottom: '1px solid #f0f0f0',
|
borderRadius: '6px',
|
||||||
},
|
border: '1px solid #e5e7eb',
|
||||||
body: {
|
background: 'white',
|
||||||
padding: '12px',
|
cursor: 'grab',
|
||||||
height: 'calc(100vh - 250px)',
|
transition: 'all 0.2s ease-in-out',
|
||||||
overflowY: 'auto',
|
marginBottom: '6px'
|
||||||
overflowX: 'hidden'
|
}}
|
||||||
}
|
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';
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Collapse
|
<div style={{
|
||||||
defaultActiveKey={['1', '2', '3', '4']}
|
fontSize: '16px',
|
||||||
ghost
|
width: '20px',
|
||||||
items={collapseItems}
|
textAlign: 'center',
|
||||||
style={{
|
color: nodeDefinition.uiConfig.style.fill
|
||||||
background: 'transparent',
|
}}>
|
||||||
}}
|
{getNodeIcon(nodeDefinition.uiConfig.style.icon)}
|
||||||
/>
|
</div>
|
||||||
</Card>
|
<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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -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;
|
|
||||||
@ -1,163 +1,168 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Button, Space, Tooltip, Modal } from 'antd';
|
import { Button, Divider, Tooltip } from 'antd';
|
||||||
import {
|
import {
|
||||||
ArrowLeftOutlined,
|
|
||||||
SaveOutlined,
|
SaveOutlined,
|
||||||
CopyOutlined,
|
|
||||||
DeleteOutlined,
|
|
||||||
UndoOutlined,
|
UndoOutlined,
|
||||||
RedoOutlined,
|
RedoOutlined,
|
||||||
ScissorOutlined,
|
|
||||||
SnippetsOutlined,
|
|
||||||
SelectOutlined,
|
|
||||||
ZoomInOutlined,
|
ZoomInOutlined,
|
||||||
ZoomOutOutlined,
|
ZoomOutOutlined,
|
||||||
|
ExpandOutlined,
|
||||||
|
ArrowLeftOutlined
|
||||||
} from '@ant-design/icons';
|
} from '@ant-design/icons';
|
||||||
|
|
||||||
interface WorkflowToolbarProps {
|
interface WorkflowToolbarProps {
|
||||||
title: string;
|
title?: string;
|
||||||
scale: number;
|
onSave?: () => void;
|
||||||
canUndo: boolean;
|
onUndo?: () => void;
|
||||||
canRedo: boolean;
|
onRedo?: () => void;
|
||||||
onBack: () => void;
|
onZoomIn?: () => void;
|
||||||
onSave: () => void;
|
onZoomOut?: () => void;
|
||||||
onUndo: () => void;
|
onFitView?: () => void;
|
||||||
onRedo: () => void;
|
onBack?: () => void;
|
||||||
onCopy: () => void;
|
canUndo?: boolean;
|
||||||
onCut: () => void;
|
canRedo?: boolean;
|
||||||
onPaste: () => void;
|
zoom?: number;
|
||||||
onZoomIn: () => void;
|
className?: string;
|
||||||
onZoomOut: () => void;
|
|
||||||
onSelectAll: () => void;
|
|
||||||
onDelete: () => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 工作流设计器工具栏组件
|
|
||||||
*/
|
|
||||||
const WorkflowToolbar: React.FC<WorkflowToolbarProps> = ({
|
const WorkflowToolbar: React.FC<WorkflowToolbarProps> = ({
|
||||||
title,
|
title = '工作流设计器',
|
||||||
scale,
|
|
||||||
canUndo,
|
|
||||||
canRedo,
|
|
||||||
onBack,
|
|
||||||
onSave,
|
onSave,
|
||||||
onUndo,
|
onUndo,
|
||||||
onRedo,
|
onRedo,
|
||||||
onCopy,
|
|
||||||
onCut,
|
|
||||||
onPaste,
|
|
||||||
onZoomIn,
|
onZoomIn,
|
||||||
onZoomOut,
|
onZoomOut,
|
||||||
onSelectAll,
|
onFitView,
|
||||||
onDelete
|
onBack,
|
||||||
|
canUndo = false,
|
||||||
|
canRedo = false,
|
||||||
|
zoom = 1,
|
||||||
|
className = ''
|
||||||
}) => {
|
}) => {
|
||||||
const handleDelete = () => {
|
|
||||||
Modal.confirm({
|
|
||||||
title: '确认删除',
|
|
||||||
content: '确定要删除选中的元素吗?',
|
|
||||||
onOk: onDelete
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="header">
|
<div
|
||||||
<Space>
|
className={`workflow-toolbar ${className}`}
|
||||||
<Tooltip title="返回">
|
style={{
|
||||||
<Button
|
height: '56px',
|
||||||
className="back-button"
|
background: 'white',
|
||||||
icon={<ArrowLeftOutlined/>}
|
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}
|
onClick={onBack}
|
||||||
|
style={{ color: '#6b7280' }}
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<span>{title}</span>
|
<Divider type="vertical" />
|
||||||
</Space>
|
<h2 style={{
|
||||||
<div className="actions">
|
margin: 0,
|
||||||
<Space>
|
fontSize: '16px',
|
||||||
<Space.Compact>
|
fontWeight: '600',
|
||||||
<Tooltip title="撤销">
|
color: '#374151'
|
||||||
<Button
|
}}>
|
||||||
icon={<UndoOutlined/>}
|
{title}
|
||||||
onClick={onUndo}
|
</h2>
|
||||||
disabled={!canUndo}
|
</div>
|
||||||
>
|
|
||||||
撤销
|
{/* 右侧:操作按钮区域 */}
|
||||||
</Button>
|
<div style={{ display: 'flex', alignItems: 'center', gap: '6px' }}>
|
||||||
</Tooltip>
|
{/* 撤销/重做 */}
|
||||||
<Tooltip title="重做">
|
<Tooltip title="撤销 (Ctrl+Z)">
|
||||||
<Button
|
<Button
|
||||||
icon={<RedoOutlined/>}
|
type="text"
|
||||||
onClick={onRedo}
|
icon={<UndoOutlined />}
|
||||||
disabled={!canRedo}
|
onClick={onUndo}
|
||||||
>
|
disabled={!canUndo}
|
||||||
重做
|
size="middle"
|
||||||
</Button>
|
style={{
|
||||||
</Tooltip>
|
color: canUndo ? '#374151' : '#d1d5db',
|
||||||
</Space.Compact>
|
}}
|
||||||
<Space.Compact>
|
/>
|
||||||
<Tooltip title="剪切">
|
</Tooltip>
|
||||||
<Button
|
<Tooltip title="重做 (Ctrl+Shift+Z)">
|
||||||
icon={<ScissorOutlined/>}
|
<Button
|
||||||
onClick={onCut}
|
type="text"
|
||||||
/>
|
icon={<RedoOutlined />}
|
||||||
</Tooltip>
|
onClick={onRedo}
|
||||||
<Tooltip title="复制">
|
disabled={!canRedo}
|
||||||
<Button
|
size="middle"
|
||||||
icon={<CopyOutlined/>}
|
style={{
|
||||||
onClick={onCopy}
|
color: canRedo ? '#374151' : '#d1d5db',
|
||||||
/>
|
}}
|
||||||
</Tooltip>
|
/>
|
||||||
<Tooltip title="粘贴">
|
</Tooltip>
|
||||||
<Button
|
|
||||||
icon={<SnippetsOutlined/>}
|
<Divider type="vertical" style={{ margin: '0 4px' }} />
|
||||||
onClick={onPaste}
|
|
||||||
/>
|
{/* 视图操作 */}
|
||||||
</Tooltip>
|
<Tooltip title="放大 (+)">
|
||||||
</Space.Compact>
|
<Button
|
||||||
<Space.Compact>
|
type="text"
|
||||||
<Tooltip title="放大">
|
icon={<ZoomInOutlined />}
|
||||||
<Button
|
onClick={onZoomIn}
|
||||||
icon={<ZoomInOutlined/>}
|
size="middle"
|
||||||
onClick={onZoomIn}
|
/>
|
||||||
disabled={scale >= 2}
|
</Tooltip>
|
||||||
>
|
<Tooltip title="缩小 (-)">
|
||||||
放大
|
<Button
|
||||||
</Button>
|
type="text"
|
||||||
</Tooltip>
|
icon={<ZoomOutOutlined />}
|
||||||
<Tooltip title="缩小">
|
onClick={onZoomOut}
|
||||||
<Button
|
size="middle"
|
||||||
icon={<ZoomOutOutlined/>}
|
/>
|
||||||
onClick={onZoomOut}
|
</Tooltip>
|
||||||
disabled={scale <= 0.2}
|
<Tooltip title="适应视图">
|
||||||
>
|
<Button
|
||||||
缩小
|
type="text"
|
||||||
</Button>
|
icon={<ExpandOutlined />}
|
||||||
</Tooltip>
|
onClick={onFitView}
|
||||||
</Space.Compact>
|
size="middle"
|
||||||
<Space.Compact>
|
/>
|
||||||
<Tooltip title="全选">
|
</Tooltip>
|
||||||
<Button
|
|
||||||
icon={<SelectOutlined/>}
|
{/* 缩放比例显示 */}
|
||||||
onClick={onSelectAll}
|
<div style={{
|
||||||
/>
|
background: '#f8fafc',
|
||||||
</Tooltip>
|
padding: '6px 12px',
|
||||||
<Tooltip title="删除">
|
borderRadius: '6px',
|
||||||
<Button
|
fontSize: '12px',
|
||||||
icon={<DeleteOutlined/>}
|
color: '#475569',
|
||||||
onClick={handleDelete}
|
fontFamily: 'ui-monospace, monospace',
|
||||||
/>
|
minWidth: '60px',
|
||||||
</Tooltip>
|
textAlign: 'center',
|
||||||
</Space.Compact>
|
fontWeight: '600',
|
||||||
<Tooltip title="保存">
|
border: '1px solid #e2e8f0',
|
||||||
<Button
|
marginLeft: '4px'
|
||||||
type="primary"
|
}}>
|
||||||
icon={<SaveOutlined/>}
|
{Math.round(zoom * 100)}%
|
||||||
onClick={onSave}
|
</div>
|
||||||
>
|
|
||||||
保存
|
<Divider type="vertical" style={{ margin: '0 8px' }} />
|
||||||
</Button>
|
|
||||||
</Tooltip>
|
{/* 保存按钮(最右侧) */}
|
||||||
</Space>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -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
|
|
||||||
};
|
|
||||||
};
|
|
||||||
@ -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
|
|
||||||
};
|
|
||||||
};
|
|
||||||
@ -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
|
|
||||||
};
|
|
||||||
};
|
|
||||||
@ -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
|
|
||||||
};
|
|
||||||
};
|
|
||||||
@ -1,171 +1,267 @@
|
|||||||
.workflow-design {
|
// 工作流设计器 - React Flow版本
|
||||||
position: relative;
|
// 变量定义
|
||||||
height: calc(100vh - 184px);
|
@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;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
background: #f8fafc;
|
overflow: hidden;
|
||||||
|
background: #ffffff;
|
||||||
|
position: relative;
|
||||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||||
|
|
||||||
.header {
|
// React Flow 样式覆盖
|
||||||
padding: 16px 24px;
|
.react-flow {
|
||||||
border-bottom: 1px solid #f0f0f0;
|
width: 100% !important;
|
||||||
background: #ffffff;
|
height: 100% !important;
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
flex-shrink: 0;
|
|
||||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
|
|
||||||
|
|
||||||
.back-button {
|
&__renderer {
|
||||||
margin-right: 16px;
|
width: 100% !important;
|
||||||
|
height: 100% !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.actions {
|
// 自定义控制按钮样式
|
||||||
.ant-space-compact {
|
&__controls {
|
||||||
margin-right: 8px;
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.content {
|
// 内容区域
|
||||||
|
.workflow-content-area {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-height: 0;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
padding: 16px;
|
overflow: hidden;
|
||||||
gap: 16px;
|
position: relative;
|
||||||
overflow: hidden; // 防止外层出现滚动条
|
min-height: 0;
|
||||||
|
|
||||||
.sidebar {
|
// 节点面板区域
|
||||||
width: 280px;
|
.node-panel {
|
||||||
flex-shrink: 0;
|
width: @sidebar-width;
|
||||||
border-radius: 8px;
|
background: #f8fafc;
|
||||||
background: #ffffff;
|
border-right: 1px solid #e5e7eb;
|
||||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
overflow: hidden;
|
||||||
border: 1px solid #f0f0f0;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
overflow: hidden;
|
box-shadow: 2px 0 4px rgba(0, 0, 0, 0.05);
|
||||||
|
|
||||||
:global {
|
// 面板头部
|
||||||
.ant-collapse {
|
.panel-header {
|
||||||
border: none;
|
padding: 16px;
|
||||||
background: transparent;
|
background: #ffffff;
|
||||||
flex: 1;
|
border-bottom: 1px solid #e5e7eb;
|
||||||
overflow-y: auto; // 只在折叠面板内部显示滚动条
|
|
||||||
|
|
||||||
.ant-collapse-item {
|
h3 {
|
||||||
border-radius: 0;
|
margin: 0;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #374151;
|
||||||
|
}
|
||||||
|
|
||||||
.ant-collapse-header {
|
p {
|
||||||
padding: 8px 16px;
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ant-collapse-content-box {
|
.node-desc {
|
||||||
padding: 0;
|
font-size: 11px;
|
||||||
|
color: #6b7280;
|
||||||
|
line-height: 1.2;
|
||||||
|
margin-top: 2px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.node-item {
|
// 使用提示
|
||||||
padding: 8px 16px;
|
.panel-tips {
|
||||||
margin: 4px 8px;
|
padding: 12px;
|
||||||
border: 1px solid #d9d9d9;
|
background: #f1f5f9;
|
||||||
border-radius: 4px;
|
border-top: 1px solid #e5e7eb;
|
||||||
cursor: move;
|
font-size: 11px;
|
||||||
transition: all 0.3s;
|
color: #64748b;
|
||||||
background: #ffffff;
|
line-height: 1.4;
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background: #f5f5f5;
|
|
||||||
border-color: #1890ff;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.main-area {
|
// 画布区域
|
||||||
|
.workflow-canvas-area {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
display: flex;
|
position: relative;
|
||||||
flex-direction: column;
|
overflow: hidden;
|
||||||
min-width: 0;
|
background: #f8fafc;
|
||||||
|
|
||||||
.workflow-container {
|
// 画布状态面板
|
||||||
flex: 1;
|
.canvas-status {
|
||||||
position: relative;
|
position: absolute;
|
||||||
border: 1px solid #d9d9d9;
|
top: 12px;
|
||||||
border-radius: 4px;
|
left: 12px;
|
||||||
background: #ffffff;
|
background: rgba(255, 255, 255, 0.95);
|
||||||
overflow: hidden;
|
padding: 8px 12px;
|
||||||
|
border-radius: 6px;
|
||||||
.workflow-canvas {
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||||
width: 100%;
|
font-size: 12px;
|
||||||
height: 100%;
|
color: #6b7280;
|
||||||
}
|
z-index: 10;
|
||||||
|
backdrop-filter: blur(4px);
|
||||||
.minimap-container {
|
|
||||||
position: absolute;
|
|
||||||
right: 20px;
|
|
||||||
bottom: 20px;
|
|
||||||
width: var(--minimap-width);
|
|
||||||
height: var(--minimap-height);
|
|
||||||
border: 1px solid #f0f0f0;
|
|
||||||
border-radius: 4px;
|
|
||||||
background: #fff;
|
|
||||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
|
||||||
z-index: 1;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
:global {
|
// 响应式适配
|
||||||
.node-selected {
|
@media (max-width: 1024px) {
|
||||||
> rect {
|
.workflow-content-area .node-panel {
|
||||||
stroke: #1890ff;
|
width: 220px;
|
||||||
stroke-width: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
> path {
|
|
||||||
stroke: #1890ff;
|
|
||||||
stroke-width: 2px;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.x6-node-selected {
|
@media (max-width: 768px) {
|
||||||
rect, ellipse {
|
.workflow-content-area {
|
||||||
stroke: #1890ff;
|
flex-direction: column;
|
||||||
stroke-width: 2px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.x6-edge-selected {
|
.node-panel {
|
||||||
path {
|
width: 100%;
|
||||||
stroke: #1890ff;
|
height: 200px;
|
||||||
stroke-width: 2px !important;
|
border-right: none;
|
||||||
}
|
border-bottom: 1px solid #e5e7eb;
|
||||||
}
|
|
||||||
|
|
||||||
// 右键菜单样式
|
|
||||||
.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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 工具栏特殊样式
|
||||||
|
.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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -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 { useParams, useNavigate } from 'react-router-dom';
|
||||||
|
import { message } from 'antd';
|
||||||
|
import { ReactFlowProvider, useReactFlow } from '@xyflow/react';
|
||||||
|
|
||||||
import WorkflowToolbar from './components/WorkflowToolbar';
|
import WorkflowToolbar from './components/WorkflowToolbar';
|
||||||
import WorkflowCanvas from './components/WorkflowCanvas';
|
import NodePanel from './components/NodePanel';
|
||||||
import NodeConfigDrawer from './components/NodeConfigModal';
|
import FlowCanvas from './components/FlowCanvas';
|
||||||
import ExpressionModal from './components/ExpressionModal';
|
import NodeConfigModal from './components/NodeConfigModal';
|
||||||
import { useWorkflowGraph } from './hooks/useWorkflowGraph';
|
import EdgeConfigModal, { type EdgeCondition } from './components/EdgeConfigModal';
|
||||||
import { useWorkflowData } from './hooks/useWorkflowData';
|
import type { FlowNode, FlowEdge, FlowNodeData } from './types';
|
||||||
import { useWorkflowModals } from './hooks/useWorkflowModals';
|
import type { WorkflowNodeDefinition } from './nodes/types';
|
||||||
import { useWorkflowDragDrop } from './hooks/useWorkflowDragDrop';
|
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';
|
import './index.less';
|
||||||
|
|
||||||
/**
|
const WorkflowDesignInner: React.FC = () => {
|
||||||
* 重构后的工作流设计器主组件
|
|
||||||
*/
|
|
||||||
const WorkflowDesign: React.FC = () => {
|
|
||||||
const { id } = useParams<{ id: string }>();
|
const { id } = useParams<{ id: string }>();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
const {
|
||||||
|
getNodes,
|
||||||
|
setNodes,
|
||||||
|
getEdges,
|
||||||
|
setEdges,
|
||||||
|
screenToFlowPosition,
|
||||||
|
fitView,
|
||||||
|
zoomIn,
|
||||||
|
zoomOut,
|
||||||
|
getZoom
|
||||||
|
} = useReactFlow();
|
||||||
|
|
||||||
// 数据管理
|
const [workflowTitle, setWorkflowTitle] = useState('新建工作流');
|
||||||
const {
|
const [currentZoom, setCurrentZoom] = useState(1); // 当前缩放比例
|
||||||
nodeDefinitions,
|
const reactFlowWrapper = useRef<HTMLDivElement>(null);
|
||||||
isNodeDefinitionsLoaded,
|
|
||||||
title,
|
// 当前工作流ID
|
||||||
loadDefinitionDetail,
|
const currentWorkflowId = id ? parseInt(id) : undefined;
|
||||||
saveWorkflow
|
|
||||||
} = useWorkflowData();
|
// 节点配置模态框状态
|
||||||
|
const [configModalVisible, setConfigModalVisible] = useState(false);
|
||||||
// 弹窗管理
|
const [configNode, setConfigNode] = useState<FlowNode | null>(null);
|
||||||
const {
|
|
||||||
selectedNode,
|
// 边配置模态框状态
|
||||||
selectedNodeDefinition,
|
const [edgeConfigModalVisible, setEdgeConfigModalVisible] = useState(false);
|
||||||
selectedEdge,
|
const [configEdge, setConfigEdge] = useState<FlowEdge | null>(null);
|
||||||
configModalVisible,
|
|
||||||
expressionModalVisible,
|
// 保存和加载hooks
|
||||||
handleNodeEdit,
|
const { hasUnsavedChanges, saveWorkflow, markUnsaved } = useWorkflowSave();
|
||||||
handleEdgeEdit,
|
const { workflowDefinition, loadWorkflow } = useWorkflowLoad();
|
||||||
handleNodeConfigUpdate,
|
|
||||||
handleConditionUpdate,
|
// 历史记录管理
|
||||||
closeNodeConfigModal,
|
const history = useHistory();
|
||||||
closeExpressionModal
|
|
||||||
} = useWorkflowModals();
|
// 剪贴板(用于复制粘贴)
|
||||||
|
const clipboard = useRef<{ nodes: FlowNode[]; edges: FlowEdge[] } | null>(null);
|
||||||
// 图形管理
|
|
||||||
const {
|
|
||||||
graph,
|
|
||||||
scale,
|
|
||||||
graphContainerRef,
|
|
||||||
minimapContainerRef,
|
|
||||||
graphOperations
|
|
||||||
} = useWorkflowGraph(
|
|
||||||
nodeDefinitions,
|
|
||||||
isNodeDefinitionsLoaded,
|
|
||||||
handleNodeEdit,
|
|
||||||
handleEdgeEdit
|
|
||||||
);
|
|
||||||
|
|
||||||
// 拖拽管理
|
|
||||||
const {
|
|
||||||
handleNodeDragStart,
|
|
||||||
handleDrop,
|
|
||||||
handleDragOver
|
|
||||||
} = useWorkflowDragDrop(graph, nodeDefinitions);
|
|
||||||
|
|
||||||
// 加载工作流数据
|
// 加载工作流数据
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
console.log('工作流数据加载检查:', {
|
const loadData = async () => {
|
||||||
hasGraph: !!graph,
|
if (currentWorkflowId) {
|
||||||
hasId: !!id,
|
const data = await loadWorkflow(currentWorkflowId);
|
||||||
id,
|
if (data) {
|
||||||
isNodeDefinitionsLoaded
|
setNodes(data.nodes);
|
||||||
});
|
setEdges(data.edges);
|
||||||
|
setWorkflowTitle(data.definition.name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
if (!id) {
|
loadData();
|
||||||
console.error('工作流ID缺失,无法加载定义数据');
|
}, [currentWorkflowId, loadWorkflow, setNodes, setEdges]);
|
||||||
return;
|
|
||||||
}
|
// 初始化缩放比例
|
||||||
|
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 (graph && id && isNodeDefinitionsLoaded) {
|
return () => clearTimeout(timer);
|
||||||
console.log('开始加载工作流定义详情:', id);
|
}, [fitView, getZoom]);
|
||||||
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'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
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'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}, [graph, id, isNodeDefinitionsLoaded, loadDefinitionDetail]);
|
];
|
||||||
|
|
||||||
|
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 = useCallback(async () => {
|
||||||
const handleSave = () => {
|
const nodes = getNodes() as FlowNode[];
|
||||||
if (!graph) {
|
const edges = getEdges() as FlowEdge[];
|
||||||
console.error('Graph 实例不存在');
|
|
||||||
|
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;
|
return;
|
||||||
}
|
}
|
||||||
saveWorkflow(graph);
|
|
||||||
};
|
const { nodes: copiedNodes, edges: copiedEdges } = clipboard.current;
|
||||||
const handleZoomIn = () => graphOperations.zoom(0.1);
|
const offset = 50; // 粘贴偏移量
|
||||||
const handleZoomOut = () => graphOperations.zoom(-0.1);
|
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 (
|
return (
|
||||||
<div className="workflow-design">
|
<div
|
||||||
|
className="workflow-design-container"
|
||||||
|
style={{
|
||||||
|
// 确保覆盖父容器的overflow设置
|
||||||
|
overflow: 'hidden'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* 工具栏 */}
|
||||||
<WorkflowToolbar
|
<WorkflowToolbar
|
||||||
title={title}
|
title={`${workflowTitle}${hasUnsavedChanges ? ' *' : ''}`}
|
||||||
scale={scale}
|
|
||||||
canUndo={graphOperations.canUndo()}
|
|
||||||
canRedo={graphOperations.canRedo()}
|
|
||||||
onBack={handleBack}
|
|
||||||
onSave={handleSave}
|
onSave={handleSave}
|
||||||
onUndo={graphOperations.undo}
|
onBack={handleBack}
|
||||||
onRedo={graphOperations.redo}
|
onUndo={handleUndo}
|
||||||
onCopy={graphOperations.copy}
|
onRedo={handleRedo}
|
||||||
onCut={graphOperations.cut}
|
|
||||||
onPaste={graphOperations.paste}
|
|
||||||
onZoomIn={handleZoomIn}
|
onZoomIn={handleZoomIn}
|
||||||
onZoomOut={handleZoomOut}
|
onZoomOut={handleZoomOut}
|
||||||
onSelectAll={graphOperations.selectAll}
|
onFitView={handleFitView}
|
||||||
onDelete={graphOperations.deleteSelected}
|
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}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<WorkflowCanvas
|
{/* 边条件配置弹窗 */}
|
||||||
graphContainerRef={graphContainerRef}
|
<EdgeConfigModal
|
||||||
minimapContainerRef={minimapContainerRef}
|
visible={edgeConfigModalVisible}
|
||||||
nodeDefinitions={nodeDefinitions}
|
edge={configEdge}
|
||||||
onNodeDragStart={handleNodeDragStart}
|
onOk={handleEdgeConditionUpdate}
|
||||||
onDrop={handleDrop}
|
onCancel={handleCloseEdgeConfigModal}
|
||||||
onDragOver={handleDragOver}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{configModalVisible && selectedNode && selectedNodeDefinition && (
|
|
||||||
<NodeConfigDrawer
|
|
||||||
visible={configModalVisible}
|
|
||||||
node={selectedNode}
|
|
||||||
nodeDefinition={selectedNodeDefinition}
|
|
||||||
onCancel={closeNodeConfigModal}
|
|
||||||
onOk={handleNodeConfigUpdate}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{selectedEdge && (
|
|
||||||
<ExpressionModal
|
|
||||||
visible={expressionModalVisible}
|
|
||||||
edge={selectedEdge}
|
|
||||||
onOk={handleConditionUpdate}
|
|
||||||
onCancel={closeExpressionModal}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const WorkflowDesign: React.FC = () => {
|
||||||
|
return (
|
||||||
|
<ReactFlowProvider>
|
||||||
|
<WorkflowDesignInner />
|
||||||
|
</ReactFlowProvider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export default WorkflowDesign;
|
export default WorkflowDesign;
|
||||||
|
|||||||
@ -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"]
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@ -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"]
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@ -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"]
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@ -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"]
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@ -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
|
|
||||||
};
|
|
||||||
@ -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);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
@ -1,5 +1,4 @@
|
|||||||
|
// 节点分类
|
||||||
// 节点分类 (从 ../types.ts 合并)
|
|
||||||
export enum NodeCategory {
|
export enum NodeCategory {
|
||||||
EVENT = 'EVENT',
|
EVENT = 'EVENT',
|
||||||
TASK = 'TASK',
|
TASK = 'TASK',
|
||||||
@ -7,100 +6,7 @@ export enum NodeCategory {
|
|||||||
CONTAINER = 'CONTAINER'
|
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 {
|
export enum NodeType {
|
||||||
START_EVENT = 'START_EVENT',
|
START_EVENT = 'START_EVENT',
|
||||||
END_EVENT = 'END_EVENT',
|
END_EVENT = 'END_EVENT',
|
||||||
@ -114,6 +20,58 @@ export enum NodeType {
|
|||||||
CALL_ACTIVITY = 'CALL_ACTIVITY'
|
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 {
|
export interface BaseNodeDefinition {
|
||||||
@ -150,5 +108,10 @@ export interface NodeInstanceData {
|
|||||||
|
|
||||||
// UI位置信息
|
// UI位置信息
|
||||||
position: { x: number; y: number };
|
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;
|
||||||
|
};
|
||||||
|
|||||||
@ -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);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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', () => {
|
|
||||||
// 可以在这里添加额外的逻辑
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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
|
|
||||||
}
|
|
||||||
});
|
|
||||||
@ -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');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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};
|
|
||||||
};
|
|
||||||
@ -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;
|
valid: boolean;
|
||||||
message?: string;
|
message?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 校验节点配置
|
* 验证工作流是否为空
|
||||||
* @param node 节点
|
|
||||||
* @param nodeDefinition 节点定义
|
|
||||||
*/
|
*/
|
||||||
const validateNodeConfig = (node: Cell, nodeDefinition: any): ValidationResult => {
|
const validateNotEmpty = (nodes: FlowNode[]): 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();
|
|
||||||
if (nodes.length === 0) {
|
if (nodes.length === 0) {
|
||||||
return {
|
return {
|
||||||
valid: false,
|
valid: false,
|
||||||
message: '流程图中没有任何节点'
|
message: '流程图中没有任何节点,请至少添加一个节点'
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return { valid: true };
|
return { valid: true };
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 校验必要节点
|
* 验证必需的开始和结束节点
|
||||||
* @param graph 流程图实例
|
|
||||||
*/
|
*/
|
||||||
const validateRequiredNodes = (graph: Graph): ValidationResult => {
|
const validateRequiredNodes = (nodes: FlowNode[]): ValidationResult => {
|
||||||
const nodes = graph.getNodes();
|
|
||||||
|
|
||||||
// 检查开始节点
|
// 检查开始节点
|
||||||
const hasStartNode = nodes.some(node => node.getProp('nodeType') === 'START_EVENT');
|
const hasStartNode = nodes.some(node => node.data.nodeType === NodeType.START_EVENT);
|
||||||
if (!hasStartNode) {
|
if (!hasStartNode) {
|
||||||
return {
|
return {
|
||||||
valid: false,
|
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) {
|
if (!hasEndNode) {
|
||||||
return {
|
return {
|
||||||
valid: false,
|
valid: false,
|
||||||
@ -87,35 +48,66 @@ const validateRequiredNodes = (graph: Graph): ValidationResult => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 校验节点连接
|
* 验证节点连接完整性
|
||||||
* @param graph 流程图实例
|
* 检查是否有孤立的节点(除了开始和结束节点可能只有单向连接)
|
||||||
*/
|
*/
|
||||||
const validateNodeConnections = (graph: Graph): ValidationResult => {
|
const validateNodeConnections = (nodes: FlowNode[], edges: FlowEdge[]): ValidationResult => {
|
||||||
const nodes = graph.getNodes();
|
if (nodes.length <= 1) {
|
||||||
const edges = graph.getEdges();
|
return { valid: true }; // 单节点或空流程不需要验证连接
|
||||||
|
|
||||||
if (edges.length < nodes.length - 1) {
|
|
||||||
return {
|
|
||||||
valid: false,
|
|
||||||
message: '存在未连接的节点,请确保所有节点都已正确连接'
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return { valid: true };
|
// 构建节点连接映射
|
||||||
};
|
const nodeConnections = new Map<string, { incoming: number; outgoing: number }>();
|
||||||
|
|
||||||
/**
|
|
||||||
* 校验所有节点配置
|
|
||||||
* @param graph 流程图实例
|
|
||||||
*/
|
|
||||||
const validateAllNodesConfig = (graph: Graph): ValidationResult => {
|
|
||||||
const nodes = graph.getNodes();
|
|
||||||
|
|
||||||
for (const node of nodes) {
|
nodes.forEach(node => {
|
||||||
const nodeDefinition = node.getProp('nodeDefinition');
|
nodeConnections.set(node.id, { incoming: 0, outgoing: 0 });
|
||||||
const result = validateNodeConfig(node, nodeDefinition);
|
});
|
||||||
if (!result.valid) {
|
|
||||||
return result;
|
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}"没有输出连接,请确保它连接到其他节点`
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -123,20 +115,88 @@ const validateAllNodesConfig = (graph: Graph): ValidationResult => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 校验整个流程图
|
* 验证节点配置完整性
|
||||||
* @param graph 流程图实例
|
* 检查节点是否有必填配置
|
||||||
*/
|
*/
|
||||||
export const validateWorkflow = (graph: Graph): ValidationResult => {
|
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 = [
|
const validators = [
|
||||||
validateGraphNotEmpty,
|
() => validateNotEmpty(nodes),
|
||||||
validateRequiredNodes,
|
() => validateRequiredNodes(nodes),
|
||||||
validateAllNodesConfig,
|
() => validateNodeConnections(nodes, edges),
|
||||||
validateNodeConnections
|
() => validateNodeConfigs(nodes),
|
||||||
|
() => validateEdgeConditions(edges),
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// 依次执行验证
|
||||||
for (const validator of validators) {
|
for (const validator of validators) {
|
||||||
const result = validator(graph);
|
const result = validator();
|
||||||
if (!result.valid) {
|
if (!result.valid) {
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
@ -144,3 +204,4 @@ export const validateWorkflow = (graph: Graph): ValidationResult => {
|
|||||||
|
|
||||||
return { valid: true };
|
return { valid: true };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -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;
|
|
||||||
@ -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;
|
|
||||||
@ -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`);
|
|
||||||
@ -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[];
|
|
||||||
}
|
|
||||||
@ -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;
|
|
||||||
@ -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;
|
|
||||||
@ -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;
|
|
||||||
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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;
|
|
||||||
@ -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;
|
|
||||||
};
|
|
||||||
@ -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 };
|
|
||||||
};
|
|
||||||
|
|
||||||
@ -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;
|
|
||||||
@ -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'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@ -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;
|
|
||||||
}
|
|
||||||
@ -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;
|
|
||||||
@ -45,11 +45,7 @@ const JenkinsManagerList = lazy(() => import('../pages/Deploy/JenkinsManager/Lis
|
|||||||
const GitManagerList = lazy(() => import('../pages/Deploy/GitManager/List'));
|
const GitManagerList = lazy(() => import('../pages/Deploy/GitManager/List'));
|
||||||
const External = lazy(() => import('../pages/Deploy/External'));
|
const External = lazy(() => import('../pages/Deploy/External'));
|
||||||
|
|
||||||
// Workflow2 React Flow 版本
|
// Workflow2 相关路由已迁移到 Workflow,删除旧路由
|
||||||
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'));
|
|
||||||
|
|
||||||
// 创建路由
|
// 创建路由
|
||||||
const router = createBrowserRouter([
|
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: '*',
|
path: '*',
|
||||||
element: <Navigate to="/dashboard"/>
|
element: <Navigate to="/dashboard"/>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user