流程定义
This commit is contained in:
parent
7e94b1f37c
commit
d048858015
@ -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('获取节点类型失败');
|
||||||
|
|||||||
@ -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 { 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;
|
||||||
|
};
|
||||||
|
|||||||
@ -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;
|
||||||
|
};
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user