From 664641283c6c913bad91ee51130379d082ef8074 Mon Sep 17 00:00:00 2001 From: dengqichen Date: Wed, 11 Dec 2024 18:25:56 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E5=B7=A5=E5=85=B7=E6=A0=8F?= =?UTF-8?q?=E6=8F=90=E7=A4=BA=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Designer/components/NodePanel/index.less | 119 +++++------ .../Designer/components/NodePanel/index.tsx | 51 +++-- .../Definition/Designer/configs/nodeConfig.ts | 20 ++ .../Workflow/Definition/Designer/index.tsx | 120 +++++------ .../Workflow/Definition/Designer/types.ts | 4 +- .../Definition/Designer/types/graph.ts | 97 +++++++++ .../Definition/Designer/utils/nodeUtils.ts | 195 +++++++++++------- 7 files changed, 380 insertions(+), 226 deletions(-) create mode 100644 frontend/src/pages/Workflow/Definition/Designer/types/graph.ts diff --git a/frontend/src/pages/Workflow/Definition/Designer/components/NodePanel/index.less b/frontend/src/pages/Workflow/Definition/Designer/components/NodePanel/index.less index d64759ba..178d6615 100644 --- a/frontend/src/pages/Workflow/Definition/Designer/components/NodePanel/index.less +++ b/frontend/src/pages/Workflow/Definition/Designer/components/NodePanel/index.less @@ -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; } } } diff --git a/frontend/src/pages/Workflow/Definition/Designer/components/NodePanel/index.tsx b/frontend/src/pages/Workflow/Definition/Designer/components/NodePanel/index.tsx index 8fca8159..38f8b264 100644 --- a/frontend/src/pages/Workflow/Definition/Designer/components/NodePanel/index.tsx +++ b/frontend/src/pages/Workflow/Definition/Designer/components/NodePanel/index.tsx @@ -17,6 +17,7 @@ const NodePanel: React.FC = ({ 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 = ({ 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); + }, {} as Record) : {}; // 将分组转换为 Tabs 项 const tabItems = Object.entries(groupedNodeTypes).map(([category, types]) => ({ @@ -47,7 +48,10 @@ const NodePanel: React.FC = ({ onNodeDragStart }) => { children: (
{types.map((nodeType) => { - const graphConfig = JSON.parse(nodeType.graphConfig || '{}'); + const graphConfig = typeof nodeType.graphConfig === 'string' + ? JSON.parse(nodeType.graphConfig) + : nodeType.graphConfig; + return (
= ({ onNodeDragStart }) => { })); onNodeDragStart(nodeType); }} - style={{ - borderColor: graphConfig.attrs?.body?.stroke || '#5F95FF', - backgroundColor: graphConfig.attrs?.body?.fill || '#fff' - }} > -
- {graphConfig.attrs?.icon?.xlinkHref && ( - {nodeType.name} +
+ {graphConfig.properties?.shape?.const === 'serviceTask' && ( + + + + + + + )}
{nodeType.name}
+
{nodeType.description}
); })} @@ -98,7 +97,7 @@ const NodePanel: React.FC = ({ onNodeDragStart }) => {
@@ -106,13 +105,13 @@ const NodePanel: React.FC = ({ onNodeDragStart }) => { }; // 获取类别标签 -const getCategoryLabel = (category: string) => { - const labels: Record = { - TASK: '任务节点', - EVENT: '事件节点', - GATEWAY: '网关节点', +function getCategoryLabel(category: string): string { + const categoryMap: Record = { + [NodeCategory.TASK]: '任务节点', + [NodeCategory.EVENT]: '事件节点', + [NodeCategory.GATEWAY]: '网关节点' }; - return labels[category] || category; -}; + return categoryMap[category] || category; +} export default NodePanel; \ No newline at end of file diff --git a/frontend/src/pages/Workflow/Definition/Designer/configs/nodeConfig.ts b/frontend/src/pages/Workflow/Definition/Designer/configs/nodeConfig.ts index 4737f65b..37932261 100644 --- a/frontend/src/pages/Workflow/Definition/Designer/configs/nodeConfig.ts +++ b/frontend/src/pages/Workflow/Definition/Designer/configs/nodeConfig.ts @@ -37,6 +37,26 @@ export const NODE_CONFIG: Record = { } } }, + 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', diff --git a/frontend/src/pages/Workflow/Definition/Designer/index.tsx b/frontend/src/pages/Workflow/Definition/Designer/index.tsx index c7f4814f..e04b9182 100644 --- a/frontend/src/pages/Workflow/Definition/Designer/index.tsx +++ b/frontend/src/pages/Workflow/Definition/Designer/index.tsx @@ -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 (
diff --git a/frontend/src/pages/Workflow/Definition/Designer/types.ts b/frontend/src/pages/Workflow/Definition/Designer/types.ts index 6fdf4b94..95a2bf36 100644 --- a/frontend/src/pages/Workflow/Definition/Designer/types.ts +++ b/frontend/src/pages/Workflow/Definition/Designer/types.ts @@ -68,7 +68,5 @@ export interface WorkflowDefinition { id: number; name: string; description?: string; - graphDefinition: string; - nodeConfig: string; - status: string; + bpmnJson: string; } diff --git a/frontend/src/pages/Workflow/Definition/Designer/types/graph.ts b/frontend/src/pages/Workflow/Definition/Designer/types/graph.ts new file mode 100644 index 00000000..8cea55a3 --- /dev/null +++ b/frontend/src/pages/Workflow/Definition/Designer/types/graph.ts @@ -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; + 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; +} + +/** + * 边配置 + */ +export interface EdgeConfig { + condition?: string; + expression?: string; + type?: 'sequence' | 'message' | 'association'; +} + +/** + * 节点配置 + */ +export interface NodeConfig { + type?: string; + implementation?: string; + fields?: Record; + 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; +} diff --git a/frontend/src/pages/Workflow/Definition/Designer/utils/nodeUtils.ts b/frontend/src/pages/Workflow/Definition/Designer/utils/nodeUtils.ts index 074a5db7..4b8e6808 100644 --- a/frontend/src/pages/Workflow/Definition/Designer/utils/nodeUtils.ts +++ b/frontend/src/pages/Workflow/Definition/Designer/utils/nodeUtils.ts @@ -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 = { + 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 = { + 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 {