增加工具栏提示。
This commit is contained in:
parent
c93847b482
commit
4f938e1c1c
@ -1,16 +1,17 @@
|
||||
你是一名java前端开发工程师,对你有以下要求
|
||||
1. 缺陷修正:
|
||||
1. 一直说中文
|
||||
2. 缺陷修正:
|
||||
- 在提出修复建议前,应充分分析问题
|
||||
- 提供精准、有针对性的解决方案
|
||||
- 解释bug的根本原因
|
||||
|
||||
2. 保持简单:
|
||||
3. 保持简单:
|
||||
- 优先考虑可读性和可维护性
|
||||
- 避免过度工程化的解决方案
|
||||
- 尽可能使用标准库和模式
|
||||
- 遵循正确、最佳实践、DRY原则、无错误、功能齐全的代码编写原则
|
||||
|
||||
3. 代码更改:
|
||||
4. 代码更改:
|
||||
- 在做出改变之前提出一个清晰的计划
|
||||
- 一次将所有修改应用于单个文件
|
||||
- 请勿修改不相关的文件
|
||||
|
||||
@ -1,21 +1,159 @@
|
||||
你是一名java前端开发工程师,对你有以下要求
|
||||
1. 缺陷修正:
|
||||
- 在提出修复建议前,应充分分析问题
|
||||
- 提供精准、有针对性的解决方案
|
||||
- 解释bug的根本原因
|
||||
你是一名高级前端开发人员,是ReactJS, NextJS, JavaScript, TypeScript, HTML, CSS和现代UI/UX框架(例如,TailwindCSS, Shadcn, Radix)的专家。你深思熟虑,给出细致入微的答案,并且善于推理。你细心地提供准确、真实、深思熟虑的答案,是推理的天才。
|
||||
# 严格遵循的要求
|
||||
- 不要随意删除代码,请先详细浏览下现在所有的代码,再进行询问修改,得到确认再修改。重点切记
|
||||
- 首先,一步一步地思考——详细描述你在伪代码中构建什么的计划。
|
||||
- 确认,然后写代码!
|
||||
- 始终编写正确、最佳实践、DRY原则(不要重复自己)、无错误、功能齐全且可工作的代码,还应与下面代码实施指南中列出的规则保持一致。
|
||||
- 专注于简单易读的代码,而不是高性能。
|
||||
- 完全实现所有要求的功能。
|
||||
- 不要留下待办事项、占位符或缺失的部分。
|
||||
- 确保代码完整!彻底确认。
|
||||
- 包括所有必需的导入的包,并确保关键组件的正确命名。
|
||||
- 如果你认为可能没有正确答案,你就说出来。
|
||||
- 如果你不知道答案,就说出来,而不是猜测。
|
||||
- 可以提出合理化的建议,但是需要等待是否可以。
|
||||
- 对于新设计的实体类、字段、方法都要写注释,对于实际的逻辑要有逻辑注释。
|
||||
- 不要随意修改现在的接口调用路径,如果不知道接口是什么,可以问。
|
||||
#代码实现指南
|
||||
在编写代码时遵循以下规则:
|
||||
- 尽可能使用早期返回,使代码更具可读性。
|
||||
- 总是使用顺风类样式HTML元素;避免使用CSS或标签。
|
||||
- 尽可能在类标记中使用“ class: ”而不是第三操作符。
|
||||
- 使用描述性变量名和函数/const名。此外,事件函数应该以“handle”前缀命名,就像onClick的“handleClick”和onKeyDown的“handleKeyDown”。
|
||||
- 在元素上实现可访问性特性。例如,一个标签应该有tabindex= " 0 "、aria-label、on:click和on:keydown以及类似的属性。
|
||||
— 使用const代替函数,例如:const toggle =() =>。另外,如果可能的话,定义一个类型。
|
||||
|
||||
2. 保持简单:
|
||||
- 优先考虑可读性和可维护性
|
||||
- 避免过度工程化的解决方案
|
||||
- 尽可能使用标准库和模式
|
||||
- 遵循正确、最佳实践、DRY原则、无错误、功能齐全的代码编写原则
|
||||
# Deploy Ease Platform 前端开发规范
|
||||
|
||||
3. 代码更改:
|
||||
- 在做出改变之前提出一个清晰的计划
|
||||
- 一次将所有修改应用于单个文件
|
||||
- 请勿修改不相关的文件
|
||||
## 1. 项目结构规范
|
||||
|
||||
4. 代码注释:
|
||||
- 尽量都编写代码注释,写明实现的方式
|
||||
```
|
||||
src/
|
||||
├── components/ # 公共组件
|
||||
├── pages/ # 页面组件
|
||||
│ └── System/ # 系统管理模块
|
||||
├── layouts/ # 布局组件
|
||||
├── router/index.ts # 路由配置
|
||||
├── store/ # 状态管理
|
||||
├── services/ # API 服务
|
||||
├── utils/ # 工具函数
|
||||
├── hooks/ # 自定义 Hooks
|
||||
└── types/ # TS 类型定义
|
||||
│ └── pages.ts # 分页基础类
|
||||
|
||||
记住要始终考虑每个项目的背景和特定需求。
|
||||
ModuleName/ # 模块结构
|
||||
├── components/ # 模块私有组件
|
||||
├── type.ts # 类型定义
|
||||
├── service.ts # API 服务
|
||||
└── index.tsx # 模块入口
|
||||
```
|
||||
|
||||
## 2. 命名与类型规范
|
||||
|
||||
1. 文件命名:
|
||||
- 组件:PascalCase(`UserProfile.tsx`)
|
||||
- 工具函数:camelCase(`formatDate.ts`)
|
||||
- 样式:组件同名(`UserProfile.module.css`)
|
||||
- Redux:`xxxSlice.ts`
|
||||
- 类型:`types.ts`
|
||||
- 服务:`service.ts`
|
||||
|
||||
2. 变量命名:
|
||||
- 常量:UPPER_SNAKE_CASE
|
||||
- 变量:camelCase
|
||||
- 接口:以 `I` 开头,PascalCase
|
||||
- 类型:以 `T` 开头,PascalCase
|
||||
- 事件处理:`handle` 前缀
|
||||
- 异步函数:动词开头(`fetchData`)
|
||||
|
||||
3. 要使用和继承定义了基础类型src\types\base.ts
|
||||
|
||||
## 3. 服务层规范
|
||||
|
||||
1. 方法定义:
|
||||
```typescript
|
||||
// 推荐写法 - 使用 request 工具
|
||||
export const resetPassword = (id: number, password: string) =>
|
||||
request.post<void>(`/api/v1/users/${id}/reset-password`, { password });
|
||||
|
||||
// 标准 CRUD 接口
|
||||
export const getList = (params?: Query) =>
|
||||
request.get<Page<Response>>('/api/v1/xxx/page', { params }); // 列表接口统一使用 /page 后缀
|
||||
|
||||
export const create = (data: Request) =>
|
||||
request.post<Response>('/api/v1/xxx', data);
|
||||
|
||||
export const update = (id: number, data: Request) =>
|
||||
request.put<Response>(`/api/v1/xxx/${id}`, data);
|
||||
|
||||
export const remove = (id: number) =>
|
||||
request.delete(`/api/v1/xxx/${id}`);
|
||||
|
||||
export const batchRemove = (ids: number[]) =>
|
||||
request.post('/api/v1/xxx/batch-delete', { ids });
|
||||
|
||||
export const exportData = (params?: Query) =>
|
||||
request.download('/api/v1/xxx/export', undefined, { params });
|
||||
```
|
||||
|
||||
2. 规范要点:
|
||||
- 统一使用 src\utils\request.ts
|
||||
- 使用泛型指定响应数据类型
|
||||
- 错误处理在拦截器中统一处理
|
||||
- 使用模板字符串拼接路径
|
||||
- API 路径使用 `/api/v1/` 前缀
|
||||
- 资源使用复数形式(users, roles)
|
||||
- 特殊操作使用动词(export, import)
|
||||
|
||||
3. 列表数据获取规范:
|
||||
- API 路径必须以 `/page` 结尾,例如:`/api/v1/workflow-definitions/page`
|
||||
- 服务层方法定义示例:
|
||||
```typescript
|
||||
export const getDefinitions = (params?: WorkflowDefinitionQuery) =>
|
||||
request.get<Page<WorkflowDefinitionResponse>>(`${DEFINITION_URL}/page`, { params });
|
||||
```
|
||||
- 组件中获取列表数据:
|
||||
```typescript
|
||||
const response = await getDefinitions();
|
||||
if (response) {
|
||||
setList(response.content); // 使用 response.content 获取列表数据
|
||||
}
|
||||
```
|
||||
- 分页数据结构统一使用 `Page<T>` 类型
|
||||
- 列表数据必须位于返回结果的 `content` 字段中
|
||||
|
||||
## 4. 类型处理规则:
|
||||
- request 工具的泛型参数 T 表示业务数据类型
|
||||
- 响应拦截器负责从 Response<T> 中提取 data
|
||||
- 服务方法直接使用业务数据类型作为泛型参数
|
||||
- 组件中可以直接使用返回的业务数据
|
||||
- TypeScript 类型系统能正确推断类型
|
||||
|
||||
## 5. React 开发规范
|
||||
|
||||
1. 组件开发:
|
||||
- 使用函数组件和箭头函数
|
||||
- Props 类型必须定义
|
||||
- 必须提供默认值
|
||||
- 使用 memo 优化
|
||||
- 复杂组件需拆分
|
||||
|
||||
2. Hooks 使用:
|
||||
- 自定义 hooks 以 `use` 开头
|
||||
- 使用 TypeScript 泛型
|
||||
- 使用 useCallback 和 useMemo
|
||||
- 使用 Promise.all 处理并行请求
|
||||
|
||||
3. 状态管理:
|
||||
- 使用 Redux Toolkit
|
||||
- 持久化数据存储在 localStorage
|
||||
- Token、用户信息、菜单统一管理
|
||||
|
||||
## 6. 样式与布局规范
|
||||
|
||||
1. CSS 规范:
|
||||
- 使用 CSS Modules
|
||||
- 类名使用 kebab-case
|
||||
- 避免内联样式
|
||||
- 响应式适配
|
||||
- 支持暗色主题
|
||||
@ -1,332 +0,0 @@
|
||||
# 工作流数据格式说明
|
||||
|
||||
## 1. 节点类型数据示例
|
||||
|
||||
```json
|
||||
{
|
||||
"id": 2003,
|
||||
"createTime": "2024-12-05 12:40:03",
|
||||
"createBy": "system",
|
||||
"updateTime": "2024-12-05 12:40:03",
|
||||
"updateBy": "system",
|
||||
"version": 1,
|
||||
"deleted": false,
|
||||
"extraData": null,
|
||||
"code": "SHELL",
|
||||
"name": "Shell脚本节点",
|
||||
"description": "执行Shell脚本的任务节点",
|
||||
"category": "TASK",
|
||||
"icon": "code",
|
||||
"color": "#1890ff",
|
||||
"executors": [
|
||||
{
|
||||
"code": "SHELL",
|
||||
"name": "Shell脚本执行器",
|
||||
"description": "执行Shell脚本,支持配置超时时间和工作目录",
|
||||
"configSchema": "{\"type\":\"object\",\"required\":[\"script\"],\"properties\":{\"script\":{\"type\":\"string\",\"title\":\"脚本内容\",\"format\":\"shell\",\"description\":\"需要执行的Shell脚本内容\"},\"timeout\":{\"type\":\"number\",\"title\":\"超时时间\",\"description\":\"脚本执行的最大时间(秒)\",\"minimum\":1,\"maximum\":3600,\"default\":300},\"workingDir\":{\"type\":\"string\",\"title\":\"工作目录\",\"description\":\"脚本执行的工作目录\",\"default\":\"/tmp\"}}}",
|
||||
"defaultConfig": null
|
||||
}
|
||||
],
|
||||
"configSchema": "{\n \"type\": \"object\",\n \"properties\": {\n \"name\": {\n \"type\": \"string\",\n \"title\": \"节点名称\",\n \"minLength\": 1,\n \"maxLength\": 50\n },\n \"description\": {\n \"type\": \"string\",\n \"title\": \"节点描述\",\n \"maxLength\": 200\n },\n \"executor\": {\n \"type\": \"string\",\n \"title\": \"执行器\",\n \"enum\": [\"SHELL\"],\n \"enumNames\": [\"Shell脚本执行器\"]\n },\n \"retryTimes\": {\n \"type\": \"number\",\n \"title\": \"重试次数\",\n \"minimum\": 0,\n \"maximum\": 3,\n \"default\": 0\n },\n \"retryInterval\": {\n \"type\": \"number\",\n \"title\": \"重试间隔(秒)\",\n \"minimum\": 1,\n \"maximum\": 300,\n \"default\": 60\n }\n },\n \"required\": [\"name\", \"executor\"]\n}",
|
||||
"defaultConfig": "{\"name\": \"Shell脚本\", \"executor\": \"SHELL\", \"retryTimes\": 0, \"retryInterval\": 60}",
|
||||
"enabled": true
|
||||
}
|
||||
```
|
||||
|
||||
## 2. 流程设计数据示例
|
||||
|
||||
```json
|
||||
{
|
||||
"nodes": [
|
||||
{
|
||||
"id": "node_1",
|
||||
"type": "START",
|
||||
"position": { "x": 100, "y": 100 },
|
||||
"data": {
|
||||
"name": "开始",
|
||||
"description": "流程开始节点",
|
||||
"config": {
|
||||
"name": "开始",
|
||||
"description": "这是一个开始节点"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"edges": [
|
||||
{
|
||||
"id": "edge_1",
|
||||
"source": "node_1",
|
||||
"target": "node_2",
|
||||
"type": "default",
|
||||
"data": {
|
||||
"condition": null
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## 3. 关键字段说明
|
||||
|
||||
### configSchema(节点配置模式)
|
||||
**用途**:
|
||||
1. 前端动态生成配置表单
|
||||
2. 后端验证配置数据的合法性
|
||||
3. 提供配置项的约束和验证规则
|
||||
|
||||
### defaultConfig(默认配置)
|
||||
**用途**:
|
||||
1. 新建节点时的默认值
|
||||
2. 重置配置时的参考值
|
||||
3. 必须符合configSchema定义的格式
|
||||
|
||||
### executors(执行器定义)
|
||||
**用途**:
|
||||
1. 定义节点支持的执行器类型
|
||||
2. 每个执行器的配置要求
|
||||
3. 用于任务节点的具体执行逻辑
|
||||
|
||||
## 4. 字段关系说明
|
||||
|
||||
1. configSchema定义节点的整体配置结构
|
||||
2. defaultConfig提供符合configSchema的默认值
|
||||
3. executors中的configSchema定义具体执行器的配置结构
|
||||
4. 实际节点配置时,executorConfig需要符合选定执行器的configSchema
|
||||
|
||||
## 5. 前端实现指南
|
||||
|
||||
### 5.1 工作流设计器组件架构
|
||||
|
||||
推荐使用组件化设计,主要包含以下组件:
|
||||
|
||||
1. **WorkflowDesigner(工作流设计器)**
|
||||
- 整体容器组件
|
||||
- 负责状态管理
|
||||
- 处理快捷键
|
||||
- 工具栏集成
|
||||
|
||||
2. **NodePanel(节点面板)**
|
||||
- 显示可用节点类型
|
||||
- 支持拖拽创建节点
|
||||
- 节点分类展示
|
||||
|
||||
3. **Canvas(画布)**
|
||||
- 节点和连线的可视化
|
||||
- 处理拖拽和连线
|
||||
- 网格背景
|
||||
- 缩放和平移
|
||||
|
||||
4. **NodeConfig(节点配置)**
|
||||
- 动态表单生成
|
||||
- 配置验证
|
||||
- 实时预览
|
||||
|
||||
### 5.2 接口说明
|
||||
|
||||
#### 5.2.1 节点类型接口
|
||||
```typescript
|
||||
// 获取节点类型列表
|
||||
GET /api/v1/node-types
|
||||
Response: {
|
||||
code: number;
|
||||
data: NodeType[];
|
||||
message: string;
|
||||
}
|
||||
|
||||
// 获取单个节点类型详情
|
||||
GET /api/v1/node-types/{id}
|
||||
Response: {
|
||||
code: number;
|
||||
data: NodeType;
|
||||
message: string;
|
||||
}
|
||||
```
|
||||
|
||||
#### 5.2.2 工作流设计接口
|
||||
```typescript
|
||||
// 保存工作流设计
|
||||
POST /api/v1/workflows/{id}/design
|
||||
Request: {
|
||||
nodes: Node[];
|
||||
edges: Edge[];
|
||||
}
|
||||
Response: {
|
||||
code: number;
|
||||
data: boolean;
|
||||
message: string;
|
||||
}
|
||||
|
||||
// 获取工作流设计
|
||||
GET /api/v1/workflows/{id}/design
|
||||
Response: {
|
||||
code: number;
|
||||
data: {
|
||||
nodes: Node[];
|
||||
edges: Edge[];
|
||||
};
|
||||
message: string;
|
||||
}
|
||||
```
|
||||
|
||||
### 5.3 节点连线实现
|
||||
|
||||
#### 5.3.1 连接点(Anchors)
|
||||
每种类型节点的连接点定义:
|
||||
```typescript
|
||||
interface NodeAnchor {
|
||||
id: string;
|
||||
type: 'input' | 'output';
|
||||
position: 'top' | 'right' | 'bottom' | 'left';
|
||||
allowMultiple?: boolean; // 是否允许多条连线
|
||||
}
|
||||
|
||||
const nodeAnchors = {
|
||||
START: [
|
||||
{ id: 'output', type: 'output', position: 'bottom', allowMultiple: true }
|
||||
],
|
||||
END: [
|
||||
{ id: 'input', type: 'input', position: 'top', allowMultiple: false }
|
||||
],
|
||||
TASK: [
|
||||
{ id: 'input', type: 'input', position: 'top', allowMultiple: false },
|
||||
{ id: 'output', type: 'output', position: 'bottom', allowMultiple: true }
|
||||
],
|
||||
GATEWAY: [
|
||||
{ id: 'input', type: 'input', position: 'top', allowMultiple: false },
|
||||
{ id: 'output', type: 'output', position: 'bottom', allowMultiple: true }
|
||||
]
|
||||
};
|
||||
```
|
||||
|
||||
#### 5.3.2 连线验证规则
|
||||
```typescript
|
||||
interface ConnectionValidation {
|
||||
sourceNode: Node;
|
||||
targetNode: Node;
|
||||
sourceAnchor: NodeAnchor;
|
||||
targetAnchor: NodeAnchor;
|
||||
}
|
||||
|
||||
function validateConnection({
|
||||
sourceNode,
|
||||
targetNode,
|
||||
sourceAnchor,
|
||||
targetAnchor
|
||||
}: ConnectionValidation): boolean {
|
||||
// 1. 检查源节点和目标节点是否有效
|
||||
if (!sourceNode || !targetNode) return false;
|
||||
|
||||
// 2. 检查是否形成循环
|
||||
if (wouldCreateCycle(sourceNode, targetNode)) return false;
|
||||
|
||||
// 3. 检查锚点类型匹配
|
||||
if (sourceAnchor.type !== 'output' || targetAnchor.type !== 'input') return false;
|
||||
|
||||
// 4. 检查目标锚点是否已被占用(如果不允许多重连接)
|
||||
if (!targetAnchor.allowMultiple && hasExistingConnection(targetNode, targetAnchor)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
```
|
||||
|
||||
#### 5.3.3 连线样式配置
|
||||
```typescript
|
||||
const edgeStyles = {
|
||||
default: {
|
||||
type: 'smoothstep', // 平滑阶梯线
|
||||
animated: false,
|
||||
style: {
|
||||
stroke: '#b1b1b7',
|
||||
strokeWidth: 2
|
||||
}
|
||||
},
|
||||
selected: {
|
||||
style: {
|
||||
stroke: '#1890ff',
|
||||
strokeWidth: 2
|
||||
}
|
||||
},
|
||||
conditional: {
|
||||
type: 'smoothstep',
|
||||
animated: true,
|
||||
style: {
|
||||
stroke: '#722ed1',
|
||||
strokeWidth: 2,
|
||||
strokeDasharray: '5,5'
|
||||
}
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
### 5.4 状态管理建议
|
||||
|
||||
推荐使用状态管理库(如Redux或MobX)管理以下状态:
|
||||
|
||||
1. **全局状态**
|
||||
- 当前工作流设计
|
||||
- 可用节点类型
|
||||
- 画布缩放级别
|
||||
- 选中的节点/连线
|
||||
|
||||
2. **节点状态**
|
||||
- 位置信息
|
||||
- 配置数据
|
||||
- 验证状态
|
||||
|
||||
3. **连线状态**
|
||||
- 连接关系
|
||||
- 条件配置
|
||||
- 样式信息
|
||||
|
||||
### 5.5 性能优化建议
|
||||
|
||||
1. **渲染优化**
|
||||
- 使用React.memo()优化节点渲染
|
||||
- 实现虚拟滚动
|
||||
- 大量节点时使用分层渲染
|
||||
|
||||
2. **状态更新优化**
|
||||
- 批量更新状态
|
||||
- 使用不可变数据结构
|
||||
- 实现节点位置的防抖
|
||||
|
||||
3. **交互优化**
|
||||
- 拖拽时使用节点预览
|
||||
- 连线时显示对齐参考线
|
||||
- 支持快捷键操作
|
||||
|
||||
### 5.6 错误处理
|
||||
|
||||
1. **前端验证**
|
||||
- 节点配置验证
|
||||
- 连线规则验证
|
||||
- 数据完整性检查
|
||||
|
||||
2. **错误提示**
|
||||
- 友好的错误信息
|
||||
- 错误定位高亮
|
||||
- 操作建议提示
|
||||
|
||||
3. **异常恢复**
|
||||
- 自动保存
|
||||
- 操作撤销/重做
|
||||
- 状态恢复机制
|
||||
|
||||
## 6. 最佳实践建议
|
||||
|
||||
1. **代码组织**
|
||||
- 使用TypeScript确保类型安全
|
||||
- 遵循组件设计原则
|
||||
- 实现完整的测试覆盖
|
||||
|
||||
2. **用户体验**
|
||||
- 实现撤销/重做功能
|
||||
- 支持键盘快捷键
|
||||
- 添加操作引导
|
||||
|
||||
3. **可扩展性**
|
||||
- 支持自定义节点
|
||||
- 支持自定义连线样式
|
||||
- 预留扩展接口
|
||||
4869
frontend/pnpm-lock.yaml
Normal file
4869
frontend/pnpm-lock.yaml
Normal file
File diff suppressed because it is too large
Load Diff
2
frontend/src/pages/System/External/types.ts
vendored
2
frontend/src/pages/System/External/types.ts
vendored
@ -1,5 +1,5 @@
|
||||
import { BaseResponse } from '@/types/base/response';
|
||||
import { BaseQuery } from '@/types/base/query';
|
||||
import {BaseQuery} from "@/types/base";
|
||||
|
||||
// 系统类型枚举
|
||||
export enum SystemType {
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import {BaseQuery} from "@/types/base/query.ts";
|
||||
import {BaseRequest} from "@/types/base/request.ts";
|
||||
import {BaseResponse} from "@/types/base/response.ts";
|
||||
import {BaseQuery} from "@/types/base";
|
||||
|
||||
export enum MenuTypeEnum {
|
||||
DIRECTORY = 1, // 目录
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import type { BaseResponse } from '@/types/base/response';
|
||||
import {BaseQuery} from "@/types/base/query.ts";
|
||||
import {BaseRequest} from "@/types/base/request.ts";
|
||||
import {BaseQuery} from "@/types/base";
|
||||
|
||||
// 权限类型枚举
|
||||
export enum PermissionType {
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { BaseQuery } from '@/types/base/query';
|
||||
import {BaseQuery} from "@/types/base";
|
||||
|
||||
export interface RoleQuery extends BaseQuery {
|
||||
name?: string;
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import type { BaseResponse } from '@/types/base/response';
|
||||
import type { BaseQuery } from '@/types/base/query';
|
||||
import {BaseQuery} from "@/types/base";
|
||||
|
||||
// 用户查询参数
|
||||
export interface UserQuery extends BaseQuery {
|
||||
|
||||
@ -1,65 +0,0 @@
|
||||
import React from 'react';
|
||||
import { Form, Input, InputNumber } from 'antd';
|
||||
import { Edge } from '@antv/x6';
|
||||
|
||||
interface EdgeConfigProps {
|
||||
edge: Edge;
|
||||
form: any;
|
||||
onValuesChange?: (changedValues: any, allValues: any) => void;
|
||||
}
|
||||
|
||||
interface EdgeData {
|
||||
condition?: string;
|
||||
description?: string;
|
||||
priority?: number;
|
||||
}
|
||||
|
||||
const EdgeConfig: React.FC<EdgeConfigProps> = ({ edge, form, onValuesChange }) => {
|
||||
return (
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
onValuesChange={onValuesChange}
|
||||
initialValues={edge.getData() as EdgeData}
|
||||
>
|
||||
<Form.Item
|
||||
label="条件表达式"
|
||||
name="condition"
|
||||
tooltip="使用SpEL表达式,例如:#{user.age > 18}"
|
||||
rules={[{ required: true, message: '请输入条件表达式' }]}
|
||||
>
|
||||
<Input.TextArea
|
||||
placeholder="请输入条件表达式"
|
||||
autoSize={{ minRows: 3, maxRows: 6 }}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label="描述"
|
||||
name="description"
|
||||
tooltip="条件的说明文字"
|
||||
>
|
||||
<Input.TextArea
|
||||
placeholder="请输入条件描述"
|
||||
autoSize={{ minRows: 2, maxRows: 4 }}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label="优先级"
|
||||
name="priority"
|
||||
tooltip="数字越小优先级越高"
|
||||
rules={[
|
||||
{ type: 'number', min: 0, max: 100, message: '优先级范围为0-100' }
|
||||
]}
|
||||
>
|
||||
<InputNumber
|
||||
placeholder="请输入优先级"
|
||||
style={{ width: '100%' }}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
|
||||
export default EdgeConfig;
|
||||
@ -1,257 +0,0 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import { Form, Input, Select, InputNumber, Divider, Tooltip } from 'antd';
|
||||
import { InfoCircleOutlined } from '@ant-design/icons';
|
||||
import type { Rule } from 'antd/es/form';
|
||||
import { NodeType } from '../../../../types';
|
||||
import { validateNodeConfig } from './validate';
|
||||
|
||||
interface NodeConfigProps {
|
||||
nodeType: NodeType;
|
||||
form: any;
|
||||
onValuesChange?: (changedValues: any, allValues: any) => void;
|
||||
}
|
||||
|
||||
interface JsonSchema {
|
||||
type: string;
|
||||
title?: string;
|
||||
description?: string;
|
||||
properties?: Record<string, JsonSchema>;
|
||||
required?: string[];
|
||||
minimum?: number;
|
||||
maximum?: number;
|
||||
minLength?: number;
|
||||
maxLength?: number;
|
||||
format?: string;
|
||||
default?: any;
|
||||
enum?: any[];
|
||||
enumNames?: string[];
|
||||
additionalProperties?: JsonSchema;
|
||||
}
|
||||
|
||||
const parseJsonSafely = (jsonString: string): any => {
|
||||
if (!jsonString) return {};
|
||||
|
||||
try {
|
||||
// 如果已经是对象,直接返回
|
||||
if (typeof jsonString === 'object') {
|
||||
return jsonString;
|
||||
}
|
||||
|
||||
// 打印原始字符串内容,帮助调试
|
||||
console.log('原始 JSON 字符串:', jsonString);
|
||||
|
||||
// 尝试直接解析
|
||||
try {
|
||||
return JSON.parse(jsonString);
|
||||
} catch (e) {
|
||||
console.log('直接解析失败,尝试预处理...');
|
||||
}
|
||||
|
||||
// 预处理字符串
|
||||
let processedString = jsonString;
|
||||
|
||||
// 1. 处理换行符
|
||||
processedString = processedString.replace(/[\r\n]+/g, ' ');
|
||||
|
||||
// 2. 处理转义字符
|
||||
processedString = processedString.replace(/\\/g, '\\\\')
|
||||
.replace(/\\\\"/g, '\\"')
|
||||
.replace(/\\\\n/g, '\\n')
|
||||
.replace(/\\\\r/g, '\\r')
|
||||
.replace(/\\\\t/g, '\\t');
|
||||
|
||||
// 3. 确保属性名称正确引用
|
||||
processedString = processedString.replace(/(['"])?([a-zA-Z0-9_]+)(['"])?:/g, '"$2":');
|
||||
|
||||
console.log('处理后的 JSON 字符串:', processedString);
|
||||
|
||||
return JSON.parse(processedString);
|
||||
} catch (error) {
|
||||
console.error('JSON解析错误:', error);
|
||||
console.error('解析失败的字符串:', jsonString);
|
||||
// 返回空对象而不是 null,避免后续操作出错
|
||||
return {};
|
||||
}
|
||||
};
|
||||
|
||||
const NodeConfig: React.FC<NodeConfigProps> = ({ nodeType, form, onValuesChange }) => {
|
||||
// 解析节点配置模式
|
||||
const nodeSchema = useMemo(() => {
|
||||
if (!nodeType.configSchema) return null;
|
||||
return parseJsonSafely(nodeType.configSchema);
|
||||
}, [nodeType.configSchema]);
|
||||
|
||||
// 解析节点默认配置
|
||||
const nodeDefaultConfig = useMemo(() => {
|
||||
if (!nodeType.defaultConfig) return {};
|
||||
return parseJsonSafely(nodeType.defaultConfig) || {};
|
||||
}, [nodeType.defaultConfig]);
|
||||
|
||||
// 当前选中的执行器
|
||||
const selectedExecutor = Form.useWatch('executor', form);
|
||||
|
||||
// 获取当前执行器的配置模式
|
||||
const executorSchema = useMemo(() => {
|
||||
if (!selectedExecutor || !nodeType.executors) return null;
|
||||
const executor = nodeType.executors.find(e => e.code === selectedExecutor);
|
||||
if (!executor?.configSchema) return null;
|
||||
return parseJsonSafely(executor.configSchema);
|
||||
}, [selectedExecutor, nodeType.executors]);
|
||||
|
||||
// 验证配置
|
||||
const validateField = async (_: any, value: any) => {
|
||||
const allValues = form.getFieldsValue();
|
||||
const validationResult = validateNodeConfig(nodeType, allValues);
|
||||
|
||||
if (!validationResult.valid) {
|
||||
const fieldErrors = validationResult.errors.filter(error =>
|
||||
error.toLowerCase().includes(_?.field?.toLowerCase() || '')
|
||||
);
|
||||
if (fieldErrors.length > 0) {
|
||||
throw new Error(fieldErrors.join('; '));
|
||||
}
|
||||
}
|
||||
return Promise.resolve();
|
||||
};
|
||||
|
||||
// 根据schema生成表单验证规则
|
||||
const generateRules = (schema: JsonSchema, fieldName: string): Rule[] => {
|
||||
const rules: Rule[] = [];
|
||||
|
||||
if (schema.required?.includes(fieldName)) {
|
||||
rules.push({ required: true, message: `请输入${schema.title || fieldName}` });
|
||||
}
|
||||
|
||||
if (schema.type === 'string') {
|
||||
if (schema.minLength !== undefined) {
|
||||
rules.push({ min: schema.minLength, message: `${schema.title || fieldName}最少${schema.minLength}个字符` });
|
||||
}
|
||||
if (schema.maxLength !== undefined) {
|
||||
rules.push({ max: schema.maxLength, message: `${schema.title || fieldName}最多${schema.maxLength}个字符` });
|
||||
}
|
||||
}
|
||||
|
||||
if (schema.type === 'number') {
|
||||
if (schema.minimum !== undefined) {
|
||||
rules.push({ type: 'number', min: schema.minimum, message: `${schema.title || fieldName}不能小于${schema.minimum}` });
|
||||
}
|
||||
if (schema.maximum !== undefined) {
|
||||
rules.push({ type: 'number', max: schema.maximum, message: `${schema.title || fieldName}不能大于${schema.maximum}` });
|
||||
}
|
||||
}
|
||||
|
||||
rules.push({ validator: validateField });
|
||||
return rules;
|
||||
};
|
||||
|
||||
// 根据schema生成表单项
|
||||
const renderFormItem = (fieldName: string, fieldSchema: JsonSchema) => {
|
||||
const rules = generateRules(fieldSchema, fieldName);
|
||||
const commonProps = {
|
||||
label: (
|
||||
<span>
|
||||
{fieldSchema.title || fieldName}
|
||||
{fieldSchema.description && (
|
||||
<Tooltip title={fieldSchema.description}>
|
||||
<InfoCircleOutlined style={{ marginLeft: 4 }} />
|
||||
</Tooltip>
|
||||
)}
|
||||
</span>
|
||||
),
|
||||
name: fieldName,
|
||||
rules
|
||||
};
|
||||
|
||||
switch (fieldSchema.type) {
|
||||
case 'string':
|
||||
if (fieldSchema.enum) {
|
||||
return (
|
||||
<Form.Item {...commonProps} key={fieldName}>
|
||||
<Select
|
||||
placeholder={`请选择${fieldSchema.title || fieldName}`}
|
||||
options={fieldSchema.enum.map((value, index) => ({
|
||||
label: fieldSchema.enumNames?.[index] || value,
|
||||
value
|
||||
}))}
|
||||
/>
|
||||
</Form.Item>
|
||||
);
|
||||
}
|
||||
if (fieldSchema.format === 'shell') {
|
||||
return (
|
||||
<Form.Item {...commonProps} key={fieldName}>
|
||||
<Input.TextArea rows={6} placeholder={`请输入${fieldSchema.title || fieldName}`} />
|
||||
</Form.Item>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Form.Item {...commonProps} key={fieldName}>
|
||||
<Input placeholder={`请输入${fieldSchema.title || fieldName}`} />
|
||||
</Form.Item>
|
||||
);
|
||||
|
||||
case 'number':
|
||||
return (
|
||||
<Form.Item {...commonProps} key={fieldName}>
|
||||
<InputNumber
|
||||
style={{ width: '100%' }}
|
||||
min={fieldSchema.minimum}
|
||||
max={fieldSchema.maximum}
|
||||
placeholder={`请输入${fieldSchema.title || fieldName}`}
|
||||
/>
|
||||
</Form.Item>
|
||||
);
|
||||
|
||||
case 'object':
|
||||
if (fieldSchema.additionalProperties) {
|
||||
return (
|
||||
<Form.Item {...commonProps} key={fieldName}>
|
||||
<Input.TextArea
|
||||
placeholder={`请输入${fieldSchema.title || fieldName},格式为 key=value`}
|
||||
rows={4}
|
||||
/>
|
||||
</Form.Item>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
// 渲染节点基本配置
|
||||
const renderNodeConfig = () => {
|
||||
if (!nodeSchema?.properties) return null;
|
||||
return Object.entries(nodeSchema.properties).map(([fieldName, fieldSchema]) =>
|
||||
renderFormItem(fieldName, fieldSchema)
|
||||
);
|
||||
};
|
||||
|
||||
// 渲染执行器配置
|
||||
const renderExecutorConfig = () => {
|
||||
if (!executorSchema?.properties) return null;
|
||||
return Object.entries(executorSchema.properties).map(([fieldName, fieldSchema]) =>
|
||||
renderFormItem(fieldName, fieldSchema)
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
onValuesChange={onValuesChange}
|
||||
initialValues={{ ...nodeDefaultConfig }}
|
||||
>
|
||||
{renderNodeConfig()}
|
||||
{selectedExecutor && (
|
||||
<>
|
||||
<Divider />
|
||||
{renderExecutorConfig()}
|
||||
</>
|
||||
)}
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
|
||||
export default NodeConfig;
|
||||
@ -1,78 +0,0 @@
|
||||
import Ajv from 'ajv';
|
||||
import addFormats from 'ajv-formats';
|
||||
import {NodeTypeEnum} from "@/pages/Workflow/Definition/Designer/components/NodePanel/types";
|
||||
|
||||
const ajv = new Ajv({
|
||||
allErrors: true,
|
||||
verbose: true,
|
||||
$data: true,
|
||||
strict: false
|
||||
});
|
||||
|
||||
addFormats(ajv);
|
||||
|
||||
export interface ValidationResult {
|
||||
valid: boolean;
|
||||
errors: string[];
|
||||
}
|
||||
|
||||
export const validateNodeConfig = (
|
||||
nodeType: NodeTypeEnum,
|
||||
formData: any
|
||||
): ValidationResult => {
|
||||
console.log('Validating node config:', { nodeType, formData });
|
||||
|
||||
try {
|
||||
// 1. 解析节点的 schema
|
||||
const nodeSchema = JSON.parse(nodeType.configSchema);
|
||||
console.log('Node schema:', nodeSchema);
|
||||
|
||||
let errors: string[] = [];
|
||||
|
||||
// 2. 验证基本配置
|
||||
const validateNode = ajv.compile(nodeSchema);
|
||||
const nodeValid = validateNode(formData);
|
||||
console.log('Node validation result:', { valid: nodeValid, errors: validateNode.errors });
|
||||
|
||||
if (!nodeValid && validateNode.errors) {
|
||||
errors = validateNode.errors.map(err => {
|
||||
const field = err.instancePath.replace('/', '') || '配置';
|
||||
return `${field}: ${err.message}`;
|
||||
});
|
||||
}
|
||||
|
||||
// 3. 如果有执行器配置,验证执行器配置
|
||||
if (formData.executor && nodeType.executors?.length > 0) {
|
||||
const executor = nodeType.executors.find(e => e.code === formData.executor);
|
||||
if (executor?.configSchema) {
|
||||
console.log('Validating executor config:', executor.configSchema);
|
||||
const executorSchema = JSON.parse(executor.configSchema);
|
||||
const validateExecutor = ajv.compile(executorSchema);
|
||||
const executorValid = validateExecutor(formData);
|
||||
console.log('Executor validation result:', { valid: executorValid, errors: validateExecutor.errors });
|
||||
|
||||
if (!executorValid && validateExecutor.errors) {
|
||||
errors = errors.concat(
|
||||
validateExecutor.errors.map(err => {
|
||||
const field = err.instancePath.replace('/', '') || '执行器配置';
|
||||
return `${field}: ${err.message}`;
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const result = {
|
||||
valid: errors.length === 0,
|
||||
errors
|
||||
};
|
||||
console.log('Final validation result:', result);
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.error('Schema validation error:', error);
|
||||
return {
|
||||
valid: false,
|
||||
errors: ['配置验证失败:schema 解析错误']
|
||||
};
|
||||
}
|
||||
};
|
||||
@ -1,67 +0,0 @@
|
||||
.node-panel {
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.node-panel-loading {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.node-panel-content {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
|
||||
gap: 12px;
|
||||
padding: 12px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.node-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 12px;
|
||||
border: 1px solid #e8e8e8;
|
||||
border-radius: 4px;
|
||||
cursor: move;
|
||||
transition: all 0.3s;
|
||||
background: white;
|
||||
}
|
||||
|
||||
.node-card:hover {
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.node-icon {
|
||||
font-size: 24px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.node-name {
|
||||
font-size: 12px;
|
||||
text-align: center;
|
||||
color: #333;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.node-tabs {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.node-tabs :global(.ant-tabs-content) {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.node-tabs :global(.ant-tabs-tabpane) {
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
@ -1,110 +0,0 @@
|
||||
.node-panel {
|
||||
height: 100%;
|
||||
border-right: 1px solid #e8e8e8;
|
||||
background-color: #fff;
|
||||
|
||||
&-tabs {
|
||||
height: 100%;
|
||||
|
||||
:global {
|
||||
.ant-tabs-nav {
|
||||
margin: 0;
|
||||
padding: 0 12px;
|
||||
background-color: #fafafa;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
|
||||
.ant-tabs-tab {
|
||||
padding: 8px 12px;
|
||||
margin: 0 4px !important;
|
||||
transition: all 0.3s;
|
||||
border-radius: 4px 4px 0 0;
|
||||
|
||||
&:hover {
|
||||
color: #1890ff;
|
||||
background: rgba(24, 144, 255, 0.1);
|
||||
}
|
||||
|
||||
&.ant-tabs-tab-active {
|
||||
background: #fff;
|
||||
border-bottom-color: #fff;
|
||||
|
||||
.ant-tabs-tab-btn {
|
||||
color: #1890ff;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.ant-tabs-content {
|
||||
height: calc(100% - 44px);
|
||||
padding: 12px;
|
||||
overflow: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.node-panel-content {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
|
||||
gap: 16px;
|
||||
padding: 8px;
|
||||
|
||||
.node-item {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 90px;
|
||||
padding: 16px;
|
||||
border-radius: 4px;
|
||||
cursor: move;
|
||||
transition: all 0.3s;
|
||||
user-select: none;
|
||||
|
||||
&:hover {
|
||||
box-shadow: 0 2px 8px rgba(24, 144, 255, 0.15);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.node-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: 8px;
|
||||
font-size: 24px;
|
||||
line-height: 1;
|
||||
|
||||
.anticon {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.node-name {
|
||||
margin-bottom: 4px;
|
||||
color: #262626;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
text-align: center;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.node-desc {
|
||||
color: #8c8c8c;
|
||||
font-size: 12px;
|
||||
text-align: center;
|
||||
line-height: 1.4;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,121 +0,0 @@
|
||||
:global {
|
||||
.workflow-node-tabs {
|
||||
height: 100%;
|
||||
|
||||
.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;
|
||||
}
|
||||
}
|
||||
|
||||
.workflow-node-card {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
.workflow-node-icon {
|
||||
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 {
|
||||
transform: scale(1.1);
|
||||
|
||||
.anticon {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.workflow-node-name {
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
text-align: center;
|
||||
line-height: 1.5;
|
||||
margin: 0;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
@ -1,13 +0,0 @@
|
||||
declare namespace NodePanelModuleLessNamespace {
|
||||
export interface INodePanelModuleLess {
|
||||
nodeTabs: string;
|
||||
nodeCard: string;
|
||||
nodeIcon: string;
|
||||
nodeName: string;
|
||||
}
|
||||
}
|
||||
|
||||
declare module '*.less' {
|
||||
const content: NodePanelModuleLessNamespace.INodePanelModuleLess;
|
||||
export default content;
|
||||
}
|
||||
@ -1,169 +0,0 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Tabs, Spin } from 'antd';
|
||||
import * as Icons from '@ant-design/icons';
|
||||
import { NodeType, NodeCategory } from './types';
|
||||
import { getNodeTypes } from '../../../../service';
|
||||
import './index.less';
|
||||
|
||||
interface NodePanelProps {
|
||||
onNodeDragStart: (nodeType: NodeType) => void;
|
||||
}
|
||||
|
||||
const NodePanel: React.FC<NodePanelProps> = ({ onNodeDragStart }) => {
|
||||
const [nodeTypes, setNodeTypes] = useState<NodeType[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchNodeTypes = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const response = await getNodeTypes({ enabled: true });
|
||||
console.log('节点类型原始数据:', response);
|
||||
if (response?.content) {
|
||||
// 处理每个节点的样式
|
||||
const processedTypes = response.content.map(node => ({
|
||||
...node,
|
||||
// 确保graphConfig中的style存在
|
||||
style: node.graphConfig?.style || {
|
||||
fill: '#ffffff',
|
||||
stroke: '#1890ff',
|
||||
strokeWidth: 2,
|
||||
icon: 'api',
|
||||
iconColor: '#1890ff'
|
||||
}
|
||||
}));
|
||||
console.log('处理后的节点数据:', processedTypes);
|
||||
setNodeTypes(processedTypes);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取节点类型失败:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchNodeTypes();
|
||||
}, []);
|
||||
|
||||
// 获取图标组件
|
||||
const getIcon = (iconName: string) => {
|
||||
if (!iconName) return null;
|
||||
|
||||
// 转换图标名称为大驼峰格式
|
||||
const formatIconName = (name: string) => {
|
||||
return name
|
||||
.split('-')
|
||||
.map(part => part.charAt(0).toUpperCase() + part.slice(1))
|
||||
.join('');
|
||||
};
|
||||
|
||||
// 尝试多种图标命名方式
|
||||
const iconVariants = [
|
||||
formatIconName(iconName),
|
||||
`${formatIconName(iconName)}Outlined`,
|
||||
`${formatIconName(iconName)}Filled`,
|
||||
`${formatIconName(iconName)}TwoTone`
|
||||
];
|
||||
|
||||
console.log('尝试加载图标:', iconName, iconVariants);
|
||||
|
||||
for (const variant of iconVariants) {
|
||||
const IconComponent = (Icons as any)[variant];
|
||||
if (IconComponent) {
|
||||
console.log('成功找到图标组件:', variant);
|
||||
return React.createElement(IconComponent);
|
||||
}
|
||||
}
|
||||
console.warn('未找到图标组件:', iconName, iconVariants);
|
||||
return null;
|
||||
};
|
||||
|
||||
// 按类别分组节点类型
|
||||
const groupedNodeTypes = nodeTypes.reduce((acc, nodeType) => {
|
||||
const category = nodeType.category || NodeCategory.TASK;
|
||||
if (!acc[category]) {
|
||||
acc[category] = [];
|
||||
}
|
||||
acc[category].push(nodeType);
|
||||
return acc;
|
||||
}, {} as Record<string, NodeType[]>);
|
||||
|
||||
// 获取类别标签
|
||||
const getCategoryLabel = (category: string): string => {
|
||||
switch (category) {
|
||||
case NodeCategory.EVENT:
|
||||
return '事件节点';
|
||||
case NodeCategory.TASK:
|
||||
return '任务节点';
|
||||
case NodeCategory.GATEWAY:
|
||||
return '网关节点';
|
||||
case NodeCategory.CONTAINER:
|
||||
return '容器节点';
|
||||
default:
|
||||
return '其他节点';
|
||||
}
|
||||
};
|
||||
|
||||
// 将分组转换为 Tabs 项
|
||||
const tabItems = Object.entries(groupedNodeTypes).map(([category, types]) => ({
|
||||
key: category,
|
||||
label: getCategoryLabel(category),
|
||||
children: (
|
||||
<div className="node-panel-content">
|
||||
{types.map((nodeType) => {
|
||||
// 从graphConfig中获取样式
|
||||
const style = nodeType.graphConfig?.style || nodeType.style || {};
|
||||
console.log('节点样式:', nodeType.type, style);
|
||||
const icon = getIcon(style.icon);
|
||||
|
||||
const nodeStyle = {
|
||||
backgroundColor: style.fill || '#ffffff',
|
||||
borderColor: style.stroke || '#d9d9d9',
|
||||
borderWidth: `${style.strokeWidth || 1}px`,
|
||||
borderStyle: 'solid'
|
||||
};
|
||||
|
||||
console.log('应用的样式:', nodeType.type, nodeStyle);
|
||||
|
||||
return (
|
||||
<div
|
||||
key={nodeType.type}
|
||||
className="node-item"
|
||||
draggable
|
||||
onDragStart={(e) => {
|
||||
e.dataTransfer.effectAllowed = 'move';
|
||||
onNodeDragStart(nodeType);
|
||||
}}
|
||||
style={nodeStyle}
|
||||
>
|
||||
{icon && (
|
||||
<span
|
||||
className="node-icon"
|
||||
style={{ color: style.iconColor || '#1890ff' }}
|
||||
>
|
||||
{icon}
|
||||
</span>
|
||||
)}
|
||||
<span className="node-name">{nodeType.name}</span>
|
||||
<span className="node-desc">{nodeType.description}</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
),
|
||||
}));
|
||||
|
||||
return (
|
||||
<div className="node-panel">
|
||||
<Spin spinning={loading}>
|
||||
<Tabs
|
||||
defaultActiveKey={NodeCategory.EVENT}
|
||||
items={tabItems}
|
||||
className="node-panel-tabs"
|
||||
/>
|
||||
</Spin>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default NodePanel;
|
||||
@ -1,62 +0,0 @@
|
||||
import { BaseResponse } from '@/types/base/response';
|
||||
|
||||
/**
|
||||
* 节点样式
|
||||
*/
|
||||
export interface NodeStyle {
|
||||
fill: string; // 填充颜色
|
||||
stroke: string; // 边框颜色
|
||||
strokeWidth: number; // 边框宽度
|
||||
icon: string; // 图标名称
|
||||
iconColor: string; // 图标颜色
|
||||
}
|
||||
|
||||
/**
|
||||
* 节点类别
|
||||
*/
|
||||
export enum NodeCategory {
|
||||
EVENT = 'EVENT', // 事件节点
|
||||
TASK = 'TASK', // 任务节点
|
||||
GATEWAY = 'GATEWAY', // 网关节点
|
||||
CONTAINER = 'CONTAINER' // 容器节点
|
||||
}
|
||||
|
||||
/**
|
||||
* 节点类型
|
||||
*/
|
||||
export enum NodeTypeEnum {
|
||||
START_EVENT = 'START_EVENT', // 开始节点
|
||||
END_EVENT = 'END_EVENT', // 结束节点
|
||||
USER_TASK = 'USER_TASK', // 用户任务
|
||||
SERVICE_TASK = 'SERVICE_TASK', // 服务任务
|
||||
SCRIPT_TASK = 'SCRIPT_TASK', // 脚本任务
|
||||
EXCLUSIVE_GATEWAY = 'EXCLUSIVE_GATEWAY', // 排他网关
|
||||
PARALLEL_GATEWAY = 'PARALLEL_GATEWAY', // 并行网关
|
||||
SUB_PROCESS = 'SUB_PROCESS', // 子流程
|
||||
CALL_ACTIVITY = 'CALL_ACTIVITY' // 调用活动
|
||||
}
|
||||
|
||||
/**
|
||||
* 节点图形配置
|
||||
*/
|
||||
export interface NodeGraphConfig {
|
||||
style: NodeStyle; // 节点样式
|
||||
ports?: any[]; // 连接点配置
|
||||
shape?: string; // 节点形状
|
||||
}
|
||||
|
||||
/**
|
||||
* 节点类型定义
|
||||
*/
|
||||
export interface NodeType extends BaseResponse {
|
||||
type: NodeTypeEnum; // 节点类型
|
||||
name: string; // 节点名称
|
||||
description: string; // 节点描述
|
||||
category: NodeCategory; // 节点分类
|
||||
graphConfig: NodeGraphConfig; // 图形配置
|
||||
style?: NodeStyle; // 兼容旧版本的样式配置
|
||||
orderNum: number; // 排序号
|
||||
enabled: boolean; // 是否启用
|
||||
deleted: boolean; // 是否删除
|
||||
extraData: any; // 扩展数据
|
||||
}
|
||||
@ -1,54 +0,0 @@
|
||||
.workflow-toolbar {
|
||||
padding: 8px 16px;
|
||||
background: #fff;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
.ant-btn {
|
||||
padding: 4px 8px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
|
||||
&:hover {
|
||||
color: #1890ff;
|
||||
background: #f5f5f5;
|
||||
}
|
||||
|
||||
&[disabled] {
|
||||
color: #d9d9d9;
|
||||
background: transparent;
|
||||
cursor: not-allowed;
|
||||
|
||||
&:hover {
|
||||
color: #d9d9d9;
|
||||
background: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
&-dangerous {
|
||||
color: #ff4d4f;
|
||||
|
||||
&:hover {
|
||||
color: #ff7875;
|
||||
background: #fff1f0;
|
||||
}
|
||||
|
||||
&[disabled] {
|
||||
color: #d9d9d9;
|
||||
background: transparent;
|
||||
|
||||
&:hover {
|
||||
color: #d9d9d9;
|
||||
background: transparent;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.ant-divider {
|
||||
height: 16px;
|
||||
margin: 0 8px;
|
||||
}
|
||||
}
|
||||
@ -1,334 +0,0 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import { Space, Button, Tooltip, Divider, message } from 'antd';
|
||||
import {
|
||||
ZoomInOutlined,
|
||||
ZoomOutOutlined,
|
||||
FullscreenOutlined,
|
||||
OneToOneOutlined,
|
||||
SelectOutlined,
|
||||
DeleteOutlined,
|
||||
UndoOutlined,
|
||||
RedoOutlined,
|
||||
CopyOutlined,
|
||||
SnippetsOutlined,
|
||||
CheckCircleOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import { Graph, Cell } from '@antv/x6';
|
||||
import { Selection } from '@antv/x6-plugin-selection';
|
||||
import { History } from '@antv/x6-plugin-history';
|
||||
import { Clipboard } from '@antv/x6-plugin-clipboard';
|
||||
import { Transform } from '@antv/x6-plugin-transform';
|
||||
import { Keyboard } from '@antv/x6-plugin-keyboard';
|
||||
import { validateFlow, hasCycle } from '../../validate';
|
||||
import './index.less';
|
||||
|
||||
interface ToolbarProps {
|
||||
graph: Graph | undefined;
|
||||
}
|
||||
|
||||
const Toolbar: React.FC<ToolbarProps> = ({ graph }) => {
|
||||
useEffect(() => {
|
||||
if (graph) {
|
||||
// 启用选择插件
|
||||
graph.use(
|
||||
new Selection({
|
||||
enabled: true,
|
||||
multiple: true,
|
||||
rubberband: true,
|
||||
movable: true,
|
||||
showNodeSelectionBox: true,
|
||||
showEdgeSelectionBox: true,
|
||||
})
|
||||
);
|
||||
// 启用历史记录
|
||||
graph.use(
|
||||
new History({
|
||||
enabled: true,
|
||||
})
|
||||
);
|
||||
// 启用剪贴板
|
||||
graph.use(
|
||||
new Clipboard({
|
||||
enabled: true,
|
||||
})
|
||||
);
|
||||
// 启用变换
|
||||
graph.use(
|
||||
new Transform({
|
||||
resizing: {
|
||||
enabled: true,
|
||||
},
|
||||
rotating: {
|
||||
enabled: true,
|
||||
},
|
||||
})
|
||||
);
|
||||
// 启用键盘
|
||||
graph.use(new Keyboard({
|
||||
enabled: true,
|
||||
}));
|
||||
|
||||
// 启用画布交互
|
||||
graph.enableSelection();
|
||||
graph.enableRubberband();
|
||||
|
||||
const keyboard = graph.getPlugin<Keyboard>('keyboard');
|
||||
if (keyboard) {
|
||||
// 复制粘贴快捷键
|
||||
keyboard.bindKey(['meta+c', 'ctrl+c'], () => {
|
||||
const selection = graph.getPlugin<Selection>('selection');
|
||||
const clipboard = graph.getPlugin<Clipboard>('clipboard');
|
||||
const cells = selection?.getSelectedCells();
|
||||
if (cells?.length) {
|
||||
clipboard?.copy(cells);
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
keyboard.bindKey(['meta+v', 'ctrl+v'], () => {
|
||||
const clipboard = graph.getPlugin<Clipboard>('clipboard');
|
||||
const selection = graph.getPlugin<Selection>('selection');
|
||||
if (clipboard && !clipboard.isEmpty()) {
|
||||
const cells = clipboard.paste();
|
||||
selection?.clean();
|
||||
cells.forEach((cell: Cell) => selection?.select(cell));
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
// 全选快捷键
|
||||
keyboard.bindKey(['meta+a', 'ctrl+a'], () => {
|
||||
const selection = graph.getPlugin<Selection>('selection');
|
||||
const cells = graph.getCells();
|
||||
selection?.clean();
|
||||
cells.forEach((cell: Cell) => selection?.select(cell));
|
||||
return false;
|
||||
});
|
||||
|
||||
// 删除快捷键
|
||||
keyboard.bindKey(['backspace', 'delete'], () => {
|
||||
const selection = graph.getPlugin<Selection>('selection');
|
||||
const cells = selection?.getSelectedCells();
|
||||
if (cells?.length) {
|
||||
graph.removeCells(cells);
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
// 撤销重做快捷键
|
||||
keyboard.bindKey(['meta+z', 'ctrl+z'], () => {
|
||||
const history = graph.getPlugin<History>('history');
|
||||
if (history?.canUndo()) {
|
||||
history.undo();
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
keyboard.bindKey(['meta+shift+z', 'ctrl+shift+z'], () => {
|
||||
const history = graph.getPlugin<History>('history');
|
||||
if (history?.canRedo()) {
|
||||
history.redo();
|
||||
}
|
||||
return false;
|
||||
});
|
||||
}
|
||||
}
|
||||
}, [graph]);
|
||||
|
||||
// 缩放画布
|
||||
const zoom = (delta: number) => {
|
||||
if (!graph) return;
|
||||
const currentZoom = graph.zoom();
|
||||
const nextZoom = delta > 0
|
||||
? Math.min(2, currentZoom + 0.1) // 放大,最大2倍
|
||||
: Math.max(0.5, currentZoom - 0.1); // 缩小,最小0.5倍
|
||||
graph.zoomTo(nextZoom);
|
||||
graph.centerContent(); // 居中显示内容
|
||||
};
|
||||
|
||||
// 适应画布
|
||||
const fitContent = () => {
|
||||
if (!graph) return;
|
||||
graph.zoomToFit({ padding: 20, maxScale: 2 });
|
||||
graph.centerContent();
|
||||
};
|
||||
|
||||
// 实际大小
|
||||
const resetZoom = () => {
|
||||
if (!graph) return;
|
||||
graph.scale(1);
|
||||
graph.centerContent();
|
||||
};
|
||||
|
||||
// 全选
|
||||
const selectAll = () => {
|
||||
if (!graph) return;
|
||||
const cells = graph.getCells();
|
||||
const selection = graph.getPlugin<Selection>('selection');
|
||||
selection?.clean();
|
||||
cells.forEach((cell: Cell) => selection?.select(cell));
|
||||
};
|
||||
|
||||
// 删除选中
|
||||
const deleteSelected = () => {
|
||||
if (!graph) return;
|
||||
const selection = graph.getPlugin<Selection>('selection');
|
||||
const cells = selection?.getSelectedCells();
|
||||
if (cells?.length) {
|
||||
graph.removeCells(cells);
|
||||
}
|
||||
};
|
||||
|
||||
// 撤销
|
||||
const undo = () => {
|
||||
if (!graph) return;
|
||||
const history = graph.getPlugin<History>('history');
|
||||
if (history?.canUndo()) {
|
||||
history.undo();
|
||||
}
|
||||
};
|
||||
|
||||
// 重做
|
||||
const redo = () => {
|
||||
if (!graph) return;
|
||||
const history = graph.getPlugin<History>('history');
|
||||
if (history?.canRedo()) {
|
||||
history.redo();
|
||||
}
|
||||
};
|
||||
|
||||
// 复制
|
||||
const copy = () => {
|
||||
if (!graph) return;
|
||||
const clipboard = graph.getPlugin<Clipboard>('clipboard');
|
||||
const selection = graph.getPlugin<Selection>('selection');
|
||||
const cells = selection?.getSelectedCells();
|
||||
if (cells?.length) {
|
||||
clipboard?.copy(cells);
|
||||
}
|
||||
};
|
||||
|
||||
// 粘贴
|
||||
const paste = () => {
|
||||
if (!graph) return;
|
||||
const clipboard = graph.getPlugin<Clipboard>('clipboard');
|
||||
const selection = graph.getPlugin<Selection>('selection');
|
||||
if (clipboard && !clipboard.isEmpty()) {
|
||||
const cells = clipboard.paste();
|
||||
selection?.clean();
|
||||
cells.forEach((cell: Cell) => selection?.select(cell));
|
||||
}
|
||||
};
|
||||
|
||||
// 检查是否有选中的节点或边
|
||||
const hasSelection = () => {
|
||||
if (!graph) return false;
|
||||
const selection = graph.getPlugin<Selection>('selection');
|
||||
return (selection?.getSelectedCells().length || 0) > 0;
|
||||
};
|
||||
|
||||
// 检查是否可以粘贴
|
||||
const canPaste = () => {
|
||||
if (!graph) return false;
|
||||
const clipboard = graph.getPlugin<Clipboard>('clipboard');
|
||||
return !clipboard?.isEmpty();
|
||||
};
|
||||
|
||||
// 验证流程
|
||||
const validateWorkflow = () => {
|
||||
if (!graph) return;
|
||||
|
||||
const result = validateFlow(graph);
|
||||
const hasCycleResult = hasCycle(graph);
|
||||
|
||||
if (hasCycleResult) {
|
||||
message.error('流程图中存在循环,请检查');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!result.valid) {
|
||||
message.error(result.errors.join('\n'));
|
||||
return;
|
||||
}
|
||||
|
||||
message.success('流程验证通过');
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="workflow-toolbar">
|
||||
<Space split={<Divider type="vertical" />}>
|
||||
<Space>
|
||||
<Tooltip title="放大 (Ctrl + 鼠标滚轮)">
|
||||
<Button icon={<ZoomInOutlined />} onClick={() => zoom(0.1)} />
|
||||
</Tooltip>
|
||||
<Tooltip title="缩小 (Ctrl + 鼠标滚轮)">
|
||||
<Button icon={<ZoomOutOutlined />} onClick={() => zoom(-0.1)} />
|
||||
</Tooltip>
|
||||
<Tooltip title="适应画布 (双击空白处)">
|
||||
<Button icon={<FullscreenOutlined />} onClick={fitContent} />
|
||||
</Tooltip>
|
||||
<Tooltip title="实际大小 (100%)">
|
||||
<Button icon={<OneToOneOutlined />} onClick={resetZoom} />
|
||||
</Tooltip>
|
||||
</Space>
|
||||
<Space>
|
||||
<Tooltip title="全选 (Ctrl + A)">
|
||||
<Button icon={<SelectOutlined />} onClick={selectAll} />
|
||||
</Tooltip>
|
||||
<Tooltip title="删除 (Delete)">
|
||||
<Button
|
||||
icon={<DeleteOutlined />}
|
||||
onClick={deleteSelected}
|
||||
disabled={!hasSelection()}
|
||||
/>
|
||||
</Tooltip>
|
||||
</Space>
|
||||
<Space>
|
||||
<Tooltip title="撤销 (Ctrl + Z)">
|
||||
<Button icon={<UndoOutlined />} onClick={undo} />
|
||||
</Tooltip>
|
||||
<Tooltip title="重做 (Ctrl + Shift + Z)">
|
||||
<Button icon={<RedoOutlined />} onClick={redo} />
|
||||
</Tooltip>
|
||||
</Space>
|
||||
<Space>
|
||||
<Tooltip title="复制 (Ctrl + C)">
|
||||
<Button
|
||||
icon={<CopyOutlined />}
|
||||
onClick={copy}
|
||||
disabled={!hasSelection()}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Tooltip title="粘贴 (Ctrl + V)">
|
||||
<Button
|
||||
icon={<SnippetsOutlined />}
|
||||
onClick={paste}
|
||||
disabled={!canPaste()}
|
||||
/>
|
||||
</Tooltip>
|
||||
</Space>
|
||||
<Space>
|
||||
<Tooltip title="验证流程图是否符合规范">
|
||||
<Button
|
||||
icon={<CheckCircleOutlined />}
|
||||
onClick={validateWorkflow}
|
||||
type="primary"
|
||||
size="middle"
|
||||
style={{
|
||||
backgroundColor: '#52c41a',
|
||||
borderColor: '#52c41a',
|
||||
fontWeight: 500,
|
||||
boxShadow: '0 2px 0 rgba(82, 196, 26, 0.1)',
|
||||
marginLeft: 8
|
||||
}}
|
||||
>
|
||||
验证流程
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</Space>
|
||||
</Space>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Toolbar;
|
||||
@ -1,106 +0,0 @@
|
||||
:global {
|
||||
.workflow-designer {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
&-container {
|
||||
height: calc(100vh - 170px);
|
||||
height: 100%;
|
||||
background: #fff;
|
||||
border-radius: 2px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
&-sider {
|
||||
background: #fff;
|
||||
border-right: 1px solid #f0f0f0;
|
||||
overflow: auto;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
&-content {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
padding: 16px;
|
||||
flex: 1;
|
||||
min-height: 0; // 允许内容区域收缩
|
||||
padding: 8px; // 减小内边距
|
||||
background: #fafafa;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
&-graph {
|
||||
height: 100%;
|
||||
flex: 1;
|
||||
min-height: 0; // 允许内容区域收缩
|
||||
background: #fff;
|
||||
border: 1px solid #f0f0f0;
|
||||
border-radius: 2px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
&-minimap {
|
||||
position: absolute;
|
||||
right: 20px;
|
||||
bottom: 20px;
|
||||
border: 1px solid #e5e5e5;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
|
||||
background-color: #fff;
|
||||
overflow: hidden;
|
||||
z-index: 999;
|
||||
|
||||
:global {
|
||||
.x6-widget-minimap-viewport {
|
||||
border: 2px solid #1890ff;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.x6-widget-minimap-viewport-zoom {
|
||||
border-color: #52c41a;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.ant-card-body {
|
||||
height: calc(100% - 57px);
|
||||
padding: 0;
|
||||
|
||||
.ant-card {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.ant-card-head {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.ant-card-body {
|
||||
flex: 1;
|
||||
padding: 0;
|
||||
min-height: 0; // 允许内容区域收缩
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
||||
.ant-layout {
|
||||
height: 100%;
|
||||
flex: 1;
|
||||
min-height: 0; // 允许内容区域收缩
|
||||
background: #fff;
|
||||
display: flex;
|
||||
|
||||
.ant-layout-content {
|
||||
flex: 1;
|
||||
position: relative;
|
||||
padding: 8px; // 减小内边距
|
||||
background: #fafafa;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,13 +0,0 @@
|
||||
declare namespace DesignerModuleLessNamespace {
|
||||
export interface IDesignerModuleLess {
|
||||
container: string;
|
||||
sider: string;
|
||||
content: string;
|
||||
graph: string;
|
||||
}
|
||||
}
|
||||
|
||||
declare module '*.less' {
|
||||
const content: DesignerModuleLessNamespace.IDesignerModuleLess;
|
||||
export default content;
|
||||
}
|
||||
@ -1,649 +0,0 @@
|
||||
import React, {useEffect, useRef, useState} from 'react';
|
||||
import {useNavigate, useParams} from 'react-router-dom';
|
||||
import {Button, Card, Layout, message, Space, Spin, Drawer, Form, Dropdown} from 'antd';
|
||||
import {ArrowLeftOutlined, SaveOutlined, DeleteOutlined, CopyOutlined, SettingOutlined, ClearOutlined, FullscreenOutlined} from '@ant-design/icons';
|
||||
import {getDefinition, updateDefinition, getNodeTypes} from '../../service';
|
||||
import {WorkflowDefinition, WorkflowStatus} from '../../../Workflow/types';
|
||||
import {Graph, Node, Cell, Edge} from '@antv/x6';
|
||||
import '@antv/x6-react-shape';
|
||||
import './index.module.less';
|
||||
import NodePanel from './components/NodePanel';
|
||||
import NodeConfig from './components/NodeConfig';
|
||||
import Toolbar from './components/Toolbar';
|
||||
import EdgeConfig from './components/EdgeConfig';
|
||||
import { validateFlow, hasCycle } from './validate';
|
||||
import { generateNodeStyle, generatePorts, calculateCanvasPosition, getNodeShape, getNodeSize } from './utils/nodeUtils';
|
||||
import { initGraph } from './utils/graphUtils';
|
||||
import { NodeType, NodeData, WorkflowGraph } from './types';
|
||||
|
||||
const {Sider, Content} = Layout;
|
||||
|
||||
interface NodeData {
|
||||
type: string;
|
||||
name?: string;
|
||||
description?: string;
|
||||
config: {
|
||||
executor?: string;
|
||||
retryTimes?: number;
|
||||
retryInterval?: number;
|
||||
script?: string;
|
||||
timeout?: number;
|
||||
workingDirectory?: string;
|
||||
environment?: string;
|
||||
successExitCode?: string;
|
||||
[key: string]: any;
|
||||
};
|
||||
}
|
||||
|
||||
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 [graph, setGraph] = useState<Graph>();
|
||||
const draggedNodeRef = useRef<NodeType>();
|
||||
const [configVisible, setConfigVisible] = useState(false);
|
||||
const [currentNode, setCurrentNode] = useState<Node>();
|
||||
const [currentNodeType, setCurrentNodeType] = useState<NodeType>();
|
||||
const [nodeTypes, setNodeTypes] = useState<NodeType[]>([]);
|
||||
const [form] = Form.useForm();
|
||||
const [currentEdge, setCurrentEdge] = useState<Edge>();
|
||||
const [edgeConfigVisible, setEdgeConfigVisible] = useState(false);
|
||||
const [edgeForm] = Form.useForm();
|
||||
|
||||
// 右键菜单状态
|
||||
const [contextMenu, setContextMenu] = useState<{
|
||||
x: number;
|
||||
y: number;
|
||||
visible: boolean;
|
||||
type: 'node' | 'edge' | 'canvas';
|
||||
cell?: Cell;
|
||||
}>({
|
||||
x: 0,
|
||||
y: 0,
|
||||
visible: false,
|
||||
type: 'canvas',
|
||||
});
|
||||
|
||||
// 右键菜单项
|
||||
const menuItems = {
|
||||
node: [
|
||||
{
|
||||
key: 'delete',
|
||||
label: '删除节点',
|
||||
icon: <DeleteOutlined />,
|
||||
onClick: () => {
|
||||
if (contextMenu.cell) {
|
||||
contextMenu.cell.remove();
|
||||
}
|
||||
setContextMenu(prev => ({ ...prev, visible: false }));
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'copy',
|
||||
label: '复制节点',
|
||||
icon: <CopyOutlined />,
|
||||
onClick: () => {
|
||||
if (contextMenu.cell && contextMenu.cell.isNode()) {
|
||||
const pos = contextMenu.cell.position();
|
||||
const newCell = contextMenu.cell.clone();
|
||||
newCell.position(pos.x + 20, pos.y + 20);
|
||||
graph?.addCell(newCell);
|
||||
}
|
||||
setContextMenu(prev => ({ ...prev, visible: false }));
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'config',
|
||||
label: '配置节点',
|
||||
icon: <SettingOutlined />,
|
||||
onClick: () => {
|
||||
if (contextMenu.cell && contextMenu.cell.isNode()) {
|
||||
setCurrentNode(contextMenu.cell);
|
||||
const data = contextMenu.cell.getData() as NodeData;
|
||||
console.log(data)
|
||||
// const nodeType = nodeTypes.find(type => type.type === data.type);
|
||||
// if (nodeType) {
|
||||
// setCurrentNodeType(nodeType);
|
||||
// const formValues = {
|
||||
// name: data.name || nodeType.name,
|
||||
// description: data.description,
|
||||
// ...data.config
|
||||
// };
|
||||
// form.setFieldsValue(formValues);
|
||||
// }
|
||||
setConfigVisible(true);
|
||||
}
|
||||
setContextMenu(prev => ({ ...prev, visible: false }));
|
||||
},
|
||||
},
|
||||
],
|
||||
edge: [
|
||||
{
|
||||
key: 'config',
|
||||
label: '配置连线',
|
||||
icon: <SettingOutlined />,
|
||||
onClick: () => {
|
||||
if (contextMenu.cell && contextMenu.cell.isEdge()) {
|
||||
handleEdgeConfig(contextMenu.cell);
|
||||
}
|
||||
setContextMenu(prev => ({ ...prev, visible: false }));
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'delete',
|
||||
label: '删除连线',
|
||||
icon: <DeleteOutlined />,
|
||||
onClick: () => {
|
||||
if (contextMenu.cell) {
|
||||
contextMenu.cell.remove();
|
||||
}
|
||||
setContextMenu(prev => ({ ...prev, visible: false }));
|
||||
},
|
||||
},
|
||||
],
|
||||
canvas: [
|
||||
{
|
||||
key: 'clear',
|
||||
label: '清空画布',
|
||||
icon: <ClearOutlined />,
|
||||
onClick: () => {
|
||||
graph?.clearCells();
|
||||
setContextMenu(prev => ({ ...prev, visible: false }));
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'fit',
|
||||
label: '适应画布',
|
||||
icon: <FullscreenOutlined />,
|
||||
onClick: () => {
|
||||
graph?.zoomToFit({ padding: 20 });
|
||||
setContextMenu(prev => ({ ...prev, visible: false }));
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
// 获取所有节点类型
|
||||
const fetchNodeTypes = async () => {
|
||||
try {
|
||||
const response = await getNodeTypes({enabled: true});
|
||||
if (response?.content && Array.isArray(response.content)) {
|
||||
setNodeTypes(response.content);
|
||||
return response.content;
|
||||
} else {
|
||||
console.error('获取节点类型返回格式错误:', response);
|
||||
message.error('获取节点类型失败:返回格式错误');
|
||||
return [];
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取节点类型失败:', error);
|
||||
message.error('获取节点类型失败');
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
// 首次加载获取节点类型
|
||||
useEffect(() => {
|
||||
fetchNodeTypes();
|
||||
}, []);
|
||||
|
||||
// 初始化图形
|
||||
useEffect(() => {
|
||||
if (detail && containerRef.current) {
|
||||
const graph = initGraph({
|
||||
container: containerRef.current,
|
||||
miniMapContainer: document.getElementById('workflow-minimap')!,
|
||||
onContextMenu: (params) => setContextMenu(params),
|
||||
onNodeClick: (node) => {
|
||||
setCurrentNode(node);
|
||||
const data = node.getData() as NodeData;
|
||||
console.log('Node clicked, data:', data);
|
||||
|
||||
if (data) {
|
||||
const nodeType = nodeTypes.find(type => type.type === data.type);
|
||||
if (nodeType) {
|
||||
setCurrentNodeType(nodeType);
|
||||
const formValues = {
|
||||
name: data.name || nodeType.name,
|
||||
description: data.description,
|
||||
...data.config
|
||||
};
|
||||
console.log('Setting form values:', formValues);
|
||||
form.setFieldsValue(formValues);
|
||||
setConfigVisible(true);
|
||||
} else {
|
||||
message.error('未找到对应的节点类型');
|
||||
}
|
||||
}
|
||||
},
|
||||
onGraphChange: (g) => setGraph(g),
|
||||
onDragOver: handleDragOver,
|
||||
onDrop: handleDrop,
|
||||
flowDetail: detail
|
||||
});
|
||||
|
||||
graphRef.current = graph;
|
||||
setGraph(graph);
|
||||
|
||||
// 加载流程图数据
|
||||
if (detail) {
|
||||
loadGraphData(graph, detail);
|
||||
}
|
||||
|
||||
// 清理函数
|
||||
return () => {
|
||||
if (containerRef.current) {
|
||||
containerRef.current.removeEventListener('dragover', handleDragOver);
|
||||
containerRef.current.removeEventListener('drop', handleDrop);
|
||||
}
|
||||
if (graphRef.current) {
|
||||
graphRef.current.dispose();
|
||||
}
|
||||
};
|
||||
}
|
||||
}, [detail, nodeTypes]);
|
||||
|
||||
// 处理拖拽移动
|
||||
const handleDragOver = (e: DragEvent) => {
|
||||
e.preventDefault();
|
||||
e.dataTransfer!.dropEffect = 'copy';
|
||||
};
|
||||
|
||||
// 处理拖拽放置
|
||||
const handleDrop = (e: DragEvent) => {
|
||||
e.preventDefault();
|
||||
const nodeType = draggedNodeRef.current;
|
||||
if (!nodeType || !graphRef.current || !containerRef.current) {
|
||||
message.error('无效的节点类型或画布未初始化');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const rect = containerRef.current.getBoundingClientRect();
|
||||
const matrix = graphRef.current.matrix();
|
||||
|
||||
// 使用新的工具函数计算画布位置
|
||||
const dropPosition = calculateCanvasPosition(
|
||||
e.clientX,
|
||||
e.clientY,
|
||||
rect,
|
||||
{
|
||||
scale: matrix.a,
|
||||
offsetX: matrix.e,
|
||||
offsetY: matrix.f,
|
||||
}
|
||||
);
|
||||
|
||||
// 获取节点大小
|
||||
let size;
|
||||
if (nodeType.type === 'start' || nodeType.type === 'end') {
|
||||
size = { width: 40, height: 40 };
|
||||
} else {
|
||||
size = { width: 100, height: 60 };
|
||||
}
|
||||
|
||||
// 创建节点
|
||||
const node = graphRef.current.addNode({
|
||||
shape: getNodeShape(nodeType.type),
|
||||
attrs: generateNodeStyle(nodeType.type, nodeType.name),
|
||||
data: {
|
||||
type: nodeType.type,
|
||||
config: {
|
||||
type: nodeType.type,
|
||||
name: nodeType.name
|
||||
}
|
||||
},
|
||||
position: dropPosition,
|
||||
size: size,
|
||||
ports: generatePorts(nodeType.type),
|
||||
});
|
||||
|
||||
// 选中新创建的节点并打开配置
|
||||
graphRef.current.cleanSelection();
|
||||
graphRef.current.select(node);
|
||||
setCurrentNode(node);
|
||||
setCurrentNodeType(nodeType);
|
||||
form.setFieldsValue({ name: nodeType.name });
|
||||
setConfigVisible(true);
|
||||
|
||||
message.success('节点创建成功');
|
||||
} catch (error) {
|
||||
console.error('创建节点失败:', error);
|
||||
message.error('创建节点失败');
|
||||
}
|
||||
};
|
||||
|
||||
// 验证Shell节点配置
|
||||
function validateShellConfig(config: any): boolean {
|
||||
if (!config.script?.trim()) {
|
||||
throw new Error('脚本内容不能为空');
|
||||
}
|
||||
if (config.timeout !== undefined && config.timeout <= 0) {
|
||||
throw new Error('超时时间必须大于0');
|
||||
}
|
||||
if (config.retryTimes !== undefined && config.retryTimes < 0) {
|
||||
throw new Error('重试次数不能为负数');
|
||||
}
|
||||
if (config.retryInterval !== undefined && config.retryInterval < 0) {
|
||||
throw new Error('重试间不能为负数');
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// 处理配置保存
|
||||
const handleConfigSave = async () => {
|
||||
try {
|
||||
const values = await form.validateFields();
|
||||
if (currentNode) {
|
||||
const data = currentNode.getData() as NodeData;
|
||||
const {name, description, ...config} = values;
|
||||
|
||||
// 如果是Shell节点,验证配置
|
||||
if (data.type === 'SHELL' && config.executor === 'SHELL') {
|
||||
validateShellConfig(config);
|
||||
}
|
||||
|
||||
// 更新节点数据
|
||||
currentNode.setData({
|
||||
...data,
|
||||
name,
|
||||
description,
|
||||
config,
|
||||
});
|
||||
|
||||
// 更新节点标签
|
||||
currentNode.setAttrByPath('label/text', name);
|
||||
|
||||
message.success('配置保存成功');
|
||||
setConfigVisible(false);
|
||||
}
|
||||
} catch (error) {
|
||||
// 表单验证失败或Shell配置验证失败
|
||||
message.error((error as Error).message);
|
||||
}
|
||||
};
|
||||
|
||||
// 获取详情
|
||||
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 result = validateFlow(graphRef.current);
|
||||
const hasCycleResult = hasCycle(graphRef.current);
|
||||
|
||||
if (hasCycleResult) {
|
||||
message.error('流程图中存在循环,请检查');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!result.valid) {
|
||||
message.error(result.errors.join('\n'));
|
||||
return;
|
||||
}
|
||||
|
||||
const graphData = graphRef.current.toJSON();
|
||||
const workflowGraph = convertGraphToWorkflowGraph(graphData);
|
||||
|
||||
// 构建更新数据
|
||||
const data = {
|
||||
...detail,
|
||||
graph: workflowGraph,
|
||||
bpmnJson: graphData
|
||||
};
|
||||
|
||||
await updateDefinition(parseInt(id), data);
|
||||
message.success('保存成功');
|
||||
} catch (error) {
|
||||
message.error('保存失败');
|
||||
console.error('保存失败:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// 处理返回
|
||||
const handleBack = () => {
|
||||
navigate('/workflow/definition');
|
||||
};
|
||||
|
||||
// 首次加载
|
||||
useEffect(() => {
|
||||
fetchDetail();
|
||||
}, [id]);
|
||||
|
||||
// 处理节点拖拽开始
|
||||
const handleNodeDragStart = (nodeType: NodeType) => {
|
||||
draggedNodeRef.current = nodeType;
|
||||
};
|
||||
|
||||
// 加载流程图数据
|
||||
const loadGraphData = (graph: Graph, detail: WorkflowDefinition) => {
|
||||
try {
|
||||
console.log('Loading graph data:', detail);
|
||||
if (detail.graph) {
|
||||
const cells = [
|
||||
...detail.graph.nodes.map(node => ({
|
||||
id: node.id,
|
||||
shape: getNodeShape(node.type),
|
||||
attrs: generateNodeStyle(node.type, node.name || node.type),
|
||||
data: {
|
||||
type: node.type,
|
||||
config: node.config
|
||||
},
|
||||
position: node.position,
|
||||
size: node.size // 直接使用数据中的 size
|
||||
})),
|
||||
...detail.graph.edges.map(edge => ({
|
||||
id: edge.id,
|
||||
shape: 'edge',
|
||||
source: edge.source,
|
||||
target: edge.target,
|
||||
data: {
|
||||
type: 'edge',
|
||||
label: edge.name,
|
||||
config: edge.config
|
||||
}
|
||||
}))
|
||||
];
|
||||
graph.fromJSON({ cells });
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载流程图数据失败:', error);
|
||||
message.error('加载流程图数据失败');
|
||||
}
|
||||
};
|
||||
|
||||
// 添加连线配置处理函数
|
||||
const handleEdgeConfig = (edge: Edge) => {
|
||||
setCurrentEdge(edge);
|
||||
const data = edge.getData() || {};
|
||||
edgeForm.setFieldsValue(data);
|
||||
setEdgeConfigVisible(true);
|
||||
};
|
||||
|
||||
// 添加连线配置保存函数
|
||||
const handleEdgeConfigSubmit = async () => {
|
||||
if (!currentEdge) return;
|
||||
|
||||
try {
|
||||
const values = await edgeForm.validateFields();
|
||||
currentEdge.setData(values);
|
||||
|
||||
// 更新连线标签
|
||||
if (values.description) {
|
||||
currentEdge.setLabelAt(0, values.description);
|
||||
}
|
||||
|
||||
setEdgeConfigVisible(false);
|
||||
} catch (error) {
|
||||
// 表单验证失败
|
||||
}
|
||||
};
|
||||
|
||||
// 监听画布大小变化
|
||||
useEffect(() => {
|
||||
if (!containerRef.current) return;
|
||||
|
||||
const resizeObserver = new ResizeObserver(() => {
|
||||
if (graphRef.current) {
|
||||
const container = containerRef.current;
|
||||
if (container) {
|
||||
const { width, height } = container.getBoundingClientRect();
|
||||
graphRef.current.resize(width, height);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
resizeObserver.observe(containerRef.current);
|
||||
|
||||
return () => {
|
||||
resizeObserver.disconnect();
|
||||
};
|
||||
}, []);
|
||||
|
||||
// 转换图形数据为工作流图数据
|
||||
const convertGraphToWorkflowGraph = (graphData: any): WorkflowGraph => {
|
||||
const nodes = graphData.cells
|
||||
.filter((cell: any) => cell.shape !== 'edge')
|
||||
.map((node: any) => ({
|
||||
id: node.id,
|
||||
type: node.data.type,
|
||||
name: node.data?.name || '',
|
||||
position: node.position,
|
||||
size: node.size,
|
||||
config: node.data?.config || {}
|
||||
}));
|
||||
|
||||
const edges = graphData.cells
|
||||
.filter((cell: any) => cell.shape === 'edge')
|
||||
.map((edge: any) => ({
|
||||
id: edge.id,
|
||||
source: edge.source.cell,
|
||||
target: edge.target.cell,
|
||||
name: edge.data?.name || '',
|
||||
config: edge.data?.config || {}
|
||||
}));
|
||||
|
||||
return { nodes, edges };
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div style={{textAlign: 'center', padding: 100}}>
|
||||
<Spin size="large"/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Card
|
||||
title={
|
||||
<div style={{ display: 'flex', justifyContent: 'flex-end', alignItems: 'center' }}>
|
||||
<Space>
|
||||
<Button type="primary" icon={<SaveOutlined/>} onClick={handleSave}>保存</Button>
|
||||
<Button icon={<ArrowLeftOutlined/>} onClick={() => navigate(-1)}>返回</Button>
|
||||
</Space>
|
||||
</div>
|
||||
}
|
||||
className="workflow-designer"
|
||||
>
|
||||
<Layout>
|
||||
<Sider width={250} className="workflow-designer-sider">
|
||||
<NodePanel onNodeDragStart={handleNodeDragStart}/>
|
||||
</Sider>
|
||||
<Layout>
|
||||
<Toolbar graph={graph}/>
|
||||
<Content className="workflow-designer-content">
|
||||
<div ref={containerRef} className="workflow-designer-graph"/>
|
||||
<div id="workflow-minimap" className="workflow-designer-minimap"/>
|
||||
<Dropdown
|
||||
menu={{
|
||||
items: menuItems[contextMenu.type],
|
||||
onClick: ({ key, domEvent }) => {
|
||||
domEvent.stopPropagation();
|
||||
},
|
||||
}}
|
||||
open={contextMenu.visible}
|
||||
trigger={['contextMenu']}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
position: 'fixed',
|
||||
left: contextMenu.x,
|
||||
top: contextMenu.y,
|
||||
width: 1,
|
||||
height: 1,
|
||||
}}
|
||||
/>
|
||||
</Dropdown>
|
||||
</Content>
|
||||
</Layout>
|
||||
</Layout>
|
||||
|
||||
<Drawer
|
||||
title="节点配置"
|
||||
width={400}
|
||||
open={configVisible}
|
||||
onClose={() => setConfigVisible(false)}
|
||||
extra={
|
||||
<Space>
|
||||
<Button onClick={() => setConfigVisible(false)}>取消</Button>
|
||||
<Button type="primary" onClick={handleConfigSave}>
|
||||
确定
|
||||
</Button>
|
||||
</Space>
|
||||
}
|
||||
>
|
||||
{currentNodeType && (
|
||||
<NodeConfig nodeType={currentNodeType} form={form}/>
|
||||
)}
|
||||
</Drawer>
|
||||
|
||||
<Drawer
|
||||
title="连线配置"
|
||||
placement="right"
|
||||
width={400}
|
||||
onClose={() => setEdgeConfigVisible(false)}
|
||||
open={edgeConfigVisible}
|
||||
extra={
|
||||
<Space>
|
||||
<Button onClick={() => setEdgeConfigVisible(false)}>取消</Button>
|
||||
<Button type="primary" onClick={handleEdgeConfigSubmit}>
|
||||
确定
|
||||
</Button>
|
||||
</Space>
|
||||
}
|
||||
>
|
||||
{currentEdge && (
|
||||
<EdgeConfig
|
||||
edge={currentEdge}
|
||||
form={edgeForm}
|
||||
onValuesChange={(changedValues, allValues) => {
|
||||
if (changedValues.description) {
|
||||
currentEdge.setLabelAt(0, changedValues.description);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Drawer>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default FlowDesigner;
|
||||
@ -1,144 +0,0 @@
|
||||
/**
|
||||
* 位置信息
|
||||
*/
|
||||
export interface Position {
|
||||
x: number;
|
||||
y: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 尺寸信息
|
||||
*/
|
||||
export interface Size {
|
||||
width: number;
|
||||
height: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 节点类型枚举
|
||||
*/
|
||||
export enum NodeType {
|
||||
START = 'startEvent',
|
||||
END = 'endEvent',
|
||||
USER_TASK = 'userTask',
|
||||
SERVICE_TASK = 'serviceTask',
|
||||
SCRIPT_TASK = 'scriptTask',
|
||||
EXCLUSIVE_GATEWAY = 'exclusiveGateway',
|
||||
PARALLEL_GATEWAY = 'parallelGateway',
|
||||
SUBPROCESS = 'subProcess',
|
||||
CALL_ACTIVITY = 'callActivity'
|
||||
}
|
||||
|
||||
/**
|
||||
* 节点配置
|
||||
*/
|
||||
export interface NodeConfig {
|
||||
size?: Size;
|
||||
shape?: 'circle' | 'rect' | 'polygon';
|
||||
theme?: {
|
||||
fill: string;
|
||||
stroke: string;
|
||||
};
|
||||
label?: string;
|
||||
extras?: {
|
||||
rx?: number;
|
||||
ry?: number;
|
||||
icon?: {
|
||||
'xlink:href': string;
|
||||
width: number;
|
||||
height: number;
|
||||
x: number;
|
||||
y: number;
|
||||
};
|
||||
};
|
||||
assignee?: string;
|
||||
candidateUsers?: string[];
|
||||
candidateGroups?: string[];
|
||||
dueDate?: string;
|
||||
priority?: number;
|
||||
formKey?: string;
|
||||
executor?: string;
|
||||
retryTimes?: number;
|
||||
retryInterval?: number;
|
||||
script?: string;
|
||||
timeout?: number;
|
||||
workingDirectory?: string;
|
||||
environment?: string;
|
||||
successExitCode?: string;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 工作流节点
|
||||
*/
|
||||
export interface WorkflowNode {
|
||||
id: string;
|
||||
type: NodeType;
|
||||
name: string;
|
||||
position: Position;
|
||||
size: Size;
|
||||
config?: NodeConfig;
|
||||
properties?: Record<string, any>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 工作流边
|
||||
*/
|
||||
export interface WorkflowEdge {
|
||||
id: string;
|
||||
source: string;
|
||||
target: string;
|
||||
sourcePortId?: string;
|
||||
targetPortId?: string;
|
||||
name?: string;
|
||||
condition?: string;
|
||||
description?: string;
|
||||
priority?: number;
|
||||
config?: {
|
||||
condition?: string;
|
||||
expression?: string;
|
||||
type?: 'sequence' | 'message' | 'association';
|
||||
[key: string]: any;
|
||||
};
|
||||
properties?: Record<string, any>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 工作流图形
|
||||
*/
|
||||
export interface WorkflowGraph {
|
||||
nodes: WorkflowNode[];
|
||||
edges: WorkflowEdge[];
|
||||
properties?: WorkflowProperties;
|
||||
}
|
||||
|
||||
/**
|
||||
* 工作流属性
|
||||
*/
|
||||
export interface WorkflowProperties {
|
||||
name: string;
|
||||
key?: string;
|
||||
description?: string;
|
||||
version?: number;
|
||||
category?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 节点数据
|
||||
*/
|
||||
export interface NodeData {
|
||||
type: string;
|
||||
name?: string;
|
||||
description?: string;
|
||||
config?: NodeConfig;
|
||||
}
|
||||
|
||||
/**
|
||||
* 边数据
|
||||
*/
|
||||
export interface EdgeData {
|
||||
condition?: string;
|
||||
description?: string;
|
||||
priority?: number;
|
||||
}
|
||||
@ -1,24 +0,0 @@
|
||||
export class WorkflowError extends Error {
|
||||
constructor(message: string, public code: string) {
|
||||
super(message);
|
||||
this.name = 'WorkflowError';
|
||||
}
|
||||
}
|
||||
|
||||
export class NodeConfigError extends WorkflowError {
|
||||
constructor(message: string) {
|
||||
super(message, 'NODE_CONFIG_ERROR');
|
||||
this.name = 'NodeConfigError';
|
||||
}
|
||||
}
|
||||
|
||||
export class NodeCreationError extends WorkflowError {
|
||||
constructor(message: string) {
|
||||
super(message, 'NODE_CREATION_ERROR');
|
||||
this.name = 'NodeCreationError';
|
||||
}
|
||||
}
|
||||
|
||||
export const isWorkflowError = (error: unknown): error is WorkflowError => {
|
||||
return error instanceof WorkflowError;
|
||||
};
|
||||
@ -1,379 +0,0 @@
|
||||
import { Graph, Node, Edge } from '@antv/x6';
|
||||
import { Selection } from '@antv/x6-plugin-selection';
|
||||
import { Keyboard } from '@antv/x6-plugin-keyboard';
|
||||
import { Clipboard } from '@antv/x6-plugin-clipboard';
|
||||
import { History } from '@antv/x6-plugin-history';
|
||||
import { Transform } from '@antv/x6-plugin-transform';
|
||||
import { Snapline } from '@antv/x6-plugin-snapline';
|
||||
import { MiniMap } from '@antv/x6-plugin-minimap';
|
||||
import {
|
||||
NodeData,
|
||||
} from '../types';
|
||||
import {WorkflowDefinition} from "@/pages/Workflow/Definition/types";
|
||||
|
||||
// Initialize graph with enhanced configuration
|
||||
export const initGraph = ({
|
||||
container,
|
||||
miniMapContainer,
|
||||
onContextMenu,
|
||||
onNodeClick,
|
||||
onGraphChange,
|
||||
onDragOver,
|
||||
onDrop,
|
||||
flowDetail
|
||||
}: {
|
||||
container: HTMLDivElement;
|
||||
miniMapContainer: HTMLElement;
|
||||
onContextMenu: (params: any) => void;
|
||||
onNodeClick: (node: Node) => void;
|
||||
onGraphChange: (graph: Graph) => void;
|
||||
onDragOver: (e: DragEvent) => void;
|
||||
onDrop: (e: DragEvent) => void;
|
||||
flowDetail?: WorkflowDefinition;
|
||||
}): Graph => {
|
||||
const graph = new Graph({
|
||||
container,
|
||||
grid: {
|
||||
size: 10,
|
||||
visible: true,
|
||||
type: 'mesh',
|
||||
args: {
|
||||
color: '#cccccc',
|
||||
thickness: 1,
|
||||
},
|
||||
},
|
||||
mousewheel: {
|
||||
enabled: true,
|
||||
modifiers: [],
|
||||
minScale: 0.2,
|
||||
maxScale: 2,
|
||||
factor: 1.1,
|
||||
},
|
||||
scroller: {
|
||||
enabled: true,
|
||||
pannable: true,
|
||||
autoResize: true,
|
||||
padding: 50,
|
||||
},
|
||||
panning: {
|
||||
enabled: true,
|
||||
eventTypes: ['rightMouseDown'],
|
||||
},
|
||||
connecting: {
|
||||
snap: true,
|
||||
allowBlank: false,
|
||||
allowLoop: false,
|
||||
allowNode: false,
|
||||
allowEdge: false,
|
||||
connector: {
|
||||
name: 'rounded',
|
||||
args: {
|
||||
radius: 8,
|
||||
},
|
||||
},
|
||||
router: {
|
||||
name: 'manhattan',
|
||||
args: {
|
||||
padding: 1,
|
||||
},
|
||||
},
|
||||
validateConnection({sourceCell, targetCell, sourceMagnet, targetMagnet}) {
|
||||
if (sourceCell === targetCell) {
|
||||
return false;
|
||||
}
|
||||
if (!sourceMagnet || !targetMagnet) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
},
|
||||
defaultEdge: {
|
||||
attrs: {
|
||||
line: {
|
||||
stroke: '#5F95FF',
|
||||
strokeWidth: 1,
|
||||
targetMarker: {
|
||||
name: 'classic',
|
||||
size: 8,
|
||||
},
|
||||
},
|
||||
},
|
||||
router: {
|
||||
name: 'manhattan',
|
||||
args: {
|
||||
padding: 1,
|
||||
},
|
||||
},
|
||||
connector: {
|
||||
name: 'rounded',
|
||||
args: {
|
||||
radius: 8,
|
||||
},
|
||||
},
|
||||
},
|
||||
highlighting: {
|
||||
magnetAvailable: {
|
||||
name: 'stroke',
|
||||
args: {
|
||||
padding: 4,
|
||||
attrs: {
|
||||
strokeWidth: 4,
|
||||
stroke: '#52c41a',
|
||||
},
|
||||
},
|
||||
},
|
||||
magnetAdsorbed: {
|
||||
name: 'stroke',
|
||||
args: {
|
||||
padding: 4,
|
||||
attrs: {
|
||||
strokeWidth: 4,
|
||||
stroke: '#1890ff',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
keyboard: true,
|
||||
clipboard: true,
|
||||
history: true,
|
||||
selecting: {
|
||||
enabled: true,
|
||||
multiple: true,
|
||||
rubberband: true,
|
||||
movable: true,
|
||||
showNodeSelectionBox: true,
|
||||
},
|
||||
snapline: true,
|
||||
background: {
|
||||
color: '#ffffff',
|
||||
},
|
||||
});
|
||||
|
||||
// Enable plugins with enhanced configuration
|
||||
graph.use(
|
||||
new Selection({
|
||||
enabled: true,
|
||||
multiple: true,
|
||||
rubberband: true,
|
||||
rubberEdge: true,
|
||||
movable: true,
|
||||
showNodeSelectionBox: true,
|
||||
showEdgeSelectionBox: true,
|
||||
selectCellOnMoved: false,
|
||||
selectEdgeOnMoved: false,
|
||||
selectNodeOnMoved: false,
|
||||
className: 'node-selected',
|
||||
strict: true,
|
||||
})
|
||||
);
|
||||
|
||||
// Initialize other plugins
|
||||
graph.use(new Keyboard({ enabled: true }));
|
||||
graph.use(new Clipboard({ enabled: true }));
|
||||
graph.use(new History({ enabled: true }));
|
||||
graph.use(
|
||||
new Transform({
|
||||
resizing: {
|
||||
enabled: true,
|
||||
minWidth: 1,
|
||||
minHeight: 1,
|
||||
orthogonal: true,
|
||||
restricted: true,
|
||||
},
|
||||
rotating: {
|
||||
enabled: true,
|
||||
grid: 15,
|
||||
},
|
||||
})
|
||||
);
|
||||
graph.use(new Snapline({ enabled: true }));
|
||||
|
||||
// Initialize minimap if container provided
|
||||
if (miniMapContainer) {
|
||||
graph.use(
|
||||
new MiniMap({
|
||||
container: miniMapContainer,
|
||||
width: 200,
|
||||
height: 150,
|
||||
padding: 20,
|
||||
scalable: true,
|
||||
minScale: 0.1,
|
||||
maxScale: 3,
|
||||
fitToContent: true,
|
||||
viewport: {
|
||||
padding: 10,
|
||||
fitOnViewportChanged: true,
|
||||
},
|
||||
graphOptions: {
|
||||
async: true,
|
||||
grid: false,
|
||||
background: { color: '#f5f5f5' },
|
||||
interacting: { nodeMovable: false },
|
||||
connecting: { enabled: false },
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
// Update minimap on graph changes
|
||||
graph.on('scale translate', () => {
|
||||
if (graph.minimap) {
|
||||
graph.minimap.updateViewport();
|
||||
graph.minimap.scaleTo(0.1);
|
||||
graph.minimap.centerContent();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Event listeners
|
||||
container.addEventListener('mousedown', (e: MouseEvent) => {
|
||||
if (e.button === 2) e.preventDefault();
|
||||
});
|
||||
|
||||
container.addEventListener('contextmenu', (e: Event) => {
|
||||
e.preventDefault();
|
||||
});
|
||||
|
||||
// Graph events
|
||||
graph.on('cell:contextmenu', ({ cell, e }) => {
|
||||
e.preventDefault();
|
||||
onContextMenu({
|
||||
x: e.clientX,
|
||||
y: e.clientY,
|
||||
visible: true,
|
||||
type: cell.isNode() ? 'node' : 'edge',
|
||||
cell,
|
||||
});
|
||||
});
|
||||
|
||||
graph.on('blank:contextmenu', ({ e }) => {
|
||||
e.preventDefault();
|
||||
onContextMenu({
|
||||
x: e.clientX,
|
||||
y: e.clientY,
|
||||
visible: true,
|
||||
type: 'canvas',
|
||||
});
|
||||
});
|
||||
|
||||
graph.on('blank:click cell:click', () => {
|
||||
onContextMenu({ visible: false, x: 0, y: 0, type: 'canvas' });
|
||||
});
|
||||
|
||||
graph.on('node:dblclick', ({ node }) => {
|
||||
onNodeClick(node);
|
||||
});
|
||||
|
||||
graph.on('selection:changed', () => {
|
||||
onGraphChange(graph);
|
||||
});
|
||||
|
||||
// Drag and drop handlers
|
||||
container.addEventListener('dragover', onDragOver);
|
||||
container.addEventListener('drop', onDrop);
|
||||
|
||||
// Load initial data if provided
|
||||
if (flowDetail?.graphDefinition) {
|
||||
const graphData = JSON.parse(flowDetail.graphDefinition);
|
||||
graph.fromJSON(graphData);
|
||||
requestAnimationFrame(() => {
|
||||
graph.zoomToFit({ padding: 50, maxScale: 1 });
|
||||
graph.centerContent();
|
||||
});
|
||||
}
|
||||
|
||||
return graph;
|
||||
};
|
||||
|
||||
// Load graph data
|
||||
export const loadGraphData = (graph: Graph, data: GraphData) => {
|
||||
graph.clearCells();
|
||||
|
||||
// Load nodes
|
||||
data.nodes.forEach((nodeData) => {
|
||||
graph.addNode({
|
||||
id: nodeData.id,
|
||||
shape: 'react-shape',
|
||||
x: nodeData.position.x,
|
||||
y: nodeData.position.y,
|
||||
width: nodeData.size.width,
|
||||
height: nodeData.size.height,
|
||||
attrs: generateNodeStyle(nodeData.type, nodeData.name),
|
||||
ports: generatePorts(nodeData.type),
|
||||
data: nodeData,
|
||||
});
|
||||
});
|
||||
|
||||
// Load edges
|
||||
data.edges.forEach((edgeData) => {
|
||||
graph.addEdge({
|
||||
id: edgeData.id,
|
||||
source: edgeData.source,
|
||||
target: edgeData.target,
|
||||
attrs: generateEdgeStyle(edgeData),
|
||||
data: edgeData,
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// Generate edge style
|
||||
export const generateEdgeStyle = (edgeData: EdgeData): EdgeStyle => {
|
||||
return {
|
||||
line: {
|
||||
stroke: '#5F95FF',
|
||||
strokeWidth: 2,
|
||||
targetMarker: {
|
||||
name: 'classic',
|
||||
size: 8,
|
||||
},
|
||||
},
|
||||
label: edgeData.name
|
||||
? {
|
||||
text: edgeData.name,
|
||||
fill: '#333',
|
||||
fontSize: 12,
|
||||
}
|
||||
: undefined,
|
||||
};
|
||||
};
|
||||
|
||||
// Export graph data
|
||||
export const exportGraphData = (graph: Graph): GraphData => {
|
||||
return {
|
||||
nodes: graph.getNodes().map((node) => node.getData() as NodeData),
|
||||
edges: graph.getEdges().map((edge) => edge.getData() as EdgeData),
|
||||
properties: {},
|
||||
};
|
||||
};
|
||||
|
||||
// Validate graph data
|
||||
export const validateGraphData = (graphData: GraphData): string[] => {
|
||||
const errors: string[] = [];
|
||||
|
||||
// Validate start node
|
||||
const startNodes = graphData.nodes.filter(
|
||||
(node) => node.type === 'startEvent'
|
||||
);
|
||||
if (startNodes.length !== 1) {
|
||||
errors.push('必须有且仅有一个开始节点');
|
||||
}
|
||||
|
||||
// Validate end nodes
|
||||
const endNodes = graphData.nodes.filter((node) => node.type === 'endEvent');
|
||||
if (endNodes.length === 0) {
|
||||
errors.push('必须至少有一个结束节点');
|
||||
}
|
||||
|
||||
// Validate node connections
|
||||
graphData.nodes.forEach((node) => {
|
||||
if (node.type !== 'endEvent') {
|
||||
const outgoingEdges = graphData.edges.filter(
|
||||
(edge) => edge.source === node.id
|
||||
);
|
||||
if (outgoingEdges.length === 0) {
|
||||
errors.push(`节点 "${node.name}" 没有出边`);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return errors;
|
||||
};
|
||||
@ -1,193 +0,0 @@
|
||||
import { Position, Size, NodeType, NodeData, GraphConfig, PortGroup, GraphAttrs } from '../types';
|
||||
import { NodeConfigError } from './errors';
|
||||
|
||||
interface CanvasMatrix {
|
||||
scale: number;
|
||||
offsetX: number;
|
||||
offsetY: number;
|
||||
}
|
||||
|
||||
export const calculateCanvasPosition = (
|
||||
clientX: number,
|
||||
clientY: number,
|
||||
containerRect: DOMRect,
|
||||
matrix: CanvasMatrix
|
||||
): Position => {
|
||||
const point: Position = {
|
||||
x: clientX - containerRect.left,
|
||||
y: clientY - containerRect.top,
|
||||
};
|
||||
|
||||
return {
|
||||
x: (point.x - matrix.offsetX) / matrix.scale,
|
||||
y: (point.y - matrix.offsetY) / matrix.scale,
|
||||
};
|
||||
};
|
||||
|
||||
// 获取节点形状
|
||||
export const getNodeShape = (nodeType: string): GraphConfig['shape'] => {
|
||||
switch (nodeType) {
|
||||
case 'startEvent':
|
||||
case 'endEvent':
|
||||
return 'circle';
|
||||
case 'exclusiveGateway':
|
||||
case 'parallelGateway':
|
||||
return 'diamond';
|
||||
case 'userTask':
|
||||
case 'serviceTask':
|
||||
case 'shellTask':
|
||||
return 'rect';
|
||||
default:
|
||||
return 'rect';
|
||||
}
|
||||
};
|
||||
|
||||
// 节点图标映射
|
||||
const NODE_ICONS: Record<string, string> = {
|
||||
startEvent: 'data:image/svg+xml;base64,PHN2ZyB0PSIxNzAzODQ4NTM1NDY5IiBjbGFzcz0iaWNvbiIgdmlld0JveD0iMCAwIDEwMjQgMTAyNCIgdmVyc2lvbj0iMS4xIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHAtaWQ9IjQxNjEiIHdpZHRoPSIyMDAiIGhlaWdodD0iMjAwIj48cGF0aCBkPSJNNTEyIDY0QzI2NC42IDY0IDY0IDI2NC42IDY0IDUxMnMyMDAuNiA0NDggNDQ4IDQ0OCA0NDgtMjAwLjYgNDQ4LTQ0OFM3NTkuNCA2NCA1MTIgNjR6bTE5MiAzMzZjMCA0LjQtMy42IDgtOCA4SDU0NHYxNTJjMCA0LjQtMy42IDgtOCA4aC00OGMtNC40IDAtOC0zLjYtOC04VjQwOEgzMjhjLTQuNCAwLTgtMy42LTgtOHYtNDhjMC00LjQgMy42LTggOC04aDE1MlYxOTJjMC00LjQgMy42LTggOC04aDQ4YzQuNCAwIDggMy42IDggOHYxNTJoMTUyYzQuNCAwIDggMy42IDggOHY0OHoiIGZpbGw9IiM1MmM0MWEiIHAtaWQ9IjQxNjIiPjwvcGF0aD48L3N2Zz4=',
|
||||
endEvent: 'data:image/svg+xml;base64,PHN2ZyB0PSIxNzAzODQ4NTM1NDY5IiBjbGFzcz0iaWNvbiIgdmlld0JveD0iMCAwIDEwMjQgMTAyNCIgdmVyc2lvbj0iMS4xIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHAtaWQ9IjQxNjEiIHdpZHRoPSIyMDAiIGhlaWdodD0iMjAwIj48cGF0aCBkPSJNNTEyIDY0QzI2NC42IDY0IDY0IDI2NC42IDY0IDUxMnMyMDAuNiA0NDggNDQ4IDQ0OCA0NDgtMjAwLjYgNDQ4LTQ0OFM3NTkuNCA2NCA1MTIgNjR6bTE5MiAyODhjMCA0LjQtMy42IDgtOCA4SDMyOGMtNC40IDAtOC0zLjYtOC04di00OGMwLTQuNCAzLjYtOCA4LThoMzY4YzQuNCAwIDggMy42IDggOHY0OHoiIGZpbGw9IiNmZjRkNGYiIHAtaWQ9IjQxNjIiPjwvcGF0aD48L3N2Zz4=',
|
||||
userTask: 'data:image/svg+xml;base64,PHN2ZyB0PSIxNzAzODQ4NTM1NDY5IiBjbGFzcz0iaWNvbiIgdmlld0JveD0iMCAwIDEwMjQgMTAyNCIgdmVyc2lvbj0iMS4xIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHAtaWQ9IjQxNjEiIHdpZHRoPSIyMDAiIGhlaWdodD0iMjAwIj48cGF0aCBkPSJNODU4LjUgNzYzLjZjLTE4LjktMjYuMi00Ny45LTQxLjctNzkuOS00MS43SDI0Ni40Yy0zMiAwLTYxIDI1LjUtNzkuOSA0MS43LTE4LjkgMTYuMi0yOS45IDM3LjItMjkuOSA1OS42IDAgMjIuNCAyNi44IDQwLjYgNTkuOCA0MC42aDYzMi4yYzMzIDAgNTkuOC0xOC4yIDU5LjgtNDAuNiAwLTIyLjQtMTEtNDMuNC0yOS45LTU5LjZ6TTUxMiAyNTZjODguNCAwIDE2MCA3MS42IDE2MCAxNjBzLTcxLjYgMTYwLTE2MCAxNjAtMTYwLTcxLjYtMTYwLTE2MCA3MS42LTE2MCAxNjAtMTYweiIgZmlsbD0iI2ZhOGMxNiIgcC1pZD0iNDE2MiI+PC9wYXRoPjwvc3ZnPg==',
|
||||
shellTask: 'data:image/svg+xml;base64,PHN2ZyB0PSIxNzAzODQ4NTM1NDY5IiBjbGFzcz0iaWNvbiIgdmlld0JveD0iMCAwIDEwMjQgMTAyNCIgdmVyc2lvbj0iMS4xIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHAtaWQ9IjQxNjEiIHdpZHRoPSIyMDAiIGhlaWdodD0iMjAwIj48cGF0aCBkPSJNMTYwIDI1NnY1MTJoNzA0VjI1NkgxNjB6IG02NDAgNDQ4SDE5MlYyODhoNjA4djQxNnpNMjI0IDQ4MGwxMjgtMTI4IDQ1LjMgNDUuMy04Mi43IDgyLjcgODIuNyA4Mi43TDM1MiA2MDhsLTEyOC0xMjh6IG0yMzEuMSAxOTBsLTQ1LjMtNDUuMyAxNTItMTUyIDQ1LjMgNDUuM2wtMTUyIDE1MnoiIGZpbGw9IiMxODkwZmYiIHAtaWQ9IjQxNjIiPjwvcGF0aD48L3N2Zz4=',
|
||||
exclusiveGateway: 'data:image/svg+xml;base64,PHN2ZyB0PSIxNzAzODQ4NzAzODY0IiBjbGFzcz0iaWNvbiIgdmlld0JveD0iMCAwIDEwMjQgMTAyNCIgdmVyc2lvbj0iMS4xIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHAtaWQ9IjY1NTUiIHdpZHRoPSIyMDAiIGhlaWdodD0iMjAwIj48cGF0aCBkPSJNNzgyLjcgNDQxLjRMNTQ1IDMwNC44YzAuMy0wLjMgMC40LTAuNyAwLjctMUw3ODIuNyA0NDEuNHogbTExOC45IDQzMi45TDIxNy43IDE1OC4xQzIwMi4xIDE0Mi41IDE3NiAxNDIuNSAxNjAuNCAxNTguMWMtMTUuNiAxNS42LTE1LjYgNDEuNyAwIDU3LjNsNjgzLjkgNzE2LjJjMTUuNiAxNS42IDQxLjcgMTUuNiA1Ny4zIDBzMTUuNi00MS43IDAtNTcuM3ogbS00MjYtMjQuNmMtMC4zIDAuMy0wLjQgMC43LTAuNyAxTDIzNy42IDU4Mi42YzAuMy0wLjMgMC40LTAuNyAwLjctMWwyMzcuMyAyNjguMXogbTIzNy4zLTI2Ny4xTDQ3NS42IDg1MC43YzAuMy0wLjMgMC40LTAuNyAwLjctMUw3MTMuNiA1ODIuNnoiIGZpbGw9IiM3MjJlZDEiIHAtaWQ9IjY1NTYiPjwvcGF0aD48L3N2Zz4=',
|
||||
parallelGateway: 'data:image/svg+xml;base64,PHN2ZyB0PSIxNzAzODQ4NzAzODY0IiBjbGFzcz0iaWNvbiIgdmlld0JveD0iMCAwIDEwMjQgMTAyNCIgdmVyc2lvbj0iMS4xIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHAtaWQ9IjY1NTUiIHdpZHRoPSIyMDAiIGhlaWdodD0iMjAwIj48cGF0aCBkPSJNNDAwIDUwMEg0MDB2MjAwSDEwMFY1MDB6IiBmaWxsPSIjNzIyZWQxIiBwLWlkPSI2NTU2Ij48L3BhdGg+PC9zdmc+',
|
||||
};
|
||||
|
||||
// 节点主题映射
|
||||
const NODE_THEMES: Record<string, { fill: string; stroke: string }> = {
|
||||
startEvent: { fill: '#f6ffed', stroke: '#52c41a' },
|
||||
endEvent: { fill: '#fff1f0', stroke: '#ff4d4f' },
|
||||
userTask: { fill: '#fff7e6', stroke: '#fa8c16' },
|
||||
shellTask: { fill: '#e6f7ff', stroke: '#1890ff' },
|
||||
exclusiveGateway: { fill: '#f9f0ff', stroke: '#722ed1' },
|
||||
parallelGateway: { fill: '#f9f0ff', stroke: '#722ed1' },
|
||||
};
|
||||
|
||||
// 生成节点样式
|
||||
export const generateNodeStyle = (nodeType: string, label?: string): GraphAttrs => {
|
||||
const theme = NODE_THEMES[nodeType] || { fill: '#ffffff', stroke: '#333333' };
|
||||
const shape = getNodeShape(nodeType);
|
||||
const icon = NODE_ICONS[nodeType];
|
||||
|
||||
const style: GraphAttrs = {
|
||||
body: {
|
||||
fill: theme.fill,
|
||||
stroke: theme.stroke,
|
||||
strokeWidth: nodeType === 'endEvent' ? 4 : 2,
|
||||
},
|
||||
label: {
|
||||
text: label || '',
|
||||
fill: '#333333',
|
||||
fontSize: 12,
|
||||
},
|
||||
};
|
||||
|
||||
if (icon) {
|
||||
style.image = {
|
||||
'xlink:href': icon,
|
||||
width: 16,
|
||||
height: 16,
|
||||
x: shape === 'circle' ? 12 : 8,
|
||||
y: shape === 'circle' ? 12 : 8,
|
||||
};
|
||||
}
|
||||
|
||||
return style;
|
||||
};
|
||||
|
||||
// 生成端口配置
|
||||
export const generatePorts = (nodeType: string): { groups: Record<string, PortGroup> } => {
|
||||
const defaultPortGroup: PortGroup = {
|
||||
position: 'top',
|
||||
attrs: {
|
||||
circle: {
|
||||
r: 4,
|
||||
magnet: true,
|
||||
stroke: '#5F95FF',
|
||||
strokeWidth: 1,
|
||||
fill: '#fff',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
return {
|
||||
groups: {
|
||||
top: { ...defaultPortGroup, position: 'top' },
|
||||
right: { ...defaultPortGroup, position: 'right' },
|
||||
bottom: { ...defaultPortGroup, position: 'bottom' },
|
||||
left: { ...defaultPortGroup, position: 'left' },
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
// 计算节点位置
|
||||
export const calculateNodePosition = (nodeType: string, dropPosition: Position): Position => {
|
||||
const shape = getNodeShape(nodeType);
|
||||
const size = getNodeSize(nodeType);
|
||||
|
||||
// 调整位置使节点中心对齐到鼠标位置
|
||||
return {
|
||||
x: dropPosition.x - size.width / 2,
|
||||
y: dropPosition.y - size.height / 2,
|
||||
};
|
||||
};
|
||||
|
||||
// 获取节点大小
|
||||
export const getNodeSize = (nodeType: string): Size => {
|
||||
switch (nodeType) {
|
||||
case 'startEvent':
|
||||
case 'endEvent':
|
||||
return { width: 40, height: 40 };
|
||||
case 'exclusiveGateway':
|
||||
case 'parallelGateway':
|
||||
return { width: 60, height: 60 };
|
||||
case 'userTask':
|
||||
case 'serviceTask':
|
||||
case 'shellTask':
|
||||
return { width: 120, height: 60 };
|
||||
default:
|
||||
return { width: 100, height: 50 };
|
||||
}
|
||||
};
|
||||
|
||||
// 创建节点数据
|
||||
export const createNodeData = (
|
||||
nodeType: NodeType,
|
||||
id: string,
|
||||
position: Position,
|
||||
name?: string
|
||||
): NodeData => {
|
||||
return {
|
||||
id,
|
||||
type: nodeType.type,
|
||||
name: name || nodeType.name,
|
||||
position,
|
||||
size: getNodeSize(nodeType.type),
|
||||
config: {
|
||||
type: nodeType.type,
|
||||
...JSON.parse(nodeType.flowableConfig || '{}'),
|
||||
},
|
||||
properties: {},
|
||||
};
|
||||
};
|
||||
|
||||
// 验证节点配置
|
||||
export const validateNodeConfig = (nodeData: NodeData, nodeType: NodeType): string[] => {
|
||||
const errors: string[] = [];
|
||||
|
||||
// 验证必填属性
|
||||
if (!nodeData.name) {
|
||||
errors.push('节点名称不能为空');
|
||||
}
|
||||
|
||||
// 验证表单配置
|
||||
const formConfig = nodeType.formConfig;
|
||||
if (formConfig?.properties) {
|
||||
formConfig.properties.forEach(prop => {
|
||||
if (prop.required && !nodeData.config[prop.name]) {
|
||||
errors.push(`${prop.label}不能为空`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return errors;
|
||||
};
|
||||
@ -1,155 +0,0 @@
|
||||
import { Graph } from '@antv/x6';
|
||||
import { NodeType } from './types';
|
||||
|
||||
export interface ValidationResult {
|
||||
valid: boolean;
|
||||
errors: string[];
|
||||
}
|
||||
|
||||
export const validateFlow = (graph: Graph): ValidationResult => {
|
||||
const result: ValidationResult = {
|
||||
valid: true,
|
||||
errors: [],
|
||||
};
|
||||
|
||||
const nodes = graph.getNodes();
|
||||
const edges = graph.getEdges();
|
||||
|
||||
// 验证是否有节点
|
||||
if (nodes.length === 0) {
|
||||
result.errors.push('流程必须包含至少一个节点');
|
||||
result.valid = false;
|
||||
return result;
|
||||
}
|
||||
|
||||
// 验证开始节点
|
||||
const startNodes = nodes.filter(node => node.getData()?.type === NodeType.START);
|
||||
if (startNodes.length === 0) {
|
||||
result.errors.push('流程必须包含一个开始节点');
|
||||
result.valid = false;
|
||||
} else if (startNodes.length > 1) {
|
||||
result.errors.push('流程只能包含一个开始节点');
|
||||
result.valid = false;
|
||||
}
|
||||
|
||||
// 验证结束节点
|
||||
const endNodes = nodes.filter(node => node.getData()?.type === NodeType.END);
|
||||
if (endNodes.length === 0) {
|
||||
result.errors.push('流程必须包含至少一个结束节点');
|
||||
result.valid = false;
|
||||
} else if (endNodes.length > 1) {
|
||||
result.errors.push('流程只能包含一个结束节点');
|
||||
result.valid = false;
|
||||
}
|
||||
|
||||
// 验证孤立节点
|
||||
nodes.forEach(node => {
|
||||
const incomingEdges = graph.getIncomingEdges(node) || [];
|
||||
const outgoingEdges = graph.getOutgoingEdges(node) || [];
|
||||
const nodeData = node.getData();
|
||||
const nodeType = nodeData?.type;
|
||||
const nodeName = nodeData?.name || '未命名';
|
||||
|
||||
if (nodeType === NodeType.START && incomingEdges.length > 0) {
|
||||
result.errors.push('开始节点不能有入边');
|
||||
result.valid = false;
|
||||
}
|
||||
|
||||
if (nodeType === NodeType.END && outgoingEdges.length > 0) {
|
||||
result.errors.push('结束节点不能有出边');
|
||||
result.valid = false;
|
||||
}
|
||||
|
||||
if (nodeType !== NodeType.START && nodeType !== NodeType.END &&
|
||||
(incomingEdges.length === 0 || outgoingEdges.length === 0)) {
|
||||
result.errors.push(`节点 "${nodeName}" 未完全连接`);
|
||||
result.valid = false;
|
||||
}
|
||||
});
|
||||
|
||||
// 验证网关配对
|
||||
const validateGateways = () => {
|
||||
const exclusiveGateways = nodes.filter(
|
||||
node => node.getData()?.type === NodeType.EXCLUSIVE_GATEWAY
|
||||
);
|
||||
const parallelGateways = nodes.filter(
|
||||
node => node.getData()?.type === NodeType.PARALLEL_GATEWAY
|
||||
);
|
||||
|
||||
// 验证排他网关
|
||||
exclusiveGateways.forEach(gateway => {
|
||||
const outgoingEdges = graph.getOutgoingEdges(gateway) || [];
|
||||
const incomingEdges = graph.getIncomingEdges(gateway) || [];
|
||||
const gatewayData = gateway.getData();
|
||||
const gatewayName = gatewayData?.name || '未命名';
|
||||
|
||||
if (outgoingEdges.length < 2) {
|
||||
result.errors.push(`排他网关 "${gatewayName}" 必须至少有两个出口`);
|
||||
result.valid = false;
|
||||
}
|
||||
|
||||
if (incomingEdges.length < 1) {
|
||||
result.errors.push(`排他网关 "${gatewayName}" 必须至少有一个入口`);
|
||||
result.valid = false;
|
||||
}
|
||||
});
|
||||
|
||||
// 验证并行网关
|
||||
parallelGateways.forEach(gateway => {
|
||||
const outgoingEdges = graph.getOutgoingEdges(gateway) || [];
|
||||
const incomingEdges = graph.getIncomingEdges(gateway) || [];
|
||||
const gatewayData = gateway.getData();
|
||||
const gatewayName = gatewayData?.name || '未命名';
|
||||
|
||||
if (outgoingEdges.length < 2) {
|
||||
result.errors.push(`并行网关 "${gatewayName}" 必须至少有两个出口`);
|
||||
result.valid = false;
|
||||
}
|
||||
|
||||
if (incomingEdges.length < 1) {
|
||||
result.errors.push(`并行网关 "${gatewayName}" 必须至少有一个入口`);
|
||||
result.valid = false;
|
||||
}
|
||||
});
|
||||
};
|
||||
validateGateways();
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
// 检查是否存在环路
|
||||
export const hasCycle = (graph: Graph): boolean => {
|
||||
const visited = new Set<string>();
|
||||
const recursionStack = new Set<string>();
|
||||
|
||||
const dfs = (nodeId: string): boolean => {
|
||||
visited.add(nodeId);
|
||||
recursionStack.add(nodeId);
|
||||
|
||||
const outgoingEdges = graph.getOutgoingEdges(nodeId);
|
||||
if (outgoingEdges) {
|
||||
for (const edge of outgoingEdges) {
|
||||
const targetId = edge.getTargetCellId();
|
||||
if (!visited.has(targetId)) {
|
||||
if (dfs(targetId)) {
|
||||
return true;
|
||||
}
|
||||
} else if (recursionStack.has(targetId)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
recursionStack.delete(nodeId);
|
||||
return false;
|
||||
};
|
||||
|
||||
const nodes = graph.getNodes();
|
||||
for (const node of nodes) {
|
||||
if (!visited.has(node.id) && dfs(node.id)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
100
frontend/src/pages/Workflow/Definition/List/index.tsx
Normal file
100
frontend/src/pages/Workflow/Definition/List/index.tsx
Normal file
@ -0,0 +1,100 @@
|
||||
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;
|
||||
@ -1,412 +0,0 @@
|
||||
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,
|
||||
updateDefinition,
|
||||
deleteDefinition,
|
||||
disableDefinition,
|
||||
enableDefinition,
|
||||
getDefinitions,
|
||||
publishDefinition
|
||||
} from '../service';
|
||||
import {
|
||||
WorkflowDefinition,
|
||||
WorkflowStatus,
|
||||
WorkflowDefinitionBase
|
||||
} from '../types';
|
||||
import {DeleteOutlined, 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 [editingRecord, setEditingRecord] = useState<WorkflowDefinition | null>(null);
|
||||
|
||||
// 获取列表数据
|
||||
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 handleSubmit = async (values: WorkflowDefinitionBase) => {
|
||||
try {
|
||||
if (editingRecord) {
|
||||
// 更新流程
|
||||
const updateData = {
|
||||
...values,
|
||||
status: editingRecord.status,
|
||||
flowVersion: editingRecord.flowVersion
|
||||
};
|
||||
await updateDefinition(editingRecord.id, updateData);
|
||||
message.success('更新成功');
|
||||
} else {
|
||||
// 创建流程
|
||||
const data = {
|
||||
...values,
|
||||
status: WorkflowStatus.DRAFT,
|
||||
flowVersion: 1
|
||||
};
|
||||
await createDefinition(data);
|
||||
message.success('创建成功');
|
||||
}
|
||||
setCreateModalVisible(false);
|
||||
setEditingRecord(null);
|
||||
form.resetFields();
|
||||
fetchList();
|
||||
} catch (error) {
|
||||
// 错误已在请求拦截器中处理
|
||||
}
|
||||
};
|
||||
|
||||
// 处理弹窗关闭
|
||||
const handleModalClose = () => {
|
||||
setCreateModalVisible(false);
|
||||
setEditingRecord(null);
|
||||
form.resetFields();
|
||||
};
|
||||
|
||||
// 处理删除
|
||||
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 handleEdit = (record: WorkflowDefinition) => {
|
||||
setEditingRecord(record);
|
||||
form.setFieldsValue({
|
||||
key: record.key,
|
||||
name: record.name,
|
||||
description: record.description
|
||||
});
|
||||
setCreateModalVisible(true);
|
||||
};
|
||||
|
||||
// 表格列定义
|
||||
const columns = [
|
||||
{
|
||||
title: 'ID',
|
||||
dataIndex: 'id',
|
||||
key: 'id',
|
||||
width: 80,
|
||||
},
|
||||
{
|
||||
title: '名称',
|
||||
dataIndex: 'name',
|
||||
key: 'name',
|
||||
width: 150,
|
||||
ellipsis: true,
|
||||
},
|
||||
{
|
||||
title: '标识',
|
||||
dataIndex: 'key',
|
||||
key: 'key',
|
||||
width: 150,
|
||||
ellipsis: true,
|
||||
},
|
||||
{
|
||||
title: '版本',
|
||||
dataIndex: 'flowVersion',
|
||||
key: 'flowVersion',
|
||||
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: 'description',
|
||||
key: 'description',
|
||||
width: 200,
|
||||
ellipsis: true,
|
||||
},
|
||||
{
|
||||
title: '创建时间',
|
||||
dataIndex: 'createTime',
|
||||
key: 'createTime',
|
||||
width: 180,
|
||||
},
|
||||
{
|
||||
title: '创建人',
|
||||
dataIndex: 'createBy',
|
||||
key: 'createBy',
|
||||
width: 100,
|
||||
},
|
||||
{
|
||||
title: '更新时间',
|
||||
dataIndex: 'updateTime',
|
||||
key: 'updateTime',
|
||||
width: 180,
|
||||
},
|
||||
{
|
||||
title: '更新人',
|
||||
dataIndex: 'updateBy',
|
||||
key: 'updateBy',
|
||||
width: 100,
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
fixed: 'right' as const,
|
||||
width: 380,
|
||||
render: (_: any, record: WorkflowDefinition) => (
|
||||
<Space>
|
||||
{record.status === WorkflowStatus.DRAFT && (
|
||||
<Button
|
||||
type="link"
|
||||
onClick={() => handleEdit(record)}
|
||||
>
|
||||
编辑
|
||||
</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={editingRecord ? "编辑流程" : "新建流程"}
|
||||
open={createModalVisible}
|
||||
onCancel={handleModalClose}
|
||||
footer={null}
|
||||
>
|
||||
<Form
|
||||
form={form}
|
||||
labelCol={{span: 6}}
|
||||
wrapperCol={{span: 18}}
|
||||
onFinish={handleSubmit}
|
||||
>
|
||||
<Form.Item
|
||||
name="key"
|
||||
label="业务标识"
|
||||
rules={[
|
||||
{required: true, message: '请输入业务标识'},
|
||||
{pattern: /^[A-Z_]+$/, message: '标识只能包含大写字母和下划线'},
|
||||
]}
|
||||
>
|
||||
<Input
|
||||
placeholder="请输入业务标识"
|
||||
disabled={!!editingRecord}
|
||||
/>
|
||||
</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: 6, span: 18}}>
|
||||
<Space>
|
||||
<Button type="primary" htmlType="submit">
|
||||
确定
|
||||
</Button>
|
||||
<Button onClick={handleModalClose}>
|
||||
取消
|
||||
</Button>
|
||||
</Space>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default WorkflowDefinitionList;
|
||||
@ -1,209 +0,0 @@
|
||||
import { BaseResponse } from '@/types/base/response';
|
||||
|
||||
/**
|
||||
* 节点图形配置
|
||||
*/
|
||||
interface NodeGraphStyle {
|
||||
fill: string;
|
||||
stroke: string;
|
||||
icon?: string;
|
||||
iconColor?: string;
|
||||
strokeWidth?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 节点端口组配置
|
||||
*/
|
||||
interface PortGroup {
|
||||
position: 'left' | 'right';
|
||||
attrs: {
|
||||
circle: {
|
||||
r: number;
|
||||
fill: string;
|
||||
stroke: string;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 节点端口配置
|
||||
*/
|
||||
interface NodePorts {
|
||||
groups: {
|
||||
in?: PortGroup;
|
||||
out?: PortGroup;
|
||||
};
|
||||
types: ('in' | 'out')[];
|
||||
}
|
||||
|
||||
/**
|
||||
* 节点图形定义
|
||||
*/
|
||||
interface NodeGraph {
|
||||
shape: 'circle' | 'rectangle';
|
||||
size: {
|
||||
width: number;
|
||||
height: number;
|
||||
};
|
||||
style: NodeGraphStyle;
|
||||
ports: NodePorts;
|
||||
}
|
||||
|
||||
/**
|
||||
* 工作流节点配置
|
||||
*/
|
||||
interface NodeConfig {
|
||||
name: string;
|
||||
description?: string;
|
||||
script?: string;
|
||||
language?: string;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
/**
|
||||
* 工作流节点定义
|
||||
*/
|
||||
interface WorkflowNode {
|
||||
id: string;
|
||||
code: string;
|
||||
type: string;
|
||||
name: string;
|
||||
graph: NodeGraph;
|
||||
config: NodeConfig;
|
||||
}
|
||||
|
||||
/**
|
||||
* 工作流边配置
|
||||
*/
|
||||
interface EdgeConfig {
|
||||
type: 'sequence';
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
/**
|
||||
* 工作流边定义
|
||||
*/
|
||||
interface WorkflowEdge {
|
||||
id: string;
|
||||
from: string;
|
||||
to: string;
|
||||
name: string;
|
||||
config: EdgeConfig;
|
||||
properties?: Record<string, any>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 工作流状态
|
||||
*/
|
||||
export enum WorkflowStatus {
|
||||
DRAFT = 'DRAFT', // 草稿
|
||||
PUBLISHED = 'PUBLISHED', // 已发布
|
||||
DISABLED = 'DISABLED' // 已禁用
|
||||
}
|
||||
|
||||
/**
|
||||
* 工作流图形定义
|
||||
*/
|
||||
export interface WorkflowGraph {
|
||||
nodes: WorkflowNode[];
|
||||
edges: WorkflowEdge[];
|
||||
}
|
||||
|
||||
/**
|
||||
* 工作流定义基本信息
|
||||
*/
|
||||
export interface WorkflowDefinitionBase {
|
||||
name: string; // 工作流名称
|
||||
key: string; // 工作流标识
|
||||
flowVersion?: number; // 流程版本
|
||||
description?: string; // 描述信息
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建工作流定义请求
|
||||
*/
|
||||
export interface CreateWorkflowDefinitionRequest extends WorkflowDefinitionBase {
|
||||
status: WorkflowStatus; // 工作流状态
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新工作流定义请求
|
||||
*/
|
||||
export interface UpdateWorkflowDefinitionRequest extends WorkflowDefinitionBase {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 表单配置
|
||||
*/
|
||||
export interface FormConfig {
|
||||
formItems: any[]; // 表单项配置
|
||||
}
|
||||
|
||||
/**
|
||||
* 工作流定义
|
||||
*/
|
||||
export interface WorkflowDefinition extends BaseResponse {
|
||||
name: string; // 工作流名称
|
||||
key: string; // 工作流标识
|
||||
flowVersion: number; // 流程版本
|
||||
bpmnXml: string | null; // BPMN XML定义
|
||||
graph: WorkflowGraph; // 图形定义
|
||||
formConfig: FormConfig; // 表单配置
|
||||
status: WorkflowStatus; // 工作流状态
|
||||
description: string; // 描述信息
|
||||
deleted: boolean; // 是否删除
|
||||
extraData: any; // 扩展数据
|
||||
}
|
||||
|
||||
/**
|
||||
* 工作流配置的解析和序列化工具
|
||||
*/
|
||||
export const WorkflowConfigUtils = {
|
||||
/**
|
||||
* 解析节点配置
|
||||
*/
|
||||
parseNodeConfig: (config: string): Record<string, any> => {
|
||||
try {
|
||||
return JSON.parse(config);
|
||||
} catch {
|
||||
return {nodes: []};
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 解析转换配置
|
||||
*/
|
||||
parseTransitionConfig: (config: string): Record<string, any> => {
|
||||
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 {};
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 导出NodePanel中的所有类型
|
||||
export * from './Designer/components/NodePanel/types';
|
||||
@ -1,56 +0,0 @@
|
||||
import request from '../../utils/request';
|
||||
import {
|
||||
CreateWorkflowDefinitionRequest,
|
||||
UpdateWorkflowDefinitionRequest,
|
||||
WorkflowDefinition,
|
||||
WorkflowDefinitionPage,
|
||||
WorkflowDefinitionQuery,
|
||||
NodeType,
|
||||
NodeTypeQuery
|
||||
} from './types';
|
||||
|
||||
const WORKFLOW_DEFINITION_URL = '/api/v1/workflow/definition';
|
||||
const NODE_DEFINITION_URL = '/api/v1/workflow/node-definition';
|
||||
|
||||
// 创建工作流定义
|
||||
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`);
|
||||
|
||||
// <20><><EFBFBD>建新版本
|
||||
export const createVersion = (id: number) =>
|
||||
request.post<WorkflowDefinition>(`${WORKFLOW_DEFINITION_URL}/${id}/versions`);
|
||||
|
||||
// 获取节点类型列表
|
||||
export const getNodeTypes = (params?: NodeTypeQuery) =>
|
||||
request.get<Page<NodeType>>(`${NODE_DEFINITION_URL}/page`, { params });
|
||||
|
||||
export const getNodeTypeExecutors = (code: string) =>
|
||||
request.get<NodeType>(`${NODE_DEFINITION_URL}/${code}/executors`);
|
||||
@ -1,29 +0,0 @@
|
||||
import { Page } from '@/types/base';
|
||||
import { BaseQuery } from '@/types/base/query';
|
||||
import type { WorkflowDefinition, WorkflowStatus } from './Definition/types';
|
||||
|
||||
/**
|
||||
* 工作流定义查询参数
|
||||
*/
|
||||
export interface WorkflowDefinitionQuery extends BaseQuery {
|
||||
keyword?: string; // 关键字搜索
|
||||
status?: WorkflowStatus; // 工作流状态
|
||||
enabled?: boolean; // 是否启用
|
||||
}
|
||||
|
||||
/**
|
||||
* 工作流定义分页响应
|
||||
*/
|
||||
export type WorkflowDefinitionPage = Page<WorkflowDefinition>;
|
||||
|
||||
/**
|
||||
* 节点类型查询参数
|
||||
*/
|
||||
export interface NodeTypeQuery extends BaseQuery {
|
||||
enabled?: boolean; // 是否启用
|
||||
category?: string; // 节点类别
|
||||
}
|
||||
|
||||
// 重新导出工作流相关类型
|
||||
export type { WorkflowDefinition } from './Definition/types';
|
||||
export { WorkflowStatus } from './Definition/types';
|
||||
@ -1,33 +0,0 @@
|
||||
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 {};
|
||||
}
|
||||
}
|
||||
};
|
||||
@ -1,34 +1,50 @@
|
||||
export interface BaseResponse {
|
||||
id: number;
|
||||
createTime: string;
|
||||
updateTime: string;
|
||||
createBy?: string;
|
||||
updateBy?: string;
|
||||
enabled: boolean;
|
||||
version: number;
|
||||
id: number;
|
||||
createTime: string;
|
||||
updateTime: string;
|
||||
createBy?: string;
|
||||
updateBy?: string;
|
||||
enabled: boolean;
|
||||
version: number;
|
||||
}
|
||||
|
||||
export interface BaseQuery {
|
||||
keyword?: string;
|
||||
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;
|
||||
pageNum?: number;
|
||||
pageSize?: number;
|
||||
sortField?: string;
|
||||
sortOrder?: string;
|
||||
}
|
||||
|
||||
export interface Response<T> {
|
||||
code: number;
|
||||
message: string;
|
||||
data: T;
|
||||
success: boolean;
|
||||
code: number;
|
||||
message: string;
|
||||
data: T;
|
||||
success: boolean;
|
||||
}
|
||||
|
||||
export interface BaseRequest {
|
||||
enabled?: boolean;
|
||||
}
|
||||
|
||||
|
||||
// 分页响应数据
|
||||
export interface Page<T> {
|
||||
content: T[]; // 数据内容
|
||||
totalElements: number; // 总记录数
|
||||
totalPages: number; // 总页数
|
||||
size: number; // 每页大小
|
||||
number: number; // 当前页码(从0开始)
|
||||
numberOfElements: number; // 当前页记录数
|
||||
first: boolean; // 是否第一页
|
||||
last: boolean; // 是否最后一页
|
||||
empty: boolean; // 是否为空
|
||||
pageable: {
|
||||
pageNumber: number;
|
||||
pageSize: number;
|
||||
sort: {
|
||||
empty: boolean;
|
||||
sorted: boolean;
|
||||
unsorted: boolean;
|
||||
};
|
||||
};
|
||||
}
|
||||
@ -1,29 +0,0 @@
|
||||
// 分页请求参数
|
||||
export interface PageParams {
|
||||
pageNum: number; // 页码(从1开始)
|
||||
pageSize: number; // 每页大小
|
||||
sortField?: string; // 排序字段
|
||||
sortOrder?: string; // 排序方向
|
||||
}
|
||||
|
||||
// 分页响应数据
|
||||
export interface Page<T> {
|
||||
content: T[]; // 数据内容
|
||||
totalElements: number; // 总记录数
|
||||
totalPages: number; // 总页数
|
||||
size: number; // 每页大小
|
||||
number: number; // 当前页码(从0开始)
|
||||
numberOfElements: number; // 当前页记录数
|
||||
first: boolean; // 是否第一页
|
||||
last: boolean; // 是否最后一页
|
||||
empty: boolean; // 是否为空
|
||||
pageable: {
|
||||
pageNumber: number;
|
||||
pageSize: number;
|
||||
sort: {
|
||||
empty: boolean;
|
||||
sorted: boolean;
|
||||
unsorted: boolean;
|
||||
};
|
||||
};
|
||||
}
|
||||
@ -1,6 +0,0 @@
|
||||
export interface BaseQuery {
|
||||
pageNum?: number;
|
||||
pageSize?: number;
|
||||
sortField?: string;
|
||||
sortOrder?: string;
|
||||
}
|
||||
@ -1,3 +0,0 @@
|
||||
export interface BaseRequest {
|
||||
enabled?: boolean;
|
||||
}
|
||||
@ -1,9 +0,0 @@
|
||||
export interface BaseResponse {
|
||||
id: number;
|
||||
createTime: string;
|
||||
updateTime: string;
|
||||
createBy?: string;
|
||||
updateBy?: string;
|
||||
enabled: boolean;
|
||||
version: number;
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user