增加工具栏提示。
This commit is contained in:
parent
f69897a9d0
commit
664641283c
@ -1,21 +1,17 @@
|
||||
.node-panel {
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border-right: 1px solid #e8e8e8;
|
||||
|
||||
&-loading {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
:global {
|
||||
.ant-tabs {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.ant-tabs-nav {
|
||||
margin: 0;
|
||||
@ -56,59 +52,64 @@
|
||||
}
|
||||
}
|
||||
|
||||
.node-tabs {
|
||||
height: 100%;
|
||||
|
||||
:global(.ant-tabs-content) {
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.node-panel-content {
|
||||
padding: 16px;
|
||||
padding: 8px;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
|
||||
gap: 16px;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 8px;
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.node-card {
|
||||
position: relative;
|
||||
height: 100px;
|
||||
padding: 12px;
|
||||
background-color: #fff;
|
||||
border: 2px solid transparent;
|
||||
border-radius: 8px;
|
||||
cursor: move;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
transition: all 0.3s ease;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
|
||||
padding: 12px;
|
||||
border: 1px solid #e8e8e8;
|
||||
border-radius: 4px;
|
||||
cursor: move;
|
||||
transition: all 0.3s;
|
||||
background: #fff;
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
||||
border-color: #1890ff;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.node-icon {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 24px;
|
||||
margin-bottom: 8px;
|
||||
color: #5F95FF;
|
||||
|
||||
img {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
object-fit: contain;
|
||||
.anticon {
|
||||
font-size: 32px;
|
||||
}
|
||||
}
|
||||
|
||||
.node-name {
|
||||
font-size: 12px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.node-desc {
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
text-align: center;
|
||||
line-height: 1.2;
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -17,6 +17,7 @@ const NodePanel: React.FC<NodePanelProps> = ({ onNodeDragStart }) => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const response = await getNodeTypes({ enabled: true });
|
||||
console.log('节点类型列表:', response);
|
||||
if (response?.content) {
|
||||
setNodeTypes(response.content);
|
||||
}
|
||||
@ -31,14 +32,14 @@ const NodePanel: React.FC<NodePanelProps> = ({ onNodeDragStart }) => {
|
||||
}, []);
|
||||
|
||||
// 按类别分组节点类型
|
||||
const groupedNodeTypes = nodeTypes.reduce((acc, nodeType) => {
|
||||
const groupedNodeTypes = Array.isArray(nodeTypes) ? nodeTypes.reduce((acc, nodeType) => {
|
||||
const category = nodeType.category;
|
||||
if (!acc[category]) {
|
||||
acc[category] = [];
|
||||
}
|
||||
acc[category].push(nodeType);
|
||||
return acc;
|
||||
}, {} as Record<string, NodeType[]>);
|
||||
}, {} as Record<string, NodeType[]>) : {};
|
||||
|
||||
// 将分组转换为 Tabs 项
|
||||
const tabItems = Object.entries(groupedNodeTypes).map(([category, types]) => ({
|
||||
@ -47,7 +48,10 @@ const NodePanel: React.FC<NodePanelProps> = ({ onNodeDragStart }) => {
|
||||
children: (
|
||||
<div className="node-panel-content">
|
||||
{types.map((nodeType) => {
|
||||
const graphConfig = JSON.parse(nodeType.graphConfig || '{}');
|
||||
const graphConfig = typeof nodeType.graphConfig === 'string'
|
||||
? JSON.parse(nodeType.graphConfig)
|
||||
: nodeType.graphConfig;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={nodeType.type}
|
||||
@ -60,25 +64,20 @@ const NodePanel: React.FC<NodePanelProps> = ({ onNodeDragStart }) => {
|
||||
}));
|
||||
onNodeDragStart(nodeType);
|
||||
}}
|
||||
style={{
|
||||
borderColor: graphConfig.attrs?.body?.stroke || '#5F95FF',
|
||||
backgroundColor: graphConfig.attrs?.body?.fill || '#fff'
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="node-icon"
|
||||
style={{ color: graphConfig.attrs?.body?.stroke || '#5F95FF' }}
|
||||
>
|
||||
{graphConfig.attrs?.icon?.xlinkHref && (
|
||||
<img
|
||||
src={graphConfig.attrs.icon.xlinkHref}
|
||||
width={32}
|
||||
height={32}
|
||||
alt={nodeType.name}
|
||||
/>
|
||||
<div className="node-icon">
|
||||
{graphConfig.properties?.shape?.const === 'serviceTask' && (
|
||||
<i className="anticon">
|
||||
<svg viewBox="0 0 1024 1024" width="32" height="32">
|
||||
<path d="M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm0 820c-205.4 0-372-166.6-372-372s166.6-372 372-372 372 166.6 372 372-166.6 372-372 372z" fill="#5F95FF"/>
|
||||
<path d="M512 196c-171.4 0-316 144.6-316 316s144.6 316 316 316 316-144.6 316-316-144.6-316-316-316zm0 552c-130.1 0-236-105.9-236-236s105.9-236 236-236 236 105.9 236 236-105.9 236-236 236z" fill="#5F95FF"/>
|
||||
<path d="M512 392c-66.2 0-120 53.8-120 120s53.8 120 120 120 120-53.8 120-120-53.8-120-120-120z" fill="#5F95FF"/>
|
||||
</svg>
|
||||
</i>
|
||||
)}
|
||||
</div>
|
||||
<div className="node-name">{nodeType.name}</div>
|
||||
<div className="node-desc">{nodeType.description}</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
@ -98,7 +97,7 @@ const NodePanel: React.FC<NodePanelProps> = ({ onNodeDragStart }) => {
|
||||
<div className="node-panel">
|
||||
<Tabs
|
||||
className="node-tabs"
|
||||
defaultActiveKey="EVENT"
|
||||
defaultActiveKey="TASK"
|
||||
items={tabItems}
|
||||
/>
|
||||
</div>
|
||||
@ -106,13 +105,13 @@ const NodePanel: React.FC<NodePanelProps> = ({ onNodeDragStart }) => {
|
||||
};
|
||||
|
||||
// 获取类别标签
|
||||
const getCategoryLabel = (category: string) => {
|
||||
const labels: Record<string, string> = {
|
||||
TASK: '任务节点',
|
||||
EVENT: '事件节点',
|
||||
GATEWAY: '网关节点',
|
||||
function getCategoryLabel(category: string): string {
|
||||
const categoryMap: Record<string, string> = {
|
||||
[NodeCategory.TASK]: '任务节点',
|
||||
[NodeCategory.EVENT]: '事件节点',
|
||||
[NodeCategory.GATEWAY]: '网关节点'
|
||||
};
|
||||
return labels[category] || category;
|
||||
};
|
||||
return categoryMap[category] || category;
|
||||
}
|
||||
|
||||
export default NodePanel;
|
||||
@ -37,6 +37,26 @@ export const NODE_CONFIG: Record<string, NodeConfig> = {
|
||||
}
|
||||
}
|
||||
},
|
||||
userTask: {
|
||||
size: { width: 200, height: 80 },
|
||||
shape: 'rect',
|
||||
theme: {
|
||||
fill: '#fff7e6',
|
||||
stroke: '#fa8c16'
|
||||
},
|
||||
label: '用户任务',
|
||||
extras: {
|
||||
rx: 4,
|
||||
ry: 4,
|
||||
icon: {
|
||||
'xlink:href': 'data:image/svg+xml;base64,PHN2ZyB0PSIxNzAzODQ4NTM1NDY5IiBjbGFzcz0iaWNvbiIgdmlld0JveD0iMCAwIDEwMjQgMTAyNCIgdmVyc2lvbj0iMS4xIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHAtaWQ9IjQxNjEiIHdpZHRoPSIyMDAiIGhlaWdodD0iMjAwIj48cGF0aCBkPSJNODU4LjUgNzYzLjZjLTE4LjktMjYuMi00Ny45LTQxLjctNzkuOS00MS43SDI0Ni40Yy0zMiAwLTYxIDI1LjUtNzkuOSA0MS43LTE4LjkgMTYuMi0yOS45IDM3LjItMjkuOSA1OS42IDAgMjIuNCAyNi44IDQwLjYgNTkuOCA0MC42aDYzMi4yYzMzIDAgNTkuOC0xOC4yIDU5LjgtNDAuNiAwLTIyLjQtMTEtNDMuNC0yOS45LTU5LjZ6TTUxMiAyNTZjODguNCAwIDE2MCA3MS42IDE2MCAxNjBzLTcxLjYgMTYwLTE2MCAxNjAtMTYwLTcxLjYtMTYwLTE2MCA3MS42LTE2MCAxNjAtMTYweiIgZmlsbD0iI2ZhOGMxNiIgcC1pZD0iNDE2MiI+PC9wYXRoPjwvc3ZnPg==',
|
||||
width: 32,
|
||||
height: 32,
|
||||
x: 8,
|
||||
y: 24
|
||||
}
|
||||
}
|
||||
},
|
||||
shellTask: {
|
||||
size: { width: 200, height: 80 },
|
||||
shape: 'rect',
|
||||
|
||||
@ -12,11 +12,11 @@ import NodeConfig from './components/NodeConfig';
|
||||
import Toolbar from './components/Toolbar';
|
||||
import EdgeConfig from './components/EdgeConfig';
|
||||
import { validateFlow, hasCycle } from './validate';
|
||||
import { generateNodeStyle, generatePorts, calculateNodePosition, calculateCanvasPosition } from './utils/nodeUtils';
|
||||
import { generateNodeStyle, generatePorts, calculateNodePosition, calculateCanvasPosition, getNodeShape } from './utils/nodeUtils';
|
||||
import { NODE_CONFIG } from './configs/nodeConfig';
|
||||
import { isWorkflowError } from './utils/errors';
|
||||
import { initGraph } from './utils/graphUtils';
|
||||
import { NodeType, NodeData, EdgeData, WorkflowDefinition } from './types';
|
||||
import { NodeType, NodeData, EdgeData, WorkflowDefinition, WorkflowGraph } from './types';
|
||||
|
||||
const {Sider, Content} = Layout;
|
||||
|
||||
@ -276,11 +276,6 @@ const FlowDesigner: React.FC = () => {
|
||||
const position = calculateNodePosition(nodeType.type, dropPosition);
|
||||
const nodeStyle = generateNodeStyle(nodeType.type);
|
||||
const ports = generatePorts(nodeType.type);
|
||||
const config = NODE_CONFIG[nodeType.type];
|
||||
|
||||
if (!config) {
|
||||
throw new Error(`未找到节点类型 ${nodeType.type} 的配置`);
|
||||
}
|
||||
|
||||
// 创建节点
|
||||
const node = graphRef.current.addNode({
|
||||
@ -290,7 +285,8 @@ const FlowDesigner: React.FC = () => {
|
||||
data: {
|
||||
type: nodeType.type,
|
||||
name: nodeType.name,
|
||||
config: {} as any,
|
||||
description: nodeType.description,
|
||||
config: nodeType.flowableConfig ? JSON.parse(nodeType.flowableConfig) : {},
|
||||
},
|
||||
});
|
||||
|
||||
@ -403,37 +399,13 @@ const FlowDesigner: React.FC = () => {
|
||||
}
|
||||
|
||||
const graphData = graphRef.current.toJSON();
|
||||
|
||||
// 收集节点配置数据
|
||||
const nodes = graphRef.current.getNodes().map(node => {
|
||||
const data = node.getData() as NodeData;
|
||||
return {
|
||||
id: node.id,
|
||||
type: data.type,
|
||||
name: data.name || '',
|
||||
description: data.description,
|
||||
config: data.config || {}
|
||||
};
|
||||
});
|
||||
|
||||
// 收集连线配置数据
|
||||
const transitions = graphRef.current.getEdges().map(edge => {
|
||||
const data = edge.getData() || {};
|
||||
return {
|
||||
from: edge.getSourceCellId(),
|
||||
to: edge.getTargetCellId(),
|
||||
condition: data.condition || '',
|
||||
description: data.description || '',
|
||||
priority: data.priority || 0
|
||||
};
|
||||
});
|
||||
const workflowGraph = convertGraphToWorkflowGraph(graphData);
|
||||
|
||||
// 构建更新数据
|
||||
const data = {
|
||||
...detail,
|
||||
graphDefinition: JSON.stringify(graphData),
|
||||
nodeConfig: JSON.stringify({nodes}),
|
||||
transitionConfig: JSON.stringify({transitions})
|
||||
graph: workflowGraph,
|
||||
bpmnJson: graphData
|
||||
};
|
||||
|
||||
await updateDefinition(parseInt(id), data);
|
||||
@ -462,33 +434,41 @@ const FlowDesigner: React.FC = () => {
|
||||
// 加载流程图数据
|
||||
const loadGraphData = (graph: Graph, detail: WorkflowDefinition) => {
|
||||
try {
|
||||
console.log('Loading graph data:', detail);
|
||||
// 加载图数据
|
||||
const graphData = JSON.parse(detail.graphDefinition);
|
||||
graph.fromJSON(graphData);
|
||||
|
||||
// 加载节点配置数据
|
||||
if (detail.nodeConfig) {
|
||||
const nodeConfigData = JSON.parse(detail.nodeConfig);
|
||||
// 更新节点数据和显示
|
||||
graph.getNodes().forEach(node => {
|
||||
const nodeConfig = nodeConfigData.nodes.find((n: any) => n.id === node.id);
|
||||
if (nodeConfig) {
|
||||
console.log('Node config found:', nodeConfig.config);
|
||||
const nodeData = {
|
||||
type: nodeConfig.type,
|
||||
name: nodeConfig.name,
|
||||
description: nodeConfig.description,
|
||||
config: nodeConfig.config
|
||||
};
|
||||
console.log('Setting node data:', nodeData);
|
||||
node.setData(nodeData);
|
||||
node.setAttrByPath('label/text', nodeConfig.name);
|
||||
if (detail.bpmnJson) {
|
||||
graph.fromJSON(detail.bpmnJson);
|
||||
} else if (detail.graph) {
|
||||
// 如果没有 bpmnJson,尝试使用新的 graph 数据结构
|
||||
const cells = [
|
||||
...detail.graph.nodes.map(node => ({
|
||||
id: node.id,
|
||||
shape: getNodeShape(node.type),
|
||||
attrs: generateNodeStyle(node.type, node.name || node.type),
|
||||
data: {
|
||||
type: node.type,
|
||||
config: node.config
|
||||
},
|
||||
position: node.position,
|
||||
size: node.size || { width: 100, height: 60 }
|
||||
})),
|
||||
...detail.graph.edges.map(edge => ({
|
||||
id: edge.id,
|
||||
shape: 'edge',
|
||||
source: edge.source,
|
||||
target: edge.target,
|
||||
data: {
|
||||
type: 'edge',
|
||||
label: edge.name,
|
||||
config: edge.config
|
||||
}
|
||||
});
|
||||
}))
|
||||
];
|
||||
graph.fromJSON({ cells });
|
||||
}
|
||||
} catch (error) {
|
||||
message.error('加载流程图数据失败');
|
||||
console.error('加载流程图数据失败:', error);
|
||||
message.error('加载流程图数据失败');
|
||||
}
|
||||
};
|
||||
|
||||
@ -540,6 +520,30 @@ const FlowDesigner: React.FC = () => {
|
||||
};
|
||||
}, []);
|
||||
|
||||
// 转换图形数据为工作流图数据
|
||||
const convertGraphToWorkflowGraph = (graphData: any): WorkflowGraph => {
|
||||
const nodes = graphData.cells
|
||||
.filter((cell: any) => cell.shape !== 'edge')
|
||||
.map((node: any) => ({
|
||||
id: node.id,
|
||||
type: node.shape,
|
||||
name: node.data?.label || '',
|
||||
position: node.position,
|
||||
config: node.data?.serviceTask || {}
|
||||
}));
|
||||
|
||||
const edges = graphData.cells
|
||||
.filter((cell: any) => cell.shape === 'edge')
|
||||
.map((edge: any) => ({
|
||||
id: edge.id,
|
||||
source: edge.source,
|
||||
target: edge.target,
|
||||
name: edge.data?.label || ''
|
||||
}));
|
||||
|
||||
return { nodes, edges };
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div style={{textAlign: 'center', padding: 100}}>
|
||||
|
||||
@ -68,7 +68,5 @@ export interface WorkflowDefinition {
|
||||
id: number;
|
||||
name: string;
|
||||
description?: string;
|
||||
graphDefinition: string;
|
||||
nodeConfig: string;
|
||||
status: string;
|
||||
bpmnJson: string;
|
||||
}
|
||||
|
||||
@ -0,0 +1,97 @@
|
||||
/**
|
||||
* 工作流图形定义类型
|
||||
*/
|
||||
export interface WorkflowGraph {
|
||||
nodes: WorkflowNode[];
|
||||
edges: WorkflowEdge[];
|
||||
properties?: WorkflowProperties;
|
||||
}
|
||||
|
||||
/**
|
||||
* 工作流属性
|
||||
*/
|
||||
export interface WorkflowProperties {
|
||||
name: string;
|
||||
key?: string;
|
||||
description?: string;
|
||||
version?: number;
|
||||
category?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 工作流节点
|
||||
*/
|
||||
export interface WorkflowNode {
|
||||
id: string;
|
||||
type: NodeType;
|
||||
name: string;
|
||||
position: Position;
|
||||
config?: NodeConfig;
|
||||
properties?: Record<string, any>;
|
||||
size?: {
|
||||
width: number;
|
||||
height: number;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 节点类型枚举
|
||||
*/
|
||||
export enum NodeType {
|
||||
START = 'start',
|
||||
END = 'end',
|
||||
USER_TASK = 'userTask',
|
||||
SERVICE_TASK = 'serviceTask',
|
||||
SCRIPT_TASK = 'scriptTask',
|
||||
EXCLUSIVE_GATEWAY = 'exclusiveGateway',
|
||||
PARALLEL_GATEWAY = 'parallelGateway',
|
||||
SUBPROCESS = 'subProcess',
|
||||
CALL_ACTIVITY = 'callActivity'
|
||||
}
|
||||
|
||||
/**
|
||||
* 工作流边
|
||||
*/
|
||||
export interface WorkflowEdge {
|
||||
id: string;
|
||||
source: string;
|
||||
target: string;
|
||||
name?: string;
|
||||
config?: EdgeConfig;
|
||||
properties?: Record<string, any>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 边配置
|
||||
*/
|
||||
export interface EdgeConfig {
|
||||
condition?: string;
|
||||
expression?: string;
|
||||
type?: 'sequence' | 'message' | 'association';
|
||||
}
|
||||
|
||||
/**
|
||||
* 节点配置
|
||||
*/
|
||||
export interface NodeConfig {
|
||||
type?: string;
|
||||
implementation?: string;
|
||||
fields?: Record<string, any>;
|
||||
assignee?: string;
|
||||
candidateUsers?: string[];
|
||||
candidateGroups?: string[];
|
||||
dueDate?: string;
|
||||
priority?: number;
|
||||
formKey?: string;
|
||||
skipExpression?: string;
|
||||
isAsync?: boolean;
|
||||
exclusive?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 位置信息
|
||||
*/
|
||||
export interface Position {
|
||||
x: number;
|
||||
y: number;
|
||||
}
|
||||
@ -1,5 +1,4 @@
|
||||
import { Position, NodeConfig } from '../types';
|
||||
import { NODE_CONFIG } from '../configs/nodeConfig';
|
||||
import { Position, NodeConfig, NodeType } from '../types';
|
||||
import { NodeConfigError } from './errors';
|
||||
|
||||
interface CanvasMatrix {
|
||||
@ -25,19 +24,71 @@ export const calculateCanvasPosition = (
|
||||
};
|
||||
};
|
||||
|
||||
const getNodeConfig = (nodeType: string): NodeConfig => {
|
||||
const config = NODE_CONFIG[nodeType];
|
||||
if (!config) {
|
||||
throw new NodeConfigError(`No configuration found for node type: ${nodeType}`);
|
||||
// 获取节点形状
|
||||
export const getNodeShape = (nodeType: string): string => {
|
||||
switch (nodeType) {
|
||||
case 'start':
|
||||
return 'circle';
|
||||
case 'end':
|
||||
return 'circle';
|
||||
case 'userTask':
|
||||
case 'serviceTask':
|
||||
case 'scriptTask':
|
||||
return 'rect';
|
||||
default:
|
||||
return 'rect';
|
||||
}
|
||||
return config;
|
||||
};
|
||||
|
||||
// 节点图标映射
|
||||
const NODE_ICONS: Record<string, string> = {
|
||||
startEvent: 'data:image/svg+xml;base64,PHN2ZyB0PSIxNzAzODQ4NTM1NDY5IiBjbGFzcz0iaWNvbiIgdmlld0JveD0iMCAwIDEwMjQgMTAyNCIgdmVyc2lvbj0iMS4xIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHAtaWQ9IjQxNjEiIHdpZHRoPSIyMDAiIGhlaWdodD0iMjAwIj48cGF0aCBkPSJNNTEyIDY0QzI2NC42IDY0IDY0IDI2NC42IDY0IDUxMnMyMDAuNiA0NDggNDQ4IDQ0OCA0NDgtMjAwLjYgNDQ4LTQ0OFM3NTkuNCA2NCA1MTIgNjR6bTE5MiAzMzZjMCA0LjQtMy42IDgtOCA4SDU0NHYxNTJjMCA0LjQtMy42IDgtOCA4aC00OGMtNC40IDAtOC0zLjYtOC04VjQwOEgzMjhjLTQuNCAwLTgtMy42LTgtOHYtNDhjMC00LjQgMy42LTggOC04aDE1MlYxOTJjMC00LjQgMy42LTggOC04aDQ4YzQuNCAwIDggMy42IDggOHYxNTJoMTUyYzQuNCAwIDggMy42IDggOHY0OHoiIGZpbGw9IiM1MmM0MWEiIHAtaWQ9IjQxNjIiPjwvcGF0aD48L3N2Zz4=',
|
||||
endEvent: 'data:image/svg+xml;base64,PHN2ZyB0PSIxNzAzODQ4NTM1NDY5IiBjbGFzcz0iaWNvbiIgdmlld0JveD0iMCAwIDEwMjQgMTAyNCIgdmVyc2lvbj0iMS4xIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHAtaWQ9IjQxNjEiIHdpZHRoPSIyMDAiIGhlaWdodD0iMjAwIj48cGF0aCBkPSJNNTEyIDY0QzI2NC42IDY0IDY0IDI2NC42IDY0IDUxMnMyMDAuNiA0NDggNDQ4IDQ0OCA0NDgtMjAwLjYgNDQ4LTQ0OFM3NTkuNCA2NCA1MTIgNjR6bTE5MiAyODhjMCA0LjQtMy42IDgtOCA4SDMyOGMtNC40IDAtOC0zLjYtOC04di00OGMwLTQuNCAzLjYtOCA4LThoMzY4YzQuNCAwIDggMy42IDggOHY0OHoiIGZpbGw9IiNmZjRkNGYiIHAtaWQ9IjQxNjIiPjwvcGF0aD48L3N2Zz4=',
|
||||
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+',
|
||||
};
|
||||
|
||||
// 节点主题映射
|
||||
const NODE_THEMES: Record<string, { fill: string; stroke: string }> = {
|
||||
startEvent: { fill: '#f6ffed', stroke: '#52c41a' },
|
||||
endEvent: { fill: '#fff1f0', stroke: '#ff4d4f' },
|
||||
userTask: { fill: '#fff7e6', stroke: '#fa8c16' },
|
||||
shellTask: { fill: '#e6f7ff', stroke: '#1890ff' },
|
||||
exclusiveGateway: { fill: '#f9f0ff', stroke: '#722ed1' },
|
||||
parallelGateway: { fill: '#f9f0ff', stroke: '#722ed1' },
|
||||
};
|
||||
|
||||
// 获取节点配置
|
||||
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;
|
||||
};
|
||||
|
||||
interface NodeStyle {
|
||||
width: number;
|
||||
height: number;
|
||||
shape: 'circle' | 'rect' | 'polygon';
|
||||
attrs: {
|
||||
body: {
|
||||
fill: string;
|
||||
stroke: string;
|
||||
@ -48,9 +99,8 @@ interface NodeStyle {
|
||||
};
|
||||
label: {
|
||||
text: string;
|
||||
fill: string;
|
||||
fontSize: number;
|
||||
fontWeight: number;
|
||||
fill: string;
|
||||
refX?: number;
|
||||
refY?: number;
|
||||
textAnchor?: string;
|
||||
@ -63,55 +113,40 @@ interface NodeStyle {
|
||||
x: number;
|
||||
y: number;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export const generateNodeStyle = (nodeType: string): NodeStyle => {
|
||||
try {
|
||||
const config = getNodeConfig(nodeType);
|
||||
const isCircle = config.shape === 'circle';
|
||||
export const generateNodeStyle = (nodeType: string, label?: string): NodeStyle => {
|
||||
const theme = NODE_THEMES[nodeType] || { fill: '#fff', stroke: '#d9d9d9' };
|
||||
const icon = NODE_ICONS[nodeType];
|
||||
|
||||
return {
|
||||
width: config.size.width,
|
||||
height: config.size.height,
|
||||
shape: config.shape,
|
||||
attrs: {
|
||||
const style: NodeStyle = {
|
||||
body: {
|
||||
fill: config.theme.fill,
|
||||
stroke: config.theme.stroke,
|
||||
strokeWidth: 2,
|
||||
...(config.extras || {})
|
||||
fill: theme.fill,
|
||||
stroke: theme.stroke,
|
||||
strokeWidth: 1,
|
||||
},
|
||||
label: {
|
||||
text: config.label,
|
||||
fill: '#000000',
|
||||
text: label || nodeType, // 设置标签文本
|
||||
fontSize: 14,
|
||||
fontWeight: 500,
|
||||
textAnchor: 'middle',
|
||||
textVerticalAnchor: 'middle',
|
||||
fill: '#000000', // 设置标签文本颜色为黑色
|
||||
refX: 0.5,
|
||||
refY: 0.5,
|
||||
},
|
||||
...(config.extras?.icon ? {
|
||||
image: {
|
||||
...config.extras.icon,
|
||||
...(config.shape === 'circle' ? {
|
||||
x: (config.size.width - config.extras.icon.width) / 2,
|
||||
y: config.size.height * 0.25
|
||||
} : config.shape === 'polygon' ? {
|
||||
x: (config.size.width - config.extras.icon.width) / 2,
|
||||
y: (config.size.height - config.extras.icon.height) / 2
|
||||
} : config.extras.icon)
|
||||
}
|
||||
} : {})
|
||||
textAnchor: 'middle',
|
||||
textVerticalAnchor: 'middle',
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
if (error instanceof NodeConfigError) {
|
||||
throw error;
|
||||
}
|
||||
throw new NodeConfigError(`Failed to generate node style for type: ${nodeType}`);
|
||||
|
||||
if (icon) {
|
||||
style.image = {
|
||||
'xlink:href': icon,
|
||||
width: 16,
|
||||
height: 16,
|
||||
x: 6,
|
||||
y: 6
|
||||
};
|
||||
}
|
||||
|
||||
return style;
|
||||
};
|
||||
|
||||
interface PortConfig {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user