增加工具栏提示。
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 External = lazy(() => import('../pages/System/External'));
|
||||
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 WorkflowMonitor = lazy(() => import('../pages/Workflow/Monitor'));
|
||||
const FlowDesigner = lazy(() => import('../pages/Workflow/Definition/Designer'));
|
||||
const LogStreamPage = lazy(() => import('../pages/LogStream'));
|
||||
|
||||
// 创建路由
|
||||
@ -127,15 +127,15 @@ const router = createBrowserRouter([
|
||||
path: '',
|
||||
element: (
|
||||
<Suspense fallback={<LoadingComponent/>}>
|
||||
<WorkflowDefinition/>
|
||||
<WorkflowDefinitionList/>
|
||||
</Suspense>
|
||||
)
|
||||
},
|
||||
{
|
||||
path: 'designer/:id?',
|
||||
path: ':id/design',
|
||||
element: (
|
||||
<Suspense fallback={<LoadingComponent/>}>
|
||||
<FlowDesigner/>
|
||||
<WorkflowDesign/>
|
||||
</Suspense>
|
||||
)
|
||||
}
|
||||
|
||||
@ -26,6 +26,12 @@ export interface BaseRequest {
|
||||
enabled?: boolean;
|
||||
}
|
||||
|
||||
export interface PageParams {
|
||||
pageNum: number; // 页码(从1开始)
|
||||
pageSize: number; // 每页大小
|
||||
sortField?: string; // 排序字段
|
||||
sortOrder?: string; // 排序方向
|
||||
}
|
||||
|
||||
// 分页响应数据
|
||||
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;
|
||||
const DEFAULT_CURRENT = 1;
|
||||
export const DEFAULT_PAGE_SIZE = 10;
|
||||
export const DEFAULT_CURRENT = 1;
|
||||
|
||||
/**
|
||||
* 转换前端分页参数为后端分页参数
|
||||
@ -12,7 +12,7 @@ export const convertToPageParams = (params?: {
|
||||
pageSize?: number;
|
||||
sortField?: string;
|
||||
sortOrder?: string;
|
||||
}): PageParams => ({
|
||||
}): { sortOrder: any; sortField: string | undefined; pageSize: number; pageNum: number } => ({
|
||||
pageNum: Math.max(1, params?.current || DEFAULT_CURRENT) - 1, // 转换为从0开始的页码
|
||||
pageSize: params?.pageSize || DEFAULT_PAGE_SIZE,
|
||||
sortField: params?.sortField,
|
||||
@ -26,4 +26,4 @@ export const convertToPageInfo = (page?: Page<any>) => ({
|
||||
current: (page?.number || 0) + 1,
|
||||
pageSize: page?.size || DEFAULT_PAGE_SIZE,
|
||||
total: page?.totalElements || 0
|
||||
});
|
||||
});
|
||||
Loading…
Reference in New Issue
Block a user