增加了动态生成跟节点验证。

This commit is contained in:
戚辰先生 2024-12-06 21:42:48 +08:00
parent 6e45e3b502
commit 80de1149d3
5 changed files with 354 additions and 140 deletions

View File

@ -0,0 +1,69 @@
import { NodeConfig } from '../types';
export const NODE_CONFIG: Record<string, NodeConfig> = {
START: {
size: { width: 80, height: 80 },
shape: 'circle',
theme: {
fill: '#f6ffed',
stroke: '#52c41a'
}
},
END: {
size: { width: 80, height: 80 },
shape: 'circle',
theme: {
fill: '#fff1f0',
stroke: '#ff4d4f'
}
},
SHELL: {
size: { width: 200, height: 80 },
shape: 'rect',
theme: {
fill: '#e6f7ff',
stroke: '#1890ff'
},
extras: {
rx: 4,
ry: 4,
icon: {
'xlink:href': 'data:image/svg+xml;base64,PHN2ZyB0PSIxNzAzODQ4NTM1NDY5IiBjbGFzcz0iaWNvbiIgdmlld0JveD0iMCAwIDEwMjQgMTAyNCIgdmVyc2lvbj0iMS4xIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHAtaWQ9IjQxNjEiIHdpZHRoPSIyMDAiIGhlaWdodD0iMjAwIj48cGF0aCBkPSJNMTYwIDI1NnY1MTJoNzA0VjI1NkgxNjB6IG02NDAgNDQ4SDE5MlYyODhoNjA4djQxNnpNMjI0IDQ4MGwxMjgtMTI4IDQ1LjMgNDUuMy04Mi43IDgyLjcgODIuNyA4Mi43TDM1MiA2MDhsLTEyOC0xMjh6IG0yMzEuMSAxOTJsLTQ1LjMtNDUuMyAxNTItMTUyIDQ1LjMgNDUuM2wtMTUyIDE1MnoiIGZpbGw9IiMxODkwZmYiIHAtaWQ9IjQxNjIiPjwvcGF0aD48L3N2Zz4=',
width: 32,
height: 32,
x: 16,
y: 24
}
}
},
TIMER: {
size: { width: 200, height: 80 },
shape: 'rect',
theme: {
fill: '#fff7e6',
stroke: '#fa8c16'
},
extras: {
rx: 4,
ry: 4,
icon: {
'xlink:href': 'data:image/svg+xml;base64,PHN2ZyB0PSIxNzAzODQ4NjE2NTM2IiBjbGFzcz0iaWNvbiIgdmlld0JveD0iMCAwIDEwMjQgMTAyNCIgdmVyc2lvbj0iMS4xIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHAtaWQ9IjUzNTgiIHdpZHRoPSIyMDAiIGhlaWdodD0iMjAwIj48cGF0aCBkPSJNNTEyIDY0QzI2NC42IDY0IDY0IDI2NC42IDY0IDUxMnMyMDAuNiA0NDggNDQ4IDQ0OCA0NDgtMjAwLjYgNDQ4LTQ0OFM3NTkuNCA2NCA1MTIgNjR6IG0wIDgyMGMtMjA1LjQgMC0zNzItMTY2LjYtMzcyLTM3MnMxNjYuNi0zNzIgMzcyLTM3MiAzNzIgMTY2LjYgMzcyIDM3Mi0xNjYuNiAzNzItMzcyIDM3MnogbS00NC44LTM3Mi4zaDIzNS45djUzLjhIMzk4LjdWMjk3LjZoNjguNXYyMTQuMXoiIGZpbGw9IiNmYThjMTYiIHAtaWQ9IjUzNTkiPjwvcGF0aD48L3N2Zz4=',
width: 32,
height: 32,
x: 16,
y: 24
}
}
},
GATEWAY: {
size: { width: 80, height: 80 },
shape: 'polygon',
theme: {
fill: '#f9f0ff',
stroke: '#722ed1'
},
extras: {
refPoints: '0,10 10,0 20,10 10,20'
}
}
};

View File

@ -21,6 +21,10 @@ 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 EdgeConfig from './components/EdgeConfig'; import EdgeConfig from './components/EdgeConfig';
import { validateFlow, hasCycle } from './validate'; import { validateFlow, hasCycle } from './validate';
import { generateNodeStyle, generatePorts, calculateNodePosition, calculateCanvasPosition } from './utils/nodeUtils';
import { Position } from './types';
import { NODE_CONFIG } from './configs/nodeConfig';
import { isWorkflowError } from './utils/errors';
const {Sider, Content} = Layout; const {Sider, Content} = Layout;
@ -507,152 +511,67 @@ const FlowDesigner: React.FC = () => {
const handleDrop = (e: DragEvent) => { const handleDrop = (e: DragEvent) => {
e.preventDefault(); e.preventDefault();
const nodeType = draggedNodeRef.current; const nodeType = draggedNodeRef.current;
if (!nodeType || !graphRef.current || !containerRef.current) return; if (!nodeType || !graphRef.current || !containerRef.current) {
message.error('无效的节点类型或画布未初始化');
return;
}
// 获取画布相对位置 try {
const rect = containerRef.current.getBoundingClientRect(); const rect = containerRef.current.getBoundingClientRect();
const point = { const matrix = graphRef.current.matrix();
x: e.clientX - rect.left,
y: e.clientY - rect.top, // 使用新的工具函数计算画布位置
}; const dropPosition = calculateCanvasPosition(
e.clientX,
e.clientY,
rect,
{
scale: matrix.a,
offsetX: matrix.e,
offsetY: matrix.f,
}
);
// 获取画布缩放和平移信息 // 创建节点配置
const matrix = graphRef.current.matrix(); const position = calculateNodePosition(nodeType.code, dropPosition);
const scale = matrix.a; const nodeStyle = generateNodeStyle(nodeType.code);
const offsetX = matrix.e; const ports = generatePorts(nodeType.code);
const offsetY = matrix.f;
// 计算实际位置(考虑缩放和平移) // 创建节点
const position = { const node = graphRef.current.addNode({
x: (point.x - offsetX) / scale, ...position,
y: (point.y - offsetY) / scale, ...nodeStyle,
}; ports,
data: {
// 创建节点 type: nodeType.code,
const node = graphRef.current.addNode({ name: nodeType.name,
x: position.x - (nodeType.code === 'SHELL' ? 100 : 40), // Shell节点宽度的一半其他节点40px config: {} as any,
y: position.y - 40,
width: nodeType.code === 'SHELL' ? 200 : 80,
height: 80,
shape: nodeType.code === 'GATEWAY' ? 'polygon' : (nodeType.code === 'SHELL' ? 'rect' : 'circle'),
attrs: {
body: {
fill: nodeType.code === 'START' ? '#f6ffed' :
nodeType.code === 'END' ? '#fff1f0' :
nodeType.code === 'SHELL' ? '#e6f7ff' : '#f9f0ff',
stroke: nodeType.code === 'START' ? '#52c41a' :
nodeType.code === 'END' ? '#ff4d4f' :
nodeType.code === 'SHELL' ? '#1890ff' : '#722ed1',
strokeWidth: 2,
...(nodeType.code === 'SHELL' ? { rx: 4, ry: 4 } : {}),
...(nodeType.code === 'GATEWAY' ? { refPoints: '0,10 10,0 20,10 10,20' } : {})
}, },
label: { });
text: nodeType.name,
fill: '#000000',
fontSize: 14,
fontWeight: 500,
...(nodeType.code === 'SHELL' ? {
refX: 0.5, // 水平居中
refY: 0.5, // 垂直居中
textAnchor: 'middle', // 文本水平居中
textVerticalAnchor: 'middle' // 文本垂直居中
} : {})
},
...(nodeType.code === 'SHELL' ? {
image: {
'xlink:href': 'data:image/svg+xml;base64,PHN2ZyB0PSIxNzAzODQ4NTM1NDY5IiBjbGFzcz0iaWNvbiIgdmlld0JveD0iMCAwIDEwMjQgMTAyNCIgdmVyc2lvbj0iMS4xIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHAtaWQ9IjQxNjEiIHdpZHRoPSIyMDAiIGhlaWdodD0iMjAwIj48cGF0aCBkPSJNMTYwIDI1NnY1MTJoNzA0VjI1NkgxNjB6IG02NDAgNDQ4SDE5MlYyODhoNjA4djQxNnpNMjI0IDQ4MGwxMjgtMTI4IDQ1LjMgNDUuMy04Mi43IDgyLjcgODIuNyA4Mi43TDM1MiA2MDhsLTEyOC0xMjh6IG0yMzEuMSAxOTJsLTQ1LjMtNDUuMyAxNTItMTUyIDQ1LjMgNDUuM2wtMTUyIDE1MnoiIGZpbGw9IiMxODkwZmYiIHAtaWQ9IjQxNjIiPjwvcGF0aD48L3N2Zz4=',
width: 32,
height: 32,
x: 16,
y: 24
}
} : {})
},
ports: {
groups: {
top: {
position: 'top',
attrs: {
circle: {
r: 4,
magnet: true,
stroke: nodeType.code === 'START' ? '#52c41a' :
nodeType.code === 'END' ? '#ff4d4f' :
nodeType.code === 'SHELL' ? '#1890ff' : '#722ed1',
strokeWidth: 2,
fill: '#fff'
}
}
},
right: {
position: 'right',
attrs: {
circle: {
r: 4,
magnet: true,
stroke: nodeType.code === 'START' ? '#52c41a' :
nodeType.code === 'END' ? '#ff4d4f' :
nodeType.code === 'SHELL' ? '#1890ff' : '#722ed1',
strokeWidth: 2,
fill: '#fff'
}
}
},
bottom: {
position: 'bottom',
attrs: {
circle: {
r: 4,
magnet: true,
stroke: nodeType.code === 'START' ? '#52c41a' :
nodeType.code === 'END' ? '#ff4d4f' :
nodeType.code === 'SHELL' ? '#1890ff' : '#722ed1',
strokeWidth: 2,
fill: '#fff'
}
}
},
left: {
position: 'left',
attrs: {
circle: {
r: 4,
magnet: true,
stroke: nodeType.code === 'START' ? '#52c41a' :
nodeType.code === 'END' ? '#ff4d4f' :
nodeType.code === 'SHELL' ? '#1890ff' : '#722ed1',
strokeWidth: 2,
fill: '#fff'
}
}
}
},
items: [
{ group: 'top' },
{ group: 'right' },
{ group: 'bottom' },
{ group: 'left' }
]
},
data: {
type: nodeType.code,
name: nodeType.name,
config: {} as any,
},
});
// 选中新创建的节点 // 选中新创建的节点并打开配置
graphRef.current.cleanSelection(); graphRef.current.cleanSelection();
graphRef.current.select(node); graphRef.current.select(node);
setCurrentNode(node);
setCurrentNodeType(nodeType);
form.setFieldsValue({ name: nodeType.name });
setConfigVisible(true);
// 打开配置抽屉 message.success('节点创建成功');
setCurrentNode(node); } catch (error) {
setCurrentNodeType(nodeType); console.error('Error creating node:', error);
form.setFieldsValue({
name: nodeType.name, if (isWorkflowError(error)) {
}); message.error(`创建节点失败:${error.message}`);
setConfigVisible(true); } else {
message.error('创建节点失败:未知错误');
}
// 清理状态
setCurrentNode(undefined);
setCurrentNodeType(undefined);
setConfigVisible(false);
}
}; };
// 验证Shell节点配置 // 验证Shell节点配置

View File

@ -0,0 +1,28 @@
export interface Position {
x: number;
y: number;
}
export interface NodeConfig {
size: {
width: number;
height: number;
};
shape: 'circle' | 'rect' | 'polygon';
theme: {
fill: string;
stroke: string;
};
extras?: {
rx?: number;
ry?: number;
icon?: {
'xlink:href': string;
width: number;
height: number;
x: number;
y: number;
};
refPoints?: string;
};
}

View File

@ -0,0 +1,24 @@
export class WorkflowError extends Error {
constructor(message: string, public code: string) {
super(message);
this.name = 'WorkflowError';
}
}
export class NodeConfigError extends WorkflowError {
constructor(message: string) {
super(message, 'NODE_CONFIG_ERROR');
this.name = 'NodeConfigError';
}
}
export class NodeCreationError extends WorkflowError {
constructor(message: string) {
super(message, 'NODE_CREATION_ERROR');
this.name = 'NodeCreationError';
}
}
export const isWorkflowError = (error: unknown): error is WorkflowError => {
return error instanceof WorkflowError;
};

View File

@ -0,0 +1,174 @@
import { Position, NodeConfig } from '../types';
import { NODE_CONFIG } from '../configs/nodeConfig';
import { NodeConfigError } from './errors';
interface CanvasMatrix {
scale: number;
offsetX: number;
offsetY: number;
}
export const calculateCanvasPosition = (
clientX: number,
clientY: number,
containerRect: DOMRect,
matrix: CanvasMatrix
): Position => {
const point: Position = {
x: clientX - containerRect.left,
y: clientY - containerRect.top,
};
return {
x: (point.x - matrix.offsetX) / matrix.scale,
y: (point.y - matrix.offsetY) / matrix.scale,
};
};
const getNodeConfig = (nodeType: string): NodeConfig => {
const config = NODE_CONFIG[nodeType];
if (!config) {
throw new NodeConfigError(`No configuration found for node type: ${nodeType}`);
}
return config;
};
interface NodeStyle {
width: number;
height: number;
shape: 'circle' | 'rect' | 'polygon';
attrs: {
body: {
fill: string;
stroke: string;
strokeWidth: number;
rx?: number;
ry?: number;
refPoints?: string;
};
label: {
text: string;
fill: string;
fontSize: number;
fontWeight: number;
refX?: number;
refY?: number;
textAnchor?: string;
textVerticalAnchor?: string;
};
image?: {
'xlink:href': string;
width: number;
height: number;
x: number;
y: number;
};
};
}
export const generateNodeStyle = (nodeType: string): NodeStyle => {
try {
const config = getNodeConfig(nodeType);
return {
width: config.size.width,
height: config.size.height,
shape: config.shape,
attrs: {
body: {
fill: config.theme.fill,
stroke: config.theme.stroke,
strokeWidth: 2,
...(config.extras || {})
},
label: {
text: nodeType,
fill: '#000000',
fontSize: 14,
fontWeight: 500,
...(nodeType === 'SHELL' ? {
refX: 0.5,
refY: 0.5,
textAnchor: 'middle',
textVerticalAnchor: 'middle'
} : {})
},
...(nodeType === 'SHELL' && config.extras?.icon ? {
image: config.extras.icon
} : {})
}
};
} catch (error) {
if (error instanceof NodeConfigError) {
throw error;
}
throw new NodeConfigError(`Failed to generate node style for type: ${nodeType}`);
}
};
interface PortConfig {
groups: {
[key: string]: {
position: string;
attrs: {
circle: {
r: number;
magnet: boolean;
stroke: string;
strokeWidth: number;
fill: string;
};
};
};
};
items: Array<{ group: string }>;
}
export const generatePorts = (nodeType: string): PortConfig => {
try {
const config = getNodeConfig(nodeType);
return {
groups: ['top', 'right', 'bottom', 'left'].reduce((acc, position) => ({
...acc,
[position]: {
position,
attrs: {
circle: {
r: 4,
magnet: true,
stroke: config.theme.stroke,
strokeWidth: 2,
fill: '#fff'
}
}
}
}), {}),
items: [
{ group: 'top' },
{ group: 'right' },
{ group: 'bottom' },
{ group: 'left' }
]
};
} catch (error) {
if (error instanceof NodeConfigError) {
throw error;
}
throw new NodeConfigError(`Failed to generate ports for node type: ${nodeType}`);
}
};
export const calculateNodePosition = (nodeType: string, dropPosition: Position): Position => {
try {
const config = getNodeConfig(nodeType);
const { width, height } = config.size;
return {
x: dropPosition.x - width / 2,
y: dropPosition.y - height / 2
};
} catch (error) {
if (error instanceof NodeConfigError) {
throw error;
}
throw new NodeConfigError(`Failed to calculate position for node type: ${nodeType}`);
}
};