增加链接线验证。
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 {ArrowLeftOutlined, SaveOutlined} from '@ant-design/icons';
|
||||||
import {getDefinition, updateDefinition} from '../../service';
|
import {getDefinition, updateDefinition} from '../../service';
|
||||||
import {WorkflowDefinition, WorkflowStatus} from '../../../Workflow/types';
|
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 '@antv/x6-react-shape';
|
||||||
import {Selection} from '@antv/x6-plugin-selection';
|
import {Selection} from '@antv/x6-plugin-selection';
|
||||||
import {History} from '@antv/x6-plugin-history';
|
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 {Keyboard} from '@antv/x6-plugin-keyboard';
|
||||||
import {Snapline} from '@antv/x6-plugin-snapline';
|
import {Snapline} from '@antv/x6-plugin-snapline';
|
||||||
import {MiniMap} from '@antv/x6-plugin-minimap';
|
import {MiniMap} from '@antv/x6-plugin-minimap';
|
||||||
import {Menu} from '@antv/x6-plugin-menu';
|
|
||||||
import './index.module.less';
|
import './index.module.less';
|
||||||
import NodePanel from './components/NodePanel';
|
import NodePanel from './components/NodePanel';
|
||||||
import NodeConfig from './components/NodeConfig';
|
import NodeConfig from './components/NodeConfig';
|
||||||
import Toolbar from './components/Toolbar';
|
import Toolbar from './components/Toolbar';
|
||||||
import {NodeType, getNodeTypes} from './service';
|
import {NodeType, getNodeTypes} from './service';
|
||||||
import {DeleteOutlined, CopyOutlined, SettingOutlined, ClearOutlined, FullscreenOutlined} from '@ant-design/icons';
|
import {DeleteOutlined, CopyOutlined, SettingOutlined, ClearOutlined, FullscreenOutlined} from '@ant-design/icons';
|
||||||
import { validateFlow, hasCycle } from './validate';
|
import EdgeConfig from './components/EdgeConfig';
|
||||||
|
|
||||||
const {Sider, Content} = Layout;
|
const {Sider, Content} = Layout;
|
||||||
|
|
||||||
@ -55,6 +54,9 @@ const FlowDesigner: React.FC = () => {
|
|||||||
const [currentNodeType, setCurrentNodeType] = useState<NodeType>();
|
const [currentNodeType, setCurrentNodeType] = useState<NodeType>();
|
||||||
const [nodeTypes, setNodeTypes] = useState<NodeType[]>([]);
|
const [nodeTypes, setNodeTypes] = useState<NodeType[]>([]);
|
||||||
const [form] = Form.useForm();
|
const [form] = Form.useForm();
|
||||||
|
const [currentEdge, setCurrentEdge] = useState<Edge>();
|
||||||
|
const [edgeConfigVisible, setEdgeConfigVisible] = useState(false);
|
||||||
|
const [edgeForm] = Form.useForm();
|
||||||
|
|
||||||
// 右键菜单状态
|
// 右键菜单状态
|
||||||
const [contextMenu, setContextMenu] = useState<{
|
const [contextMenu, setContextMenu] = useState<{
|
||||||
@ -100,7 +102,7 @@ const FlowDesigner: React.FC = () => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'config',
|
key: 'config',
|
||||||
label: '<EFBFBD><EFBFBD><EFBFBD>置节点',
|
label: '配置节点',
|
||||||
icon: <SettingOutlined />,
|
icon: <SettingOutlined />,
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
if (contextMenu.cell && contextMenu.cell.isNode()) {
|
if (contextMenu.cell && contextMenu.cell.isNode()) {
|
||||||
@ -123,6 +125,17 @@ const FlowDesigner: React.FC = () => {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
edge: [
|
edge: [
|
||||||
|
{
|
||||||
|
key: 'config',
|
||||||
|
label: '配置连线',
|
||||||
|
icon: <SettingOutlined />,
|
||||||
|
onClick: () => {
|
||||||
|
if (contextMenu.cell && contextMenu.cell.isEdge()) {
|
||||||
|
handleEdgeConfig(contextMenu.cell);
|
||||||
|
}
|
||||||
|
setContextMenu(prev => ({ ...prev, visible: false }));
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
key: 'delete',
|
key: 'delete',
|
||||||
label: '删除连线',
|
label: '删除连线',
|
||||||
@ -179,17 +192,15 @@ const FlowDesigner: React.FC = () => {
|
|||||||
const initGraph = () => {
|
const initGraph = () => {
|
||||||
if (!containerRef.current) return;
|
if (!containerRef.current) return;
|
||||||
|
|
||||||
// 创建画布
|
|
||||||
const graph = new Graph({
|
const graph = new Graph({
|
||||||
container: containerRef.current,
|
container: containerRef.current,
|
||||||
autoResize: true, // 启用自动调整大小
|
|
||||||
grid: {
|
grid: {
|
||||||
visible: true,
|
visible: true,
|
||||||
type: 'mesh',
|
type: 'mesh',
|
||||||
size: 10,
|
size: 10,
|
||||||
args: {
|
args: {
|
||||||
color: '#e5e5e5', // 网格线颜色
|
color: '#e5e5e5',
|
||||||
thickness: 1, // 网格线宽度
|
thickness: 1,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
mousewheel: {
|
mousewheel: {
|
||||||
@ -199,11 +210,11 @@ const FlowDesigner: React.FC = () => {
|
|||||||
maxScale: 2,
|
maxScale: 2,
|
||||||
},
|
},
|
||||||
connecting: {
|
connecting: {
|
||||||
snap: true, // 连线时自动吸附
|
snap: true,
|
||||||
allowBlank: false, // 禁止连接到空白位置
|
allowBlank: false,
|
||||||
allowLoop: false, // 禁止自环
|
allowLoop: false,
|
||||||
allowNode: false, // 禁止直接连接到节点(必须连接到连接桩)
|
allowNode: false,
|
||||||
allowEdge: false, // 禁止边连接到边
|
allowEdge: false,
|
||||||
connector: {
|
connector: {
|
||||||
name: 'rounded',
|
name: 'rounded',
|
||||||
args: {
|
args: {
|
||||||
@ -211,21 +222,74 @@ const FlowDesigner: React.FC = () => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
router: {
|
router: {
|
||||||
name: 'manhattan', // 使用曼哈顿路由
|
name: 'manhattan',
|
||||||
args: {
|
args: {
|
||||||
padding: 1,
|
padding: 1,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
validateConnection({sourceCell, targetCell, sourceMagnet, targetMagnet}) {
|
validateConnection({sourceCell, targetCell, sourceMagnet, targetMagnet}) {
|
||||||
if (sourceCell === targetCell) {
|
if (sourceCell === targetCell) {
|
||||||
return false; // 禁止自环
|
return false;
|
||||||
}
|
}
|
||||||
if (!sourceMagnet || !targetMagnet) {
|
if (!sourceMagnet || !targetMagnet) {
|
||||||
return false; // 必须使用连接桩
|
return false;
|
||||||
}
|
}
|
||||||
return true;
|
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: {
|
highlighting: {
|
||||||
magnetAvailable: {
|
magnetAvailable: {
|
||||||
name: 'stroke',
|
name: 'stroke',
|
||||||
@ -248,7 +312,9 @@ const FlowDesigner: React.FC = () => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
keyboard: true,
|
keyboard: {
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
clipboard: {
|
clipboard: {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
},
|
},
|
||||||
@ -309,7 +375,7 @@ const FlowDesigner: React.FC = () => {
|
|||||||
minWidth: 1,
|
minWidth: 1,
|
||||||
minHeight: 1,
|
minHeight: 1,
|
||||||
orthogonal: true,
|
orthogonal: true,
|
||||||
restrict: true,
|
restricted: true,
|
||||||
},
|
},
|
||||||
rotating: {
|
rotating: {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
@ -407,7 +473,7 @@ const FlowDesigner: React.FC = () => {
|
|||||||
if (nodeType) {
|
if (nodeType) {
|
||||||
setCurrentNodeType(nodeType);
|
setCurrentNodeType(nodeType);
|
||||||
|
|
||||||
// 合并节点基本配置和执行器配置
|
// 合并节点基本配置和执器配置
|
||||||
const formValues = {
|
const formValues = {
|
||||||
name: data.name || nodeType.name,
|
name: data.name || nodeType.name,
|
||||||
description: data.description,
|
description: data.description,
|
||||||
@ -558,7 +624,7 @@ const FlowDesigner: React.FC = () => {
|
|||||||
throw new Error('重试次数不能为负数');
|
throw new Error('重试次数不能为负数');
|
||||||
}
|
}
|
||||||
if (config.retryInterval !== undefined && config.retryInterval < 0) {
|
if (config.retryInterval !== undefined && config.retryInterval < 0) {
|
||||||
throw new Error('重试间隔不能为负数');
|
throw new Error('重试间不能为负数');
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@ -596,7 +662,7 @@ const FlowDesigner: React.FC = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 获取<EFBFBD><EFBFBD>情
|
// 获取详情
|
||||||
const fetchDetail = async () => {
|
const fetchDetail = async () => {
|
||||||
if (!id) return;
|
if (!id) return;
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
@ -614,34 +680,10 @@ const FlowDesigner: React.FC = () => {
|
|||||||
const handleSave = async () => {
|
const handleSave = async () => {
|
||||||
if (!id || !detail || !graphRef.current || detail.status !== WorkflowStatus.DRAFT) return;
|
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 {
|
try {
|
||||||
// 获取图形数据
|
|
||||||
const graphData = graphRef.current.toJSON();
|
const graphData = graphRef.current.toJSON();
|
||||||
|
|
||||||
// 收<EFBFBD><EFBFBD><EFBFBD>节点配置数据
|
// 收集节点配置数据
|
||||||
const nodes = graphRef.current.getNodes().map(node => {
|
const nodes = graphRef.current.getNodes().map(node => {
|
||||||
const data = node.getData() as NodeData;
|
const data = node.getData() as NodeData;
|
||||||
return {
|
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 = {
|
const data = {
|
||||||
...detail,
|
...detail,
|
||||||
graphDefinition: JSON.stringify(graphData),
|
graphDefinition: JSON.stringify(graphData),
|
||||||
nodeConfig: JSON.stringify({nodes})
|
nodeConfig: JSON.stringify({nodes}),
|
||||||
|
transitionConfig: JSON.stringify({transitions})
|
||||||
};
|
};
|
||||||
|
|
||||||
// 调用更新接口
|
|
||||||
await updateDefinition(parseInt(id), data);
|
await updateDefinition(parseInt(id), data);
|
||||||
message.success('保存成功');
|
message.success('保存成功');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -724,7 +778,7 @@ const FlowDesigner: React.FC = () => {
|
|||||||
// 加载流程图数据
|
// 加载流程图数据
|
||||||
const loadGraphData = (graph: Graph, detail: WorkflowDefinition) => {
|
const loadGraphData = (graph: Graph, detail: WorkflowDefinition) => {
|
||||||
try {
|
try {
|
||||||
// 加载图形数据
|
// 加载图数据
|
||||||
const graphData = JSON.parse(detail.graphDefinition);
|
const graphData = JSON.parse(detail.graphDefinition);
|
||||||
graph.fromJSON(graphData);
|
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) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<div style={{textAlign: 'center', padding: 100}}>
|
<div style={{textAlign: 'center', padding: 100}}>
|
||||||
@ -825,6 +906,34 @@ const FlowDesigner: React.FC = () => {
|
|||||||
<NodeConfig nodeType={currentNodeType} form={form}/>
|
<NodeConfig nodeType={currentNodeType} form={form}/>
|
||||||
)}
|
)}
|
||||||
</Drawer>
|
</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>
|
</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