流程定义
This commit is contained in:
parent
7e94b1f37c
commit
d048858015
@ -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('获取节点类型失败');
|
||||
|
||||
@ -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;
|
||||
};
|
||||
}
|
||||
@ -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;
|
||||
};
|
||||
}
|
||||
@ -0,0 +1,4 @@
|
||||
export * from './base';
|
||||
export * from './node';
|
||||
export * from './edge';
|
||||
export * from './workflow';
|
||||
@ -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;
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
@ -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
|
||||
// Update minimap on graph changes
|
||||
graph.on('scale translate', () => {
|
||||
if (graph.minimap) {
|
||||
graph.minimap.updateViewport();
|
||||
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) => {
|
||||
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;
|
||||
};
|
||||
|
||||
@ -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;
|
||||
};
|
||||
|
||||
Loading…
Reference in New Issue
Block a user