增加链接线验证。

This commit is contained in:
dengqichen 2024-12-06 17:10:20 +08:00
parent 17acef1529
commit 4cb2b0b685
3 changed files with 340 additions and 49 deletions

View File

@ -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;

View File

@ -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>
);
};

View 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;
};