增加工具栏提示。

This commit is contained in:
dengqichen 2024-12-12 16:54:37 +08:00
parent c93847b482
commit 4f938e1c1c
41 changed files with 5176 additions and 4163 deletions

View File

@ -1,16 +1,17 @@
你是一名java前端开发工程师对你有以下要求 你是一名java前端开发工程师对你有以下要求
1. 缺陷修正: 1. 一直说中文
2. 缺陷修正:
- 在提出修复建议前,应充分分析问题 - 在提出修复建议前,应充分分析问题
- 提供精准、有针对性的解决方案 - 提供精准、有针对性的解决方案
- 解释bug的根本原因 - 解释bug的根本原因
2. 保持简单: 3. 保持简单:
- 优先考虑可读性和可维护性 - 优先考虑可读性和可维护性
- 避免过度工程化的解决方案 - 避免过度工程化的解决方案
- 尽可能使用标准库和模式 - 尽可能使用标准库和模式
- 遵循正确、最佳实践、DRY原则、无错误、功能齐全的代码编写原则 - 遵循正确、最佳实践、DRY原则、无错误、功能齐全的代码编写原则
3. 代码更改: 4. 代码更改:
- 在做出改变之前提出一个清晰的计划 - 在做出改变之前提出一个清晰的计划
- 一次将所有修改应用于单个文件 - 一次将所有修改应用于单个文件
- 请勿修改不相关的文件 - 请勿修改不相关的文件

View File

@ -1,21 +1,159 @@
你是一名java前端开发工程师对你有以下要求 你是一名高级前端开发人员是ReactJS NextJS, JavaScript, TypeScript HTML CSS和现代UI/UX框架例如TailwindCSS, Shadcn, Radix的专家。你深思熟虑给出细致入微的答案并且善于推理。你细心地提供准确、真实、深思熟虑的答案是推理的天才。
1. 缺陷修正: # 严格遵循的要求
- 在提出修复建议前,应充分分析问题 - 不要随意删除代码,请先详细浏览下现在所有的代码,再进行询问修改,得到确认再修改。重点切记
- 提供精准、有针对性的解决方案 - 首先,一步一步地思考——详细描述你在伪代码中构建什么的计划。
- 解释bug的根本原因 - 确认,然后写代码!
- 始终编写正确、最佳实践、DRY原则不要重复自己、无错误、功能齐全且可工作的代码还应与下面代码实施指南中列出的规则保持一致。
- 专注于简单易读的代码,而不是高性能。
- 完全实现所有要求的功能。
- 不要留下待办事项、占位符或缺失的部分。
- 确保代码完整!彻底确认。
- 包括所有必需的导入的包,并确保关键组件的正确命名。
- 如果你认为可能没有正确答案,你就说出来。
- 如果你不知道答案,就说出来,而不是猜测。
- 可以提出合理化的建议,但是需要等待是否可以。
- 对于新设计的实体类、字段、方法都要写注释,对于实际的逻辑要有逻辑注释。
- 不要随意修改现在的接口调用路径,如果不知道接口是什么,可以问。
#代码实现指南
在编写代码时遵循以下规则:
- 尽可能使用早期返回,使代码更具可读性。
- 总是使用顺风类样式HTML元素避免使用CSS或标签。
- 尽可能在类标记中使用“ class: ”而不是第三操作符。
- 使用描述性变量名和函数/const名。此外事件函数应该以“handle”前缀命名就像onClick的“handleClick”和onKeyDown的“handleKeyDown”。
- 在元素上实现可访问性特性。例如一个标签应该有tabindex= " 0 "、aria-label、on:click和on:keydown以及类似的属性。
— 使用const代替函数例如const toggle =() =>。另外,如果可能的话,定义一个类型。
2. 保持简单: # Deploy Ease Platform 前端开发规范
- 优先考虑可读性和可维护性
- 避免过度工程化的解决方案
- 尽可能使用标准库和模式
- 遵循正确、最佳实践、DRY原则、无错误、功能齐全的代码编写原则
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
- 避免内联样式
- 响应式适配
- 支持暗色主题

View File

@ -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

File diff suppressed because it is too large Load Diff

View File

@ -1,5 +1,5 @@
import { BaseResponse } from '@/types/base/response'; import { BaseResponse } from '@/types/base/response';
import { BaseQuery } from '@/types/base/query'; import {BaseQuery} from "@/types/base";
// 系统类型枚举 // 系统类型枚举
export enum SystemType { export enum SystemType {

View File

@ -1,6 +1,6 @@
import {BaseQuery} from "@/types/base/query.ts";
import {BaseRequest} from "@/types/base/request.ts"; import {BaseRequest} from "@/types/base/request.ts";
import {BaseResponse} from "@/types/base/response.ts"; import {BaseResponse} from "@/types/base/response.ts";
import {BaseQuery} from "@/types/base";
export enum MenuTypeEnum { export enum MenuTypeEnum {
DIRECTORY = 1, // 目录 DIRECTORY = 1, // 目录

View File

@ -1,6 +1,6 @@
import type { BaseResponse } from '@/types/base/response'; import type { BaseResponse } from '@/types/base/response';
import {BaseQuery} from "@/types/base/query.ts";
import {BaseRequest} from "@/types/base/request.ts"; import {BaseRequest} from "@/types/base/request.ts";
import {BaseQuery} from "@/types/base";
// 权限类型枚举 // 权限类型枚举
export enum PermissionType { export enum PermissionType {

View File

@ -1,4 +1,4 @@
import { BaseQuery } from '@/types/base/query'; import {BaseQuery} from "@/types/base";
export interface RoleQuery extends BaseQuery { export interface RoleQuery extends BaseQuery {
name?: string; name?: string;

View File

@ -1,5 +1,5 @@
import type { BaseResponse } from '@/types/base/response'; import type { BaseResponse } from '@/types/base/response';
import type { BaseQuery } from '@/types/base/query'; import {BaseQuery} from "@/types/base";
// 用户查询参数 // 用户查询参数
export interface UserQuery extends BaseQuery { export interface UserQuery extends BaseQuery {

View File

@ -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;

View File

@ -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;

View File

@ -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 解析错误']
};
}
};

View File

@ -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;
}

View File

@ -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;
}
}
}

View File

@ -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;
}
}

View File

@ -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;
}

View File

@ -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;

View File

@ -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; // 扩展数据
}

View File

@ -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;
}
}

View File

@ -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;

View File

@ -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;
}
}
}
}

View File

@ -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;
}

View File

@ -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;

View File

@ -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;
}

View File

@ -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;
};

View File

@ -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;
};

View File

@ -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;
};

View File

@ -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;
};

View 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;

View File

@ -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;

View File

@ -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';

View File

@ -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`);

View File

@ -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';

View File

@ -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 {};
}
}
};

View File

@ -1,34 +1,50 @@
export interface BaseResponse { export interface BaseResponse {
id: number; id: number;
createTime: string; createTime: string;
updateTime: string; updateTime: string;
createBy?: string; createBy?: string;
updateBy?: string; updateBy?: string;
enabled: boolean; enabled: boolean;
version: number; version: number;
} }
export interface BaseQuery { export interface BaseQuery {
keyword?: string; pageNum?: number;
enabled?: boolean; pageSize?: number;
startTime?: string; sortField?: string;
endTime?: string; sortOrder?: string;
}
export interface Page<T> {
content: T[];
totalElements: number;
totalPages: number;
size: number;
number: number;
empty: boolean;
first: boolean;
last: boolean;
} }
export interface Response<T> { export interface Response<T> {
code: number; code: number;
message: string; message: string;
data: T; data: T;
success: boolean; 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;
};
};
} }

View File

@ -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;
};
};
}

View File

@ -1,6 +0,0 @@
export interface BaseQuery {
pageNum?: number;
pageSize?: number;
sortField?: string;
sortOrder?: string;
}

View File

@ -1,3 +0,0 @@
export interface BaseRequest {
enabled?: boolean;
}

View File

@ -1,9 +0,0 @@
export interface BaseResponse {
id: number;
createTime: string;
updateTime: string;
createBy?: string;
updateBy?: string;
enabled: boolean;
version: number;
}