流程定义

This commit is contained in:
戚辰先生 2024-12-11 22:01:53 +08:00
parent 7e94b1f37c
commit d048858015
8 changed files with 525 additions and 375 deletions

View File

@ -170,9 +170,15 @@ const FlowDesigner: React.FC = () => {
// 获取所有节点类型 // 获取所有节点类型
const fetchNodeTypes = async () => { const fetchNodeTypes = async () => {
try { try {
const types = await getNodeTypes({enabled: true}); const response = await getNodeTypes({enabled: true});
setNodeTypes(types); if (response?.content && Array.isArray(response.content)) {
return types; setNodeTypes(response.content);
return response.content;
} else {
console.error('获取节点类型返回格式错误:', response);
message.error('获取节点类型失败:返回格式错误');
return [];
}
} catch (error) { } catch (error) {
console.error('获取节点类型失败:', error); console.error('获取节点类型失败:', error);
message.error('获取节点类型失败'); message.error('获取节点类型失败');

View File

@ -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;
};
}

View File

@ -0,0 +1,42 @@
// 连线数据
export interface EdgeData {
id: string;
source: string;
target: string;
name?: string;
config: {
type: 'sequenceFlow';
condition?: string;
defaultFlow?: boolean;
};
properties: Record<string, any>;
}
// 连线验证错误
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;
};
}

View File

@ -0,0 +1,4 @@
export * from './base';
export * from './node';
export * from './edge';
export * from './workflow';

View File

@ -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<string, PortGroup>;
};
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<string, any>;
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<string, {
string?: string;
expression?: string;
}>;
// Shell任务特有配置
script?: string;
workDir?: string;
// 用户任务特有配置
assignee?: string;
candidateUsers?: string;
candidateGroups?: string;
dueDate?: string;
};
properties: Record<string, any>;
}
// 节点验证错误
export interface NodeValidationError {
nodeId: string;
property: string;
message: string;
}

View File

@ -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<string, any>;
}
// 表单配置
export interface WorkflowFormConfig {
formItems: Array<any>; // 根据实际表单配置类型定义
}
// 工作流定义
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;
}

View File

@ -1,4 +1,4 @@
import { Graph } from '@antv/x6'; import { Graph, Node, Edge } from '@antv/x6';
import { Selection } from '@antv/x6-plugin-selection'; import { Selection } from '@antv/x6-plugin-selection';
import { Keyboard } from '@antv/x6-plugin-keyboard'; import { Keyboard } from '@antv/x6-plugin-keyboard';
import { Clipboard } from '@antv/x6-plugin-clipboard'; 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 { Transform } from '@antv/x6-plugin-transform';
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 { NodeData, NodeType, WorkflowDefinition } from '../types'; import {
import { Node } from '@antv/x6'; NodeData,
EdgeData,
WorkflowDefinition,
GraphData,
EdgeStyle,
NodeType
} from '../types';
import { generateNodeStyle, generatePorts } from './nodeUtils';
// Initialize graph with enhanced configuration
export const initGraph = ({ export const initGraph = ({
container, container,
miniMapContainer, miniMapContainer,
@ -27,15 +35,15 @@ export const initGraph = ({
onDragOver: (e: DragEvent) => void; onDragOver: (e: DragEvent) => void;
onDrop: (e: DragEvent) => void; onDrop: (e: DragEvent) => void;
flowDetail?: WorkflowDefinition; flowDetail?: WorkflowDefinition;
}) => { }): Graph => {
const graph = new Graph({ const graph = new Graph({
container, container,
grid: { grid: {
size: 10,
visible: true, visible: true,
type: 'mesh', type: 'mesh',
size: 10,
args: { args: {
color: '#e5e5e5', color: '#cccccc',
thickness: 1, thickness: 1,
}, },
}, },
@ -130,27 +138,23 @@ export const initGraph = ({
}, },
}, },
}, },
keyboard: { keyboard: true,
clipboard: true,
history: true,
selecting: {
enabled: true, enabled: true,
multiple: true,
rubberband: true,
movable: true,
showNodeSelectionBox: true,
}, },
clipboard: { snapline: true,
enabled: true,
},
history: {
enabled: true,
},
snapline: {
enabled: true,
},
translating: {
restrict: true,
},
background: { background: {
color: '#ffffff', // 画布背景色 color: '#ffffff',
}, },
}); });
// 启用必要的功能 // Enable plugins with enhanced configuration
graph.use( graph.use(
new Selection({ new Selection({
enabled: true, enabled: true,
@ -168,24 +172,10 @@ export const initGraph = ({
}) })
); );
graph.use( // Initialize other plugins
new History({ graph.use(new Keyboard({ enabled: true }));
enabled: true, graph.use(new Clipboard({ enabled: true }));
beforeAddCommand: (event: string, args: any) => { graph.use(new History({ enabled: true }));
if (event === 'cell:change:*') {
return true;
}
return true;
},
})
);
graph.use(
new Clipboard({
enabled: true,
})
);
graph.use( graph.use(
new Transform({ new Transform({
resizing: { resizing: {
@ -201,93 +191,54 @@ export const initGraph = ({
}, },
}) })
); );
graph.use(new Snapline({ enabled: true }));
graph.use( // Initialize minimap if container provided
new Keyboard({ if (miniMapContainer) {
enabled: true, graph.use(
}) new MiniMap({
); container: miniMapContainer,
width: 200,
graph.use( height: 150,
new Snapline({ padding: 20,
enabled: true, scalable: true,
}) minScale: 0.1,
); maxScale: 3,
fitToContent: true,
// 启用小地图 viewport: {
graph.use( padding: 10,
new MiniMap({ fitOnViewportChanged: true,
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',
}, },
interacting: { graphOptions: {
nodeMovable: false, async: true,
grid: false,
background: { color: '#f5f5f5' },
interacting: { nodeMovable: false },
connecting: { enabled: false },
}, },
connecting: { })
enabled: false, );
},
},
})
);
// 监听画布缩放和平移 // Update minimap on graph changes
graph.on('scale translate', () => { graph.on('scale translate', () => {
if (graph.minimap) { if (graph.minimap) {
// 使用固定的缩放比例 graph.minimap.updateViewport();
const scale = 0.1; // 固定缩放比例为 0.1 graph.minimap.scaleTo(0.1);
graph.minimap.centerContent();
// 更新视口和缩放 }
graph.minimap.updateViewport(); });
graph.minimap.scaleTo(scale); }
graph.minimap.centerContent();
}
});
// 监听鼠标右键按下 // Event listeners
container.addEventListener('mousedown', (e: MouseEvent) => { container.addEventListener('mousedown', (e: MouseEvent) => {
if (e.button === 2) { // 右键 if (e.button === 2) e.preventDefault();
e.preventDefault();
}
}); });
// 监听鼠标移动
container.addEventListener('mousemove', (e: MouseEvent) => {
});
// 监听鼠标松开
container.addEventListener('mouseup', (e: MouseEvent) => {
if (e.button === 2) {
}
});
// 监听鼠标离开容器
container.addEventListener('mouseleave', () => {
});
// 禁用默认右键菜单
container.addEventListener('contextmenu', (e: Event) => { container.addEventListener('contextmenu', (e: Event) => {
e.preventDefault(); e.preventDefault();
}); });
// 绑定事件 // Graph events
graph.on('cell:contextmenu', ({ cell, e }) => { graph.on('cell:contextmenu', ({ cell, e }) => {
e.preventDefault(); e.preventDefault();
onContextMenu({ onContextMenu({
@ -309,36 +260,26 @@ export const initGraph = ({
}); });
}); });
// 点击画布时隐藏右键菜单 graph.on('blank:click cell:click', () => {
graph.on('blank:click', () => {
onContextMenu({ visible: false, x: 0, y: 0, type: 'canvas' }); 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 }) => { graph.on('node:dblclick', ({ node }) => {
onNodeClick(node); onNodeClick(node);
}); });
// 监听选择状态变化
graph.on('selection:changed', () => { graph.on('selection:changed', () => {
onGraphChange(graph); onGraphChange(graph);
}); });
// 监听画布拖拽事件 // Drag and drop handlers
container.addEventListener('dragover', onDragOver); container.addEventListener('dragover', onDragOver);
container.addEventListener('drop', onDrop); container.addEventListener('drop', onDrop);
// 如果有初始数据,加载后自动调整视图 // Load initial data if provided
if (flowDetail?.graphDefinition) { if (flowDetail?.graphDefinition) {
const graphData = JSON.parse(flowDetail.graphDefinition); const graphData = JSON.parse(flowDetail.graphDefinition);
graph.fromJSON(graphData); graph.fromJSON(graphData);
// 等待节点渲染完成后调整视图
requestAnimationFrame(() => { requestAnimationFrame(() => {
graph.zoomToFit({ padding: 50, maxScale: 1 }); graph.zoomToFit({ padding: 50, maxScale: 1 });
graph.centerContent(); graph.centerContent();
@ -347,3 +288,97 @@ export const initGraph = ({
return graph; 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;
};

View File

@ -1,4 +1,4 @@
import { Position, NodeConfig, NodeType } from '../types'; import { Position, Size, NodeType, NodeData, GraphConfig, PortGroup, GraphAttrs } from '../types';
import { NodeConfigError } from './errors'; import { NodeConfigError } from './errors';
interface CanvasMatrix { interface CanvasMatrix {
@ -25,15 +25,17 @@ export const calculateCanvasPosition = (
}; };
// 获取节点形状 // 获取节点形状
export const getNodeShape = (nodeType: string): string => { export const getNodeShape = (nodeType: string): GraphConfig['shape'] => {
switch (nodeType) { switch (nodeType) {
case 'start': case 'startEvent':
return 'circle'; case 'endEvent':
case 'end':
return 'circle'; return 'circle';
case 'exclusiveGateway':
case 'parallelGateway':
return 'diamond';
case 'userTask': case 'userTask':
case 'serviceTask': case 'serviceTask':
case 'scriptTask': case 'shellTask':
return 'rect'; return 'rect';
default: default:
return 'rect'; return 'rect';
@ -47,7 +49,7 @@ const NODE_ICONS: Record<string, string> = {
userTask: 'data:image/svg+xml;base64,PHN2ZyB0PSIxNzAzODQ4NTM1NDY5IiBjbGFzcz0iaWNvbiIgdmlld0JveD0iMCAwIDEwMjQgMTAyNCIgdmVyc2lvbj0iMS4xIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHAtaWQ9IjQxNjEiIHdpZHRoPSIyMDAiIGhlaWdodD0iMjAwIj48cGF0aCBkPSJNODU4LjUgNzYzLjZjLTE4LjktMjYuMi00Ny45LTQxLjctNzkuOS00MS43SDI0Ni40Yy0zMiAwLTYxIDI1LjUtNzkuOSA0MS43LTE4LjkgMTYuMi0yOS45IDM3LjItMjkuOSA1OS42IDAgMjIuNCAyNi44IDQwLjYgNTkuOCA0MC42aDYzMi4yYzMzIDAgNTkuOC0xOC4yIDU5LjgtNDAuNiAwLTIyLjQtMTEtNDMuNC0yOS45LTU5LjZ6TTUxMiAyNTZjODguNCAwIDE2MCA3MS42IDE2MCAxNjBzLTcxLjYgMTYwLTE2MCAxNjAtMTYwLTcxLjYtMTYwLTE2MCA3MS42LTE2MCAxNjAtMTYweiIgZmlsbD0iI2ZhOGMxNiIgcC1pZD0iNDE2MiI+PC9wYXRoPjwvc3ZnPg==', userTask: 'data:image/svg+xml;base64,PHN2ZyB0PSIxNzAzODQ4NTM1NDY5IiBjbGFzcz0iaWNvbiIgdmlld0JveD0iMCAwIDEwMjQgMTAyNCIgdmVyc2lvbj0iMS4xIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHAtaWQ9IjQxNjEiIHdpZHRoPSIyMDAiIGhlaWdodD0iMjAwIj48cGF0aCBkPSJNODU4LjUgNzYzLjZjLTE4LjktMjYuMi00Ny45LTQxLjctNzkuOS00MS43SDI0Ni40Yy0zMiAwLTYxIDI1LjUtNzkuOSA0MS43LTE4LjkgMTYuMi0yOS45IDM3LjItMjkuOSA1OS42IDAgMjIuNCAyNi44IDQwLjYgNTkuOCA0MC42aDYzMi4yYzMzIDAgNTkuOC0xOC4yIDU5LjgtNDAuNiAwLTIyLjQtMTEtNDMuNC0yOS45LTU5LjZ6TTUxMiAyNTZjODguNCAwIDE2MCA3MS42IDE2MCAxNjBzLTcxLjYgMTYwLTE2MCAxNjAtMTYwLTcxLjYtMTYwLTE2MCA3MS42LTE2MCAxNjAtMTYweiIgZmlsbD0iI2ZhOGMxNiIgcC1pZD0iNDE2MiI+PC9wYXRoPjwvc3ZnPg==',
shellTask: 'data:image/svg+xml;base64,PHN2ZyB0PSIxNzAzODQ4NTM1NDY5IiBjbGFzcz0iaWNvbiIgdmlld0JveD0iMCAwIDEwMjQgMTAyNCIgdmVyc2lvbj0iMS4xIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHAtaWQ9IjQxNjEiIHdpZHRoPSIyMDAiIGhlaWdodD0iMjAwIj48cGF0aCBkPSJNMTYwIDI1NnY1MTJoNzA0VjI1NkgxNjB6IG02NDAgNDQ4SDE5MlYyODhoNjA4djQxNnpNMjI0IDQ4MGwxMjgtMTI4IDQ1LjMgNDUuMy04Mi43IDgyLjcgODIuNyA4Mi43TDM1MiA2MDhsLTEyOC0xMjh6IG0yMzEuMSAxOTBsLTQ1LjMtNDUuMyAxNTItMTUyIDQ1LjMgNDUuM2wtMTUyIDE1MnoiIGZpbGw9IiMxODkwZmYiIHAtaWQ9IjQxNjIiPjwvcGF0aD48L3N2Zz4=', shellTask: 'data:image/svg+xml;base64,PHN2ZyB0PSIxNzAzODQ4NTM1NDY5IiBjbGFzcz0iaWNvbiIgdmlld0JveD0iMCAwIDEwMjQgMTAyNCIgdmVyc2lvbj0iMS4xIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHAtaWQ9IjQxNjEiIHdpZHRoPSIyMDAiIGhlaWdodD0iMjAwIj48cGF0aCBkPSJNMTYwIDI1NnY1MTJoNzA0VjI1NkgxNjB6IG02NDAgNDQ4SDE5MlYyODhoNjA4djQxNnpNMjI0IDQ4MGwxMjgtMTI4IDQ1LjMgNDUuMy04Mi43IDgyLjcgODIuNyA4Mi43TDM1MiA2MDhsLTEyOC0xMjh6IG0yMzEuMSAxOTBsLTQ1LjMtNDUuMyAxNTItMTUyIDQ1LjMgNDUuM2wtMTUyIDE1MnoiIGZpbGw9IiMxODkwZmYiIHAtaWQ9IjQxNjIiPjwvcGF0aD48L3N2Zz4=',
exclusiveGateway: 'data:image/svg+xml;base64,PHN2ZyB0PSIxNzAzODQ4NzAzODY0IiBjbGFzcz0iaWNvbiIgdmlld0JveD0iMCAwIDEwMjQgMTAyNCIgdmVyc2lvbj0iMS4xIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHAtaWQ9IjY1NTUiIHdpZHRoPSIyMDAiIGhlaWdodD0iMjAwIj48cGF0aCBkPSJNNzgyLjcgNDQxLjRMNTQ1IDMwNC44YzAuMy0wLjMgMC40LTAuNyAwLjctMUw3ODIuNyA0NDEuNHogbTExOC45IDQzMi45TDIxNy43IDE1OC4xQzIwMi4xIDE0Mi41IDE3NiAxNDIuNSAxNjAuNCAxNTguMWMtMTUuNiAxNS42LTE1LjYgNDEuNyAwIDU3LjNsNjgzLjkgNzE2LjJjMTUuNiAxNS42IDQxLjcgMTUuNiA1Ny4zIDBzMTUuNi00MS43IDAtNTcuM3ogbS00MjYtMjQuNmMtMC4zIDAuMy0wLjQgMC43LTAuNyAxTDIzNy42IDU4Mi42YzAuMy0wLjMgMC40LTAuNyAwLjctMWwyMzcuMyAyNjguMXogbTIzNy4zLTI2Ny4xTDQ3NS42IDg1MC43YzAuMy0wLjMgMC40LTAuNyAwLjctMUw3MTMuNiA1ODIuNnoiIGZpbGw9IiM3MjJlZDEiIHAtaWQ9IjY1NTYiPjwvcGF0aD48L3N2Zz4=', 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<string, { fill: string; stroke: string }> = {
parallelGateway: { fill: '#f9f0ff', stroke: '#722ed1' }, parallelGateway: { fill: '#f9f0ff', stroke: '#722ed1' },
}; };
// 节点配置映射 // 生成节点样式
const NODE_CONFIG: Record<string, NodeConfig> = { export const generateNodeStyle = (nodeType: string, label?: string): GraphAttrs => {
startEvent: { const theme = NODE_THEMES[nodeType] || { fill: '#ffffff', stroke: '#333333' };
size: { width: 80, height: 80 }, const shape = getNodeShape(nodeType);
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' };
const icon = NODE_ICONS[nodeType]; const icon = NODE_ICONS[nodeType];
const style: NodeStyle = { const style: GraphAttrs = {
body: { body: {
fill: theme.fill, fill: theme.fill,
stroke: theme.stroke, stroke: theme.stroke,
strokeWidth: 1, strokeWidth: nodeType === 'endEvent' ? 4 : 2,
}, },
label: { label: {
text: label || nodeType, // 设置标签文本 text: label || '',
fontSize: 14, fill: '#333333',
fill: '#000000', // 设置标签文本颜色为黑色 fontSize: 12,
refX: 0.5, },
refY: 0.5,
textAnchor: 'middle',
textVerticalAnchor: 'middle',
}
}; };
if (icon) { if (icon) {
@ -250,78 +86,108 @@ export const generateNodeStyle = (nodeType: string, label?: string): NodeStyle =
'xlink:href': icon, 'xlink:href': icon,
width: 16, width: 16,
height: 16, height: 16,
x: 6, x: shape === 'circle' ? 12 : 8,
y: 6 y: shape === 'circle' ? 12 : 8,
}; };
} }
return style; return style;
}; };
interface PortConfig { // 生成端口配置
groups: { export const generatePorts = (nodeType: string): { groups: Record<string, PortGroup> } => {
[key: string]: { const defaultPortGroup: PortGroup = {
position: string; position: 'top',
attrs: { attrs: {
circle: { circle: {
r: number; r: 4,
magnet: boolean; magnet: true,
stroke: string; stroke: '#5F95FF',
strokeWidth: number; strokeWidth: 1,
fill: string; fill: '#fff',
}; },
}; },
};
}; };
items: Array<{ group: string }>;
}
export const generatePorts = (nodeType: string): PortConfig => { return {
try { groups: {
const config = getNodeConfig(nodeType); top: { ...defaultPortGroup, position: 'top' },
return { right: { ...defaultPortGroup, position: 'right' },
groups: ['top', 'right', 'bottom', 'left'].reduce((acc, position) => ({ bottom: { ...defaultPortGroup, position: 'bottom' },
...acc, left: { ...defaultPortGroup, position: 'left' },
[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 => { export const calculateNodePosition = (nodeType: string, dropPosition: Position): Position => {
try { const shape = getNodeShape(nodeType);
const config = getNodeConfig(nodeType); const size = getNodeSize(nodeType);
const { width, height } = config.size;
return { // 调整位置使节点中心对齐到鼠标位置
x: dropPosition.x - width / 2, return {
y: dropPosition.y - height / 2 x: dropPosition.x - size.width / 2,
}; y: dropPosition.y - size.height / 2,
} catch (error) { };
if (error instanceof NodeConfigError) { };
throw error;
} // 获取节点大小
throw new NodeConfigError(`Failed to calculate position for node type: ${nodeType}`); 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;
};