diff --git a/frontend/src/pages/Workflow/Definition/Designer/configs/nodeConfig.ts b/frontend/src/pages/Workflow/Definition/Designer/configs/nodeConfig.ts new file mode 100644 index 00000000..fbc60845 --- /dev/null +++ b/frontend/src/pages/Workflow/Definition/Designer/configs/nodeConfig.ts @@ -0,0 +1,69 @@ +import { NodeConfig } from '../types'; + +export const NODE_CONFIG: Record = { + 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' + } + } +}; diff --git a/frontend/src/pages/Workflow/Definition/Designer/index.tsx b/frontend/src/pages/Workflow/Definition/Designer/index.tsx index ca90a8fc..82022bac 100644 --- a/frontend/src/pages/Workflow/Definition/Designer/index.tsx +++ b/frontend/src/pages/Workflow/Definition/Designer/index.tsx @@ -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,152 +511,67 @@ 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; + } - // 获取画布相对位置 - const rect = containerRef.current.getBoundingClientRect(); - const point = { - x: e.clientX - rect.left, - y: e.clientY - rect.top, - }; + try { + const rect = containerRef.current.getBoundingClientRect(); + const matrix = graphRef.current.matrix(); + + // 使用新的工具函数计算画布位置 + const dropPosition = calculateCanvasPosition( + e.clientX, + e.clientY, + rect, + { + scale: matrix.a, + offsetX: matrix.e, + offsetY: matrix.f, + } + ); - // 获取画布缩放和平移信息 - const matrix = graphRef.current.matrix(); - const scale = matrix.a; - const offsetX = matrix.e; - const offsetY = matrix.f; + // 创建节点配置 + const position = calculateNodePosition(nodeType.code, dropPosition); + const nodeStyle = generateNodeStyle(nodeType.code); + const ports = generatePorts(nodeType.code); - // 计算实际位置(考虑缩放和平移) - const position = { - x: (point.x - offsetX) / scale, - y: (point.y - offsetY) / scale, - }; - - // 创建节点 - 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' } : {}) + // 创建节点 + const node = graphRef.current.addNode({ + ...position, + ...nodeStyle, + ports, + data: { + type: nodeType.code, + name: nodeType.name, + config: {} as any, }, - 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.select(node); + // 选中新创建的节点并打开配置 + graphRef.current.cleanSelection(); + graphRef.current.select(node); + setCurrentNode(node); + setCurrentNodeType(nodeType); + form.setFieldsValue({ name: nodeType.name }); + setConfigVisible(true); - // 打开配置抽屉 - setCurrentNode(node); - setCurrentNodeType(nodeType); - 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节点配置 diff --git a/frontend/src/pages/Workflow/Definition/Designer/types.ts b/frontend/src/pages/Workflow/Definition/Designer/types.ts new file mode 100644 index 00000000..c320ec19 --- /dev/null +++ b/frontend/src/pages/Workflow/Definition/Designer/types.ts @@ -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; + }; +} diff --git a/frontend/src/pages/Workflow/Definition/Designer/utils/errors.ts b/frontend/src/pages/Workflow/Definition/Designer/utils/errors.ts new file mode 100644 index 00000000..7edb710e --- /dev/null +++ b/frontend/src/pages/Workflow/Definition/Designer/utils/errors.ts @@ -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; +}; diff --git a/frontend/src/pages/Workflow/Definition/Designer/utils/nodeUtils.ts b/frontend/src/pages/Workflow/Definition/Designer/utils/nodeUtils.ts new file mode 100644 index 00000000..51b4b79f --- /dev/null +++ b/frontend/src/pages/Workflow/Definition/Designer/utils/nodeUtils.ts @@ -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}`); + } +};