节点面板

This commit is contained in:
戚辰先生 2024-12-05 20:40:04 +08:00
parent 80ddeb0ecb
commit db1fb56ced
16 changed files with 1400 additions and 56 deletions

View File

@ -3989,7 +3989,6 @@
"resolved": "https://registry.npmmirror.com/less/-/less-4.2.1.tgz",
"integrity": "sha512-CasaJidTIhWmjcqv0Uj5vccMI7pJgfD9lMkKtlnTHAdJdYK/7l8pM9tumLyJ0zhbD4KJLo/YvTj+xznQd5NBhg==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"copy-anything": "^2.0.1",
"parse-node-version": "^1.0.1",

View File

@ -0,0 +1,125 @@
.nodeTabs {
height: 100%;
:global {
.ant-tabs {
height: 100%;
.ant-tabs-nav {
margin-bottom: 12px;
padding: 0 12px;
background: #fafafa;
border-bottom: 1px solid #f0f0f0;
.ant-tabs-tab {
padding: 12px 0;
font-size: 14px;
transition: all 0.3s;
&:hover {
color: #1890ff;
}
.anticon {
margin-right: 8px;
}
}
.ant-tabs-tab-active {
.ant-tabs-tab-btn {
color: #1890ff;
font-weight: 500;
}
}
.ant-tabs-ink-bar {
background: #1890ff;
}
}
}
.ant-tabs-content {
height: calc(100% - 46px);
padding: 0 12px 12px;
overflow-y: auto;
&::-webkit-scrollbar {
width: 6px;
height: 6px;
}
&::-webkit-scrollbar-thumb {
background: #ccc;
border-radius: 3px;
&:hover {
background: #999;
}
}
&::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 3px;
}
}
.ant-empty {
margin: 32px 0;
color: #999;
}
}
}
.nodeCard {
margin-bottom: 16px;
cursor: move;
border-radius: 4px;
transition: all 0.3s;
background: #fff;
border: 1px solid #f0f0f0;
padding: 12px;
&:hover {
border-color: #1890ff;
box-shadow: 0 2px 8px rgba(24, 144, 255, 0.15);
transform: translateY(-1px);
}
.nodeIcon {
width: 40px;
height: 40px;
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
margin: 0 auto 8px;
color: #fff;
font-size: 20px;
transition: all 0.3s;
.anticon {
transition: all 0.3s;
}
}
&:hover .nodeIcon {
transform: scale(1.1);
.anticon {
transform: scale(1.1);
}
}
.nodeName {
font-size: 14px;
color: #333;
text-align: center;
line-height: 1.5;
margin: 0;
font-weight: 500;
}
&:active {
transform: scale(0.98);
}
}

View File

@ -0,0 +1,13 @@
declare namespace NodePanelModuleLessNamespace {
export interface INodePanelModuleLess {
nodeTabs: string;
nodeCard: string;
nodeIcon: string;
nodeName: string;
}
}
declare module '*.less' {
const content: NodePanelModuleLessNamespace.INodePanelModuleLess;
export default content;
}

View File

@ -0,0 +1,121 @@
import React, {useEffect, useState} from 'react';
import {Card, Empty, Spin, Tabs, Tooltip} from 'antd';
import {NodeCategory, NodeType} from '../../types';
import {getNodeTypes} from '../../service';
import * as Icons from '@ant-design/icons';
import styles from './NodePanel.module.less';
interface NodePanelProps {
onNodeDragStart: (nodeType: NodeType) => void;
}
const NodePanel: React.FC<NodePanelProps> = ({onNodeDragStart}) => {
const [loading, setLoading] = useState(false);
const [nodeTypes, setNodeTypes] = useState<NodeType[]>([]);
// 获取节点类型列表
const fetchNodeTypes = async () => {
setLoading(true);
try {
const response = await getNodeTypes({enabled: true});
if (response) {
setNodeTypes(response);
}
} finally {
setLoading(false);
}
};
useEffect(() => {
fetchNodeTypes();
}, []);
// 按分类分组节点类型
const groupedNodeTypes = nodeTypes.reduce((acc, nodeType) => {
const category = nodeType.category;
if (!acc[category]) {
acc[category] = [];
}
acc[category].push(nodeType);
return acc;
}, {} as Record<NodeCategory, NodeType[]>);
// 渲染图标
const renderIcon = (iconName: string) => {
// @ts-ignore
const Icon = Icons[iconName];
return Icon ? <Icon/> : null;
};
// 渲染节点类型卡片
const renderNodeTypeCard = (nodeType: NodeType) => (
<Tooltip
key={nodeType.code}
title={nodeType.description}
placement="right"
mouseEnterDelay={0.5}
>
<Card
className={styles.nodeCard}
draggable
onDragStart={(e) => {
e.dataTransfer.setData('nodeType', JSON.stringify(nodeType));
onNodeDragStart(nodeType);
}}
>
<div className={styles.nodeIcon} style={{backgroundColor: nodeType.color}}>
{renderIcon(nodeType.icon)}
</div>
<div className={styles.nodeName}>{nodeType.name}</div>
</Card>
</Tooltip>
);
const categoryIcons = {
[NodeCategory.TASK]: renderIcon('CodeOutlined'),
[NodeCategory.EVENT]: renderIcon('ThunderboltOutlined'),
[NodeCategory.GATEWAY]: renderIcon('ForkOutlined')
};
const categoryLabels = {
[NodeCategory.TASK]: '任务节点',
[NodeCategory.EVENT]: '事件节点',
[NodeCategory.GATEWAY]: '网关节点'
};
const tabItems = Object.values(NodeCategory).map(category => ({
key: category,
label: (
<span>
{categoryIcons[category]} {categoryLabels[category]}
<span style={{color: '#999', fontSize: 12, marginLeft: 4}}>
({groupedNodeTypes[category]?.length || 0})
</span>
</span>
),
children: groupedNodeTypes[category]?.map(renderNodeTypeCard) || (
<Empty
image={Empty.PRESENTED_IMAGE_SIMPLE}
description={`暂无${categoryLabels[category]}`}
/>
)
}));
if (loading) {
return (
<div style={{textAlign: 'center', padding: '20px 0'}}>
<Spin/>
</div>
);
}
return (
<Tabs
defaultActiveKey={NodeCategory.TASK}
items={tabItems}
className={styles.nodeTabs}
/>
);
};
export default NodePanel;

View File

@ -0,0 +1,30 @@
.container {
height: calc(100vh - 180px);
border: 1px solid #f0f0f0;
border-radius: 2px;
background: #fff;
.sider {
border-right: 1px solid #f0f0f0;
background: #fff;
.nodePanel {
padding: 16px;
height: 100%;
overflow-y: auto;
}
}
.content {
position: relative;
background: #fafafa;
.graph {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
}
}
}

View File

@ -0,0 +1,14 @@
declare namespace IndexModuleLessNamespace {
export interface IIndexModuleLess {
container: string;
sider: string;
nodePanel: string;
content: string;
graph: string;
}
}
declare module '*.less' {
const content: IndexModuleLessNamespace.IIndexModuleLess;
export default content;
}

View File

@ -0,0 +1,235 @@
import React, {useEffect, useRef, useState} from 'react';
import {useNavigate, useParams} from 'react-router-dom';
import {Button, Card, Layout, message, Space, Spin} from 'antd';
import {ArrowLeftOutlined, SaveOutlined} from '@ant-design/icons';
import {getDefinition, updateDefinition} from '../../service';
import {WorkflowDefinition, WorkflowStatus, NodeType} from '../../types';
import {Graph} from '@antv/x6';
import '@antv/x6-react-shape';
import styles from './index.module.less';
import NodePanel from './NodePanel';
const {Sider, Content} = Layout;
const FlowDesigner: React.FC = () => {
const navigate = useNavigate();
const {id} = useParams<{ id: string }>();
const [loading, setLoading] = useState(false);
const [detail, setDetail] = useState<WorkflowDefinition>();
const containerRef = useRef<HTMLDivElement>(null);
const graphRef = useRef<Graph>();
// 初始化图形
const initGraph = () => {
if (!containerRef.current) return;
// 创建画布
const graph = new Graph({
container: containerRef.current,
width: 800,
height: 600,
grid: {
size: 10,
visible: true,
type: 'dot',
args: {
color: '#ccc',
thickness: 1,
},
},
connecting: {
router: 'manhattan',
connector: {
name: 'rounded',
args: {
radius: 8,
},
},
anchor: 'center',
connectionPoint: 'anchor',
allowBlank: false,
snap: {
radius: 20,
},
createEdge() {
return this.createEdge({
attrs: {
line: {
stroke: '#5F95FF',
strokeWidth: 1,
targetMarker: {
name: 'classic',
size: 8,
},
},
},
router: {
name: 'manhattan',
},
connector: {
name: 'rounded',
args: {
radius: 8,
},
},
});
},
},
highlighting: {
magnetAvailable: {
name: 'stroke',
args: {
padding: 4,
attrs: {
strokeWidth: 4,
stroke: '#52c41a',
},
},
},
},
mousewheel: {
enabled: true,
modifiers: ['ctrl', 'meta'],
minScale: 0.5,
maxScale: 2,
},
interacting: {
nodeMovable: true,
edgeMovable: true,
edgeLabelMovable: true,
arrowheadMovable: true,
vertexMovable: true,
vertexAddable: true,
vertexDeletable: true,
},
scroller: {
enabled: true,
pannable: true,
pageVisible: false,
pageBreak: false,
},
history: {
enabled: true,
},
clipboard: {
enabled: true,
},
keyboard: {
enabled: true,
},
selecting: {
enabled: true,
multiple: true,
rubberband: true,
movable: true,
showNodeSelectionBox: true,
},
});
graphRef.current = graph;
// 加载流程图数据
if (detail) {
try {
const graphData = JSON.parse(detail.graphDefinition);
graph.fromJSON(graphData);
} catch (error) {
message.error('加载流程图数据失败');
}
}
};
// 获取详情
const fetchDetail = async () => {
if (!id) return;
setLoading(true);
try {
const response = await getDefinition(parseInt(id));
if (response) {
setDetail(response);
}
} finally {
setLoading(false);
}
};
// 处理保存
const handleSave = async () => {
if (!id || !detail || !graphRef.current || detail.status !== WorkflowStatus.DRAFT) return;
try {
const graphData = graphRef.current.toJSON();
const data = {
...detail,
graphDefinition: JSON.stringify(graphData)
};
await updateDefinition(parseInt(id), data);
message.success('保存成功');
} catch (error) {
// 错误已在请求拦截器中处理
}
};
// 处理返回
const handleBack = () => {
navigate('/workflow/definition');
};
// 首次加载
useEffect(() => {
fetchDetail();
}, [id]);
// 初始化图形
useEffect(() => {
if (detail && containerRef.current) {
initGraph();
}
}, [detail, containerRef.current]);
// 处理节点拖拽开始
const handleNodeDragStart = (nodeType: NodeType) => {
// TODO: 实现节点拖拽创建
console.log('Node drag start:', nodeType);
};
if (loading) {
return (
<div style={{textAlign: 'center', padding: 100}}>
<Spin size="large"/>
</div>
);
}
return (
<Card
title="流程设计器"
extra={
<Space>
<Button
icon={<SaveOutlined/>}
type="primary"
onClick={handleSave}
disabled={detail?.status !== WorkflowStatus.DRAFT}
>
</Button>
<Button icon={<ArrowLeftOutlined/>} onClick={handleBack}>
</Button>
</Space>
}
>
<Layout className={styles.container}>
<Sider width={280} className={styles.sider}>
<NodePanel onNodeDragStart={handleNodeDragStart}/>
</Sider>
<Content className={styles.content}>
<div ref={containerRef} className={styles.graph}/>
</Content>
</Layout>
</Card>
);
};
export default FlowDesigner;

View File

@ -0,0 +1,159 @@
import React, {useEffect, useState} from 'react';
import {useNavigate, useParams} from 'react-router-dom';
import {Button, Card, Form, Input, message, Space, Spin} from 'antd';
import {getDefinition, updateDefinition} from '../../service';
import {UpdateWorkflowDefinitionRequest, WorkflowDefinition, WorkflowStatus} from '../../types';
import {ArrowLeftOutlined} from '@ant-design/icons';
import {WorkflowConfigUtils} from '../../../Workflow/utils';
const WorkflowDefinitionEdit: React.FC = () => {
const navigate = useNavigate();
const {id} = useParams<{ id: string }>();
const [form] = Form.useForm();
const [loading, setLoading] = useState(false);
const [detail, setDetail] = useState<WorkflowDefinition>();
// 获取详情
const fetchDetail = async () => {
if (!id) return;
setLoading(true);
try {
const response = await getDefinition(parseInt(id));
if (response) {
setDetail(response);
// 设置表单初始值
form.setFieldsValue({
code: response.code,
name: response.name,
description: response.description,
});
}
} finally {
setLoading(false);
}
};
// 首次加载
useEffect(() => {
fetchDetail();
}, [id]);
// 处理保存
const handleSave = async (values: any) => {
if (!id || !detail) return;
try {
// 保留原有配置和状态
const data: UpdateWorkflowDefinitionRequest = {
...values,
status: detail.status,
version: detail.version,
enabled: detail.enabled,
nodeConfig: JSON.stringify(WorkflowConfigUtils.parseNodeConfig(detail.nodeConfig)),
transitionConfig: JSON.stringify(WorkflowConfigUtils.parseTransitionConfig(detail.transitionConfig)),
formDefinition: JSON.stringify(WorkflowConfigUtils.parseFormDefinition(detail.formDefinition)),
graphDefinition: JSON.stringify(WorkflowConfigUtils.parseGraphDefinition(detail.graphDefinition))
};
await updateDefinition(parseInt(id), data);
message.success('保存成功');
navigate('/workflow/definition');
} catch (error) {
// 错误已在请求拦截器中处理
}
};
// 处理取消
const handleCancel = () => {
navigate('/workflow/definition');
};
if (loading) {
return (
<div style={{textAlign: 'center', padding: 100}}>
<Spin size="large"/>
</div>
);
}
return (
<Card
title="编辑流程定义"
extra={
<Button icon={<ArrowLeftOutlined/>} onClick={handleCancel}>
</Button>
}
>
<div style={{maxWidth: 800, margin: '0 auto'}}>
{detail?.status !== WorkflowStatus.DRAFT && (
<div style={{marginBottom: 24, padding: '12px 24px', backgroundColor: '#fff2f0', border: '1px solid #ffccc7', borderRadius: 4, color: '#ff4d4f'}}>
稿
</div>
)}
<Form
form={form}
labelCol={{span: 6}}
wrapperCol={{span: 18}}
onFinish={handleSave}
layout="horizontal"
size="large"
>
<Form.Item
name="code"
label="流程编码"
rules={[
{required: true, message: '请输入流程编码'},
{pattern: /^[A-Z_]+$/, message: '编码只能包含大写字母和下划线'},
]}
extra="编码只能包含大写字母和下划线例如DEPLOY_WORKFLOW"
>
<Input
placeholder="请输入流程编码"
disabled={detail?.status !== WorkflowStatus.DRAFT}
allowClear
/>
</Form.Item>
<Form.Item
name="name"
label="流程名称"
rules={[{required: true, message: '请输入流程名称'}]}
extra="给流程起一个简单易懂的名称"
>
<Input
placeholder="请输入流程名称"
disabled={detail?.status !== WorkflowStatus.DRAFT}
allowClear
/>
</Form.Item>
<Form.Item
name="description"
label="流程描述"
extra="详细描述流程的用途、步骤和注意事项"
>
<Input.TextArea
placeholder="请输入流程描述"
disabled={detail?.status !== WorkflowStatus.DRAFT}
allowClear
rows={4}
/>
</Form.Item>
<Form.Item wrapperCol={{offset: 6, span: 18}}>
<Space size="large">
<Button
type="primary"
htmlType="submit"
disabled={detail?.status !== WorkflowStatus.DRAFT}
>
</Button>
<Button onClick={handleCancel}></Button>
</Space>
</Form.Item>
</Form>
</div>
</Card>
);
};
export default WorkflowDefinitionEdit;

View File

@ -0,0 +1,366 @@
import React, {useEffect, useState} from 'react';
import {Button, Card, Form, Input, message, Modal, Select, Space, Table, Tag} from 'antd';
import {useNavigate} from 'react-router-dom';
import {
createDefinition,
deleteDefinition,
disableDefinition,
enableDefinition,
getDefinitions,
publishDefinition
} from '../service';
import {
CreateWorkflowDefinitionRequest,
WorkflowDefinition,
WorkflowStatus,
WorkflowDefinitionBase
} from '../types';
import {DeleteOutlined, EditOutlined, PlusOutlined} from '@ant-design/icons';
const {confirm} = Modal;
const WorkflowDefinitionList: React.FC = () => {
const navigate = useNavigate();
const [form] = Form.useForm();
const [loading, setLoading] = useState(false);
const [list, setList] = useState<WorkflowDefinition[]>([]);
const [total, setTotal] = useState(0);
const [current, setCurrent] = useState(1);
const [size, setSize] = useState(10);
const [createModalVisible, setCreateModalVisible] = useState(false);
// 获取列表数据
const fetchList = async (page = current, pageSize = size) => {
setLoading(true);
try {
const params = {
page: page - 1, // 后端页码从0开始
size: pageSize,
...form.getFieldsValue()
};
const response = await getDefinitions(params);
if (response?.content) {
setList(response.content);
setTotal(response.totalElements);
}
} finally {
setLoading(false);
}
};
// 首次加载
useEffect(() => {
fetchList();
}, []);
// 处理表格变化
const handleTableChange = (pagination: any) => {
setCurrent(pagination.current);
setSize(pagination.pageSize);
fetchList(pagination.current, pagination.pageSize);
};
// 处理搜索
const handleSearch = () => {
setCurrent(1);
fetchList(1);
};
// 处理重置
const handleReset = () => {
form.resetFields();
setCurrent(1);
fetchList(1);
};
// 处理创建
const handleCreate = async (values: WorkflowDefinitionBase) => {
try {
// 设置系统字段的默认值
const data = {
...values,
status: WorkflowStatus.DRAFT,
version: 1,
enabled: true,
nodeConfig: JSON.stringify({
nodes: []
}),
transitionConfig: JSON.stringify({
transitions: []
}),
formDefinition: JSON.stringify({}),
graphDefinition: JSON.stringify({})
};
await createDefinition(data);
message.success('创建成功');
setCreateModalVisible(false);
fetchList();
} catch (error) {
// 错误已在请求拦截器中处理
}
};
// 处理删除
const handleDelete = (record: WorkflowDefinition) => {
confirm({
title: '确认删除',
content: `确定要删除工作流"${record.name}"吗?`,
onOk: async () => {
try {
await deleteDefinition(record.id);
message.success('删除成功');
fetchList();
} catch (error) {
// 错误已在请求拦截器中处理
}
}
});
};
// 处理发布
const handlePublish = async (record: WorkflowDefinition) => {
try {
await publishDefinition(record.id);
message.success('发布成功');
fetchList();
} catch (error) {
// 错误已在请求拦截器中处理
}
};
// 处理启用/禁用
const handleToggleEnable = async (record: WorkflowDefinition) => {
try {
if (record.enabled) {
await disableDefinition(record.id);
message.success('禁用成功');
} else {
await enableDefinition(record.id);
message.success('启用成功');
}
fetchList();
} catch (error) {
// 错误已在请求拦截器中处理
}
};
// 表格列定义
const columns = [
{
title: '编码',
dataIndex: 'code',
key: 'code',
width: 150,
ellipsis: true,
},
{
title: '名称',
dataIndex: 'name',
key: 'name',
width: 150,
ellipsis: true,
},
{
title: '描述',
dataIndex: 'description',
key: 'description',
width: 200,
ellipsis: true,
},
{
title: '版本',
dataIndex: 'version',
key: 'version',
width: 80,
},
{
title: '状态',
dataIndex: 'status',
key: 'status',
width: 100,
render: (status: WorkflowStatus) => {
const statusMap = {
[WorkflowStatus.DRAFT]: {color: 'default', text: '草稿'},
[WorkflowStatus.PUBLISHED]: {color: 'success', text: '已发布'},
[WorkflowStatus.DISABLED]: {color: 'error', text: '已禁用'},
};
const {color, text} = statusMap[status];
return <Tag color={color}>{text}</Tag>;
},
},
{
title: '是否启用',
dataIndex: 'enabled',
key: 'enabled',
width: 100,
render: (enabled: boolean) => (
<Tag color={enabled ? 'success' : 'default'}>{enabled ? '已启用' : '已禁用'}</Tag>
),
},
{
title: '创建时间',
dataIndex: 'createTime',
key: 'createTime',
width: 180,
},
{
title: '操作',
key: 'action',
fixed: 'right' as const,
width: 380,
render: (_: any, record: WorkflowDefinition) => (
<Space>
<Button
type="link"
icon={<EditOutlined/>}
onClick={() => navigate(`/workflow/definition/edit/${record.id}`)}
>
</Button>
<Button
type="link"
onClick={() => navigate(`/workflow/definition/designer/${record.id}`)}
>
</Button>
{record.status === WorkflowStatus.DRAFT && (
<Button type="link" onClick={() => handlePublish(record)}>
</Button>
)}
<Button
type="link"
onClick={() => handleToggleEnable(record)}
>
{record.enabled ? '禁用' : '启用'}
</Button>
<Button
type="link"
danger
icon={<DeleteOutlined/>}
onClick={() => handleDelete(record)}
>
</Button>
</Space>
),
},
];
return (
<Card title="流程定义">
{/* 搜索表单 */}
<Form form={form} layout="inline" style={{marginBottom: 16}}>
<Form.Item name="keyword" label="关键字">
<Input placeholder="请输入编码或名称" allowClear/>
</Form.Item>
<Form.Item name="status" label="状态">
<Select
placeholder="请选择状态"
allowClear
style={{width: 150}}
options={[
{label: '草稿', value: WorkflowStatus.DRAFT},
{label: '已发布', value: WorkflowStatus.PUBLISHED},
{label: '已禁用', value: WorkflowStatus.DISABLED},
]}
/>
</Form.Item>
<Form.Item name="enabled" label="是否启用">
<Select
placeholder="请选择是否启用"
allowClear
style={{width: 150}}
options={[
{label: '已启用', value: true},
{label: '已禁用', value: false},
]}
/>
</Form.Item>
<Form.Item>
<Space>
<Button type="primary" onClick={handleSearch}>
</Button>
<Button onClick={handleReset}></Button>
</Space>
</Form.Item>
</Form>
{/* 工具栏 */}
<div style={{marginBottom: 16}}>
<Button
type="primary"
icon={<PlusOutlined/>}
onClick={() => setCreateModalVisible(true)}
>
</Button>
</div>
{/* 列表 */}
<Table
columns={columns}
dataSource={list}
rowKey="id"
loading={loading}
scroll={{ x: 1500 }}
pagination={{
current,
pageSize: size,
total,
showSizeChanger: true,
showQuickJumper: true,
}}
onChange={handleTableChange}
/>
{/* 创建表单弹窗 */}
<Modal
title="新建流程"
open={createModalVisible}
onCancel={() => setCreateModalVisible(false)}
footer={null}
>
<Form
labelCol={{span: 4}}
wrapperCol={{span: 20}}
onFinish={handleCreate}
>
<Form.Item
name="code"
label="编码"
rules={[
{required: true, message: '请输入流程编码'},
{pattern: /^[A-Z_]+$/, message: '编码只能包含大写字母和下划线'},
]}
>
<Input placeholder="请输入流程编码"/>
</Form.Item>
<Form.Item
name="name"
label="名称"
rules={[{required: true, message: '请输入流程名称'}]}
>
<Input placeholder="请输入流程名称"/>
</Form.Item>
<Form.Item name="description" label="描述">
<Input.TextArea placeholder="请输入流程描述"/>
</Form.Item>
<Form.Item wrapperCol={{offset: 4, span: 20}}>
<Space>
<Button type="primary" htmlType="submit">
</Button>
<Button onClick={() => setCreateModalVisible(false)}>
</Button>
</Space>
</Form.Item>
</Form>
</Modal>
</Card>
);
};
export default WorkflowDefinitionList;

View File

@ -0,0 +1,12 @@
import React from 'react';
import {Card} from 'antd';
const WorkflowInstance: React.FC = () => {
return (
<Card title="流程实例">
<div></div>
</Card>
);
};
export default WorkflowInstance;

View File

@ -0,0 +1,12 @@
import React from 'react';
import {Card} from 'antd';
const WorkflowMonitor: React.FC = () => {
return (
<Card title="流程监控">
<div></div>
</Card>
);
};
export default WorkflowMonitor;

View File

@ -0,0 +1,56 @@
import request from '../../utils/request';
import {
CreateWorkflowDefinitionRequest,
UpdateWorkflowDefinitionRequest,
WorkflowDefinition,
WorkflowDefinitionPage,
WorkflowDefinitionQuery,
NodeType,
NodeTypeQuery
} from './types';
const WORKFLOW_DEFINITION_URL = '/api/v1/workflow-definitions';
const NODE_TYPE_URL = '/api/v1/node-types';
// 创建工作流定义
export const createDefinition = (data: CreateWorkflowDefinitionRequest) =>
request.post<WorkflowDefinition>(WORKFLOW_DEFINITION_URL, data);
// 更新工作流定义
export const updateDefinition = (id: number, data: UpdateWorkflowDefinitionRequest) =>
request.put<WorkflowDefinition>(`${WORKFLOW_DEFINITION_URL}/${id}`, data);
// 获取工作流定义列表
export const getDefinitions = (params?: WorkflowDefinitionQuery) =>
request.get<WorkflowDefinitionPage>(`${WORKFLOW_DEFINITION_URL}/page`, { params });
// 获取工作流定义详情
export const getDefinition = (id: number) =>
request.get<WorkflowDefinition>(`${WORKFLOW_DEFINITION_URL}/${id}`);
// 删除工作流定义
export const deleteDefinition = (id: number) =>
request.delete(`${WORKFLOW_DEFINITION_URL}/${id}`);
// 发布工作流定义
export const publishDefinition = (id: number) =>
request.post<WorkflowDefinition>(`${WORKFLOW_DEFINITION_URL}/${id}/publish`);
// 禁用工作流定义
export const disableDefinition = (id: number) =>
request.post<WorkflowDefinition>(`${WORKFLOW_DEFINITION_URL}/${id}/disable`);
// 启用工作流定义
export const enableDefinition = (id: number) =>
request.post<WorkflowDefinition>(`${WORKFLOW_DEFINITION_URL}/${id}/enable`);
// 创建新版本
export const createVersion = (id: number) =>
request.post<WorkflowDefinition>(`${WORKFLOW_DEFINITION_URL}/${id}/versions`);
// 节点类型相关接口
export const getNodeTypes = (params?: NodeTypeQuery) =>
request.get<NodeType[]>(NODE_TYPE_URL, { params });
export const getNodeTypeExecutors = (code: string) =>
request.get<NodeType>(`${NODE_TYPE_URL}/${code}/executors`);

View File

@ -0,0 +1,151 @@
import {Page} from '../../types/base';
// 工作流状态枚举
export enum WorkflowStatus {
DRAFT = 'DRAFT', // 草稿
PUBLISHED = 'PUBLISHED', // 已发布
DISABLED = 'DISABLED' // 已禁用
}
// 节点类型分类枚举
export enum NodeCategory {
TASK = 'TASK', // 任务节点
EVENT = 'EVENT', // 事件节点
GATEWAY = 'GATEWAY' // 网关节点
}
// 节点类型查询参数
export interface NodeTypeQuery {
enabled?: boolean;
category?: NodeCategory;
}
// 节点执行器
export interface NodeExecutor {
code: string;
name: string;
description: string;
configSchema: string; // JSON Schema
defaultConfig?: string;
}
// 节点类型
export interface NodeType {
id: number;
code: string;
name: string;
category: NodeCategory;
description: string;
enabled: boolean;
icon: string;
color: string;
executors: NodeExecutor[];
configSchema: string; // JSON Schema
defaultConfig: string;
createTime: string;
updateTime: string;
}
// 工作流定义查询参数
export interface WorkflowDefinitionQuery {
page?: number;
size?: number;
sort?: string[];
keyword?: string;
status?: WorkflowStatus;
enabled?: boolean;
}
// 节点配置
export interface NodeConfig {
nodes: {
id: string;
type: string;
name: string;
config: Record<string, any>;
}[];
}
// 流转配置
export interface TransitionConfig {
transitions: {
from: string;
to: string;
condition: string;
}[];
}
// 工作流定义基本信息
export interface WorkflowDefinitionBase {
code: string;
name: string;
description?: string;
}
// 工作流定义
export interface WorkflowDefinition extends WorkflowDefinitionBase {
id: number;
version: number;
status: WorkflowStatus;
enabled: boolean;
nodeConfig: string; // JSON string of NodeConfig
transitionConfig: string; // JSON string of TransitionConfig
formDefinition: string; // JSON string
graphDefinition: string; // JSON string
createTime: string;
updateTime: string;
}
// 创建工作流定义请求
export interface CreateWorkflowDefinitionRequest extends WorkflowDefinitionBase {
status: WorkflowStatus;
version: number;
enabled: boolean;
nodeConfig: string; // JSON string of NodeConfig
transitionConfig: string; // JSON string of TransitionConfig
formDefinition: string; // JSON string
graphDefinition: string; // JSON string
}
// 更新工作流定义请求
export interface UpdateWorkflowDefinitionRequest extends WorkflowDefinitionBase {
nodeConfig?: string; // JSON string of NodeConfig
transitionConfig?: string; // JSON string of TransitionConfig
formDefinition?: string; // JSON string
graphDefinition?: string; // JSON string
}
// 分页响应
export type WorkflowDefinitionPage = Page<WorkflowDefinition>;
// 工作流配置的解析和序列化工具
export const WorkflowConfigUtils = {
parseNodeConfig: (config: string): NodeConfig => {
try {
return JSON.parse(config);
} catch {
return { nodes: [] };
}
},
parseTransitionConfig: (config: string): TransitionConfig => {
try {
return JSON.parse(config);
} catch {
return { transitions: [] };
}
},
parseFormDefinition: (config: string): Record<string, any> => {
try {
return JSON.parse(config);
} catch {
return {};
}
},
parseGraphDefinition: (config: string): Record<string, any> => {
try {
return JSON.parse(config);
} catch {
return {};
}
}
};

View File

@ -0,0 +1,33 @@
import {NodeConfig, TransitionConfig} from './types';
// 工作流配置的解析和序列化工具
export const WorkflowConfigUtils = {
parseNodeConfig: (config: string): NodeConfig => {
try {
return JSON.parse(config);
} catch {
return {nodes: []};
}
},
parseTransitionConfig: (config: string): TransitionConfig => {
try {
return JSON.parse(config);
} catch {
return {transitions: []};
}
},
parseFormDefinition: (config: string): Record<string, any> => {
try {
return JSON.parse(config);
} catch {
return {};
}
},
parseGraphDefinition: (config: string): Record<string, any> => {
try {
return JSON.parse(config);
} catch {
return {};
}
}
};

View File

@ -32,11 +32,11 @@ 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 WorkflowDefinitionEdit = lazy(() => import('../pages/Workflow/Definition/Edit'));
// const WorkflowInstance = lazy(() => import('../pages/Workflow/Instance'));
// const WorkflowMonitor = lazy(() => import('../pages/Workflow/Monitor'));
// const FlowDesigner = lazy(() => import('../pages/Workflow/Definition/Designer'));
const WorkflowDefinition = lazy(() => import('../pages/Workflow/Definition'));
const WorkflowDefinitionEdit = lazy(() => import('../pages/Workflow/Definition/Edit'));
const WorkflowInstance = lazy(() => import('../pages/Workflow/Instance'));
const WorkflowMonitor = lazy(() => import('../pages/Workflow/Monitor'));
const FlowDesigner = lazy(() => import('../pages/Workflow/Definition/Designer'));
// 创建路由
const router = createBrowserRouter([
@ -117,56 +117,56 @@ const router = createBrowserRouter([
</Suspense>
)
},
// {
// path: 'workflow',
// children: [
// {
// path: 'definition',
// children: [
// {
// path: '',
// element: (
// <Suspense fallback={<LoadingComponent/>}>
// <WorkflowDefinition/>
// </Suspense>
// )
// },
// {
// path: 'edit/:id?',
// element: (
// <Suspense fallback={<LoadingComponent/>}>
// <WorkflowDefinitionEdit/>
// </Suspense>
// )
// },
// {
// path: 'designer/:id?',
// element: (
// <Suspense fallback={<LoadingComponent/>}>
// <FlowDesigner/>
// </Suspense>
// )
// }
// ]
// },
// {
// path: 'instance',
// element: (
// <Suspense fallback={<LoadingComponent/>}>
// <WorkflowInstance/>
// </Suspense>
// )
// },
// {
// path: 'monitor',
// element: (
// <Suspense fallback={<LoadingComponent/>}>
// <WorkflowMonitor/>
// </Suspense>
// )
// }
// ]
// },
{
path: 'workflow',
children: [
{
path: 'definition',
children: [
{
path: '',
element: (
<Suspense fallback={<LoadingComponent/>}>
<WorkflowDefinition/>
</Suspense>
)
},
{
path: 'edit/:id?',
element: (
<Suspense fallback={<LoadingComponent/>}>
<WorkflowDefinitionEdit/>
</Suspense>
)
},
{
path: 'designer/:id?',
element: (
<Suspense fallback={<LoadingComponent/>}>
<FlowDesigner/>
</Suspense>
)
}
]
},
{
path: 'instance',
element: (
<Suspense fallback={<LoadingComponent/>}>
<WorkflowInstance/>
</Suspense>
)
},
{
path: 'monitor',
element: (
<Suspense fallback={<LoadingComponent/>}>
<WorkflowMonitor/>
</Suspense>
)
}
]
},
{
path: '*',
element: <Navigate to="/dashboard"/>

View File

@ -13,4 +13,22 @@ export interface BaseQuery {
enabled?: boolean;
startTime?: string;
endTime?: string;
}
export interface Page<T> {
content: T[];
totalElements: number;
totalPages: number;
size: number;
number: number;
empty: boolean;
first: boolean;
last: boolean;
}
export interface Response<T> {
code: number;
message: string;
data: T;
success: boolean;
}