增加链接线验证。
This commit is contained in:
parent
17acef1529
commit
4cb2b0b685
@ -0,0 +1,65 @@
|
||||
import React from 'react';
|
||||
import { Form, Input, InputNumber } from 'antd';
|
||||
import { Edge } from '@antv/x6';
|
||||
|
||||
interface EdgeConfigProps {
|
||||
edge: Edge;
|
||||
form: any;
|
||||
onValuesChange?: (changedValues: any, allValues: any) => void;
|
||||
}
|
||||
|
||||
interface EdgeData {
|
||||
condition?: string;
|
||||
description?: string;
|
||||
priority?: number;
|
||||
}
|
||||
|
||||
const EdgeConfig: React.FC<EdgeConfigProps> = ({ edge, form, onValuesChange }) => {
|
||||
return (
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
onValuesChange={onValuesChange}
|
||||
initialValues={edge.getData() as EdgeData}
|
||||
>
|
||||
<Form.Item
|
||||
label="条件表达式"
|
||||
name="condition"
|
||||
tooltip="使用SpEL表达式,例如:#{user.age > 18}"
|
||||
rules={[{ required: true, message: '请输入条件表达式' }]}
|
||||
>
|
||||
<Input.TextArea
|
||||
placeholder="请输入条件表达式"
|
||||
autoSize={{ minRows: 3, maxRows: 6 }}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label="描述"
|
||||
name="description"
|
||||
tooltip="条件的说明文字"
|
||||
>
|
||||
<Input.TextArea
|
||||
placeholder="请输入条件描述"
|
||||
autoSize={{ minRows: 2, maxRows: 4 }}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label="优先级"
|
||||
name="priority"
|
||||
tooltip="数字越小优先级越高"
|
||||
rules={[
|
||||
{ type: 'number', min: 0, max: 100, message: '优先级范围为0-100' }
|
||||
]}
|
||||
>
|
||||
<InputNumber
|
||||
placeholder="请输入优先级"
|
||||
style={{ width: '100%' }}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
|
||||
export default EdgeConfig;
|
||||
@ -4,7 +4,7 @@ import {Button, Card, Layout, message, Space, Spin, Drawer, Form, Dropdown} from
|
||||
import {ArrowLeftOutlined, SaveOutlined} from '@ant-design/icons';
|
||||
import {getDefinition, updateDefinition} from '../../service';
|
||||
import {WorkflowDefinition, WorkflowStatus} from '../../../Workflow/types';
|
||||
import {Graph, Node, Cell} from '@antv/x6';
|
||||
import {Graph, Node, Cell, Edge, Shape} from '@antv/x6';
|
||||
import '@antv/x6-react-shape';
|
||||
import {Selection} from '@antv/x6-plugin-selection';
|
||||
import {History} from '@antv/x6-plugin-history';
|
||||
@ -13,14 +13,13 @@ import {Transform} from '@antv/x6-plugin-transform';
|
||||
import {Keyboard} from '@antv/x6-plugin-keyboard';
|
||||
import {Snapline} from '@antv/x6-plugin-snapline';
|
||||
import {MiniMap} from '@antv/x6-plugin-minimap';
|
||||
import {Menu} from '@antv/x6-plugin-menu';
|
||||
import './index.module.less';
|
||||
import NodePanel from './components/NodePanel';
|
||||
import NodeConfig from './components/NodeConfig';
|
||||
import Toolbar from './components/Toolbar';
|
||||
import {NodeType, getNodeTypes} from './service';
|
||||
import {DeleteOutlined, CopyOutlined, SettingOutlined, ClearOutlined, FullscreenOutlined} from '@ant-design/icons';
|
||||
import { validateFlow, hasCycle } from './validate';
|
||||
import EdgeConfig from './components/EdgeConfig';
|
||||
|
||||
const {Sider, Content} = Layout;
|
||||
|
||||
@ -55,6 +54,9 @@ const FlowDesigner: React.FC = () => {
|
||||
const [currentNodeType, setCurrentNodeType] = useState<NodeType>();
|
||||
const [nodeTypes, setNodeTypes] = useState<NodeType[]>([]);
|
||||
const [form] = Form.useForm();
|
||||
const [currentEdge, setCurrentEdge] = useState<Edge>();
|
||||
const [edgeConfigVisible, setEdgeConfigVisible] = useState(false);
|
||||
const [edgeForm] = Form.useForm();
|
||||
|
||||
// 右键菜单状态
|
||||
const [contextMenu, setContextMenu] = useState<{
|
||||
@ -100,7 +102,7 @@ const FlowDesigner: React.FC = () => {
|
||||
},
|
||||
{
|
||||
key: 'config',
|
||||
label: '<EFBFBD><EFBFBD><EFBFBD>置节点',
|
||||
label: '配置节点',
|
||||
icon: <SettingOutlined />,
|
||||
onClick: () => {
|
||||
if (contextMenu.cell && contextMenu.cell.isNode()) {
|
||||
@ -123,6 +125,17 @@ const FlowDesigner: React.FC = () => {
|
||||
},
|
||||
],
|
||||
edge: [
|
||||
{
|
||||
key: 'config',
|
||||
label: '配置连线',
|
||||
icon: <SettingOutlined />,
|
||||
onClick: () => {
|
||||
if (contextMenu.cell && contextMenu.cell.isEdge()) {
|
||||
handleEdgeConfig(contextMenu.cell);
|
||||
}
|
||||
setContextMenu(prev => ({ ...prev, visible: false }));
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'delete',
|
||||
label: '删除连线',
|
||||
@ -179,17 +192,15 @@ const FlowDesigner: React.FC = () => {
|
||||
const initGraph = () => {
|
||||
if (!containerRef.current) return;
|
||||
|
||||
// 创建画布
|
||||
const graph = new Graph({
|
||||
container: containerRef.current,
|
||||
autoResize: true, // 启用自动调整大小
|
||||
grid: {
|
||||
visible: true,
|
||||
type: 'mesh',
|
||||
size: 10,
|
||||
args: {
|
||||
color: '#e5e5e5', // 网格线颜色
|
||||
thickness: 1, // 网格线宽度
|
||||
color: '#e5e5e5',
|
||||
thickness: 1,
|
||||
},
|
||||
},
|
||||
mousewheel: {
|
||||
@ -199,11 +210,11 @@ const FlowDesigner: React.FC = () => {
|
||||
maxScale: 2,
|
||||
},
|
||||
connecting: {
|
||||
snap: true, // 连线时自动吸附
|
||||
allowBlank: false, // 禁止连接到空白位置
|
||||
allowLoop: false, // 禁止自环
|
||||
allowNode: false, // 禁止直接连接到节点(必须连接到连接桩)
|
||||
allowEdge: false, // 禁止边连接到边
|
||||
snap: true,
|
||||
allowBlank: false,
|
||||
allowLoop: false,
|
||||
allowNode: false,
|
||||
allowEdge: false,
|
||||
connector: {
|
||||
name: 'rounded',
|
||||
args: {
|
||||
@ -211,21 +222,74 @@ const FlowDesigner: React.FC = () => {
|
||||
},
|
||||
},
|
||||
router: {
|
||||
name: 'manhattan', // 使用曼哈顿路由
|
||||
name: 'manhattan',
|
||||
args: {
|
||||
padding: 1,
|
||||
},
|
||||
},
|
||||
validateConnection({sourceCell, targetCell, sourceMagnet, targetMagnet}) {
|
||||
if (sourceCell === targetCell) {
|
||||
return false; // 禁止自环
|
||||
return false;
|
||||
}
|
||||
if (!sourceMagnet || !targetMagnet) {
|
||||
return false; // 必须使用连接桩
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
},
|
||||
defaultEdge: {
|
||||
attrs: {
|
||||
line: {
|
||||
stroke: '#5F95FF',
|
||||
strokeWidth: 1,
|
||||
targetMarker: {
|
||||
name: 'classic',
|
||||
size: 8,
|
||||
},
|
||||
},
|
||||
},
|
||||
router: {
|
||||
name: 'manhattan',
|
||||
args: {
|
||||
padding: 1,
|
||||
},
|
||||
},
|
||||
connector: {
|
||||
name: 'rounded',
|
||||
args: {
|
||||
radius: 8,
|
||||
},
|
||||
},
|
||||
labels: [
|
||||
{
|
||||
attrs: {
|
||||
label: {
|
||||
text: '',
|
||||
fill: '#333',
|
||||
fontSize: 12,
|
||||
},
|
||||
rect: {
|
||||
fill: '#fff',
|
||||
stroke: '#5F95FF',
|
||||
strokeWidth: 1,
|
||||
rx: 3,
|
||||
ry: 3,
|
||||
refWidth: 1,
|
||||
refHeight: 1,
|
||||
refX: 0,
|
||||
refY: 0,
|
||||
},
|
||||
},
|
||||
position: {
|
||||
distance: 0.5,
|
||||
offset: {
|
||||
x: 0,
|
||||
y: -10,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
highlighting: {
|
||||
magnetAvailable: {
|
||||
name: 'stroke',
|
||||
@ -248,7 +312,9 @@ const FlowDesigner: React.FC = () => {
|
||||
},
|
||||
},
|
||||
},
|
||||
keyboard: true,
|
||||
keyboard: {
|
||||
enabled: true,
|
||||
},
|
||||
clipboard: {
|
||||
enabled: true,
|
||||
},
|
||||
@ -309,7 +375,7 @@ const FlowDesigner: React.FC = () => {
|
||||
minWidth: 1,
|
||||
minHeight: 1,
|
||||
orthogonal: true,
|
||||
restrict: true,
|
||||
restricted: true,
|
||||
},
|
||||
rotating: {
|
||||
enabled: true,
|
||||
@ -407,7 +473,7 @@ const FlowDesigner: React.FC = () => {
|
||||
if (nodeType) {
|
||||
setCurrentNodeType(nodeType);
|
||||
|
||||
// 合并节点基本配置和执行器配置
|
||||
// 合并节点基本配置和执器配置
|
||||
const formValues = {
|
||||
name: data.name || nodeType.name,
|
||||
description: data.description,
|
||||
@ -558,7 +624,7 @@ const FlowDesigner: React.FC = () => {
|
||||
throw new Error('重试次数不能为负数');
|
||||
}
|
||||
if (config.retryInterval !== undefined && config.retryInterval < 0) {
|
||||
throw new Error('重试间隔不能为负数');
|
||||
throw new Error('重试间不能为负数');
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@ -596,7 +662,7 @@ const FlowDesigner: React.FC = () => {
|
||||
}
|
||||
};
|
||||
|
||||
// 获取<EFBFBD><EFBFBD>情
|
||||
// 获取详情
|
||||
const fetchDetail = async () => {
|
||||
if (!id) return;
|
||||
setLoading(true);
|
||||
@ -614,34 +680,10 @@ const FlowDesigner: React.FC = () => {
|
||||
const handleSave = async () => {
|
||||
if (!id || !detail || !graphRef.current || detail.status !== WorkflowStatus.DRAFT) return;
|
||||
|
||||
// 先进行流程验证
|
||||
const result = validateFlow(graphRef.current);
|
||||
const hasCycleResult = hasCycle(graphRef.current);
|
||||
|
||||
if (hasCycleResult) {
|
||||
result.errors.push('流程图中存在循环依赖');
|
||||
result.valid = false;
|
||||
}
|
||||
|
||||
if (!result.valid) {
|
||||
message.error(
|
||||
<div>
|
||||
<div>流程验证失败,无法保存:</div>
|
||||
<ul style={{ marginBottom: 0 }}>
|
||||
{result.errors.map((error, index) => (
|
||||
<li key={index}>{error}</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 获取图形数据
|
||||
const graphData = graphRef.current.toJSON();
|
||||
|
||||
// 收<EFBFBD><EFBFBD><EFBFBD>节点配置数据
|
||||
// 收集节点配置数据
|
||||
const nodes = graphRef.current.getNodes().map(node => {
|
||||
const data = node.getData() as NodeData;
|
||||
return {
|
||||
@ -653,14 +695,26 @@ const FlowDesigner: React.FC = () => {
|
||||
};
|
||||
});
|
||||
|
||||
// 收集连线配置数据
|
||||
const transitions = graphRef.current.getEdges().map(edge => {
|
||||
const data = edge.getData() || {};
|
||||
return {
|
||||
from: edge.getSourceCellId(),
|
||||
to: edge.getTargetCellId(),
|
||||
condition: data.condition || '',
|
||||
description: data.description || '',
|
||||
priority: data.priority || 0
|
||||
};
|
||||
});
|
||||
|
||||
// 构建更新数据
|
||||
const data = {
|
||||
...detail,
|
||||
graphDefinition: JSON.stringify(graphData),
|
||||
nodeConfig: JSON.stringify({nodes})
|
||||
nodeConfig: JSON.stringify({nodes}),
|
||||
transitionConfig: JSON.stringify({transitions})
|
||||
};
|
||||
|
||||
// 调用更新接口
|
||||
await updateDefinition(parseInt(id), data);
|
||||
message.success('保存成功');
|
||||
} catch (error) {
|
||||
@ -724,7 +778,7 @@ const FlowDesigner: React.FC = () => {
|
||||
// 加载流程图数据
|
||||
const loadGraphData = (graph: Graph, detail: WorkflowDefinition) => {
|
||||
try {
|
||||
// 加载图形数据
|
||||
// 加载图数据
|
||||
const graphData = JSON.parse(detail.graphDefinition);
|
||||
graph.fromJSON(graphData);
|
||||
|
||||
@ -754,6 +808,33 @@ const FlowDesigner: React.FC = () => {
|
||||
}
|
||||
};
|
||||
|
||||
// 添加连线配置处理函数
|
||||
const handleEdgeConfig = (edge: Edge) => {
|
||||
setCurrentEdge(edge);
|
||||
const data = edge.getData() || {};
|
||||
edgeForm.setFieldsValue(data);
|
||||
setEdgeConfigVisible(true);
|
||||
};
|
||||
|
||||
// 添加连线配置保存函数
|
||||
const handleEdgeConfigSubmit = async () => {
|
||||
if (!currentEdge) return;
|
||||
|
||||
try {
|
||||
const values = await edgeForm.validateFields();
|
||||
currentEdge.setData(values);
|
||||
|
||||
// 更新连线标签
|
||||
if (values.description) {
|
||||
currentEdge.setLabelAt(0, values.description);
|
||||
}
|
||||
|
||||
setEdgeConfigVisible(false);
|
||||
} catch (error) {
|
||||
// 表单验证失败
|
||||
}
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div style={{textAlign: 'center', padding: 100}}>
|
||||
@ -825,6 +906,34 @@ const FlowDesigner: React.FC = () => {
|
||||
<NodeConfig nodeType={currentNodeType} form={form}/>
|
||||
)}
|
||||
</Drawer>
|
||||
|
||||
<Drawer
|
||||
title="连线配置"
|
||||
placement="right"
|
||||
width={400}
|
||||
onClose={() => setEdgeConfigVisible(false)}
|
||||
open={edgeConfigVisible}
|
||||
extra={
|
||||
<Space>
|
||||
<Button onClick={() => setEdgeConfigVisible(false)}>取消</Button>
|
||||
<Button type="primary" onClick={handleEdgeConfigSubmit}>
|
||||
确定
|
||||
</Button>
|
||||
</Space>
|
||||
}
|
||||
>
|
||||
{currentEdge && (
|
||||
<EdgeConfig
|
||||
edge={currentEdge}
|
||||
form={edgeForm}
|
||||
onValuesChange={(changedValues, allValues) => {
|
||||
if (changedValues.description) {
|
||||
currentEdge.setLabelAt(0, changedValues.description);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Drawer>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
117
frontend/src/pages/Workflow/Definition/Designer/validate.ts
Normal file
117
frontend/src/pages/Workflow/Definition/Designer/validate.ts
Normal file
@ -0,0 +1,117 @@
|
||||
import { Graph } from '@antv/x6';
|
||||
|
||||
interface ValidationResult {
|
||||
valid: boolean;
|
||||
errors: string[];
|
||||
}
|
||||
|
||||
export const validateFlow = (graph: Graph): ValidationResult => {
|
||||
const result: ValidationResult = {
|
||||
valid: true,
|
||||
errors: [],
|
||||
};
|
||||
|
||||
const nodes = graph.getNodes();
|
||||
const edges = graph.getEdges();
|
||||
|
||||
// 验证开始节点
|
||||
const startNodes = nodes.filter(node => node.getData()?.type === 'START');
|
||||
if (startNodes.length === 0) {
|
||||
result.errors.push('流程必须包含一个开始节点');
|
||||
result.valid = false;
|
||||
} else if (startNodes.length > 1) {
|
||||
result.errors.push('流程只能包含一个开始节点');
|
||||
result.valid = false;
|
||||
}
|
||||
|
||||
// 验证结束节点
|
||||
const endNodes = nodes.filter(node => node.getData()?.type === 'END');
|
||||
if (endNodes.length === 0) {
|
||||
result.errors.push('流程必须包含至少一个结束节点');
|
||||
result.valid = false;
|
||||
}
|
||||
|
||||
// 验证孤立节点
|
||||
nodes.forEach(node => {
|
||||
const incomingEdges = graph.getIncomingEdges(node) || [];
|
||||
const outgoingEdges = graph.getOutgoingEdges(node) || [];
|
||||
const nodeData = node.getData();
|
||||
const nodeType = nodeData?.type;
|
||||
const nodeName = nodeData?.name || '未命名';
|
||||
|
||||
if (nodeType === 'START' && incomingEdges.length > 0) {
|
||||
result.errors.push('开始节点不能有入边');
|
||||
result.valid = false;
|
||||
}
|
||||
|
||||
if (nodeType === 'END' && outgoingEdges.length > 0) {
|
||||
result.errors.push('结束节点不能有出边');
|
||||
result.valid = false;
|
||||
}
|
||||
|
||||
if (nodeType !== 'START' && nodeType !== 'END' &&
|
||||
(incomingEdges.length === 0 || outgoingEdges.length === 0)) {
|
||||
result.errors.push(`节点 "${nodeName}" 未完全连接`);
|
||||
result.valid = false;
|
||||
}
|
||||
});
|
||||
|
||||
// 验证网关配对
|
||||
const validateGatewayPairs = () => {
|
||||
const gatewayNodes = nodes.filter(node => node.getData()?.type === 'GATEWAY');
|
||||
gatewayNodes.forEach(gateway => {
|
||||
const outgoingEdges = graph.getOutgoingEdges(gateway) || [];
|
||||
const incomingEdges = graph.getIncomingEdges(gateway) || [];
|
||||
const gatewayData = gateway.getData();
|
||||
const gatewayName = gatewayData?.name || '未命名';
|
||||
|
||||
if (gatewayData?.config?.type === 'PARALLEL' ||
|
||||
gatewayData?.config?.type === 'INCLUSIVE') {
|
||||
if (outgoingEdges.length < 2) {
|
||||
result.errors.push(`并行/包容网关 "${gatewayName}" 必须有至少两个出口`);
|
||||
result.valid = false;
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
validateGatewayPairs();
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
// 检查是否存在环路
|
||||
export const hasCycle = (graph: Graph): boolean => {
|
||||
const visited = new Set<string>();
|
||||
const recursionStack = new Set<string>();
|
||||
|
||||
const dfs = (nodeId: string): boolean => {
|
||||
visited.add(nodeId);
|
||||
recursionStack.add(nodeId);
|
||||
|
||||
const outgoingEdges = graph.getOutgoingEdges(nodeId);
|
||||
if (outgoingEdges) {
|
||||
for (const edge of outgoingEdges) {
|
||||
const targetId = edge.getTargetCellId();
|
||||
if (!visited.has(targetId)) {
|
||||
if (dfs(targetId)) {
|
||||
return true;
|
||||
}
|
||||
} else if (recursionStack.has(targetId)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
recursionStack.delete(nodeId);
|
||||
return false;
|
||||
};
|
||||
|
||||
const nodes = graph.getNodes();
|
||||
for (const node of nodes) {
|
||||
if (!visited.has(node.id) && dfs(node.id)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
Loading…
Reference in New Issue
Block a user