增加工具栏提示。
This commit is contained in:
parent
4f938e1c1c
commit
61661e2a09
@ -0,0 +1,241 @@
|
|||||||
|
import React, {useState, useEffect} from 'react';
|
||||||
|
import {Card, Collapse, Tooltip, message} from 'antd';
|
||||||
|
import type {NodeDefinition, NodeCategory} from '../types';
|
||||||
|
import {
|
||||||
|
PlayCircleOutlined,
|
||||||
|
StopOutlined,
|
||||||
|
UserOutlined,
|
||||||
|
ApiOutlined,
|
||||||
|
CodeOutlined,
|
||||||
|
NodeIndexOutlined,
|
||||||
|
SplitCellsOutlined,
|
||||||
|
AppstoreOutlined,
|
||||||
|
BranchesOutlined
|
||||||
|
} from '@ant-design/icons';
|
||||||
|
import {getNodeDefinitionList} from "@/pages/Workflow/Definition/Design/service";
|
||||||
|
|
||||||
|
const {Panel} = Collapse;
|
||||||
|
|
||||||
|
// 图标映射配置
|
||||||
|
const iconMap: Record<string, any> = {
|
||||||
|
'play-circle': PlayCircleOutlined,
|
||||||
|
'stop': StopOutlined,
|
||||||
|
'user': UserOutlined,
|
||||||
|
'api': ApiOutlined,
|
||||||
|
'code': CodeOutlined,
|
||||||
|
'fork': NodeIndexOutlined,
|
||||||
|
'branches': SplitCellsOutlined,
|
||||||
|
'apartment': AppstoreOutlined
|
||||||
|
};
|
||||||
|
|
||||||
|
// 节点类型到图标的映射
|
||||||
|
const typeIconMap: Record<string, any> = {
|
||||||
|
'START_EVENT': PlayCircleOutlined,
|
||||||
|
'END_EVENT': StopOutlined,
|
||||||
|
'USER_TASK': UserOutlined,
|
||||||
|
'SERVICE_TASK': ApiOutlined,
|
||||||
|
'SCRIPT_TASK': CodeOutlined,
|
||||||
|
'EXCLUSIVE_GATEWAY': NodeIndexOutlined,
|
||||||
|
'PARALLEL_GATEWAY': SplitCellsOutlined,
|
||||||
|
'SUB_PROCESS': AppstoreOutlined,
|
||||||
|
'CALL_ACTIVITY': BranchesOutlined
|
||||||
|
};
|
||||||
|
|
||||||
|
// 节点分类配置
|
||||||
|
const categoryConfig: Record<NodeCategory, {
|
||||||
|
label: string;
|
||||||
|
key: string;
|
||||||
|
}> = {
|
||||||
|
EVENT: {label: '事件节点', key: '1'},
|
||||||
|
TASK: {label: '任务节点', key: '2'},
|
||||||
|
GATEWAY: {label: '网关节点', key: '3'},
|
||||||
|
CONTAINER: {label: '容器节点', key: '4'},
|
||||||
|
};
|
||||||
|
|
||||||
|
interface NodePanelProps {
|
||||||
|
onNodeDragStart?: (node: NodeDefinition, e: React.DragEvent) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const NodePanel: React.FC<NodePanelProps> = ({onNodeDragStart}) => {
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [nodeDefinitions, setNodeDefinitions] = useState<NodeDefinition[]>([]);
|
||||||
|
|
||||||
|
// 加载节点定义列表
|
||||||
|
const loadNodeDefinitions = async () => {
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
const data = await getNodeDefinitionList();
|
||||||
|
setNodeDefinitions(data);
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof Error) {
|
||||||
|
message.error(error.message);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
loadNodeDefinitions();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// 按分类对节点进行分组
|
||||||
|
const groupedNodes = nodeDefinitions.reduce((acc, node) => {
|
||||||
|
if (!acc[node.category]) {
|
||||||
|
acc[node.category] = [];
|
||||||
|
}
|
||||||
|
acc[node.category].push(node);
|
||||||
|
return acc;
|
||||||
|
}, {} as Record<NodeCategory, NodeDefinition[]>);
|
||||||
|
|
||||||
|
// 处理节点拖拽开始事件
|
||||||
|
const handleDragStart = (node: NodeDefinition, e: React.DragEvent) => {
|
||||||
|
e.dataTransfer.setData('node', JSON.stringify(node));
|
||||||
|
onNodeDragStart?.(node, e);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 渲染节点图标
|
||||||
|
const renderNodeIcon = (node: NodeDefinition) => {
|
||||||
|
const iconName = node.graphConfig.uiSchema.style.icon;
|
||||||
|
// 首先尝试使用配置的图标
|
||||||
|
let IconComponent = iconMap[iconName];
|
||||||
|
|
||||||
|
// 如果没有找到对应的图标,使用节点类型对应的默认图标
|
||||||
|
if (!IconComponent) {
|
||||||
|
IconComponent = typeIconMap[node.type] || AppstoreOutlined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<IconComponent
|
||||||
|
style={{
|
||||||
|
color: node.graphConfig.uiSchema.style.iconColor || '#1890ff',
|
||||||
|
fontSize: '16px',
|
||||||
|
marginRight: '6px'
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getNodeItemStyle = (node: NodeDefinition) => ({
|
||||||
|
width: '100%',
|
||||||
|
padding: '10px 12px',
|
||||||
|
border: `1px solid ${node.graphConfig.uiSchema.style.stroke}`,
|
||||||
|
borderRadius: '6px',
|
||||||
|
cursor: 'move',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
gap: '10px',
|
||||||
|
background: node.graphConfig.uiSchema.style.fill,
|
||||||
|
transition: 'all 0.3s',
|
||||||
|
boxShadow: '0 1px 2px rgba(0,0,0,0.05)',
|
||||||
|
'&:hover': {
|
||||||
|
transform: 'translateY(-1px)',
|
||||||
|
boxShadow: '0 3px 6px rgba(0,0,0,0.1)',
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const tooltipStyle = {
|
||||||
|
maxWidth: '300px'
|
||||||
|
};
|
||||||
|
|
||||||
|
const tooltipOverlayInnerStyle = {
|
||||||
|
padding: '12px 16px'
|
||||||
|
};
|
||||||
|
|
||||||
|
// 构建折叠面板的 items
|
||||||
|
const collapseItems = Object.entries(categoryConfig).map(([category, {label, key}]) => ({
|
||||||
|
key,
|
||||||
|
label: <span style={{ fontSize: '14px', fontWeight: 500 }}>{label}</span>,
|
||||||
|
children: (
|
||||||
|
<div style={{
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
gap: '12px',
|
||||||
|
padding: '8px 4px'
|
||||||
|
}}>
|
||||||
|
{groupedNodes[category as NodeCategory]?.map(node => (
|
||||||
|
<Tooltip
|
||||||
|
key={node.id}
|
||||||
|
title={
|
||||||
|
<div>
|
||||||
|
<div style={{ fontSize: '14px', fontWeight: 500 }}>{node.description}</div>
|
||||||
|
<div style={{ marginTop: 12 }}>
|
||||||
|
<div style={{ fontSize: '13px', color: '#8c8c8c' }}>功能特点:</div>
|
||||||
|
<ul style={{
|
||||||
|
paddingLeft: 16,
|
||||||
|
margin: '8px 0',
|
||||||
|
fontSize: '13px',
|
||||||
|
color: '#595959'
|
||||||
|
}}>
|
||||||
|
{node.graphConfig.details.features.map((feature, index) => (
|
||||||
|
<li key={index}>{feature}</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
overlayStyle={tooltipStyle}
|
||||||
|
overlayInnerStyle={tooltipOverlayInnerStyle}
|
||||||
|
placement="right"
|
||||||
|
arrow={false}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
draggable
|
||||||
|
onDragStart={(e) => handleDragStart(node, e)}
|
||||||
|
style={getNodeItemStyle(node)}
|
||||||
|
>
|
||||||
|
<div style={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
gap: '6px'
|
||||||
|
}}>
|
||||||
|
{renderNodeIcon(node)}
|
||||||
|
<span style={{
|
||||||
|
fontSize: '13px',
|
||||||
|
color: '#262626',
|
||||||
|
overflow: 'hidden',
|
||||||
|
textOverflow: 'ellipsis',
|
||||||
|
whiteSpace: 'nowrap',
|
||||||
|
}}>{node.name}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
}));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card
|
||||||
|
title="流程节点"
|
||||||
|
size="small"
|
||||||
|
loading={loading}
|
||||||
|
styles={{
|
||||||
|
header: {
|
||||||
|
padding: '12px 16px',
|
||||||
|
fontSize: '15px',
|
||||||
|
fontWeight: 500,
|
||||||
|
borderBottom: '1px solid #f0f0f0',
|
||||||
|
},
|
||||||
|
body: {
|
||||||
|
padding: '12px',
|
||||||
|
height: 'calc(100vh - 200px)',
|
||||||
|
overflowY: 'auto'
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Collapse
|
||||||
|
defaultActiveKey={['1', '2', '3', '4']}
|
||||||
|
ghost
|
||||||
|
items={collapseItems}
|
||||||
|
style={{
|
||||||
|
background: 'transparent',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default NodePanel;
|
||||||
263
frontend/src/pages/Workflow/Definition/Design/index.tsx
Normal file
263
frontend/src/pages/Workflow/Definition/Design/index.tsx
Normal file
@ -0,0 +1,263 @@
|
|||||||
|
import React, { useEffect, useState, useRef } from 'react';
|
||||||
|
import { useParams, useNavigate } from 'react-router-dom';
|
||||||
|
import { Button, Space, Card, Row, Col } from 'antd';
|
||||||
|
import { ArrowLeftOutlined, SaveOutlined, PlayCircleOutlined } from '@ant-design/icons';
|
||||||
|
import { Graph } from '@antv/x6';
|
||||||
|
import { getDefinitionDetail } from '../service';
|
||||||
|
import NodePanel from './components/NodePanel';
|
||||||
|
import { NodeDefinition } from './types';
|
||||||
|
|
||||||
|
const WorkflowDesign: React.FC = () => {
|
||||||
|
const { id } = useParams<{ id: string }>();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const [title, setTitle] = useState<string>('工作流设计');
|
||||||
|
const graphContainerRef = useRef<HTMLDivElement>(null);
|
||||||
|
const [graph, setGraph] = useState<Graph | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (id) {
|
||||||
|
loadDefinitionDetail();
|
||||||
|
}
|
||||||
|
}, [id]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (graphContainerRef.current) {
|
||||||
|
const graph = new Graph({
|
||||||
|
container: graphContainerRef.current,
|
||||||
|
grid: {
|
||||||
|
size: 10,
|
||||||
|
visible: true,
|
||||||
|
type: 'dot',
|
||||||
|
args: {
|
||||||
|
color: '#a0a0a0',
|
||||||
|
thickness: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
connecting: {
|
||||||
|
router: 'manhattan',
|
||||||
|
connector: {
|
||||||
|
name: 'rounded',
|
||||||
|
args: {
|
||||||
|
radius: 8,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
anchor: 'center',
|
||||||
|
connectionPoint: 'anchor',
|
||||||
|
allowBlank: false,
|
||||||
|
snap: true,
|
||||||
|
createEdge() {
|
||||||
|
return this.createEdge({
|
||||||
|
attrs: {
|
||||||
|
line: {
|
||||||
|
stroke: '#1890ff',
|
||||||
|
strokeWidth: 2,
|
||||||
|
targetMarker: {
|
||||||
|
name: 'classic',
|
||||||
|
size: 8,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
highlighting: {
|
||||||
|
magnetAvailable: {
|
||||||
|
name: 'stroke',
|
||||||
|
args: {
|
||||||
|
padding: 4,
|
||||||
|
attrs: {
|
||||||
|
strokeWidth: 4,
|
||||||
|
stroke: '#52c41a',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
snapline: true,
|
||||||
|
history: true,
|
||||||
|
clipboard: true,
|
||||||
|
selecting: true,
|
||||||
|
keyboard: true,
|
||||||
|
background: {
|
||||||
|
color: '#f5f5f5',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
setGraph(graph);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
graph.dispose();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}, [graphContainerRef]);
|
||||||
|
|
||||||
|
const loadDefinitionDetail = async () => {
|
||||||
|
try {
|
||||||
|
const response = await getDefinitionDetail(id);
|
||||||
|
if (response.success) {
|
||||||
|
setTitle(`工作流设计 - ${response.data.name}`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to load workflow definition:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleNodeDragStart = (node: NodeDefinition, e: React.DragEvent) => {
|
||||||
|
if (graph) {
|
||||||
|
const { clientX, clientY } = e;
|
||||||
|
const point = graph.clientToLocal({ x: clientX, y: clientY });
|
||||||
|
|
||||||
|
const nodeConfig = {
|
||||||
|
shape: node.graphConfig.uiSchema.shape,
|
||||||
|
width: node.graphConfig.uiSchema.size.width,
|
||||||
|
height: node.graphConfig.uiSchema.size.height,
|
||||||
|
attrs: {
|
||||||
|
body: {
|
||||||
|
fill: node.graphConfig.uiSchema.style.fill,
|
||||||
|
stroke: node.graphConfig.uiSchema.style.stroke,
|
||||||
|
strokeWidth: node.graphConfig.uiSchema.style.strokeWidth,
|
||||||
|
},
|
||||||
|
label: {
|
||||||
|
text: node.name,
|
||||||
|
fill: '#000000',
|
||||||
|
fontSize: 12,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
ports: {
|
||||||
|
groups: {
|
||||||
|
top: {
|
||||||
|
position: 'top',
|
||||||
|
attrs: {
|
||||||
|
circle: {
|
||||||
|
r: 4,
|
||||||
|
magnet: true,
|
||||||
|
stroke: '#5F95FF',
|
||||||
|
strokeWidth: 1,
|
||||||
|
fill: '#fff',
|
||||||
|
style: {
|
||||||
|
visibility: 'hidden',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
right: {
|
||||||
|
position: 'right',
|
||||||
|
attrs: {
|
||||||
|
circle: {
|
||||||
|
r: 4,
|
||||||
|
magnet: true,
|
||||||
|
stroke: '#5F95FF',
|
||||||
|
strokeWidth: 1,
|
||||||
|
fill: '#fff',
|
||||||
|
style: {
|
||||||
|
visibility: 'hidden',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
bottom: {
|
||||||
|
position: 'bottom',
|
||||||
|
attrs: {
|
||||||
|
circle: {
|
||||||
|
r: 4,
|
||||||
|
magnet: true,
|
||||||
|
stroke: '#5F95FF',
|
||||||
|
strokeWidth: 1,
|
||||||
|
fill: '#fff',
|
||||||
|
style: {
|
||||||
|
visibility: 'hidden',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
left: {
|
||||||
|
position: 'left',
|
||||||
|
attrs: {
|
||||||
|
circle: {
|
||||||
|
r: 4,
|
||||||
|
magnet: true,
|
||||||
|
stroke: '#5F95FF',
|
||||||
|
strokeWidth: 1,
|
||||||
|
fill: '#fff',
|
||||||
|
style: {
|
||||||
|
visibility: 'hidden',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
items: [
|
||||||
|
{ group: 'top' },
|
||||||
|
{ group: 'right' },
|
||||||
|
{ group: 'bottom' },
|
||||||
|
{ group: 'left' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
graph.addNode({
|
||||||
|
...nodeConfig,
|
||||||
|
x: point.x,
|
||||||
|
y: point.y,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ padding: '24px' }}>
|
||||||
|
<Card
|
||||||
|
title={title}
|
||||||
|
extra={
|
||||||
|
<Space>
|
||||||
|
<Button
|
||||||
|
icon={<SaveOutlined />}
|
||||||
|
type="primary"
|
||||||
|
onClick={() => console.log('Save workflow')}
|
||||||
|
>
|
||||||
|
保存
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
icon={<PlayCircleOutlined />}
|
||||||
|
onClick={() => console.log('Deploy workflow')}
|
||||||
|
>
|
||||||
|
发布
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
icon={<ArrowLeftOutlined />}
|
||||||
|
onClick={() => navigate('/workflow/definition')}
|
||||||
|
>
|
||||||
|
返回
|
||||||
|
</Button>
|
||||||
|
</Space>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Row gutter={16}>
|
||||||
|
<Col span={6}>
|
||||||
|
<NodePanel onNodeDragStart={handleNodeDragStart} />
|
||||||
|
</Col>
|
||||||
|
<Col span={18}>
|
||||||
|
<Card
|
||||||
|
size="small"
|
||||||
|
bodyStyle={{
|
||||||
|
padding: 0,
|
||||||
|
height: 'calc(100vh - 250px)',
|
||||||
|
background: '#f5f5f5',
|
||||||
|
border: '1px solid #d9d9d9',
|
||||||
|
borderRadius: '4px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
ref={graphContainerRef}
|
||||||
|
style={{
|
||||||
|
width: '100%',
|
||||||
|
height: '100%',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default WorkflowDesign;
|
||||||
11
frontend/src/pages/Workflow/Definition/Design/service.ts
Normal file
11
frontend/src/pages/Workflow/Definition/Design/service.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
// 该文件已不再需要,可以删除
|
||||||
|
|
||||||
|
import request from '@/utils/request';
|
||||||
|
|
||||||
|
const NODE_DEFINITION_URL = '/api/v1/workflow/node-definition';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取节点定义列表
|
||||||
|
*/
|
||||||
|
export const getNodeDefinitionList = () =>
|
||||||
|
request.get(`${NODE_DEFINITION_URL}/list`);
|
||||||
77
frontend/src/pages/Workflow/Definition/Design/types.ts
Normal file
77
frontend/src/pages/Workflow/Definition/Design/types.ts
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
// 节点类型
|
||||||
|
export type NodeType = 'START_EVENT' | 'END_EVENT' | 'USER_TASK' | 'SERVICE_TASK' | 'SCRIPT_TASK' | 'EXCLUSIVE_GATEWAY' | 'PARALLEL_GATEWAY' | 'SUB_PROCESS' | 'CALL_ACTIVITY';
|
||||||
|
|
||||||
|
// 节点分类
|
||||||
|
export type NodeCategory = 'EVENT' | 'TASK' | 'GATEWAY' | 'CONTAINER';
|
||||||
|
|
||||||
|
// 节点定义
|
||||||
|
export interface NodeDefinition {
|
||||||
|
id: number;
|
||||||
|
type: NodeType;
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
category: NodeCategory;
|
||||||
|
graphConfig: {
|
||||||
|
code: string;
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
details: {
|
||||||
|
description: string;
|
||||||
|
features: string[];
|
||||||
|
scenarios: string[];
|
||||||
|
};
|
||||||
|
configSchema: {
|
||||||
|
type: string;
|
||||||
|
properties: Record<string, any>;
|
||||||
|
required: string[];
|
||||||
|
};
|
||||||
|
uiSchema: {
|
||||||
|
shape: 'circle' | 'rectangle' | 'diamond';
|
||||||
|
size: {
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
};
|
||||||
|
style: {
|
||||||
|
fill: string;
|
||||||
|
stroke: string;
|
||||||
|
strokeWidth: number;
|
||||||
|
icon: string;
|
||||||
|
iconColor: string;
|
||||||
|
};
|
||||||
|
ports: {
|
||||||
|
groups: {
|
||||||
|
in?: {
|
||||||
|
position: string;
|
||||||
|
attrs: {
|
||||||
|
circle: {
|
||||||
|
r: number;
|
||||||
|
fill: string;
|
||||||
|
stroke: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
out?: {
|
||||||
|
position: string;
|
||||||
|
attrs: {
|
||||||
|
circle: {
|
||||||
|
r: number;
|
||||||
|
fill: string;
|
||||||
|
stroke: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
orderNum: number;
|
||||||
|
enabled: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 节点列表响应
|
||||||
|
export interface NodeDefinitionResponse {
|
||||||
|
success: boolean;
|
||||||
|
message: string;
|
||||||
|
data: NodeDefinition[];
|
||||||
|
code: number;
|
||||||
|
}
|
||||||
@ -1,100 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import { Table, Card, Button, Space, Tag, message } from 'antd';
|
|
||||||
import { PlusOutlined } from '@ant-design/icons';
|
|
||||||
import { useRequest } from 'ahooks';
|
|
||||||
import { history } from 'umi';
|
|
||||||
import { queryWorkflowDefinitions } from './service';
|
|
||||||
|
|
||||||
const WorkflowDefinitionList: React.FC = () => {
|
|
||||||
const { data, loading, refresh } = useRequest(queryWorkflowDefinitions, {
|
|
||||||
defaultParams: [{ current: 1, pageSize: 10 }],
|
|
||||||
});
|
|
||||||
|
|
||||||
const columns = [
|
|
||||||
{
|
|
||||||
title: '流程名称',
|
|
||||||
dataIndex: 'name',
|
|
||||||
key: 'name',
|
|
||||||
render: (text: string, record: any) => (
|
|
||||||
<a onClick={() => history.push(`/workflow/definition/detail/${record.id}`)}>{text}</a>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '流程标识',
|
|
||||||
dataIndex: 'key',
|
|
||||||
key: 'key',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '版本',
|
|
||||||
dataIndex: 'flowVersion',
|
|
||||||
key: 'flowVersion',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '状态',
|
|
||||||
dataIndex: 'status',
|
|
||||||
key: 'status',
|
|
||||||
render: (status: string) => (
|
|
||||||
<Tag color={status === 'DRAFT' ? 'orange' : 'green'}>
|
|
||||||
{status === 'DRAFT' ? '草稿' : '已发布'}
|
|
||||||
</Tag>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '描述',
|
|
||||||
dataIndex: 'description',
|
|
||||||
key: 'description',
|
|
||||||
ellipsis: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '操作',
|
|
||||||
key: 'action',
|
|
||||||
render: (_: any, record: any) => (
|
|
||||||
<Space size="middle">
|
|
||||||
<a onClick={() => history.push(`/workflow/definition/edit/${record.id}`)}>编辑</a>
|
|
||||||
<a onClick={() => handleDeploy(record.id)}>发布</a>
|
|
||||||
<a onClick={() => handleDelete(record.id)}>删除</a>
|
|
||||||
</Space>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const handleDeploy = async (id: number) => {
|
|
||||||
message.success('发布成功');
|
|
||||||
refresh();
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDelete = async (id: number) => {
|
|
||||||
message.success('删除成功');
|
|
||||||
refresh();
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Card
|
|
||||||
title="流程定义列表"
|
|
||||||
extra={
|
|
||||||
<Button
|
|
||||||
type="primary"
|
|
||||||
icon={<PlusOutlined />}
|
|
||||||
onClick={() => history.push('/workflow/definition/create')}
|
|
||||||
>
|
|
||||||
新建流程
|
|
||||||
</Button>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Table
|
|
||||||
columns={columns}
|
|
||||||
dataSource={data?.content}
|
|
||||||
loading={loading}
|
|
||||||
rowKey="id"
|
|
||||||
pagination={{
|
|
||||||
total: data?.totalElements,
|
|
||||||
pageSize: 10,
|
|
||||||
showSizeChanger: true,
|
|
||||||
showQuickJumper: true,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default WorkflowDefinitionList;
|
|
||||||
90
frontend/src/pages/Workflow/Definition/components/edit.tsx
Normal file
90
frontend/src/pages/Workflow/Definition/components/edit.tsx
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
import React, {useEffect} from 'react';
|
||||||
|
import {Modal, Form, Input, message} from 'antd';
|
||||||
|
import type {WorkflowDefinition} from '../types';
|
||||||
|
import {saveDefinition, updateDefinition} from '../service';
|
||||||
|
|
||||||
|
interface EditModalProps {
|
||||||
|
visible: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
onSuccess?: () => void;
|
||||||
|
record?: WorkflowDefinition;
|
||||||
|
}
|
||||||
|
|
||||||
|
const EditModal: React.FC<EditModalProps> = ({visible, onClose, onSuccess, record}) => {
|
||||||
|
const [form] = Form.useForm();
|
||||||
|
const isEdit = !!record;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (visible && record) {
|
||||||
|
form.setFieldsValue(record);
|
||||||
|
}
|
||||||
|
}, [visible, record]);
|
||||||
|
|
||||||
|
const handleOk = async () => {
|
||||||
|
try {
|
||||||
|
const values = await form.validateFields();
|
||||||
|
if (isEdit) {
|
||||||
|
await updateDefinition(record.id, {
|
||||||
|
...record,
|
||||||
|
...values,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
await saveDefinition({
|
||||||
|
...values,
|
||||||
|
flowVersion: 1,
|
||||||
|
status: 'DRAFT'
|
||||||
|
} as WorkflowDefinition);
|
||||||
|
}
|
||||||
|
message.success(isEdit ? '更新成功' : '保存成功');
|
||||||
|
onSuccess?.();
|
||||||
|
onClose();
|
||||||
|
form.resetFields();
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof Error) {
|
||||||
|
message.error(error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
title={isEdit ? '编辑流程' : '新建流程'}
|
||||||
|
open={visible}
|
||||||
|
onOk={handleOk}
|
||||||
|
onCancel={() => {
|
||||||
|
onClose();
|
||||||
|
form.resetFields();
|
||||||
|
}}
|
||||||
|
destroyOnClose
|
||||||
|
>
|
||||||
|
<Form
|
||||||
|
form={form}
|
||||||
|
layout="vertical"
|
||||||
|
preserve={false}
|
||||||
|
>
|
||||||
|
<Form.Item
|
||||||
|
name="name"
|
||||||
|
label="流程名称"
|
||||||
|
rules={[{required: true, message: '请输入流程名称'}]}
|
||||||
|
>
|
||||||
|
<Input placeholder="请输入流程名称"/>
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
name="key"
|
||||||
|
label="流程标识"
|
||||||
|
rules={[{required: true, message: '请输入流程标识'}]}
|
||||||
|
>
|
||||||
|
<Input placeholder="请输入流程标识" disabled={isEdit}/>
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
name="description"
|
||||||
|
label="描述"
|
||||||
|
>
|
||||||
|
<Input.TextArea placeholder="请输入流程描述"/>
|
||||||
|
</Form.Item>
|
||||||
|
</Form>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default EditModal;
|
||||||
196
frontend/src/pages/Workflow/Definition/index.tsx
Normal file
196
frontend/src/pages/Workflow/Definition/index.tsx
Normal file
@ -0,0 +1,196 @@
|
|||||||
|
import React, {useState, useEffect} from 'react';
|
||||||
|
import {Table, Card, Button, Space, Tag, message, Modal} from 'antd';
|
||||||
|
import {PlusOutlined, ExclamationCircleOutlined} from '@ant-design/icons';
|
||||||
|
import {useNavigate} from 'react-router-dom';
|
||||||
|
import * as service from './service';
|
||||||
|
import type {WorkflowDefinition, WorkflowDefinitionQuery} from './types';
|
||||||
|
import {DEFAULT_PAGE_SIZE, DEFAULT_CURRENT} from '@/utils/page';
|
||||||
|
import EditModal from './components/Edit';
|
||||||
|
|
||||||
|
const {confirm} = Modal;
|
||||||
|
|
||||||
|
const WorkflowDefinitionList: React.FC = () => {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [pageData, setPageData] = useState<{
|
||||||
|
content: WorkflowDefinition[];
|
||||||
|
totalElements: number;
|
||||||
|
size: number;
|
||||||
|
number: number;
|
||||||
|
} | null>(null);
|
||||||
|
const [modalVisible, setModalVisible] = useState(false);
|
||||||
|
const [currentRecord, setCurrentRecord] = useState<WorkflowDefinition>();
|
||||||
|
const [query, setQuery] = useState<WorkflowDefinitionQuery>({
|
||||||
|
pageNum: DEFAULT_CURRENT - 1,
|
||||||
|
pageSize: DEFAULT_PAGE_SIZE
|
||||||
|
});
|
||||||
|
|
||||||
|
const loadData = async (params: WorkflowDefinitionQuery) => {
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
const data = await service.getDefinitions(params);
|
||||||
|
setPageData(data);
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof Error) {
|
||||||
|
message.error(error.message);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
loadData(query);
|
||||||
|
}, [query]);
|
||||||
|
|
||||||
|
const handleCreateFlow = () => {
|
||||||
|
setCurrentRecord(undefined);
|
||||||
|
setModalVisible(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleEditFlow = (record: WorkflowDefinition) => {
|
||||||
|
setCurrentRecord(record);
|
||||||
|
setModalVisible(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDesignFlow = (record: WorkflowDefinition) => {
|
||||||
|
navigate(`/workflow/definition/${record.id}/design`);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleModalClose = () => {
|
||||||
|
setModalVisible(false);
|
||||||
|
setCurrentRecord(undefined);
|
||||||
|
};
|
||||||
|
|
||||||
|
const columns = [
|
||||||
|
{
|
||||||
|
title: '流程名称',
|
||||||
|
dataIndex: 'name',
|
||||||
|
key: 'name',
|
||||||
|
render: (text: string, record: WorkflowDefinition) => (
|
||||||
|
<a onClick={() => navigate(`/workflow-definitions/${record.id}`)}>{text}</a>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '流程标识',
|
||||||
|
dataIndex: 'key',
|
||||||
|
key: 'key',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '版本',
|
||||||
|
dataIndex: 'flowVersion',
|
||||||
|
key: 'flowVersion',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '状态',
|
||||||
|
dataIndex: 'status',
|
||||||
|
key: 'status',
|
||||||
|
render: (status: string) => (
|
||||||
|
<Tag color={status === 'DRAFT' ? 'orange' : 'green'}>
|
||||||
|
{status === 'DRAFT' ? '草稿' : '已发布'}
|
||||||
|
</Tag>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '描述',
|
||||||
|
dataIndex: 'description',
|
||||||
|
key: 'description',
|
||||||
|
ellipsis: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '操作',
|
||||||
|
key: 'action',
|
||||||
|
render: (_: any, record: WorkflowDefinition) => (
|
||||||
|
<Space size="middle">
|
||||||
|
{record.status === 'DRAFT' && (
|
||||||
|
<>
|
||||||
|
<a onClick={() => handleEditFlow(record)}>编辑</a>
|
||||||
|
<a onClick={() => handleDesignFlow(record)}>设计</a>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{record.status === 'DRAFT' && (
|
||||||
|
<a onClick={() => handleDeploy(record.id)}>发布</a>
|
||||||
|
)}
|
||||||
|
<a onClick={() => handleDelete(record.id)}>删除</a>
|
||||||
|
</Space>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const handleDeploy = async (id: number) => {
|
||||||
|
confirm({
|
||||||
|
title: '确认发布',
|
||||||
|
icon: <ExclamationCircleOutlined/>,
|
||||||
|
content: '确定要发布该流程定义吗?发布后将不能修改。',
|
||||||
|
onOk: async () => {
|
||||||
|
try {
|
||||||
|
await service.deployDefinition(id);
|
||||||
|
message.success('发布成功');
|
||||||
|
loadData(query);
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof Error) {
|
||||||
|
message.error(error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDelete = async (id: number) => {
|
||||||
|
confirm({
|
||||||
|
title: '确认删除',
|
||||||
|
icon: <ExclamationCircleOutlined/>,
|
||||||
|
content: '确定要删除该流程定义吗?删除后不可恢复。',
|
||||||
|
onOk: async () => {
|
||||||
|
try {
|
||||||
|
await service.deleteDefinition(id);
|
||||||
|
message.success('删除成功');
|
||||||
|
loadData(query);
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof Error) {
|
||||||
|
message.error(error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card
|
||||||
|
title={
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
icon={<PlusOutlined/>}
|
||||||
|
onClick={handleCreateFlow}
|
||||||
|
>
|
||||||
|
新建流程
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Table
|
||||||
|
columns={columns}
|
||||||
|
dataSource={pageData?.content}
|
||||||
|
loading={loading}
|
||||||
|
rowKey="id"
|
||||||
|
pagination={{
|
||||||
|
total: pageData?.totalElements,
|
||||||
|
pageSize: pageData?.size || DEFAULT_PAGE_SIZE,
|
||||||
|
current: (pageData?.number || 0) + 1,
|
||||||
|
showSizeChanger: true,
|
||||||
|
showQuickJumper: true,
|
||||||
|
onChange: (page, pageSize) => {
|
||||||
|
setQuery({...query, pageNum: page - 1, pageSize});
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<EditModal
|
||||||
|
visible={modalVisible}
|
||||||
|
onClose={handleModalClose}
|
||||||
|
onSuccess={() => loadData(query)}
|
||||||
|
record={currentRecord}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default WorkflowDefinitionList;
|
||||||
23
frontend/src/pages/Workflow/Definition/service.ts
Normal file
23
frontend/src/pages/Workflow/Definition/service.ts
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import request from '@/utils/request';
|
||||||
|
import {WorkflowDefinition, WorkflowDefinitionQuery} from './types';
|
||||||
|
import {Page} from '@/types/base';
|
||||||
|
|
||||||
|
const DEFINITION_URL = '/api/v1/workflow/definition';
|
||||||
|
|
||||||
|
export const getDefinitions = (params?: WorkflowDefinitionQuery) =>
|
||||||
|
request.get<Page<WorkflowDefinition>>(`${DEFINITION_URL}/page`, {params});
|
||||||
|
|
||||||
|
export const getDefinitionDetail = (id: number) =>
|
||||||
|
request.get<WorkflowDefinition>(`${DEFINITION_URL}/${id}`);
|
||||||
|
|
||||||
|
export const deployDefinition = (id: number) =>
|
||||||
|
request.post<void>(`${DEFINITION_URL}/${id}/deploy`);
|
||||||
|
|
||||||
|
export const deleteDefinition = (id: number) =>
|
||||||
|
request.delete<void>(`${DEFINITION_URL}/${id}`);
|
||||||
|
|
||||||
|
export const saveDefinition = (data: WorkflowDefinition) =>
|
||||||
|
request.post<WorkflowDefinition>(`${DEFINITION_URL}`, data);
|
||||||
|
|
||||||
|
export const updateDefinition = (id: number, data: WorkflowDefinition) =>
|
||||||
|
request.put<WorkflowDefinition>(`${DEFINITION_URL}/${id}`, data);
|
||||||
22
frontend/src/pages/Workflow/Definition/types.ts
Normal file
22
frontend/src/pages/Workflow/Definition/types.ts
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import { BaseResponse, BaseQuery } from '@/types/base';
|
||||||
|
|
||||||
|
export interface WorkflowDefinition extends BaseResponse {
|
||||||
|
name: string;
|
||||||
|
key: string;
|
||||||
|
flowVersion: number;
|
||||||
|
status: string;
|
||||||
|
description: string;
|
||||||
|
graph: {
|
||||||
|
nodes: any[];
|
||||||
|
edges: any[];
|
||||||
|
};
|
||||||
|
formConfig: {
|
||||||
|
formItems: any[];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WorkflowDefinitionQuery extends BaseQuery {
|
||||||
|
name?: string;
|
||||||
|
key?: string;
|
||||||
|
status?: string;
|
||||||
|
}
|
||||||
@ -32,10 +32,10 @@ const Menu = lazy(() => import('../pages/System/Menu'));
|
|||||||
const Department = lazy(() => import('../pages/System/Department'));
|
const Department = lazy(() => import('../pages/System/Department'));
|
||||||
const External = lazy(() => import('../pages/System/External'));
|
const External = lazy(() => import('../pages/System/External'));
|
||||||
const X6Test = lazy(() => import('../pages/X6Test'));
|
const X6Test = lazy(() => import('../pages/X6Test'));
|
||||||
const WorkflowDefinition = lazy(() => import('../pages/Workflow/Definition'));
|
const WorkflowDefinitionList = lazy(() => import('../pages/Workflow/Definition'));
|
||||||
|
const WorkflowDesign = lazy(() => import('../pages/Workflow/Definition/Design'));
|
||||||
const WorkflowInstance = lazy(() => import('../pages/Workflow/Instance'));
|
const WorkflowInstance = lazy(() => import('../pages/Workflow/Instance'));
|
||||||
const WorkflowMonitor = lazy(() => import('../pages/Workflow/Monitor'));
|
const WorkflowMonitor = lazy(() => import('../pages/Workflow/Monitor'));
|
||||||
const FlowDesigner = lazy(() => import('../pages/Workflow/Definition/Designer'));
|
|
||||||
const LogStreamPage = lazy(() => import('../pages/LogStream'));
|
const LogStreamPage = lazy(() => import('../pages/LogStream'));
|
||||||
|
|
||||||
// 创建路由
|
// 创建路由
|
||||||
@ -127,15 +127,15 @@ const router = createBrowserRouter([
|
|||||||
path: '',
|
path: '',
|
||||||
element: (
|
element: (
|
||||||
<Suspense fallback={<LoadingComponent/>}>
|
<Suspense fallback={<LoadingComponent/>}>
|
||||||
<WorkflowDefinition/>
|
<WorkflowDefinitionList/>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'designer/:id?',
|
path: ':id/design',
|
||||||
element: (
|
element: (
|
||||||
<Suspense fallback={<LoadingComponent/>}>
|
<Suspense fallback={<LoadingComponent/>}>
|
||||||
<FlowDesigner/>
|
<WorkflowDesign/>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -26,6 +26,12 @@ export interface BaseRequest {
|
|||||||
enabled?: boolean;
|
enabled?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface PageParams {
|
||||||
|
pageNum: number; // 页码(从1开始)
|
||||||
|
pageSize: number; // 每页大小
|
||||||
|
sortField?: string; // 排序字段
|
||||||
|
sortOrder?: string; // 排序方向
|
||||||
|
}
|
||||||
|
|
||||||
// 分页响应数据
|
// 分页响应数据
|
||||||
export interface Page<T> {
|
export interface Page<T> {
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
import type {Page, PageParams} from '@/types/base/page';
|
import type {Page, PageParams} from '@/types/base';
|
||||||
|
|
||||||
// 默认分页参数
|
// 默认分页参数
|
||||||
const DEFAULT_PAGE_SIZE = 10;
|
export const DEFAULT_PAGE_SIZE = 10;
|
||||||
const DEFAULT_CURRENT = 1;
|
export const DEFAULT_CURRENT = 1;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 转换前端分页参数为后端分页参数
|
* 转换前端分页参数为后端分页参数
|
||||||
@ -12,7 +12,7 @@ export const convertToPageParams = (params?: {
|
|||||||
pageSize?: number;
|
pageSize?: number;
|
||||||
sortField?: string;
|
sortField?: string;
|
||||||
sortOrder?: string;
|
sortOrder?: string;
|
||||||
}): PageParams => ({
|
}): { sortOrder: any; sortField: string | undefined; pageSize: number; pageNum: number } => ({
|
||||||
pageNum: Math.max(1, params?.current || DEFAULT_CURRENT) - 1, // 转换为从0开始的页码
|
pageNum: Math.max(1, params?.current || DEFAULT_CURRENT) - 1, // 转换为从0开始的页码
|
||||||
pageSize: params?.pageSize || DEFAULT_PAGE_SIZE,
|
pageSize: params?.pageSize || DEFAULT_PAGE_SIZE,
|
||||||
sortField: params?.sortField,
|
sortField: params?.sortField,
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user