增加工具栏提示。

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,21 +1,17 @@
.node-panel { .node-panel {
height: 100%; height: 100%;
overflow: hidden; border-right: 1px solid #e8e8e8;
display: flex;
flex-direction: column;
&-loading { &-loading {
height: 100%;
display: flex; display: flex;
align-items: center;
justify-content: center; justify-content: center;
align-items: center;
height: 100%;
} }
:global { :global {
.ant-tabs { .ant-tabs {
height: 100%; height: 100%;
display: flex;
flex-direction: column;
.ant-tabs-nav { .ant-tabs-nav {
margin: 0; margin: 0;
@ -56,59 +52,64 @@
} }
} }
.node-tabs {
height: 100%;
:global(.ant-tabs-content) {
height: 100%;
}
}
.node-panel-content { .node-panel-content {
padding: 16px; padding: 8px;
display: grid; display: grid;
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr)); grid-template-columns: repeat(2, 1fr);
gap: 16px; gap: 8px;
height: 100%;
overflow-y: auto; overflow-y: auto;
}
.node-card { .node-card {
position: relative;
height: 100px;
padding: 12px;
background-color: #fff;
border: 2px solid transparent;
border-radius: 8px;
cursor: move;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
justify-content: center; padding: 12px;
gap: 8px; border: 1px solid #e8e8e8;
transition: all 0.3s ease; border-radius: 4px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05); cursor: move;
transition: all 0.3s;
background: #fff;
&:hover { &:hover {
transform: translateY(-2px); border-color: #1890ff;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
} }
.node-icon { .node-icon {
width: 40px; margin-bottom: 8px;
height: 40px; color: #5F95FF;
display: flex;
align-items: center;
justify-content: center;
font-size: 24px;
img { .anticon {
width: 32px; font-size: 32px;
height: 32px;
object-fit: contain;
} }
} }
.node-name { .node-name {
font-size: 12px; font-size: 14px;
font-weight: 500;
color: #333; color: #333;
margin-bottom: 4px;
}
.node-desc {
font-size: 12px;
color: #666;
text-align: center; text-align: center;
line-height: 1.2;
max-width: 100%;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; 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); setLoading(true);
try { try {
const response = await getNodeTypes({ enabled: true }); const response = await getNodeTypes({ enabled: true });
console.log('节点类型列表:', response);
if (response?.content) { if (response?.content) {
setNodeTypes(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; const category = nodeType.category;
if (!acc[category]) { if (!acc[category]) {
acc[category] = []; acc[category] = [];
} }
acc[category].push(nodeType); acc[category].push(nodeType);
return acc; return acc;
}, {} as Record<string, NodeType[]>); }, {} as Record<string, NodeType[]>) : {};
// 将分组转换为 Tabs 项 // 将分组转换为 Tabs 项
const tabItems = Object.entries(groupedNodeTypes).map(([category, types]) => ({ const tabItems = Object.entries(groupedNodeTypes).map(([category, types]) => ({
@ -47,7 +48,10 @@ const NodePanel: React.FC<NodePanelProps> = ({ onNodeDragStart }) => {
children: ( children: (
<div className="node-panel-content"> <div className="node-panel-content">
{types.map((nodeType) => { {types.map((nodeType) => {
const graphConfig = JSON.parse(nodeType.graphConfig || '{}'); const graphConfig = typeof nodeType.graphConfig === 'string'
? JSON.parse(nodeType.graphConfig)
: nodeType.graphConfig;
return ( return (
<div <div
key={nodeType.type} key={nodeType.type}
@ -60,25 +64,20 @@ const NodePanel: React.FC<NodePanelProps> = ({ onNodeDragStart }) => {
})); }));
onNodeDragStart(nodeType); onNodeDragStart(nodeType);
}} }}
style={{
borderColor: graphConfig.attrs?.body?.stroke || '#5F95FF',
backgroundColor: graphConfig.attrs?.body?.fill || '#fff'
}}
> >
<div <div className="node-icon">
className="node-icon" {graphConfig.properties?.shape?.const === 'serviceTask' && (
style={{ color: graphConfig.attrs?.body?.stroke || '#5F95FF' }} <i className="anticon">
> <svg viewBox="0 0 1024 1024" width="32" height="32">
{graphConfig.attrs?.icon?.xlinkHref && ( <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"/>
<img <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"/>
src={graphConfig.attrs.icon.xlinkHref} <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"/>
width={32} </svg>
height={32} </i>
alt={nodeType.name}
/>
)} )}
</div> </div>
<div className="node-name">{nodeType.name}</div> <div className="node-name">{nodeType.name}</div>
<div className="node-desc">{nodeType.description}</div>
</div> </div>
); );
})} })}
@ -98,7 +97,7 @@ const NodePanel: React.FC<NodePanelProps> = ({ onNodeDragStart }) => {
<div className="node-panel"> <div className="node-panel">
<Tabs <Tabs
className="node-tabs" className="node-tabs"
defaultActiveKey="EVENT" defaultActiveKey="TASK"
items={tabItems} items={tabItems}
/> />
</div> </div>
@ -106,13 +105,13 @@ const NodePanel: React.FC<NodePanelProps> = ({ onNodeDragStart }) => {
}; };
// 获取类别标签 // 获取类别标签
const getCategoryLabel = (category: string) => { function getCategoryLabel(category: string): string {
const labels: Record<string, string> = { const categoryMap: Record<string, string> = {
TASK: '任务节点', [NodeCategory.TASK]: '任务节点',
EVENT: '事件节点', [NodeCategory.EVENT]: '事件节点',
GATEWAY: '网关节点', [NodeCategory.GATEWAY]: '网关节点'
};
return labels[category] || category;
}; };
return categoryMap[category] || category;
}
export default NodePanel; 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: { shellTask: {
size: { width: 200, height: 80 }, size: { width: 200, height: 80 },
shape: 'rect', shape: 'rect',

View File

@ -12,11 +12,11 @@ import NodeConfig from './components/NodeConfig';
import Toolbar from './components/Toolbar'; import Toolbar from './components/Toolbar';
import EdgeConfig from './components/EdgeConfig'; import EdgeConfig from './components/EdgeConfig';
import { validateFlow, hasCycle } from './validate'; 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 { NODE_CONFIG } from './configs/nodeConfig';
import { isWorkflowError } from './utils/errors'; import { isWorkflowError } from './utils/errors';
import { initGraph } from './utils/graphUtils'; import { initGraph } from './utils/graphUtils';
import { NodeType, NodeData, EdgeData, WorkflowDefinition } from './types'; import { NodeType, NodeData, EdgeData, WorkflowDefinition, WorkflowGraph } from './types';
const {Sider, Content} = Layout; const {Sider, Content} = Layout;
@ -276,11 +276,6 @@ const FlowDesigner: React.FC = () => {
const position = calculateNodePosition(nodeType.type, dropPosition); const position = calculateNodePosition(nodeType.type, dropPosition);
const nodeStyle = generateNodeStyle(nodeType.type); const nodeStyle = generateNodeStyle(nodeType.type);
const ports = generatePorts(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({ const node = graphRef.current.addNode({
@ -290,7 +285,8 @@ const FlowDesigner: React.FC = () => {
data: { data: {
type: nodeType.type, type: nodeType.type,
name: nodeType.name, 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 graphData = graphRef.current.toJSON();
const workflowGraph = convertGraphToWorkflowGraph(graphData);
// 收集节点配置数据
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 data = { const data = {
...detail, ...detail,
graphDefinition: JSON.stringify(graphData), graph: workflowGraph,
nodeConfig: JSON.stringify({nodes}), bpmnJson: graphData
transitionConfig: JSON.stringify({transitions})
}; };
await updateDefinition(parseInt(id), data); await updateDefinition(parseInt(id), data);
@ -462,33 +434,41 @@ const FlowDesigner: React.FC = () => {
// 加载流程图数据 // 加载流程图数据
const loadGraphData = (graph: Graph, detail: WorkflowDefinition) => { const loadGraphData = (graph: Graph, detail: WorkflowDefinition) => {
try { try {
console.log('Loading graph data:', detail);
// 加载图数据 // 加载图数据
const graphData = JSON.parse(detail.graphDefinition); if (detail.bpmnJson) {
graph.fromJSON(graphData); graph.fromJSON(detail.bpmnJson);
} else if (detail.graph) {
// 加载节点配置数据 // 如果没有 bpmnJson尝试使用新的 graph 数据结构
if (detail.nodeConfig) { const cells = [
const nodeConfigData = JSON.parse(detail.nodeConfig); ...detail.graph.nodes.map(node => ({
// 更新节点数据和显示 id: node.id,
graph.getNodes().forEach(node => { shape: getNodeShape(node.type),
const nodeConfig = nodeConfigData.nodes.find((n: any) => n.id === node.id); attrs: generateNodeStyle(node.type, node.name || node.type),
if (nodeConfig) { data: {
console.log('Node config found:', nodeConfig.config); type: node.type,
const nodeData = { config: node.config
type: nodeConfig.type, },
name: nodeConfig.name, position: node.position,
description: nodeConfig.description, size: node.size || { width: 100, height: 60 }
config: nodeConfig.config })),
}; ...detail.graph.edges.map(edge => ({
console.log('Setting node data:', nodeData); id: edge.id,
node.setData(nodeData); shape: 'edge',
node.setAttrByPath('label/text', nodeConfig.name); source: edge.source,
target: edge.target,
data: {
type: 'edge',
label: edge.name,
config: edge.config
} }
}); }))
];
graph.fromJSON({ cells });
} }
} catch (error) { } catch (error) {
message.error('加载流程图数据失败');
console.error('加载流程图数据失败:', 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) { if (loading) {
return ( return (
<div style={{textAlign: 'center', padding: 100}}> <div style={{textAlign: 'center', padding: 100}}>

View File

@ -68,7 +68,5 @@ export interface WorkflowDefinition {
id: number; id: number;
name: string; name: string;
description?: string; description?: string;
graphDefinition: string; bpmnJson: string;
nodeConfig: string;
status: 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 { Position, NodeConfig, NodeType } from '../types';
import { NODE_CONFIG } from '../configs/nodeConfig';
import { NodeConfigError } from './errors'; import { NodeConfigError } from './errors';
interface CanvasMatrix { interface CanvasMatrix {
@ -25,19 +24,71 @@ export const calculateCanvasPosition = (
}; };
}; };
const getNodeConfig = (nodeType: string): NodeConfig => { // 获取节点形状
const config = NODE_CONFIG[nodeType]; export const getNodeShape = (nodeType: string): string => {
if (!config) { switch (nodeType) {
throw new NodeConfigError(`No configuration found for node type: ${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 { interface NodeStyle {
width: number;
height: number;
shape: 'circle' | 'rect' | 'polygon';
attrs: {
body: { body: {
fill: string; fill: string;
stroke: string; stroke: string;
@ -48,9 +99,8 @@ interface NodeStyle {
}; };
label: { label: {
text: string; text: string;
fill: string;
fontSize: number; fontSize: number;
fontWeight: number; fill: string;
refX?: number; refX?: number;
refY?: number; refY?: number;
textAnchor?: string; textAnchor?: string;
@ -63,55 +113,40 @@ interface NodeStyle {
x: number; x: number;
y: number; y: number;
}; };
};
} }
export const generateNodeStyle = (nodeType: string): NodeStyle => { export const generateNodeStyle = (nodeType: string, label?: string): NodeStyle => {
try { const theme = NODE_THEMES[nodeType] || { fill: '#fff', stroke: '#d9d9d9' };
const config = getNodeConfig(nodeType); const icon = NODE_ICONS[nodeType];
const isCircle = config.shape === 'circle';
return { const style: NodeStyle = {
width: config.size.width,
height: config.size.height,
shape: config.shape,
attrs: {
body: { body: {
fill: config.theme.fill, fill: theme.fill,
stroke: config.theme.stroke, stroke: theme.stroke,
strokeWidth: 2, strokeWidth: 1,
...(config.extras || {})
}, },
label: { label: {
text: config.label, text: label || nodeType, // 设置标签文本
fill: '#000000',
fontSize: 14, fontSize: 14,
fontWeight: 500, fill: '#000000', // 设置标签文本颜色为黑色
textAnchor: 'middle',
textVerticalAnchor: 'middle',
refX: 0.5, refX: 0.5,
refY: 0.5, refY: 0.5,
}, textAnchor: 'middle',
...(config.extras?.icon ? { textVerticalAnchor: 'middle',
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) { if (icon) {
throw error; style.image = {
} 'xlink:href': icon,
throw new NodeConfigError(`Failed to generate node style for type: ${nodeType}`); width: 16,
height: 16,
x: 6,
y: 6
};
} }
return style;
}; };
interface PortConfig { interface PortConfig {