流程定义

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 () => {
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('获取节点类型失败');

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

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';
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<string, string> = {
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<string, { fill: string; stroke: string }> = {
parallelGateway: { fill: '#f9f0ff', stroke: '#722ed1' },
};
// 节点配置映射
const NODE_CONFIG: Record<string, NodeConfig> = {
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<string, PortGroup> } => {
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;
};