可正常保存流程
This commit is contained in:
parent
48524b9eb1
commit
1207d730db
@ -22,7 +22,8 @@
|
||||
// 工作流定义相关接口
|
||||
interface WorkflowDefinitionAPI {
|
||||
// 基础CRUD接口
|
||||
list: '/api/v1/workflow-definitions' // GET 查询列表
|
||||
page: '/api/v1/workflow-definitions/page' // GET 分页查询,支持条件筛选
|
||||
list: '/api/v1/workflow-definitions/list' // GET 查询所有(不分页)
|
||||
create: '/api/v1/workflow-definitions' // POST 创建
|
||||
update: '/api/v1/workflow-definitions/{id}' // PUT 更新
|
||||
delete: '/api/v1/workflow-definitions/{id}' // DELETE 删除
|
||||
@ -32,6 +33,31 @@ interface WorkflowDefinitionAPI {
|
||||
publish: '/api/v1/workflow-definitions/{id}/publish' // POST 发布
|
||||
disable: '/api/v1/workflow-definitions/{id}/disable' // POST 禁用
|
||||
enable: '/api/v1/workflow-definitions/{id}/enable' // POST 启用
|
||||
versions: '/api/v1/workflow-definitions/{code}/versions' // GET 查询所有版本(不分页)
|
||||
}
|
||||
|
||||
// 节点类型相关接口
|
||||
interface NodeTypeAPI {
|
||||
// 查询接口
|
||||
list: '/api/v1/node-types' // GET 获取所有可用的节点类型列表(不分页)
|
||||
getExecutors: '/api/v1/node-types/{type}/executors' // GET 获取指定节点类型支持的执行器列表
|
||||
}
|
||||
|
||||
// 分页请求参数
|
||||
interface PageQuery {
|
||||
pageNum: number // 页码,从1开始
|
||||
pageSize: number // 每页大小
|
||||
sortField?: string // 排序字段
|
||||
sortOrder?: 'ascend' | 'descend' // 排序方向
|
||||
}
|
||||
|
||||
// 分页响应结构
|
||||
interface PageResponse<T> {
|
||||
records: T[] // 数据列表
|
||||
total: number // 总记录数
|
||||
pages: number // 总页数
|
||||
pageNum: number // 当前页码
|
||||
pageSize: number // 每页大小
|
||||
}
|
||||
|
||||
// 工作流定义数据结构
|
||||
@ -42,10 +68,10 @@ interface WorkflowDefinitionDTO {
|
||||
description: string // 描述
|
||||
status: 'DRAFT' | 'PUBLISHED' | 'DISABLED' // 状态
|
||||
version: number // 版本号
|
||||
nodeConfig: string // 节点配置(JSON)
|
||||
transitionConfig: string // 流转配置(JSON)
|
||||
formDefinition: string // 表单定义
|
||||
graphDefinition: string // 图形信息
|
||||
nodeConfig: string // 节点配置(JSON),包含节点的基本信息和执行配置
|
||||
transitionConfig: string // 流转配置(JSON),定义节点间的连接和条件
|
||||
formDefinition: string // 表单定义(JSON),定义工作流的表单字段和验证规则
|
||||
graphDefinition: string // 图形信息(JSON),定义节点的位置和连线的样式
|
||||
enabled: boolean // 是否启用
|
||||
remark: string // 备注
|
||||
nodes: NodeDefinitionDTO[] // 节点定义列表
|
||||
@ -62,6 +88,173 @@ interface NodeDefinitionDTO {
|
||||
workflowDefinitionId: number // 工作流定义ID
|
||||
orderNum: number // 排序号
|
||||
}
|
||||
|
||||
// 节点配置示例
|
||||
interface NodeConfig {
|
||||
startNode: {
|
||||
type: 'START'
|
||||
name: string
|
||||
}
|
||||
endNode: {
|
||||
type: 'END'
|
||||
name: string
|
||||
}
|
||||
taskNodes: Array<{
|
||||
id: string
|
||||
type: 'TASK'
|
||||
name: string
|
||||
executor?: string // 执行器类型
|
||||
config?: any // 执行器配置
|
||||
}>
|
||||
}
|
||||
|
||||
// 流转配置示例
|
||||
interface TransitionConfig {
|
||||
transitions: Array<{
|
||||
from: string // 源节点ID
|
||||
to: string // 目标节点ID
|
||||
condition?: string // 流转条件
|
||||
}>
|
||||
}
|
||||
|
||||
// 表单定义示例
|
||||
interface FormDefinition {
|
||||
fields: Array<{
|
||||
name: string // 字段名
|
||||
label: string // 字段标签
|
||||
type: 'input' | 'select' | 'date' | 'number' // 字段类型
|
||||
required?: boolean // 是否必填
|
||||
options?: Array<{ // 选项(用于select类型)
|
||||
label: string
|
||||
value: any
|
||||
}>
|
||||
rules?: Array<{ // 验证规则
|
||||
type: string
|
||||
message: string
|
||||
}>
|
||||
}>
|
||||
}
|
||||
|
||||
// 图形定义示例
|
||||
interface GraphDefinition {
|
||||
nodes: Array<{
|
||||
id: string
|
||||
type: string
|
||||
x: number // 节点X坐标
|
||||
y: number // 节点Y坐标
|
||||
properties?: any // 节点属性
|
||||
}>
|
||||
edges: Array<{
|
||||
source: string // 源节点ID
|
||||
target: string // 目标节点ID
|
||||
properties?: any // 连线属性
|
||||
}>
|
||||
}
|
||||
|
||||
// 节点类型数据结构
|
||||
interface NodeTypeDTO {
|
||||
code: string // 节点类型编码
|
||||
name: string // 节点类型名称
|
||||
description: string // 节点类型描述
|
||||
category: 'BASIC' | 'TASK' | 'GATEWAY' | 'EVENT' // 节点类型分类
|
||||
icon: string // 节点图标
|
||||
color: string // 节点颜色
|
||||
executors?: Array<{ // 支持的执行器列表
|
||||
code: string // 执行器编码
|
||||
name: string // 执行器名称
|
||||
description: string // 执行器描述
|
||||
configSchema: any // 执行器配置模式
|
||||
}>
|
||||
configSchema?: any // 节点配置模式
|
||||
defaultConfig?: any // 默认配置
|
||||
}
|
||||
|
||||
// 节点类型示例数据
|
||||
const nodeTypes = [
|
||||
{
|
||||
code: 'START',
|
||||
name: '开始节点',
|
||||
description: '工作流的开始节点',
|
||||
category: 'BASIC',
|
||||
icon: 'play-circle',
|
||||
color: '#52c41a'
|
||||
},
|
||||
{
|
||||
code: 'END',
|
||||
name: '结束节点',
|
||||
description: '工作流的结束节点',
|
||||
category: 'BASIC',
|
||||
icon: 'stop',
|
||||
color: '#ff4d4f'
|
||||
},
|
||||
{
|
||||
code: 'TASK',
|
||||
name: '任务节点',
|
||||
description: '执行具体任务的节点',
|
||||
category: 'TASK',
|
||||
icon: 'code',
|
||||
color: '#1890ff',
|
||||
executors: [
|
||||
{
|
||||
code: 'SHELL',
|
||||
name: 'Shell脚本',
|
||||
description: '执行Shell脚本',
|
||||
configSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
script: {
|
||||
type: 'string',
|
||||
title: '脚本内容',
|
||||
format: 'shell'
|
||||
},
|
||||
timeout: {
|
||||
type: 'number',
|
||||
title: '超时时间(秒)',
|
||||
default: 300
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
code: 'JENKINS',
|
||||
name: 'Jenkins任务',
|
||||
description: '触发Jenkins构建任务',
|
||||
configSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
job: {
|
||||
type: 'string',
|
||||
title: 'Job名称'
|
||||
},
|
||||
parameters: {
|
||||
type: 'object',
|
||||
title: '构建参数'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
code: 'GATEWAY',
|
||||
name: '网关节点',
|
||||
description: '控制流程流转的节点',
|
||||
category: 'GATEWAY',
|
||||
icon: 'fork',
|
||||
color: '#faad14',
|
||||
configSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
type: {
|
||||
type: 'string',
|
||||
title: '网关类型',
|
||||
enum: ['PARALLEL', 'EXCLUSIVE', 'INCLUSIVE'],
|
||||
enumNames: ['并行网关', '排他网关', '包容网关']
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
];
|
||||
```
|
||||
|
||||
### 2.2 工作流实例管理
|
||||
@ -69,7 +262,8 @@ interface NodeDefinitionDTO {
|
||||
// 工作流实例相关接口
|
||||
interface WorkflowInstanceAPI {
|
||||
// 基础CRUD接口
|
||||
list: '/api/v1/workflow-instance' // GET 查询列表
|
||||
page: '/api/v1/workflow-instance/page' // GET 分页查询,支持条件筛选
|
||||
list: '/api/v1/workflow-instance/list' // GET 查询所有(不分页)
|
||||
get: '/api/v1/workflow-instance/{id}' // GET 获取详情
|
||||
|
||||
// 实例操作接口
|
||||
@ -98,10 +292,11 @@ interface WorkflowInstanceDTO {
|
||||
// 节点实例相关接口
|
||||
interface NodeInstanceAPI {
|
||||
// 查询接口
|
||||
list: '/api/v1/node-instance' // GET 查询列表
|
||||
page: '/api/v1/node-instance/page' // GET 分页查询,支持条件筛选
|
||||
list: '/api/v1/node-instance/list' // GET 查询所有(不分页)
|
||||
get: '/api/v1/node-instance/{id}' // GET 获取详情
|
||||
findByWorkflow: '/api/v1/node-instance/workflow/{workflowInstanceId}' // GET 查询工作流下的节点
|
||||
findByStatus: '/api/v1/node-instance/workflow/{workflowInstanceId}/status/{status}' // GET 查询指定状态的节点
|
||||
findByWorkflow: '/api/v1/node-instance/workflow/{workflowInstanceId}' // GET 查询工作流下的节点(不分页)
|
||||
findByStatus: '/api/v1/node-instance/workflow/{workflowInstanceId}/status/{status}' // GET 查询指定状态的节点(不分页)
|
||||
|
||||
// 节点操作
|
||||
updateStatus: '/api/v1/node-instance/{id}/status' // PUT 更新状态
|
||||
@ -130,12 +325,13 @@ interface NodeInstanceDTO {
|
||||
// 日志相关接口
|
||||
interface WorkflowLogAPI {
|
||||
// 基础CRUD接口
|
||||
list: '/api/v1/workflow-logs' // GET 查询列表
|
||||
page: '/api/v1/workflow-logs/page' // GET 分页查询,支持条件筛选
|
||||
list: '/api/v1/workflow-logs/list' // GET 查询所有(不分页)
|
||||
create: '/api/v1/workflow-logs' // POST 创建
|
||||
|
||||
// 特殊查询接口
|
||||
getWorkflowLogs: '/api/v1/workflow-logs/workflow/{workflowInstanceId}' // GET 查询工作流日志
|
||||
getNodeLogs: '/api/v1/workflow-logs/node/{workflowInstanceId}/{nodeId}' // GET 查询节点日志
|
||||
getWorkflowLogs: '/api/v1/workflow-logs/workflow/{workflowInstanceId}' // GET 查询工作流日志(不分页)
|
||||
getNodeLogs: '/api/v1/workflow-logs/node/{workflowInstanceId}/{nodeId}' // GET 查询节点日志(不分页)
|
||||
record: '/api/v1/workflow-logs/record' // POST 记录日志
|
||||
}
|
||||
|
||||
@ -179,19 +375,38 @@ interface WorkflowLogDTO {
|
||||
- 描述
|
||||
- 备注
|
||||
2. 流程设计器
|
||||
- 节点拖拽
|
||||
- 连线绘制
|
||||
- 节点配置
|
||||
- 节点拖拽和布局
|
||||
- 连线绘制和调整
|
||||
- 节点配置(nodeConfig)
|
||||
- 基本信息配置
|
||||
- 执行器类型选择
|
||||
- 执行器参数配置
|
||||
- 流转配置(transitionConfig)
|
||||
- 连线条件配置
|
||||
- 优先级设置
|
||||
- 条件表达式编辑
|
||||
- 流程校验
|
||||
3. 表单设计器
|
||||
3. 表单设计器(formDefinition)
|
||||
- 表单字段配置
|
||||
- 字段类型选择
|
||||
- 字段属性设置
|
||||
- 选项配置(针对select类型)
|
||||
- 字段验证规则
|
||||
- 必填验证
|
||||
- 格式验证
|
||||
- 自定义验证
|
||||
- 表单布局设计
|
||||
- 表单预览
|
||||
4. 节点配置面板
|
||||
- 节点基本信息
|
||||
- 节点类型配置
|
||||
- 执行条件配置
|
||||
- 表单关联配置
|
||||
4. 图形布局(graphDefinition)
|
||||
- 节点位置调整
|
||||
- 连线样式设置
|
||||
- 自动布局
|
||||
- 缩放和居中
|
||||
5. 版本管理
|
||||
- 版本历史查看
|
||||
- 版本对比
|
||||
- 版本回滚
|
||||
- 创建新版本
|
||||
|
||||
### 3.2 工作流实例管理(/workflow/instance)
|
||||
|
||||
@ -295,68 +510,3 @@ src/
|
||||
- refactor: 重构
|
||||
- test: 测试
|
||||
- chore: 构建过程或辅助工具的变动
|
||||
|
||||
## 五、开发流程
|
||||
|
||||
### 5.1 环境搭建
|
||||
1. 创建项目
|
||||
```bash
|
||||
yarn create umi
|
||||
```
|
||||
|
||||
2. 安装依赖
|
||||
```bash
|
||||
yarn add @ant-design/pro-components @logicflow/core @logicflow/extension form-render echarts
|
||||
```
|
||||
|
||||
### 5.2 开发步骤
|
||||
1. 搭建基础框架和路由(2天)
|
||||
2. 实现工作流定义CRUD(3天)
|
||||
3. 开发流程设计器(5天)
|
||||
4. 开发表单设计器(3天)
|
||||
5. 实现工作流实例管理(3天)
|
||||
6. 实现节点实例管理(2天)
|
||||
7. 实现日志管理(2天)
|
||||
8. 开发监控大盘(3天)
|
||||
9. 测试和优化(2天)
|
||||
|
||||
### 5.3 测试要求
|
||||
1. 单元测试覆盖率 > 80%
|
||||
2. 必须包含组件测试
|
||||
3. 必须包含集成测试
|
||||
4. 必须进行性能测试
|
||||
|
||||
### 5.4 部署要求
|
||||
1. 使用 Docker 部署
|
||||
2. 配置 Nginx 代理
|
||||
3. 启用 GZIP 压缩
|
||||
4. 配置缓存策略
|
||||
|
||||
## 六、注意事项
|
||||
|
||||
### 6.1 性能优化
|
||||
1. 使用路由懒加载
|
||||
2. 组件按需加载
|
||||
3. 大数据列表虚拟化
|
||||
4. 合理使用缓存
|
||||
5. 避免不必要的渲染
|
||||
|
||||
### 6.2 安全性
|
||||
1. 添加权限控制
|
||||
2. 防止XSS攻击
|
||||
3. 添加数据验证
|
||||
4. 敏感信息加密
|
||||
|
||||
### 6.3 用户体验
|
||||
1. 添加适当的加载状态
|
||||
2. 提供操作反馈
|
||||
3. 添加错误处理
|
||||
4. 支持快捷键操作
|
||||
5. 添加操作确认
|
||||
6. 支持数据导出
|
||||
|
||||
### 6.4 兼容性
|
||||
1. 支持主流浏览器最新版本
|
||||
2. 支持响应式布局
|
||||
3. 支持暗黑模式
|
||||
4. 支持国际化
|
||||
831
frontend/package-lock.json
generated
831
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -11,12 +11,15 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@ant-design/icons": "^5.2.6",
|
||||
"@antv/layout": "^1.2.14-beta.8",
|
||||
"@antv/x6": "^2.18.1",
|
||||
"@antv/x6-react-shape": "^2.2.3",
|
||||
"@logicflow/core": "^2.0.9",
|
||||
"@logicflow/extension": "^2.0.13",
|
||||
"@reduxjs/toolkit": "^2.0.1",
|
||||
"antd": "^5.22.2",
|
||||
"axios": "^1.6.2",
|
||||
"dagre": "^0.8.5",
|
||||
"form-render": "^2.5.1",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
@ -24,6 +27,7 @@
|
||||
"react-router-dom": "^6.21.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/dagre": "^0.7.52",
|
||||
"@types/node": "^20.10.4",
|
||||
"@types/react": "^18.2.43",
|
||||
"@types/react-dom": "^18.2.17",
|
||||
|
||||
@ -1,92 +1,86 @@
|
||||
.container {
|
||||
width: 100%;
|
||||
min-height: 600px;
|
||||
position: relative;
|
||||
background: #fff;
|
||||
height: 600px;
|
||||
background-color: #fff;
|
||||
border: 1px solid #e8e8e8;
|
||||
border-radius: 2px;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.canvas {
|
||||
flex: 1;
|
||||
min-height: 600px;
|
||||
position: relative;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
background: #fafafa;
|
||||
}
|
||||
|
||||
.canvas::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-image:
|
||||
linear-gradient(#e8e8e8 1px, transparent 1px),
|
||||
linear-gradient(90deg, #e8e8e8 1px, transparent 1px);
|
||||
background-size: 20px 20px;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.configPanel {
|
||||
width: 320px;
|
||||
.dndNode {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-left: 1px solid #e8e8e8;
|
||||
overflow-y: auto;
|
||||
background: #fff;
|
||||
box-shadow: -2px 0 8px rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
:global(.lf-control) {
|
||||
position: absolute;
|
||||
right: 330px;
|
||||
top: 10px;
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
background: #fff;
|
||||
padding: 4px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: #fff;
|
||||
border: 1px solid #1890ff;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
z-index: 2;
|
||||
cursor: move;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
:global(.lf-dnd-panel) {
|
||||
position: absolute;
|
||||
left: 10px;
|
||||
top: 10px;
|
||||
width: 160px;
|
||||
background: #fff;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
|
||||
padding: 12px;
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
z-index: 2;
|
||||
.dndNode:hover {
|
||||
background-color: #e6f7ff;
|
||||
border-color: #1890ff;
|
||||
}
|
||||
|
||||
:global(.lf-dnd-panel .lf-dnd-item) {
|
||||
:global(.lf-node) {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 35px;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
:global(.lf-node-text) {
|
||||
font-size: 12px;
|
||||
color: #333;
|
||||
text-align: center;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
:global(.lf-edge-text) {
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
background: #fff;
|
||||
padding: 2px 4px;
|
||||
border-radius: 2px;
|
||||
border: 1px solid #e8e8e8;
|
||||
border-radius: 4px;
|
||||
cursor: grab;
|
||||
}
|
||||
|
||||
:global(.lf-node-selected) {
|
||||
box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.3);
|
||||
}
|
||||
|
||||
:global(.lf-edge-selected) {
|
||||
stroke: #1890ff !important;
|
||||
stroke-width: 2px !important;
|
||||
}
|
||||
|
||||
:global(.lf-anchor) {
|
||||
stroke: #1890ff;
|
||||
fill: #fff;
|
||||
stroke-width: 1px;
|
||||
cursor: crosshair;
|
||||
}
|
||||
|
||||
:global(.lf-anchor:hover) {
|
||||
fill: #1890ff;
|
||||
}
|
||||
|
||||
:global(.lf-edge-label) {
|
||||
background: #fff;
|
||||
border: 1px solid #e8e8e8;
|
||||
border-radius: 2px;
|
||||
padding: 2px 4px;
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
user-select: none;
|
||||
transition: all 0.3s;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
:global(.lf-dnd-panel .lf-dnd-item:hover) {
|
||||
border-color: #1890ff;
|
||||
color: #1890ff;
|
||||
}
|
||||
|
||||
:global(.lf-dnd-panel .lf-dnd-item img) {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
margin-right: 8px;
|
||||
:global(.lf-edge-label:hover) {
|
||||
background: #f5f5f5;
|
||||
border-color: #d9d9d9;
|
||||
}
|
||||
@ -1,202 +1,626 @@
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import { Card } from 'antd';
|
||||
import LogicFlow from '@logicflow/core';
|
||||
import { DndPanel, SelectionSelect, Control, Menu } from '@logicflow/extension';
|
||||
import '@logicflow/core/es/index.css';
|
||||
import '@logicflow/extension/es/style/index.css';
|
||||
import { Graph, Node, Edge, Shape } from '@antv/x6';
|
||||
import dagre from 'dagre';
|
||||
import { getNodeTypes } from '@/pages/Workflow/service';
|
||||
import type { NodeTypeDTO } from '@/pages/Workflow/types';
|
||||
import styles from './index.module.css';
|
||||
import { registerNodes } from './nodes';
|
||||
import NodeConfig from '../NodeConfig';
|
||||
|
||||
// 节点类型映射
|
||||
const NODE_TYPE_MAP: Record<string, string> = {
|
||||
'TASK': 'SHELL' // 将 TASK 类型映射到 SHELL 类型
|
||||
};
|
||||
|
||||
// 特殊节点类型
|
||||
const SPECIAL_NODE_TYPES = {
|
||||
START: 'START',
|
||||
END: 'END'
|
||||
};
|
||||
|
||||
// 记录已注册的节点类型
|
||||
const registeredNodes = new Set<string>();
|
||||
|
||||
// 布局配置
|
||||
const LAYOUT_CONFIG = {
|
||||
PADDING: 20,
|
||||
NODE_MIN_DISTANCE: 100,
|
||||
RANK_SEPARATION: 100,
|
||||
NODE_SEPARATION: 80,
|
||||
EDGE_SPACING: 20,
|
||||
};
|
||||
|
||||
interface FlowDesignerProps {
|
||||
value?: string;
|
||||
onChange?: (value: string) => void;
|
||||
workflowInstanceId?: number;
|
||||
readOnly?: boolean;
|
||||
}
|
||||
|
||||
const FlowDesigner: React.FC<FlowDesignerProps> = ({
|
||||
value,
|
||||
onChange
|
||||
onChange,
|
||||
workflowInstanceId,
|
||||
readOnly = false
|
||||
}) => {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const [lf, setLf] = useState<LogicFlow>();
|
||||
const [selectedNode, setSelectedNode] = useState<any>(null);
|
||||
const graphRef = useRef<Graph>();
|
||||
const [nodeTypes, setNodeTypes] = useState<NodeTypeDTO[]>([]);
|
||||
|
||||
// 初始化 LogicFlow
|
||||
const initLogicFlow = () => {
|
||||
if (!containerRef.current) {
|
||||
console.error('Container ref is not ready');
|
||||
// 使用 dagre 布局算法
|
||||
const layout = (graph: Graph, force: boolean = false) => {
|
||||
const nodes = graph.getNodes();
|
||||
const edges = graph.getEdges();
|
||||
|
||||
if (nodes.length === 0) return;
|
||||
|
||||
// 如果不是强制布局,且所有节点都有位置信息,则不进行自动布局
|
||||
if (!force && nodes.every(node => {
|
||||
const position = node.position();
|
||||
return position.x !== undefined && position.y !== undefined;
|
||||
})) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 注册插件
|
||||
LogicFlow.use(DndPanel);
|
||||
LogicFlow.use(SelectionSelect);
|
||||
LogicFlow.use(Control);
|
||||
LogicFlow.use(Menu);
|
||||
const g = new dagre.graphlib.Graph();
|
||||
g.setGraph({
|
||||
rankdir: 'LR',
|
||||
nodesep: LAYOUT_CONFIG.NODE_SEPARATION,
|
||||
ranksep: LAYOUT_CONFIG.RANK_SEPARATION,
|
||||
edgesep: LAYOUT_CONFIG.EDGE_SPACING,
|
||||
marginx: LAYOUT_CONFIG.PADDING,
|
||||
marginy: LAYOUT_CONFIG.PADDING,
|
||||
});
|
||||
|
||||
const logicflow = new LogicFlow({
|
||||
container: containerRef.current,
|
||||
grid: true,
|
||||
nodeTextEdit: true,
|
||||
edgeTextEdit: true,
|
||||
width: containerRef.current.offsetWidth || 800,
|
||||
height: containerRef.current.offsetHeight || 600,
|
||||
style: {
|
||||
circle: {
|
||||
r: 30,
|
||||
stroke: '#000000',
|
||||
strokeWidth: 1,
|
||||
},
|
||||
rect: {
|
||||
width: 100,
|
||||
height: 50,
|
||||
stroke: '#000000',
|
||||
strokeWidth: 1,
|
||||
},
|
||||
diamond: {
|
||||
rx: 20,
|
||||
ry: 20,
|
||||
stroke: '#000000',
|
||||
strokeWidth: 1,
|
||||
},
|
||||
nodeText: {
|
||||
fontSize: 12,
|
||||
color: '#000000',
|
||||
},
|
||||
edgeText: {
|
||||
textWidth: 100,
|
||||
fontSize: 12,
|
||||
color: '#000000',
|
||||
background: {
|
||||
fill: '#FFFFFF',
|
||||
},
|
||||
},
|
||||
},
|
||||
// 设置默认节点大小
|
||||
g.setDefaultEdgeLabel(() => ({}));
|
||||
|
||||
// 添加节点
|
||||
nodes.forEach((node) => {
|
||||
g.setNode(node.id, {
|
||||
width: node.getSize().width,
|
||||
height: node.getSize().height,
|
||||
});
|
||||
});
|
||||
|
||||
console.log('LogicFlow instance created');
|
||||
// 添加边
|
||||
edges.forEach((edge) => {
|
||||
const source = edge.getSourceCellId();
|
||||
const target = edge.getTargetCellId();
|
||||
g.setEdge(source, target);
|
||||
});
|
||||
|
||||
// 注册自定义节点
|
||||
registerNodes(logicflow);
|
||||
// 执行布局
|
||||
dagre.layout(g);
|
||||
|
||||
// 配置拖拽面板
|
||||
logicflow.setPatternItems([
|
||||
{
|
||||
type: 'start',
|
||||
text: '开始节点',
|
||||
label: '开始节点',
|
||||
icon: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAYAAACNiR0NAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAADsSURBVDhP7ZQxDsIwDEV7AcZeggOwMXAFJg7BxMgBuAcrZ2DiGuwcgImNA3AFBgaWDhUlqhKnKWFh4EtWaif2s/OdAn8VY8wE13jEHR6wjUu0eIoL7ODBa0zQzlrswQWvcOcLSpzhFu2sxRN0wTZKLnGOT7jgHXZQcoYuOMKvkFZQcoMuKOdTkH4RpBWUdNEFU0idOsE7bnBYEL0gScvnkE5LgS44wA4+UHY5R3vMMtqZxB4+UQZkl+1M4hTf0K7Szjp4wQztrIMXlEFZQUkXL5jBf4KygpIuXjCDsoLyXwQzKCsoSWGwD3Q5p3qCzuvYAAAAAElFTkSuQmCC'
|
||||
},
|
||||
{
|
||||
type: 'task',
|
||||
text: '任务节点',
|
||||
label: '任务节点',
|
||||
icon: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAYAAACNiR0NAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAB5SURBVDhP7ZTBCYAwDEU7QkfxZi/eHMGbo3Rwhe7hCB3Bo+kHIZgmTUXQgw8k5Cd5tE34VUSkhw3ucYUTrNBiggUecYsNWkzwCyvc4QMtJviFJZ7wCS0mGIQVnvEFLSb4hRVe8A0tJhiEFd7wAy0m+IUVZmHxPyLZAYNFt+rXusajAAAAAElFTkSuQmCC'
|
||||
},
|
||||
{
|
||||
type: 'gateway',
|
||||
text: '网关节点',
|
||||
label: '网关节点',
|
||||
icon: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAYAAACNiR0NAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAACxSURBVDhP7ZQxDoMwDEU5AiM34AqMHXqFTj0EE2MP0L1bz8DENeicgYmNA3AFBgaWVrIiRyZRU1VIPPIT2Mb+NlYS8FeJMQ5wjkvc4QYnWKHFU5zjFtdeY4Z21mIHF7zAjS8ocYprtLMWj9AF2yi5wBk+4II32EHJKbrgAL9CWkHJFbqgnE9B+kWQVlDSRRdMIXXqCG+4wn5B9IIkLZ9DOi0FumAfW3hH2eUU7THLaGcSO/gBUb+nujmI/XsAAAAASUVORK5CYII='
|
||||
},
|
||||
{
|
||||
type: 'end',
|
||||
text: '结束节点',
|
||||
label: '结束节点',
|
||||
icon: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAYAAACNiR0NAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAADsSURBVDhP7ZQxDsIwDEV7AcZeggOwMXAFJg7BxMgBuAcrZ2DiGuwcgImNA3AFBgaWDhUlqhKnKWFh4EtWaif2s/OdAn8VY8wE13jEHR6wjUu0eIoL7ODBa0zQzlrswQWvcOcLSpzhFu2sxRN0wTZKLnGOT7jgHXZQcoYuOMKvkFZQcoMuKOdTkH4RpBWUdNEFU0idOsE7bnBYEL0gScvnkE5LgS44wA4+UHY5R3vMMtqZxB4+UQZkl+1M4hTf0K7Szjp4wQztrIMXlEFZQUkXL5jBf4KygpIuXjCDsoLyXwQzKCsoSWGwD3Q5p3qCzuvYAAAAAElFTkSuQmCC'
|
||||
}
|
||||
]);
|
||||
|
||||
// 注册事件
|
||||
logicflow.on('node:click', ({ data }) => {
|
||||
console.log('Node clicked:', data);
|
||||
const nodeModel = logicflow.getNodeModelById(data.id);
|
||||
const nodeData = {
|
||||
id: data.id,
|
||||
type: data.type,
|
||||
text: data.text,
|
||||
properties: nodeModel.getProperties()
|
||||
// 批量更新节点位置
|
||||
const updates = nodes.map((node) => {
|
||||
const nodeWithPosition = g.node(node.id);
|
||||
if (nodeWithPosition) {
|
||||
return {
|
||||
node,
|
||||
position: {
|
||||
x: nodeWithPosition.x - nodeWithPosition.width / 2,
|
||||
y: nodeWithPosition.y - nodeWithPosition.height / 2,
|
||||
},
|
||||
};
|
||||
setSelectedNode(nodeData);
|
||||
});
|
||||
|
||||
logicflow.on('blank:click', () => {
|
||||
setSelectedNode(null);
|
||||
});
|
||||
|
||||
logicflow.on('history:change', () => {
|
||||
const graphData = logicflow.getGraphData();
|
||||
onChange?.(JSON.stringify(graphData));
|
||||
});
|
||||
|
||||
// 渲染初始数据
|
||||
if (value) {
|
||||
try {
|
||||
const graphData = JSON.parse(value);
|
||||
logicflow.render(graphData);
|
||||
console.log('Initial graph data rendered:', graphData);
|
||||
} catch (error) {
|
||||
console.error('Failed to parse graph data:', error);
|
||||
logicflow.render({ nodes: [], edges: [] });
|
||||
}
|
||||
} else {
|
||||
logicflow.render({ nodes: [], edges: [] });
|
||||
console.log('Empty graph rendered');
|
||||
}
|
||||
|
||||
setLf(logicflow);
|
||||
console.log('LogicFlow initialization completed');
|
||||
|
||||
return logicflow;
|
||||
} catch (error) {
|
||||
console.error('Failed to initialize LogicFlow:', error);
|
||||
return null;
|
||||
}
|
||||
}).filter(Boolean);
|
||||
|
||||
// 使用批量更新
|
||||
graph.batchUpdate(() => {
|
||||
updates.forEach((update) => {
|
||||
if (update) {
|
||||
update.node.position(update.position.x, update.position.y);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
graph.centerContent();
|
||||
};
|
||||
|
||||
// 注册节点类型
|
||||
const registerNodeTypes = (nodeTypes: NodeTypeDTO[]) => {
|
||||
// 注册特殊节点类型
|
||||
if (!registeredNodes.has(SPECIAL_NODE_TYPES.START)) {
|
||||
Graph.registerNode(SPECIAL_NODE_TYPES.START, {
|
||||
inherit: 'circle',
|
||||
width: 40,
|
||||
height: 40,
|
||||
attrs: {
|
||||
body: {
|
||||
fill: '#52c41a',
|
||||
stroke: '#52c41a',
|
||||
strokeWidth: 2,
|
||||
},
|
||||
label: {
|
||||
text: '开始',
|
||||
fill: '#fff',
|
||||
fontSize: 12,
|
||||
fontFamily: 'Arial, helvetica, sans-serif',
|
||||
},
|
||||
},
|
||||
ports: {
|
||||
groups: {
|
||||
right: {
|
||||
position: {
|
||||
name: 'right',
|
||||
args: {
|
||||
dx: 4,
|
||||
},
|
||||
},
|
||||
attrs: {
|
||||
circle: {
|
||||
r: 4,
|
||||
magnet: true,
|
||||
stroke: '#52c41a',
|
||||
strokeWidth: 2,
|
||||
fill: '#fff',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
items: [
|
||||
{
|
||||
id: 'right',
|
||||
group: 'right',
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
registeredNodes.add(SPECIAL_NODE_TYPES.START);
|
||||
}
|
||||
|
||||
if (!registeredNodes.has(SPECIAL_NODE_TYPES.END)) {
|
||||
Graph.registerNode(SPECIAL_NODE_TYPES.END, {
|
||||
inherit: 'circle',
|
||||
width: 40,
|
||||
height: 40,
|
||||
attrs: {
|
||||
body: {
|
||||
fill: '#ff4d4f',
|
||||
stroke: '#ff4d4f',
|
||||
strokeWidth: 2,
|
||||
},
|
||||
label: {
|
||||
text: '结束',
|
||||
fill: '#fff',
|
||||
fontSize: 12,
|
||||
fontFamily: 'Arial, helvetica, sans-serif',
|
||||
},
|
||||
},
|
||||
ports: {
|
||||
groups: {
|
||||
left: {
|
||||
position: {
|
||||
name: 'left',
|
||||
args: {
|
||||
dx: -4,
|
||||
},
|
||||
},
|
||||
attrs: {
|
||||
circle: {
|
||||
r: 4,
|
||||
magnet: true,
|
||||
stroke: '#ff4d4f',
|
||||
strokeWidth: 2,
|
||||
fill: '#fff',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
items: [
|
||||
{
|
||||
id: 'left',
|
||||
group: 'left',
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
registeredNodes.add(SPECIAL_NODE_TYPES.END);
|
||||
}
|
||||
|
||||
// 注册其他节点类型
|
||||
nodeTypes.forEach(nodeType => {
|
||||
if (registeredNodes.has(nodeType.code)) return;
|
||||
|
||||
let shape: string;
|
||||
let width = 120;
|
||||
let height = 60;
|
||||
let attrs: any = {
|
||||
body: {
|
||||
fill: nodeType.color || '#fff',
|
||||
stroke: '#1890ff',
|
||||
strokeWidth: 2,
|
||||
},
|
||||
label: {
|
||||
text: nodeType.name,
|
||||
fill: '#000',
|
||||
fontSize: 12,
|
||||
fontFamily: 'Arial, helvetica, sans-serif',
|
||||
textWrap: {
|
||||
width: -10,
|
||||
height: -10,
|
||||
ellipsis: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
switch (nodeType.category) {
|
||||
case 'BASIC':
|
||||
shape = 'circle';
|
||||
width = 60;
|
||||
height = 60;
|
||||
break;
|
||||
case 'TASK':
|
||||
shape = 'rect';
|
||||
attrs.body.rx = 4;
|
||||
attrs.body.ry = 4;
|
||||
break;
|
||||
case 'GATEWAY':
|
||||
shape = 'polygon';
|
||||
attrs.body.refPoints = '0,10 10,0 20,10 10,20';
|
||||
width = 80;
|
||||
height = 80;
|
||||
break;
|
||||
case 'EVENT':
|
||||
shape = 'circle';
|
||||
width = 60;
|
||||
height = 60;
|
||||
break;
|
||||
default:
|
||||
shape = 'rect';
|
||||
}
|
||||
|
||||
// 注册节点
|
||||
Graph.registerNode(nodeType.code, {
|
||||
inherit: shape,
|
||||
width,
|
||||
height,
|
||||
attrs,
|
||||
ports: {
|
||||
groups: {
|
||||
left: {
|
||||
position: {
|
||||
name: 'left',
|
||||
args: {
|
||||
dx: -4,
|
||||
},
|
||||
},
|
||||
attrs: {
|
||||
circle: {
|
||||
r: 4,
|
||||
magnet: true,
|
||||
stroke: '#1890ff',
|
||||
strokeWidth: 2,
|
||||
fill: '#fff',
|
||||
},
|
||||
},
|
||||
label: {
|
||||
position: 'left',
|
||||
},
|
||||
},
|
||||
right: {
|
||||
position: {
|
||||
name: 'right',
|
||||
args: {
|
||||
dx: 4,
|
||||
},
|
||||
},
|
||||
attrs: {
|
||||
circle: {
|
||||
r: 4,
|
||||
magnet: true,
|
||||
stroke: '#1890ff',
|
||||
strokeWidth: 2,
|
||||
fill: '#fff',
|
||||
},
|
||||
},
|
||||
label: {
|
||||
position: 'right',
|
||||
},
|
||||
},
|
||||
},
|
||||
items: [
|
||||
{
|
||||
id: 'left',
|
||||
group: 'left',
|
||||
},
|
||||
{
|
||||
id: 'right',
|
||||
group: 'right',
|
||||
},
|
||||
],
|
||||
},
|
||||
markup: [
|
||||
{
|
||||
tagName: 'rect',
|
||||
selector: 'body',
|
||||
},
|
||||
{
|
||||
tagName: 'text',
|
||||
selector: 'label',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
registeredNodes.add(nodeType.code);
|
||||
});
|
||||
};
|
||||
|
||||
// 加载节点类型
|
||||
useEffect(() => {
|
||||
const logicflow = initLogicFlow();
|
||||
const loadNodeTypes = async () => {
|
||||
try {
|
||||
const response = await getNodeTypes();
|
||||
setNodeTypes(response);
|
||||
} catch (error) {
|
||||
console.error('加载节点类型失败:', error);
|
||||
}
|
||||
};
|
||||
loadNodeTypes();
|
||||
|
||||
return () => {
|
||||
if (logicflow) {
|
||||
console.log('Destroying LogicFlow instance');
|
||||
logicflow.destroy();
|
||||
}
|
||||
registeredNodes.clear();
|
||||
};
|
||||
}, []);
|
||||
|
||||
// 处理节点配置更新
|
||||
const handleNodeConfigSave = (config: any) => {
|
||||
if (lf && selectedNode) {
|
||||
const nodeModel = lf.getNodeModelById(selectedNode.id);
|
||||
nodeModel.updateText(config.text.value);
|
||||
nodeModel.setProperties(config);
|
||||
setSelectedNode(null);
|
||||
|
||||
// 触发 onChange
|
||||
const graphData = lf.getGraphData();
|
||||
onChange?.(JSON.stringify(graphData));
|
||||
// 初始化流程设计器
|
||||
useEffect(() => {
|
||||
if (!containerRef.current || !nodeTypes.length) {
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
registerNodeTypes(nodeTypes);
|
||||
|
||||
const graph = new Graph({
|
||||
container: containerRef.current,
|
||||
width: 800,
|
||||
height: 600,
|
||||
grid: {
|
||||
size: 10,
|
||||
visible: true,
|
||||
type: 'doubleMesh',
|
||||
args: [
|
||||
{
|
||||
color: '#eee',
|
||||
thickness: 1,
|
||||
},
|
||||
{
|
||||
color: '#ddd',
|
||||
thickness: 1,
|
||||
factor: 4,
|
||||
},
|
||||
],
|
||||
},
|
||||
connecting: {
|
||||
router: {
|
||||
name: 'manhattan',
|
||||
args: {
|
||||
padding: LAYOUT_CONFIG.PADDING,
|
||||
startDirections: ['right'],
|
||||
endDirections: ['left'],
|
||||
},
|
||||
},
|
||||
connector: {
|
||||
name: 'rounded',
|
||||
args: {
|
||||
radius: 8,
|
||||
},
|
||||
},
|
||||
anchor: 'center',
|
||||
connectionPoint: 'anchor',
|
||||
allowBlank: false,
|
||||
allowLoop: true,
|
||||
allowNode: false,
|
||||
allowEdge: false,
|
||||
snap: {
|
||||
radius: 20,
|
||||
},
|
||||
createEdge() {
|
||||
return new Shape.Edge({
|
||||
attrs: {
|
||||
line: {
|
||||
stroke: '#1890ff',
|
||||
strokeWidth: 2,
|
||||
targetMarker: {
|
||||
name: 'block',
|
||||
width: 12,
|
||||
height: 8,
|
||||
},
|
||||
},
|
||||
},
|
||||
zIndex: -1,
|
||||
router: {
|
||||
name: 'manhattan',
|
||||
args: {
|
||||
padding: LAYOUT_CONFIG.PADDING,
|
||||
startDirections: ['right'],
|
||||
endDirections: ['left'],
|
||||
},
|
||||
},
|
||||
connector: {
|
||||
name: 'rounded',
|
||||
args: {
|
||||
radius: 8,
|
||||
},
|
||||
},
|
||||
});
|
||||
},
|
||||
validateConnection({ targetMagnet, targetView, sourceView, sourceMagnet }) {
|
||||
if (!targetMagnet || !sourceMagnet) return false;
|
||||
if (sourceMagnet.getAttribute('port-group') !== 'right' ||
|
||||
targetMagnet.getAttribute('port-group') !== 'left') return false;
|
||||
if (targetView === sourceView && targetMagnet === sourceMagnet) return false;
|
||||
return true;
|
||||
},
|
||||
},
|
||||
highlighting: {
|
||||
magnetAvailable: {
|
||||
name: 'stroke',
|
||||
args: {
|
||||
padding: 4,
|
||||
attrs: {
|
||||
strokeWidth: 4,
|
||||
stroke: '#1890ff',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
mousewheel: {
|
||||
enabled: true,
|
||||
modifiers: ['ctrl', 'meta'],
|
||||
factor: 1.1,
|
||||
maxScale: 1.5,
|
||||
minScale: 0.5,
|
||||
},
|
||||
interacting: {
|
||||
nodeMovable: (view) => {
|
||||
const node = view.cell;
|
||||
// 开始和结束节点不允许移动
|
||||
if (node.shape === SPECIAL_NODE_TYPES.START || node.shape === SPECIAL_NODE_TYPES.END) {
|
||||
return false;
|
||||
}
|
||||
return !readOnly;
|
||||
},
|
||||
edgeMovable: !readOnly,
|
||||
edgeLabelMovable: !readOnly,
|
||||
vertexMovable: !readOnly,
|
||||
vertexAddable: !readOnly,
|
||||
vertexDeletable: !readOnly,
|
||||
magnetConnectable: !readOnly,
|
||||
},
|
||||
scaling: {
|
||||
min: 0.5,
|
||||
max: 1.5,
|
||||
},
|
||||
background: {
|
||||
color: '#f5f5f5',
|
||||
},
|
||||
preventDefaultBlankAction: true,
|
||||
preventDefaultContextMenu: true,
|
||||
clickThreshold: 10,
|
||||
magnetThreshold: 10,
|
||||
translating: {
|
||||
restrict: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (value) {
|
||||
try {
|
||||
const graphData = JSON.parse(value);
|
||||
|
||||
// 批量添加节点和边
|
||||
const model = {
|
||||
nodes: graphData.nodes.map((node: any) => {
|
||||
const isSpecialNode = node.type === 'START' || node.type === 'END';
|
||||
return {
|
||||
id: node.id,
|
||||
shape: isSpecialNode ? node.type : (NODE_TYPE_MAP[node.type] || node.type),
|
||||
x: node.position?.x,
|
||||
y: node.position?.y,
|
||||
label: isSpecialNode ? (node.type === 'START' ? '开始' : '结束') : (node.data?.name || '未命名节点'),
|
||||
data: {
|
||||
nodeType: isSpecialNode ? node.type : (NODE_TYPE_MAP[node.type] || node.type),
|
||||
...node.data,
|
||||
config: node.data?.config || {}
|
||||
}
|
||||
};
|
||||
}),
|
||||
edges: graphData.edges.map((edge: any) => ({
|
||||
id: edge.id,
|
||||
source: {
|
||||
cell: edge.source,
|
||||
port: 'right',
|
||||
},
|
||||
target: {
|
||||
cell: edge.target,
|
||||
port: 'left',
|
||||
},
|
||||
label: edge.data?.condition || '',
|
||||
data: {
|
||||
condition: edge.data?.condition || null
|
||||
},
|
||||
router: {
|
||||
name: 'manhattan',
|
||||
args: {
|
||||
padding: LAYOUT_CONFIG.PADDING,
|
||||
startDirections: ['right'],
|
||||
endDirections: ['left'],
|
||||
},
|
||||
},
|
||||
connector: {
|
||||
name: 'rounded',
|
||||
args: {
|
||||
radius: 8,
|
||||
},
|
||||
},
|
||||
}))
|
||||
};
|
||||
|
||||
graph.fromJSON(model);
|
||||
|
||||
// 只在初始加载时应用布局
|
||||
layout(graph, true);
|
||||
} catch (error) {
|
||||
console.error('加载流程数据失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
if (!readOnly) {
|
||||
let updateTimer: number | null = null;
|
||||
|
||||
const updateGraph = () => {
|
||||
if (updateTimer) {
|
||||
window.clearTimeout(updateTimer);
|
||||
}
|
||||
|
||||
updateTimer = window.setTimeout(() => {
|
||||
const nodes = graph.getNodes().map(node => ({
|
||||
id: node.id,
|
||||
type: node.data?.nodeType || node.shape,
|
||||
position: node.position(),
|
||||
data: {
|
||||
name: node.attr('label/text'),
|
||||
description: node.data?.description,
|
||||
config: node.data?.config || {}
|
||||
}
|
||||
}));
|
||||
|
||||
const edges = graph.getEdges().map(edge => ({
|
||||
id: edge.id,
|
||||
source: edge.getSourceCellId(),
|
||||
target: edge.getTargetCellId(),
|
||||
type: 'default',
|
||||
data: {
|
||||
condition: edge.data?.condition || null
|
||||
}
|
||||
}));
|
||||
|
||||
onChange?.(JSON.stringify({ nodes, edges }));
|
||||
}, 300);
|
||||
};
|
||||
|
||||
// 监听节点变化
|
||||
graph.on('node:moved', updateGraph);
|
||||
graph.on('cell:changed', updateGraph);
|
||||
graph.on('edge:connected', () => {
|
||||
// 只在新增连线时应用布局,且仅当节点数量大于1时
|
||||
if (graph.getNodes().length > 1) {
|
||||
layout(graph, false);
|
||||
}
|
||||
updateGraph();
|
||||
});
|
||||
}
|
||||
|
||||
graphRef.current = graph;
|
||||
|
||||
return () => {
|
||||
graph.dispose();
|
||||
};
|
||||
}, [nodeTypes, value, onChange, readOnly]);
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<div
|
||||
className={styles.canvas}
|
||||
ref={containerRef}
|
||||
style={{ minHeight: '600px' }}
|
||||
/>
|
||||
{selectedNode && (
|
||||
<div className={styles.configPanel}>
|
||||
<NodeConfig
|
||||
node={selectedNode}
|
||||
onSave={handleNodeConfigSave}
|
||||
onCancel={() => setSelectedNode(null)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div ref={containerRef} className={styles.container} />
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@ -1,118 +0,0 @@
|
||||
import LogicFlow from '@logicflow/core';
|
||||
import { CircleNode, CircleNodeModel, RectNode, RectNodeModel, DiamondNode, DiamondNodeModel } from '@logicflow/core';
|
||||
|
||||
export function registerNodes(lf: LogicFlow) {
|
||||
// 开始节点
|
||||
class StartNodeModel extends CircleNodeModel {
|
||||
initNodeData(data: any) {
|
||||
super.initNodeData(data);
|
||||
this.r = 30;
|
||||
}
|
||||
|
||||
getNodeStyle() {
|
||||
const style = super.getNodeStyle();
|
||||
return {
|
||||
...style,
|
||||
fill: '#C6E5FF',
|
||||
stroke: '#1890FF'
|
||||
};
|
||||
}
|
||||
|
||||
setAttributes() {
|
||||
this.text.editable = false;
|
||||
}
|
||||
}
|
||||
|
||||
class StartNodeView extends CircleNode { }
|
||||
|
||||
// 任务节点
|
||||
class TaskNodeModel extends RectNodeModel {
|
||||
initNodeData(data: any) {
|
||||
super.initNodeData(data);
|
||||
this.width = 120;
|
||||
this.height = 60;
|
||||
}
|
||||
|
||||
getNodeStyle() {
|
||||
const style = super.getNodeStyle();
|
||||
return {
|
||||
...style,
|
||||
fill: '#FFF',
|
||||
stroke: '#1890FF'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class TaskNodeView extends RectNode { }
|
||||
|
||||
// 网关节点
|
||||
class GatewayNodeModel extends DiamondNodeModel {
|
||||
initNodeData(data: any) {
|
||||
super.initNodeData(data);
|
||||
this.rx = 40;
|
||||
this.ry = 40;
|
||||
}
|
||||
|
||||
getNodeStyle() {
|
||||
const style = super.getNodeStyle();
|
||||
return {
|
||||
...style,
|
||||
fill: '#FFF',
|
||||
stroke: '#1890FF'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class GatewayNodeView extends DiamondNode { }
|
||||
|
||||
// 结束节点
|
||||
class EndNodeModel extends CircleNodeModel {
|
||||
initNodeData(data: any) {
|
||||
super.initNodeData(data);
|
||||
this.r = 30;
|
||||
}
|
||||
|
||||
getNodeStyle() {
|
||||
const style = super.getNodeStyle();
|
||||
return {
|
||||
...style,
|
||||
fill: '#FFE7E7',
|
||||
stroke: '#FF4D4F'
|
||||
};
|
||||
}
|
||||
|
||||
setAttributes() {
|
||||
this.text.editable = false;
|
||||
}
|
||||
}
|
||||
|
||||
class EndNodeView extends CircleNode { }
|
||||
|
||||
// 注册节点
|
||||
lf.register({
|
||||
type: 'start',
|
||||
view: StartNodeView,
|
||||
model: StartNodeModel
|
||||
});
|
||||
|
||||
lf.register({
|
||||
type: 'task',
|
||||
view: TaskNodeView,
|
||||
model: TaskNodeModel
|
||||
});
|
||||
|
||||
lf.register({
|
||||
type: 'gateway',
|
||||
view: GatewayNodeView,
|
||||
model: GatewayNodeModel
|
||||
});
|
||||
|
||||
lf.register({
|
||||
type: 'end',
|
||||
view: EndNodeView,
|
||||
model: EndNodeModel
|
||||
});
|
||||
|
||||
// 设置默认连线类型
|
||||
lf.setDefaultEdgeType('polyline');
|
||||
}
|
||||
@ -37,8 +37,8 @@ const FormDesigner: React.FC<FormDesignerProps> = ({
|
||||
console.log('Form data:', formData);
|
||||
};
|
||||
|
||||
const handleValidate = (errors: any) => {
|
||||
console.log('Form errors:', errors);
|
||||
const handleFinishFailed = (errors: any) => {
|
||||
console.log('Form validation errors:', errors);
|
||||
};
|
||||
|
||||
return (
|
||||
@ -48,7 +48,7 @@ const FormDesigner: React.FC<FormDesignerProps> = ({
|
||||
form={form}
|
||||
schema={schema}
|
||||
onFinish={handleSubmit}
|
||||
onValidate={handleValidate}
|
||||
onFinishFailed={handleFinishFailed}
|
||||
disabled={readOnly}
|
||||
/>
|
||||
</Card>
|
||||
|
||||
@ -1,27 +1,39 @@
|
||||
import React from 'react';
|
||||
import { Form, Input, Button, Space } from 'antd';
|
||||
import type { BaseNodeModel } from '@logicflow/core';
|
||||
|
||||
type NodeType = 'start' | 'end' | 'task' | 'gateway';
|
||||
import React, { useEffect } from 'react';
|
||||
import { Form, Button, Space, Input } from 'antd';
|
||||
import type { FormInstance } from 'antd';
|
||||
// @ts-ignore
|
||||
import FormRender from 'form-render';
|
||||
import type { NodeTypeDTO } from '@/pages/Workflow/types';
|
||||
|
||||
interface NodeConfigProps {
|
||||
node: {
|
||||
id: string;
|
||||
type: NodeType;
|
||||
type: string;
|
||||
text?: { value: string; x: number; y: number };
|
||||
properties?: Record<string, any>;
|
||||
};
|
||||
nodeType?: NodeTypeDTO;
|
||||
onSave: (config: any) => void;
|
||||
onCancel: () => void;
|
||||
}
|
||||
|
||||
const NodeConfig: React.FC<NodeConfigProps> = ({
|
||||
node,
|
||||
nodeType,
|
||||
onSave,
|
||||
onCancel
|
||||
}) => {
|
||||
const [form] = Form.useForm();
|
||||
|
||||
useEffect(() => {
|
||||
if (node.properties) {
|
||||
form.setFieldsValue({
|
||||
...node.properties,
|
||||
text: node.text?.value
|
||||
});
|
||||
}
|
||||
}, [node, form]);
|
||||
|
||||
const handleFinish = (values: any) => {
|
||||
onSave({
|
||||
...values,
|
||||
@ -29,65 +41,89 @@ const NodeConfig: React.FC<NodeConfigProps> = ({
|
||||
});
|
||||
};
|
||||
|
||||
const handleFinishFailed = (errors: any) => {
|
||||
console.log('Form validation errors:', errors);
|
||||
};
|
||||
|
||||
// 如果没有节点类型信息,显示基础配置
|
||||
if (!nodeType) {
|
||||
return (
|
||||
<div style={{ padding: 16 }}>
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
onFinish={handleFinish}
|
||||
onFinishFailed={handleFinishFailed}
|
||||
>
|
||||
<Form.Item
|
||||
label="节点名称"
|
||||
name="text"
|
||||
rules={[{ required: true, message: '请输入节点名称' }]}
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item>
|
||||
<Space>
|
||||
<Button type="primary" htmlType="submit">
|
||||
确定
|
||||
</Button>
|
||||
<Button onClick={onCancel}>
|
||||
取消
|
||||
</Button>
|
||||
</Space>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// 解析配置模式
|
||||
const configSchema = nodeType.configSchema ? JSON.parse(nodeType.configSchema) : {};
|
||||
const defaultConfig = nodeType.defaultConfig ? JSON.parse(nodeType.defaultConfig) : {};
|
||||
|
||||
// 合并节点类型的配置模式
|
||||
const schema = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
text: {
|
||||
title: '节点名称',
|
||||
type: 'string',
|
||||
required: true
|
||||
},
|
||||
...configSchema.properties
|
||||
},
|
||||
required: ['text', ...(configSchema.required || [])]
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{ padding: 16 }}>
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
initialValues={{
|
||||
text: node.text?.value,
|
||||
...node.properties
|
||||
}}
|
||||
<FormRender
|
||||
form={form as any}
|
||||
schema={schema}
|
||||
onFinish={handleFinish}
|
||||
>
|
||||
<Form.Item
|
||||
label="节点名称"
|
||||
name="text"
|
||||
rules={[{ required: true, message: '请输入节点名称' }]}
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
|
||||
{node.type === 'task' && (
|
||||
<>
|
||||
<Form.Item
|
||||
label="表单Key"
|
||||
name="formKey"
|
||||
rules={[{ required: true, message: '请输入表单Key' }]}
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="处理人"
|
||||
name="assignee"
|
||||
rules={[{ required: true, message: '请输入处理人' }]}
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
</>
|
||||
)}
|
||||
|
||||
{node.type === 'gateway' && (
|
||||
<Form.Item
|
||||
label="条件表达式"
|
||||
name="condition"
|
||||
rules={[{ required: true, message: '请输入条件表达式' }]}
|
||||
>
|
||||
<Input.TextArea rows={4} />
|
||||
</Form.Item>
|
||||
)}
|
||||
|
||||
<Form.Item>
|
||||
<Space>
|
||||
<Button type="primary" htmlType="submit">
|
||||
确定
|
||||
</Button>
|
||||
<Button onClick={onCancel}>
|
||||
取消
|
||||
</Button>
|
||||
</Space>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
onFinishFailed={handleFinishFailed}
|
||||
defaultValue={{
|
||||
text: node.text?.value,
|
||||
...defaultConfig
|
||||
}}
|
||||
widgets={{
|
||||
// 这里可以添加自定义组件
|
||||
}}
|
||||
watch={{
|
||||
// 这里可以添加表单联动逻辑
|
||||
}}
|
||||
/>
|
||||
<div style={{ marginTop: 16 }}>
|
||||
<Space>
|
||||
<Button type="primary" onClick={() => form.submit()}>
|
||||
确定
|
||||
</Button>
|
||||
<Button onClick={onCancel}>
|
||||
取消
|
||||
</Button>
|
||||
</Space>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@ -9,13 +9,15 @@ import type {
|
||||
WorkflowInstanceQuery,
|
||||
NodeInstanceResponse,
|
||||
WorkflowLogResponse,
|
||||
WorkflowDefinition
|
||||
WorkflowDefinition,
|
||||
NodeTypeDTO
|
||||
} from './types';
|
||||
|
||||
const DEFINITION_URL = '/api/v1/workflow-definitions';
|
||||
const INSTANCE_URL = '/api/v1/workflow-instance';
|
||||
const NODE_URL = '/api/v1/node-instance';
|
||||
const LOG_URL = '/api/v1/workflow-logs';
|
||||
const NODE_TYPE_URL = '/api/v1/node-types';
|
||||
|
||||
// 工作流定义相关接口
|
||||
export const getDefinitions = (params?: WorkflowDefinitionQuery) =>
|
||||
@ -95,3 +97,8 @@ export const saveDefinition = (data: WorkflowDefinition) => {
|
||||
}
|
||||
return request.post<WorkflowDefinitionResponse>('/api/v1/workflow/definition', data);
|
||||
};
|
||||
|
||||
// 获取节点类型列表
|
||||
export const getNodeTypes = () =>
|
||||
request.get<NodeTypeDTO[]>(NODE_TYPE_URL);
|
||||
|
||||
@ -136,3 +136,26 @@ export interface WorkflowLogResponse extends BaseResponse {
|
||||
level: LogLevel;
|
||||
detail?: string;
|
||||
}
|
||||
|
||||
// 节点类型数据结构
|
||||
export interface NodeTypeDTO extends BaseResponse {
|
||||
code: string; // 节点类型编码
|
||||
name: string; // 节点类型名称
|
||||
description: string; // 节点类型描述
|
||||
category: 'BASIC' | 'TASK' | 'GATEWAY' | 'EVENT'; // 节点类型分类
|
||||
icon: string; // 节点图标
|
||||
color: string; // 节点颜色
|
||||
executors?: Array<{ // 支持的执行器列表
|
||||
code: string; // 执行器编码
|
||||
name: string; // 执行器名称
|
||||
description: string; // 执行器描述
|
||||
configSchema: any; // 执行器配置模式
|
||||
}>;
|
||||
configSchema?: any; // 节点配置模式
|
||||
defaultConfig?: any; // 默认配置
|
||||
enabled: boolean; // 是否启用
|
||||
deleted: boolean; // 是否删除
|
||||
version: number; // 版本号
|
||||
createBy: string; // 创建人
|
||||
updateBy: string; // 更新人
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user