From d04885801585139c7eed844df4b3baba190a3af5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=88=9A=E8=BE=B0=E5=85=88=E7=94=9F?= Date: Wed, 11 Dec 2024 22:01:53 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B5=81=E7=A8=8B=E5=AE=9A=E4=B9=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Workflow/Definition/Designer/index.tsx | 12 +- .../Definition/Designer/types/base.ts | 63 ++++ .../Definition/Designer/types/edge.ts | 42 +++ .../Definition/Designer/types/index.ts | 4 + .../Definition/Designer/types/node.ts | 90 +++++ .../Definition/Designer/types/workflow.ts | 44 +++ .../Definition/Designer/utils/graphUtils.ts | 289 +++++++------- .../Definition/Designer/utils/nodeUtils.ts | 356 ++++++------------ 8 files changed, 525 insertions(+), 375 deletions(-) create mode 100644 frontend/src/pages/Workflow/Definition/Designer/types/base.ts create mode 100644 frontend/src/pages/Workflow/Definition/Designer/types/edge.ts create mode 100644 frontend/src/pages/Workflow/Definition/Designer/types/index.ts create mode 100644 frontend/src/pages/Workflow/Definition/Designer/types/node.ts create mode 100644 frontend/src/pages/Workflow/Definition/Designer/types/workflow.ts diff --git a/frontend/src/pages/Workflow/Definition/Designer/index.tsx b/frontend/src/pages/Workflow/Definition/Designer/index.tsx index 9808e68c..90ed6ed0 100644 --- a/frontend/src/pages/Workflow/Definition/Designer/index.tsx +++ b/frontend/src/pages/Workflow/Definition/Designer/index.tsx @@ -170,9 +170,15 @@ const FlowDesigner: React.FC = () => { // 获取所有节点类型 const fetchNodeTypes = async () => { try { - const types = await getNodeTypes({enabled: true}); - setNodeTypes(types); - return types; + const response = await getNodeTypes({enabled: true}); + if (response?.content && Array.isArray(response.content)) { + setNodeTypes(response.content); + return response.content; + } else { + console.error('获取节点类型返回格式错误:', response); + message.error('获取节点类型失败:返回格式错误'); + return []; + } } catch (error) { console.error('获取节点类型失败:', error); message.error('获取节点类型失败'); diff --git a/frontend/src/pages/Workflow/Definition/Designer/types/base.ts b/frontend/src/pages/Workflow/Definition/Designer/types/base.ts new file mode 100644 index 00000000..9e0330ee --- /dev/null +++ b/frontend/src/pages/Workflow/Definition/Designer/types/base.ts @@ -0,0 +1,63 @@ +import { Graph, Node, Edge } from '@antv/x6'; + +// 基础实体类型 +export interface BaseEntity { + id: number; + createTime?: string; + createBy?: string; + updateTime?: string; + updateBy?: string; + version: number; + deleted: boolean; + extraData?: any; +} + +// 位置类型 +export interface Position { + x: number; + y: number; +} + +// 尺寸类型 +export interface Size { + width: number; + height: number; +} + +// 工作流状态 +export enum WorkflowStatus { + DRAFT = 'DRAFT', // 草稿 + PUBLISHED = 'PUBLISHED', // 已发布 + DISABLED = 'DISABLED' // 已禁用 +} + +// 节点类别 +export enum NodeCategory { + EVENT = 'EVENT', // 事件节点 + TASK = 'TASK', // 任务节点 + GATEWAY = 'GATEWAY' // 网关节点 +} + +// 图形属性 +export interface GraphAttrs { + body?: { + fill?: string; + stroke?: string; + strokeWidth?: number; + rx?: number; + ry?: number; + }; + label?: { + text?: string; + fill?: string; + fontSize?: number; + fontWeight?: string; + }; + image?: { + 'xlink:href'?: string; + width?: number; + height?: number; + x?: number; + y?: number; + }; +} diff --git a/frontend/src/pages/Workflow/Definition/Designer/types/edge.ts b/frontend/src/pages/Workflow/Definition/Designer/types/edge.ts new file mode 100644 index 00000000..d0f0125a --- /dev/null +++ b/frontend/src/pages/Workflow/Definition/Designer/types/edge.ts @@ -0,0 +1,42 @@ +// 连线数据 +export interface EdgeData { + id: string; + source: string; + target: string; + name?: string; + config: { + type: 'sequenceFlow'; + condition?: string; + defaultFlow?: boolean; + }; + properties: Record; +} + +// 连线验证错误 +export interface EdgeValidationError { + edgeId: string; + property: string; + message: string; +} + +// 连线样式 +export interface EdgeStyle { + line: { + stroke: string; + strokeWidth: number; + targetMarker?: { + name: string; + size: number; + }; + sourceMarker?: { + name: string; + size: number; + }; + }; + label?: { + text: string; + fill: string; + fontSize: number; + fontWeight?: string; + }; +} diff --git a/frontend/src/pages/Workflow/Definition/Designer/types/index.ts b/frontend/src/pages/Workflow/Definition/Designer/types/index.ts new file mode 100644 index 00000000..e33e8cde --- /dev/null +++ b/frontend/src/pages/Workflow/Definition/Designer/types/index.ts @@ -0,0 +1,4 @@ +export * from './base'; +export * from './node'; +export * from './edge'; +export * from './workflow'; diff --git a/frontend/src/pages/Workflow/Definition/Designer/types/node.ts b/frontend/src/pages/Workflow/Definition/Designer/types/node.ts new file mode 100644 index 00000000..1726d0af --- /dev/null +++ b/frontend/src/pages/Workflow/Definition/Designer/types/node.ts @@ -0,0 +1,90 @@ +import { BaseEntity, NodeCategory, Position, Size, GraphAttrs } from './base'; + +// 端口组配置 +export interface PortGroup { + position: string; + attrs: { + circle: { + r: number; + magnet: boolean; + stroke?: string; + strokeWidth?: number; + fill?: string; + }; + }; +} + +// 图形配置 +export interface GraphConfig { + shape: 'circle' | 'rect' | 'diamond'; + width: number; + height: number; + ports: { + groups: Record; + }; + attrs: GraphAttrs; +} + +// 表单属性 +export interface FormProperty { + name: string; + label: string; + type: string; + required?: boolean; + default?: any; + options?: Array<{ + label: string; + value: any; + }>; +} + +// 表单配置 +export interface FormConfig { + properties: FormProperty[]; +} + +// 节点类型定义 +export interface NodeType extends BaseEntity { + type: string; + name: string; + description: string; + category: NodeCategory; + flowableConfig: Record; + graphConfig: GraphConfig; + formConfig: FormConfig; + orderNum: number; + enabled: boolean; +} + +// 节点数据 +export interface NodeData { + id: string; + type: string; + name: string; + position: Position; + size: Size; + config: { + type: string; + implementation?: string; + fields?: Record; + // Shell任务特有配置 + script?: string; + workDir?: string; + // 用户任务特有配置 + assignee?: string; + candidateUsers?: string; + candidateGroups?: string; + dueDate?: string; + }; + properties: Record; +} + +// 节点验证错误 +export interface NodeValidationError { + nodeId: string; + property: string; + message: string; +} diff --git a/frontend/src/pages/Workflow/Definition/Designer/types/workflow.ts b/frontend/src/pages/Workflow/Definition/Designer/types/workflow.ts new file mode 100644 index 00000000..0472f614 --- /dev/null +++ b/frontend/src/pages/Workflow/Definition/Designer/types/workflow.ts @@ -0,0 +1,44 @@ +import { BaseEntity, WorkflowStatus } from './base'; +import { NodeData } from './node'; +import { EdgeData } from './edge'; + +// 图形数据 +export interface GraphData { + nodes: NodeData[]; + edges: EdgeData[]; + properties: Record; +} + +// 表单配置 +export interface WorkflowFormConfig { + formItems: Array; // 根据实际表单配置类型定义 +} + +// 工作流定义 +export interface WorkflowDefinition extends BaseEntity { + name: string; + key: string; + flowVersion: number; + description?: string; + status: WorkflowStatus; + bpmnXml: string; + graph: GraphData; + formConfig: WorkflowFormConfig; +} + +// 工作流验证错误 +export interface WorkflowValidationError { + type: 'node' | 'edge' | 'workflow'; + id?: string; + message: string; +} + +// 工作流查询参数 +export interface WorkflowDefinitionQuery { + keyword?: string; + status?: WorkflowStatus; + startTime?: string; + endTime?: string; + pageSize?: number; + current?: number; +} diff --git a/frontend/src/pages/Workflow/Definition/Designer/utils/graphUtils.ts b/frontend/src/pages/Workflow/Definition/Designer/utils/graphUtils.ts index ce9ca42c..07de3e8c 100644 --- a/frontend/src/pages/Workflow/Definition/Designer/utils/graphUtils.ts +++ b/frontend/src/pages/Workflow/Definition/Designer/utils/graphUtils.ts @@ -1,4 +1,4 @@ -import { Graph } from '@antv/x6'; +import { Graph, Node, Edge } from '@antv/x6'; import { Selection } from '@antv/x6-plugin-selection'; import { Keyboard } from '@antv/x6-plugin-keyboard'; import { Clipboard } from '@antv/x6-plugin-clipboard'; @@ -6,9 +6,17 @@ import { History } from '@antv/x6-plugin-history'; import { Transform } from '@antv/x6-plugin-transform'; import { Snapline } from '@antv/x6-plugin-snapline'; import { MiniMap } from '@antv/x6-plugin-minimap'; -import { NodeData, NodeType, WorkflowDefinition } from '../types'; -import { Node } from '@antv/x6'; +import { + NodeData, + EdgeData, + WorkflowDefinition, + GraphData, + EdgeStyle, + NodeType +} from '../types'; +import { generateNodeStyle, generatePorts } from './nodeUtils'; +// Initialize graph with enhanced configuration export const initGraph = ({ container, miniMapContainer, @@ -27,15 +35,15 @@ export const initGraph = ({ onDragOver: (e: DragEvent) => void; onDrop: (e: DragEvent) => void; flowDetail?: WorkflowDefinition; -}) => { +}): Graph => { const graph = new Graph({ container, grid: { + size: 10, visible: true, type: 'mesh', - size: 10, args: { - color: '#e5e5e5', + color: '#cccccc', thickness: 1, }, }, @@ -130,27 +138,23 @@ export const initGraph = ({ }, }, }, - keyboard: { + keyboard: true, + clipboard: true, + history: true, + selecting: { enabled: true, + multiple: true, + rubberband: true, + movable: true, + showNodeSelectionBox: true, }, - clipboard: { - enabled: true, - }, - history: { - enabled: true, - }, - snapline: { - enabled: true, - }, - translating: { - restrict: true, - }, + snapline: true, background: { - color: '#ffffff', // 画布背景色 + color: '#ffffff', }, }); - // 启用必要的功能 + // Enable plugins with enhanced configuration graph.use( new Selection({ enabled: true, @@ -168,24 +172,10 @@ export const initGraph = ({ }) ); - graph.use( - new History({ - enabled: true, - beforeAddCommand: (event: string, args: any) => { - if (event === 'cell:change:*') { - return true; - } - return true; - }, - }) - ); - - graph.use( - new Clipboard({ - enabled: true, - }) - ); - + // Initialize other plugins + graph.use(new Keyboard({ enabled: true })); + graph.use(new Clipboard({ enabled: true })); + graph.use(new History({ enabled: true })); graph.use( new Transform({ resizing: { @@ -201,93 +191,54 @@ export const initGraph = ({ }, }) ); + graph.use(new Snapline({ enabled: true })); - graph.use( - new Keyboard({ - enabled: true, - }) - ); - - graph.use( - new Snapline({ - enabled: true, - }) - ); - - // 启用小地图 - graph.use( - new MiniMap({ - container: miniMapContainer, - width: 200, - height: 150, - padding: 20, - scalable: true, - minScale: 0.1, - maxScale: 3, - // 自动适应内容 - fitToContent: true, - // 视口配置 - viewport: { - padding: 10, - // 视口变化时自动适应 - fitOnViewportChanged: true, - }, - graphOptions: { - async: true, - grid: false, - background: { - color: '#f5f5f5', + // Initialize minimap if container provided + if (miniMapContainer) { + graph.use( + new MiniMap({ + container: miniMapContainer, + width: 200, + height: 150, + padding: 20, + scalable: true, + minScale: 0.1, + maxScale: 3, + fitToContent: true, + viewport: { + padding: 10, + fitOnViewportChanged: true, }, - interacting: { - nodeMovable: false, + graphOptions: { + async: true, + grid: false, + background: { color: '#f5f5f5' }, + interacting: { nodeMovable: false }, + connecting: { enabled: false }, }, - connecting: { - enabled: false, - }, - }, - }) - ); + }) + ); - // 监听画布缩放和平移 - graph.on('scale translate', () => { - if (graph.minimap) { - // 使用固定的缩放比例 - const scale = 0.1; // 固定缩放比例为 0.1 - - // 更新视口和缩放 - graph.minimap.updateViewport(); - graph.minimap.scaleTo(scale); - graph.minimap.centerContent(); - } - }); + // Update minimap on graph changes + graph.on('scale translate', () => { + if (graph.minimap) { + graph.minimap.updateViewport(); + graph.minimap.scaleTo(0.1); + graph.minimap.centerContent(); + } + }); + } - // 监听鼠标右键按下 + // Event listeners container.addEventListener('mousedown', (e: MouseEvent) => { - if (e.button === 2) { // 右键 - e.preventDefault(); - } + if (e.button === 2) e.preventDefault(); }); - // 监听鼠标移动 - container.addEventListener('mousemove', (e: MouseEvent) => { - }); - - // 监听鼠标松开 - container.addEventListener('mouseup', (e: MouseEvent) => { - if (e.button === 2) { - } - }); - - // 监听鼠标离开容器 - container.addEventListener('mouseleave', () => { - }); - - // 禁用默认右键菜单 container.addEventListener('contextmenu', (e: Event) => { e.preventDefault(); }); - // 绑定事件 + // Graph events graph.on('cell:contextmenu', ({ cell, e }) => { e.preventDefault(); onContextMenu({ @@ -309,36 +260,26 @@ export const initGraph = ({ }); }); - // 点击画布时隐藏右键菜单 - graph.on('blank:click', () => { + graph.on('blank:click cell:click', () => { onContextMenu({ visible: false, x: 0, y: 0, type: 'canvas' }); }); - // 点击节点时隐藏右键菜单 - graph.on('cell:click', () => { - onContextMenu({ visible: false, x: 0, y: 0, type: 'canvas' }); - }); - - // 监听节点双击事件 graph.on('node:dblclick', ({ node }) => { onNodeClick(node); }); - // 监听选择状态变化 graph.on('selection:changed', () => { onGraphChange(graph); }); - // 监听画布拖拽事件 + // Drag and drop handlers container.addEventListener('dragover', onDragOver); container.addEventListener('drop', onDrop); - // 如果有初始数据,加载后自动调整视图 + // Load initial data if provided if (flowDetail?.graphDefinition) { const graphData = JSON.parse(flowDetail.graphDefinition); graph.fromJSON(graphData); - - // 等待节点渲染完成后调整视图 requestAnimationFrame(() => { graph.zoomToFit({ padding: 50, maxScale: 1 }); graph.centerContent(); @@ -347,3 +288,97 @@ export const initGraph = ({ return graph; }; + +// Load graph data +export const loadGraphData = (graph: Graph, data: GraphData) => { + graph.clearCells(); + + // Load nodes + data.nodes.forEach((nodeData) => { + graph.addNode({ + id: nodeData.id, + shape: 'react-shape', + x: nodeData.position.x, + y: nodeData.position.y, + width: nodeData.size.width, + height: nodeData.size.height, + attrs: generateNodeStyle(nodeData.type, nodeData.name), + ports: generatePorts(nodeData.type), + data: nodeData, + }); + }); + + // Load edges + data.edges.forEach((edgeData) => { + graph.addEdge({ + id: edgeData.id, + source: edgeData.source, + target: edgeData.target, + attrs: generateEdgeStyle(edgeData), + data: edgeData, + }); + }); +}; + +// Generate edge style +export const generateEdgeStyle = (edgeData: EdgeData): EdgeStyle => { + return { + line: { + stroke: '#5F95FF', + strokeWidth: 2, + targetMarker: { + name: 'classic', + size: 8, + }, + }, + label: edgeData.name + ? { + text: edgeData.name, + fill: '#333', + fontSize: 12, + } + : undefined, + }; +}; + +// Export graph data +export const exportGraphData = (graph: Graph): GraphData => { + return { + nodes: graph.getNodes().map((node) => node.getData() as NodeData), + edges: graph.getEdges().map((edge) => edge.getData() as EdgeData), + properties: {}, + }; +}; + +// Validate graph data +export const validateGraphData = (graphData: GraphData): string[] => { + const errors: string[] = []; + + // Validate start node + const startNodes = graphData.nodes.filter( + (node) => node.type === 'startEvent' + ); + if (startNodes.length !== 1) { + errors.push('必须有且仅有一个开始节点'); + } + + // Validate end nodes + const endNodes = graphData.nodes.filter((node) => node.type === 'endEvent'); + if (endNodes.length === 0) { + errors.push('必须至少有一个结束节点'); + } + + // Validate node connections + graphData.nodes.forEach((node) => { + if (node.type !== 'endEvent') { + const outgoingEdges = graphData.edges.filter( + (edge) => edge.source === node.id + ); + if (outgoingEdges.length === 0) { + errors.push(`节点 "${node.name}" 没有出边`); + } + } + }); + + return errors; +}; diff --git a/frontend/src/pages/Workflow/Definition/Designer/utils/nodeUtils.ts b/frontend/src/pages/Workflow/Definition/Designer/utils/nodeUtils.ts index cf76f395..d3ce590e 100644 --- a/frontend/src/pages/Workflow/Definition/Designer/utils/nodeUtils.ts +++ b/frontend/src/pages/Workflow/Definition/Designer/utils/nodeUtils.ts @@ -1,4 +1,4 @@ -import { Position, NodeConfig, NodeType } from '../types'; +import { Position, Size, NodeType, NodeData, GraphConfig, PortGroup, GraphAttrs } from '../types'; import { NodeConfigError } from './errors'; interface CanvasMatrix { @@ -25,15 +25,17 @@ export const calculateCanvasPosition = ( }; // 获取节点形状 -export const getNodeShape = (nodeType: string): string => { +export const getNodeShape = (nodeType: string): GraphConfig['shape'] => { switch (nodeType) { - case 'start': - return 'circle'; - case 'end': + case 'startEvent': + case 'endEvent': return 'circle'; + case 'exclusiveGateway': + case 'parallelGateway': + return 'diamond'; case 'userTask': case 'serviceTask': - case 'scriptTask': + case 'shellTask': return 'rect'; default: return 'rect'; @@ -47,7 +49,7 @@ const NODE_ICONS: Record = { userTask: 'data:image/svg+xml;base64,PHN2ZyB0PSIxNzAzODQ4NTM1NDY5IiBjbGFzcz0iaWNvbiIgdmlld0JveD0iMCAwIDEwMjQgMTAyNCIgdmVyc2lvbj0iMS4xIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHAtaWQ9IjQxNjEiIHdpZHRoPSIyMDAiIGhlaWdodD0iMjAwIj48cGF0aCBkPSJNODU4LjUgNzYzLjZjLTE4LjktMjYuMi00Ny45LTQxLjctNzkuOS00MS43SDI0Ni40Yy0zMiAwLTYxIDI1LjUtNzkuOSA0MS43LTE4LjkgMTYuMi0yOS45IDM3LjItMjkuOSA1OS42IDAgMjIuNCAyNi44IDQwLjYgNTkuOCA0MC42aDYzMi4yYzMzIDAgNTkuOC0xOC4yIDU5LjgtNDAuNiAwLTIyLjQtMTEtNDMuNC0yOS45LTU5LjZ6TTUxMiAyNTZjODguNCAwIDE2MCA3MS42IDE2MCAxNjBzLTcxLjYgMTYwLTE2MCAxNjAtMTYwLTcxLjYtMTYwLTE2MCA3MS42LTE2MCAxNjAtMTYweiIgZmlsbD0iI2ZhOGMxNiIgcC1pZD0iNDE2MiI+PC9wYXRoPjwvc3ZnPg==', shellTask: 'data:image/svg+xml;base64,PHN2ZyB0PSIxNzAzODQ4NTM1NDY5IiBjbGFzcz0iaWNvbiIgdmlld0JveD0iMCAwIDEwMjQgMTAyNCIgdmVyc2lvbj0iMS4xIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHAtaWQ9IjQxNjEiIHdpZHRoPSIyMDAiIGhlaWdodD0iMjAwIj48cGF0aCBkPSJNMTYwIDI1NnY1MTJoNzA0VjI1NkgxNjB6IG02NDAgNDQ4SDE5MlYyODhoNjA4djQxNnpNMjI0IDQ4MGwxMjgtMTI4IDQ1LjMgNDUuMy04Mi43IDgyLjcgODIuNyA4Mi43TDM1MiA2MDhsLTEyOC0xMjh6IG0yMzEuMSAxOTBsLTQ1LjMtNDUuMyAxNTItMTUyIDQ1LjMgNDUuM2wtMTUyIDE1MnoiIGZpbGw9IiMxODkwZmYiIHAtaWQ9IjQxNjIiPjwvcGF0aD48L3N2Zz4=', exclusiveGateway: 'data:image/svg+xml;base64,PHN2ZyB0PSIxNzAzODQ4NzAzODY0IiBjbGFzcz0iaWNvbiIgdmlld0JveD0iMCAwIDEwMjQgMTAyNCIgdmVyc2lvbj0iMS4xIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHAtaWQ9IjY1NTUiIHdpZHRoPSIyMDAiIGhlaWdodD0iMjAwIj48cGF0aCBkPSJNNzgyLjcgNDQxLjRMNTQ1IDMwNC44YzAuMy0wLjMgMC40LTAuNyAwLjctMUw3ODIuNyA0NDEuNHogbTExOC45IDQzMi45TDIxNy43IDE1OC4xQzIwMi4xIDE0Mi41IDE3NiAxNDIuNSAxNjAuNCAxNTguMWMtMTUuNiAxNS42LTE1LjYgNDEuNyAwIDU3LjNsNjgzLjkgNzE2LjJjMTUuNiAxNS42IDQxLjcgMTUuNiA1Ny4zIDBzMTUuNi00MS43IDAtNTcuM3ogbS00MjYtMjQuNmMtMC4zIDAuMy0wLjQgMC43LTAuNyAxTDIzNy42IDU4Mi42YzAuMy0wLjMgMC40LTAuNyAwLjctMWwyMzcuMyAyNjguMXogbTIzNy4zLTI2Ny4xTDQ3NS42IDg1MC43YzAuMy0wLjMgMC40LTAuNyAwLjctMUw3MTMuNiA1ODIuNnoiIGZpbGw9IiM3MjJlZDEiIHAtaWQ9IjY1NTYiPjwvcGF0aD48L3N2Zz4=', - parallelGateway: 'data:image/svg+xml;base64,PHN2ZyB0PSIxNzAzODQ4NzAzODY0IiBjbGFzcz0iaWNvbiIgdmlld0JveD0iMCAwIDEwMjQgMTAyNCIgdmVyc2lvbj0iMS4xIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHAtaWQ9IjY1NTUiIHdpZHRoPSIyMDAiIGhlaWdodD0iMjAwIj48cGF0aCBkPSJNNDQ4IDIyNGg4MHYyNDBoMjQwdjgwSDUyOHYyNDBoLTgwVjU0NEgyMDh2LTgwaDI0MFYyMjR6IiBmaWxsPSIjNzIyZWQxIiBwLWlkPSI2NTU2Ij48L3BhdGg+PC9zdmc+', + parallelGateway: 'data:image/svg+xml;base64,PHN2ZyB0PSIxNzAzODQ4NzAzODY0IiBjbGFzcz0iaWNvbiIgdmlld0JveD0iMCAwIDEwMjQgMTAyNCIgdmVyc2lvbj0iMS4xIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHAtaWQ9IjY1NTUiIHdpZHRoPSIyMDAiIGhlaWdodD0iMjAwIj48cGF0aCBkPSJNNDAwIDUwMEg0MDB2MjAwSDEwMFY1MDB6IiBmaWxsPSIjNzIyZWQxIiBwLWlkPSI2NTU2Ij48L3BhdGg+PC9zdmc+', }; // 节点主题映射 @@ -60,189 +62,23 @@ const NODE_THEMES: Record = { parallelGateway: { fill: '#f9f0ff', stroke: '#722ed1' }, }; -// 节点配置映射 -const NODE_CONFIG: Record = { - startEvent: { - size: { width: 80, height: 80 }, - shape: 'circle', - theme: NODE_THEMES.startEvent, - label: 'startEvent', - extras: { - icon: { - 'xlink:href': NODE_ICONS.startEvent, - width: 32, - height: 32, - x: 8, - y: 24, - }, - }, - }, - endEvent: { - size: { width: 80, height: 80 }, - shape: 'circle', - theme: NODE_THEMES.endEvent, - label: 'endEvent', - extras: { - icon: { - 'xlink:href': NODE_ICONS.endEvent, - width: 32, - height: 32, - x: 8, - y: 24, - }, - }, - }, - userTask: { - size: { width: 200, height: 80 }, - shape: 'rect', - theme: NODE_THEMES.userTask, - label: 'userTask', - extras: { - rx: 4, - ry: 4, - icon: { - 'xlink:href': NODE_ICONS.userTask, - width: 32, - height: 32, - x: 8, - y: 24, - }, - }, - }, - shellTask: { - size: { width: 200, height: 80 }, - shape: 'rect', - theme: NODE_THEMES.shellTask, - label: 'shellTask', - extras: { - rx: 4, - ry: 4, - icon: { - 'xlink:href': NODE_ICONS.shellTask, - width: 32, - height: 32, - x: 8, - y: 24, - }, - }, - }, - exclusiveGateway: { - size: { width: 60, height: 60 }, - shape: 'polygon', - theme: NODE_THEMES.exclusiveGateway, - label: 'exclusiveGateway', - extras: { - refPoints: '0,30 30,0 60,30 30,60', - icon: { - 'xlink:href': NODE_ICONS.exclusiveGateway, - width: 32, - height: 32, - x: 14, - y: 14, - }, - }, - }, - parallelGateway: { - size: { width: 60, height: 60 }, - shape: 'polygon', - theme: NODE_THEMES.parallelGateway, - label: 'parallelGateway', - extras: { - refPoints: '0,30 30,0 60,30 30,60', - icon: { - 'xlink:href': NODE_ICONS.parallelGateway, - width: 32, - height: 32, - x: 14, - y: 14, - }, - }, - }, -}; - -// 获取节点配置 -export const getNodeConfig = (nodeType: string): NodeConfig => { - // 根据节点类型动态生成配置 - const isGateway = nodeType.toLowerCase().includes('gateway'); - const isEvent = nodeType.toLowerCase().includes('event'); - const isTask = nodeType.toLowerCase().includes('task'); - - const baseConfig: NodeConfig = { - size: isGateway ? { width: 60, height: 60 } : isEvent ? { width: 80, height: 80 } : { width: 200, height: 80 }, - shape: isGateway ? 'polygon' : (isEvent ? 'circle' : 'rect'), - theme: NODE_THEMES[nodeType] || { fill: '#e6f7ff', stroke: '#1890ff' }, - label: nodeType, - extras: { - ...(isGateway ? { refPoints: '0,30 30,0 60,30 30,60' } : {}), - ...(isTask ? { rx: 4, ry: 4 } : {}), - icon: { - 'xlink:href': NODE_ICONS[nodeType] || NODE_ICONS.shellTask, - width: 32, - height: 32, - x: isGateway ? 14 : 8, - y: isGateway ? 14 : 24, - }, - }, - }; - - return baseConfig; -}; - -// 获取节点默认大小 -export const getNodeSize = (nodeType: string) => { - const config = NODE_CONFIG[nodeType]; - if (!config) { - return { width: 100, height: 60 }; // 默认大小 - } - return config.size; -}; - -interface NodeStyle { - body: { - fill: string; - stroke: string; - strokeWidth: number; - rx?: number; - ry?: number; - refPoints?: string; - }; - label: { - text: string; - fontSize: number; - fill: string; - 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, label?: string): NodeStyle => { - const theme = NODE_THEMES[nodeType] || { fill: '#fff', stroke: '#d9d9d9' }; +// 生成节点样式 +export const generateNodeStyle = (nodeType: string, label?: string): GraphAttrs => { + const theme = NODE_THEMES[nodeType] || { fill: '#ffffff', stroke: '#333333' }; + const shape = getNodeShape(nodeType); const icon = NODE_ICONS[nodeType]; - const style: NodeStyle = { + const style: GraphAttrs = { body: { fill: theme.fill, stroke: theme.stroke, - strokeWidth: 1, + strokeWidth: nodeType === 'endEvent' ? 4 : 2, }, label: { - text: label || nodeType, // 设置标签文本 - fontSize: 14, - fill: '#000000', // 设置标签文本颜色为黑色 - refX: 0.5, - refY: 0.5, - textAnchor: 'middle', - textVerticalAnchor: 'middle', - } + text: label || '', + fill: '#333333', + fontSize: 12, + }, }; if (icon) { @@ -250,78 +86,108 @@ export const generateNodeStyle = (nodeType: string, label?: string): NodeStyle = 'xlink:href': icon, width: 16, height: 16, - x: 6, - y: 6 + x: shape === 'circle' ? 12 : 8, + y: shape === 'circle' ? 12 : 8, }; } return style; }; -interface PortConfig { - groups: { - [key: string]: { - position: string; - attrs: { - circle: { - r: number; - magnet: boolean; - stroke: string; - strokeWidth: number; - fill: string; - }; - }; - }; +// 生成端口配置 +export const generatePorts = (nodeType: string): { groups: Record } => { + const defaultPortGroup: PortGroup = { + position: 'top', + attrs: { + circle: { + r: 4, + magnet: true, + stroke: '#5F95FF', + strokeWidth: 1, + fill: '#fff', + }, + }, }; - 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}`); - } + return { + groups: { + top: { ...defaultPortGroup, position: 'top' }, + right: { ...defaultPortGroup, position: 'right' }, + bottom: { ...defaultPortGroup, position: 'bottom' }, + left: { ...defaultPortGroup, position: 'left' }, + }, + }; }; +// 计算节点位置 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}`); + const shape = getNodeShape(nodeType); + const size = getNodeSize(nodeType); + + // 调整位置使节点中心对齐到鼠标位置 + return { + x: dropPosition.x - size.width / 2, + y: dropPosition.y - size.height / 2, + }; +}; + +// 获取节点大小 +export const getNodeSize = (nodeType: string): Size => { + switch (nodeType) { + case 'startEvent': + case 'endEvent': + return { width: 40, height: 40 }; + case 'exclusiveGateway': + case 'parallelGateway': + return { width: 60, height: 60 }; + case 'userTask': + case 'serviceTask': + case 'shellTask': + return { width: 120, height: 60 }; + default: + return { width: 100, height: 50 }; } }; + +// 创建节点数据 +export const createNodeData = ( + nodeType: NodeType, + id: string, + position: Position, + name?: string +): NodeData => { + return { + id, + type: nodeType.type, + name: name || nodeType.name, + position, + size: getNodeSize(nodeType.type), + config: { + type: nodeType.type, + ...JSON.parse(nodeType.flowableConfig || '{}'), + }, + properties: {}, + }; +}; + +// 验证节点配置 +export const validateNodeConfig = (nodeData: NodeData, nodeType: NodeType): string[] => { + const errors: string[] = []; + + // 验证必填属性 + if (!nodeData.name) { + errors.push('节点名称不能为空'); + } + + // 验证表单配置 + const formConfig = nodeType.formConfig; + if (formConfig?.properties) { + formConfig.properties.forEach(prop => { + if (prop.required && !nodeData.config[prop.name]) { + errors.push(`${prop.label}不能为空`); + } + }); + } + + return errors; +};