增加工具栏提示。
This commit is contained in:
parent
f69897a9d0
commit
664641283c
@ -1,42 +1,38 @@
|
|||||||
.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;
|
||||||
padding: 0 16px;
|
padding: 0 16px;
|
||||||
background-color: #fafafa;
|
background-color: #fafafa;
|
||||||
border-bottom: 1px solid #f0f0f0;
|
border-bottom: 1px solid #f0f0f0;
|
||||||
|
|
||||||
.ant-tabs-tab {
|
.ant-tabs-tab {
|
||||||
padding: 8px 16px !important;
|
padding: 8px 16px !important;
|
||||||
margin: 0 !important;
|
margin: 0 !important;
|
||||||
transition: all 0.3s;
|
transition: all 0.3s;
|
||||||
border-radius: 4px 4px 0 0;
|
border-radius: 4px 4px 0 0;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
color: #1890ff;
|
color: #1890ff;
|
||||||
background: rgba(24, 144, 255, 0.1);
|
background: rgba(24, 144, 255, 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
&.ant-tabs-tab-active {
|
&.ant-tabs-tab-active {
|
||||||
background: #fff;
|
background: #fff;
|
||||||
|
|
||||||
.ant-tabs-tab-btn {
|
.ant-tabs-tab-btn {
|
||||||
color: #1890ff;
|
color: #1890ff;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
@ -48,7 +44,7 @@
|
|||||||
.ant-tabs-content-holder {
|
.ant-tabs-content-holder {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
|
|
||||||
.ant-tabs-content {
|
.ant-tabs-content {
|
||||||
height: calc(100% - 44px);
|
height: calc(100% - 44px);
|
||||||
}
|
}
|
||||||
@ -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;
|
display: flex;
|
||||||
height: 100px;
|
flex-direction: column;
|
||||||
padding: 12px;
|
align-items: center;
|
||||||
background-color: #fff;
|
padding: 12px;
|
||||||
border: 2px solid transparent;
|
border: 1px solid #e8e8e8;
|
||||||
border-radius: 8px;
|
border-radius: 4px;
|
||||||
cursor: move;
|
cursor: move;
|
||||||
display: flex;
|
transition: all 0.3s;
|
||||||
flex-direction: column;
|
background: #fff;
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
gap: 8px;
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
|
|
||||||
|
|
||||||
&: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 {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
color: #5F95FF;
|
||||||
|
|
||||||
|
.anticon {
|
||||||
|
font-size: 32px;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.node-icon {
|
.node-name {
|
||||||
width: 40px;
|
font-size: 14px;
|
||||||
height: 40px;
|
font-weight: 500;
|
||||||
display: flex;
|
color: #333;
|
||||||
align-items: center;
|
margin-bottom: 4px;
|
||||||
justify-content: center;
|
}
|
||||||
font-size: 24px;
|
|
||||||
|
|
||||||
img {
|
.node-desc {
|
||||||
width: 32px;
|
font-size: 12px;
|
||||||
height: 32px;
|
color: #666;
|
||||||
object-fit: contain;
|
text-align: center;
|
||||||
}
|
overflow: hidden;
|
||||||
}
|
text-overflow: ellipsis;
|
||||||
|
display: -webkit-box;
|
||||||
.node-name {
|
-webkit-line-clamp: 2;
|
||||||
font-size: 12px;
|
-webkit-box-orient: vertical;
|
||||||
color: #333;
|
|
||||||
text-align: center;
|
|
||||||
line-height: 1.2;
|
|
||||||
max-width: 100%;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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;
|
||||||
@ -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',
|
||||||
|
|||||||
@ -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}}>
|
||||||
|
|||||||
@ -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;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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 { Position, NodeConfig, NodeType } from '../types';
|
||||||
import { NODE_CONFIG } from '../configs/nodeConfig';
|
|
||||||
import { NodeConfigError } from './errors';
|
import { NodeConfigError } from './errors';
|
||||||
|
|
||||||
interface CanvasMatrix {
|
interface CanvasMatrix {
|
||||||
@ -25,93 +24,129 @@ 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;
|
body: {
|
||||||
height: number;
|
fill: string;
|
||||||
shape: 'circle' | 'rect' | 'polygon';
|
stroke: string;
|
||||||
attrs: {
|
strokeWidth: number;
|
||||||
body: {
|
rx?: number;
|
||||||
fill: string;
|
ry?: number;
|
||||||
stroke: string;
|
refPoints?: string;
|
||||||
strokeWidth: number;
|
};
|
||||||
rx?: number;
|
label: {
|
||||||
ry?: number;
|
text: string;
|
||||||
refPoints?: string;
|
fontSize: number;
|
||||||
};
|
fill: string;
|
||||||
label: {
|
refX?: number;
|
||||||
text: string;
|
refY?: number;
|
||||||
fill: string;
|
textAnchor?: string;
|
||||||
fontSize: number;
|
textVerticalAnchor?: string;
|
||||||
fontWeight: number;
|
};
|
||||||
refX?: number;
|
image?: {
|
||||||
refY?: number;
|
'xlink:href': string;
|
||||||
textAnchor?: string;
|
width: number;
|
||||||
textVerticalAnchor?: string;
|
height: number;
|
||||||
};
|
x: number;
|
||||||
image?: {
|
y: number;
|
||||||
'xlink:href': string;
|
|
||||||
width: number;
|
|
||||||
height: number;
|
|
||||||
x: 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';
|
|
||||||
|
const style: NodeStyle = {
|
||||||
return {
|
body: {
|
||||||
width: config.size.width,
|
fill: theme.fill,
|
||||||
height: config.size.height,
|
stroke: theme.stroke,
|
||||||
shape: config.shape,
|
strokeWidth: 1,
|
||||||
attrs: {
|
},
|
||||||
body: {
|
label: {
|
||||||
fill: config.theme.fill,
|
text: label || nodeType, // 设置标签文本
|
||||||
stroke: config.theme.stroke,
|
fontSize: 14,
|
||||||
strokeWidth: 2,
|
fill: '#000000', // 设置标签文本颜色为黑色
|
||||||
...(config.extras || {})
|
refX: 0.5,
|
||||||
},
|
refY: 0.5,
|
||||||
label: {
|
textAnchor: 'middle',
|
||||||
text: config.label,
|
textVerticalAnchor: 'middle',
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
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 {
|
interface PortConfig {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user