增加工具栏提示。

This commit is contained in:
dengqichen 2024-12-11 18:25:56 +08:00
parent f69897a9d0
commit 664641283c
7 changed files with 380 additions and 226 deletions

View File

@ -1,42 +1,38 @@
.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;
padding: 0 16px;
background-color: #fafafa;
border-bottom: 1px solid #f0f0f0;
.ant-tabs-tab {
padding: 8px 16px !important;
margin: 0 !important;
transition: all 0.3s;
border-radius: 4px 4px 0 0;
&:hover {
color: #1890ff;
background: rgba(24, 144, 255, 0.1);
}
&.ant-tabs-tab-active {
background: #fff;
.ant-tabs-tab-btn {
color: #1890ff;
font-weight: 500;
@ -48,7 +44,7 @@
.ant-tabs-content-holder {
flex: 1;
overflow: auto;
.ant-tabs-content {
height: calc(100% - 44px);
}
@ -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);
.node-card {
display: flex;
flex-direction: column;
align-items: center;
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);
&:hover {
border-color: #1890ff;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
}
.node-icon {
margin-bottom: 8px;
color: #5F95FF;
.anticon {
font-size: 32px;
}
}
.node-icon {
width: 40px;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
font-size: 24px;
.node-name {
font-size: 14px;
font-weight: 500;
color: #333;
margin-bottom: 4px;
}
img {
width: 32px;
height: 32px;
object-fit: contain;
}
}
.node-name {
font-size: 12px;
color: #333;
text-align: center;
line-height: 1.2;
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.node-desc {
font-size: 12px;
color: #666;
text-align: center;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
}
}
}

View File

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

View File

@ -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',

View File

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

View File

@ -68,7 +68,5 @@ export interface WorkflowDefinition {
id: number;
name: string;
description?: string;
graphDefinition: string;
nodeConfig: string;
status: string;
bpmnJson: string;
}

View File

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

View File

@ -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,93 +24,129 @@ 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;
strokeWidth: number;
rx?: number;
ry?: number;
refPoints?: string;
};
label: {
text: string;
fill: string;
fontSize: number;
fontWeight: number;
refX?: number;
refY?: number;
textAnchor?: string;
textVerticalAnchor?: string;
};
image?: {
'xlink:href': string;
width: number;
height: number;
x: number;
y: number;
};
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): NodeStyle => {
try {
const config = getNodeConfig(nodeType);
const isCircle = config.shape === 'circle';
return {
width: config.size.width,
height: config.size.height,
shape: config.shape,
attrs: {
body: {
fill: config.theme.fill,
stroke: config.theme.stroke,
strokeWidth: 2,
...(config.extras || {})
},
label: {
text: config.label,
fill: '#000000',
fontSize: 14,
fontWeight: 500,
textAnchor: 'middle',
textVerticalAnchor: 'middle',
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)
}
} : {})
}
};
} catch (error) {
if (error instanceof NodeConfigError) {
throw error;
export const generateNodeStyle = (nodeType: string, label?: string): NodeStyle => {
const theme = NODE_THEMES[nodeType] || { fill: '#fff', stroke: '#d9d9d9' };
const icon = NODE_ICONS[nodeType];
const style: NodeStyle = {
body: {
fill: theme.fill,
stroke: theme.stroke,
strokeWidth: 1,
},
label: {
text: label || nodeType, // 设置标签文本
fontSize: 14,
fill: '#000000', // 设置标签文本颜色为黑色
refX: 0.5,
refY: 0.5,
textAnchor: 'middle',
textVerticalAnchor: 'middle',
}
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 {