增加了动态生成跟节点验证。
This commit is contained in:
parent
6e45e3b502
commit
80de1149d3
@ -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'
|
||||
}
|
||||
}
|
||||
};
|
||||
@ -21,6 +21,10 @@ import {NodeType, getNodeTypes} from './service';
|
||||
import {DeleteOutlined, CopyOutlined, SettingOutlined, ClearOutlined, FullscreenOutlined} from '@ant-design/icons';
|
||||
import EdgeConfig from './components/EdgeConfig';
|
||||
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;
|
||||
|
||||
@ -507,134 +511,37 @@ const FlowDesigner: React.FC = () => {
|
||||
const handleDrop = (e: DragEvent) => {
|
||||
e.preventDefault();
|
||||
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 point = {
|
||||
x: e.clientX - rect.left,
|
||||
y: e.clientY - rect.top,
|
||||
};
|
||||
|
||||
// 获取画布缩放和平移信息
|
||||
const matrix = graphRef.current.matrix();
|
||||
const scale = matrix.a;
|
||||
const offsetX = matrix.e;
|
||||
const offsetY = matrix.f;
|
||||
|
||||
// 计算实际位置(考虑缩放和平移)
|
||||
const position = {
|
||||
x: (point.x - offsetX) / scale,
|
||||
y: (point.y - offsetY) / scale,
|
||||
};
|
||||
// 使用新的工具函数计算画布位置
|
||||
const dropPosition = calculateCanvasPosition(
|
||||
e.clientX,
|
||||
e.clientY,
|
||||
rect,
|
||||
{
|
||||
scale: matrix.a,
|
||||
offsetX: matrix.e,
|
||||
offsetY: matrix.f,
|
||||
}
|
||||
);
|
||||
|
||||
// 创建节点配置
|
||||
const position = calculateNodePosition(nodeType.code, dropPosition);
|
||||
const nodeStyle = generateNodeStyle(nodeType.code);
|
||||
const ports = generatePorts(nodeType.code);
|
||||
|
||||
// 创建节点
|
||||
const node = graphRef.current.addNode({
|
||||
x: position.x - (nodeType.code === 'SHELL' ? 100 : 40), // Shell节点宽度的一半,其他节点40px
|
||||
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' }
|
||||
]
|
||||
},
|
||||
...position,
|
||||
...nodeStyle,
|
||||
ports,
|
||||
data: {
|
||||
type: nodeType.code,
|
||||
name: nodeType.name,
|
||||
@ -642,17 +549,29 @@ const FlowDesigner: React.FC = () => {
|
||||
},
|
||||
});
|
||||
|
||||
// 选中新创建的节点
|
||||
// 选中新创建的节点并打开配置
|
||||
graphRef.current.cleanSelection();
|
||||
graphRef.current.select(node);
|
||||
|
||||
// 打开配置抽屉
|
||||
setCurrentNode(node);
|
||||
setCurrentNodeType(nodeType);
|
||||
form.setFieldsValue({
|
||||
name: nodeType.name,
|
||||
});
|
||||
form.setFieldsValue({ name: nodeType.name });
|
||||
setConfigVisible(true);
|
||||
|
||||
message.success('节点创建成功');
|
||||
} catch (error) {
|
||||
console.error('Error creating node:', error);
|
||||
|
||||
if (isWorkflowError(error)) {
|
||||
message.error(`创建节点失败:${error.message}`);
|
||||
} else {
|
||||
message.error('创建节点失败:未知错误');
|
||||
}
|
||||
|
||||
// 清理状态
|
||||
setCurrentNode(undefined);
|
||||
setCurrentNodeType(undefined);
|
||||
setConfigVisible(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 验证Shell节点配置
|
||||
|
||||
28
frontend/src/pages/Workflow/Definition/Designer/types.ts
Normal file
28
frontend/src/pages/Workflow/Definition/Designer/types.ts
Normal 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;
|
||||
};
|
||||
}
|
||||
@ -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;
|
||||
};
|
||||
@ -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}`);
|
||||
}
|
||||
};
|
||||
Loading…
Reference in New Issue
Block a user