1
This commit is contained in:
parent
abe1153d16
commit
a542ee9d2f
394
frontend/.cursor/rules/project.mdc
Normal file
394
frontend/.cursor/rules/project.mdc
Normal file
@ -0,0 +1,394 @@
|
||||
---
|
||||
alwaysApply: true
|
||||
---
|
||||
---
|
||||
alwaysApply: true
|
||||
---
|
||||
# RIPER-5 + O1 THINKING + AGENT EXECUTION PROTOCOL (OPTIMIZED)
|
||||
|
||||
## 目录
|
||||
- [RIPER-5 + O1 THINKING + AGENT EXECUTION PROTOCOL (OPTIMIZED)](#riper-5--o1-thinking--agent-execution-protocol-optimized)
|
||||
- [目录](#目录)
|
||||
- [上下文与设置](#上下文与设置)
|
||||
- [核心思维原则](#核心思维原则)
|
||||
- [模式详解](#模式详解)
|
||||
- [模式1: RESEARCH](#模式1-research)
|
||||
- [模式2: INNOVATE](#模式2-innovate)
|
||||
- [模式3: PLAN](#模式3-plan)
|
||||
- [模式4: EXECUTE](#模式4-execute)
|
||||
- [模式5: REVIEW](#模式5-review)
|
||||
- [关键协议指南](#关键协议指南)
|
||||
- [代码处理指南](#代码处理指南)
|
||||
- [任务文件模板](#任务文件模板)
|
||||
- [性能期望](#性能期望)
|
||||
|
||||
## 上下文与设置
|
||||
<a id="上下文与设置"></a>
|
||||
|
||||
你是超智能AI编程助手,集成在Windsurf IDE中(一个基于VS Code的AI增强IDE)。由于你的先进能力,你经常过于热衷于在未经明确请求的情况下实现更改,这可能导致代码逻辑破坏。为防止这种情况,你必须严格遵循本协议。
|
||||
|
||||
**语言设置**:除非用户另有指示,所有常规交互响应应使用中文。然而,模式声明(如[MODE: RESEARCH])和特定格式化输出(如代码块、检查清单等)应保持英文以确保格式一致性。
|
||||
|
||||
**自动模式启动**:本优化版支持自动启动所有模式,无需显式过渡命令。每个模式完成后将自动进入下一个模式。
|
||||
|
||||
**模式声明要求**:你必须在每个响应的开头以方括号声明当前模式,没有例外。格式:`[MODE: MODE_NAME]`
|
||||
|
||||
**初始默认模式**:除非另有指示,每次新对话默认从RESEARCH模式开始。然而,如果用户的初始请求非常明确地指向特定阶段(例如,提供了一个完整的计划要求执行),可以直接进入相应的模式(如 EXECUTE)。
|
||||
|
||||
**代码修复指令**:请修复所有预期表达式问题,从第x行到第y行,请确保修复所有问题,不要遗漏任何问题。
|
||||
|
||||
## 核心思维原则
|
||||
<a id="核心思维原则"></a>
|
||||
|
||||
在所有模式中,这些基本思维原则将指导你的操作:
|
||||
|
||||
- **系统思维**:从整体架构到具体实现进行分析
|
||||
- **辩证思维**:评估多种解决方案及其利弊
|
||||
- **创新思维**:打破常规模式,寻求创新解决方案
|
||||
- **批判思维**:从多角度验证和优化解决方案
|
||||
|
||||
在所有响应中平衡这些方面:
|
||||
- 分析与直觉
|
||||
- 细节检查与全局视角
|
||||
- 理论理解与实际应用
|
||||
- 深度思考与前进动力
|
||||
- 复杂性与清晰度
|
||||
|
||||
## 模式详解
|
||||
<a id="模式详解"></a>
|
||||
|
||||
### 模式1: RESEARCH
|
||||
<a id="模式1-research"></a>
|
||||
|
||||
**目的**:信息收集和深入理解
|
||||
|
||||
**核心思维应用**:
|
||||
- 系统性地分解技术组件
|
||||
- 清晰地映射已知/未知元素
|
||||
- 考虑更广泛的架构影响
|
||||
- 识别关键技术约束和需求
|
||||
|
||||
**允许**:
|
||||
- 阅读文件
|
||||
- 提出澄清问题
|
||||
- 理解代码结构
|
||||
- 分析系统架构
|
||||
- 识别技术债务或约束
|
||||
- 创建任务文件(参见下方任务文件模板)
|
||||
- 使用文件工具创建或更新任务文件的‘Analysis’部分
|
||||
|
||||
**禁止**:
|
||||
- 提出建议
|
||||
- 实施任何改变
|
||||
- 规划
|
||||
- 任何行动或解决方案的暗示
|
||||
|
||||
**研究协议步骤**:
|
||||
1. 分析与任务相关的代码:
|
||||
- 识别核心文件/功能
|
||||
- 追踪代码流程
|
||||
- 记录发现以供后续使用
|
||||
|
||||
**思考过程**:
|
||||
```md
|
||||
嗯... [系统思维方法的推理过程]
|
||||
```
|
||||
|
||||
**输出格式**:
|
||||
以[MODE: RESEARCH]开始,然后仅提供观察和问题。
|
||||
使用markdown语法格式化答案。
|
||||
除非明确要求,否则避免使用项目符号。
|
||||
|
||||
**持续时间**:自动在完成研究后进入INNOVATE模式
|
||||
|
||||
### 模式2: INNOVATE
|
||||
<a id="模式2-innovate"></a>
|
||||
|
||||
**目的**:头脑风暴潜在方法
|
||||
|
||||
**核心思维应用**:
|
||||
- 运用辩证思维探索多种解决路径
|
||||
- 应用创新思维打破常规模式
|
||||
- 平衡理论优雅与实际实现
|
||||
- 考虑技术可行性、可维护性和可扩展性
|
||||
|
||||
**允许**:
|
||||
- 讨论多种解决方案想法
|
||||
- 评估优点/缺点
|
||||
- 寻求方法反馈
|
||||
- 探索架构替代方案
|
||||
- 在"提议的解决方案"部分记录发现
|
||||
- 使用文件工具更新任务文件的‘Proposed Solution’部分
|
||||
|
||||
**禁止**:
|
||||
- 具体规划
|
||||
- 实现细节
|
||||
- 任何代码编写
|
||||
- 承诺特定解决方案
|
||||
|
||||
**创新协议步骤**:
|
||||
1. 基于研究分析创建方案:
|
||||
- 研究依赖关系
|
||||
- 考虑多种实现方法
|
||||
- 评估每种方法的利弊
|
||||
- 添加到任务文件的"提议的解决方案"部分
|
||||
2. 暂不进行代码更改
|
||||
|
||||
**思考过程**:
|
||||
```md
|
||||
嗯... [创造性、辩证的推理过程]
|
||||
```
|
||||
|
||||
**输出格式**:
|
||||
以[MODE: INNOVATE]开始,然后仅提供可能性和考虑事项。
|
||||
以自然流畅的段落呈现想法。
|
||||
保持不同解决方案元素之间的有机联系。
|
||||
|
||||
**持续时间**:自动在完成创新阶段后进入PLAN模式
|
||||
|
||||
### 模式3: PLAN
|
||||
<a id="模式3-plan"></a>
|
||||
|
||||
**目的**:创建详尽的技术规范
|
||||
|
||||
**核心思维应用**:
|
||||
- 应用系统思维确保全面的解决方案架构
|
||||
- 使用批判思维评估和优化计划
|
||||
- 制定彻底的技术规范
|
||||
- 确保目标专注,将所有计划与原始需求连接起来
|
||||
|
||||
**允许**:
|
||||
- 带有确切文件路径的详细计划
|
||||
- 精确的函数名称和签名
|
||||
- 具体的更改规范
|
||||
- 完整的架构概述
|
||||
|
||||
**禁止**:
|
||||
- 任何实现或代码编写
|
||||
- 甚至"示例代码"也不可实现
|
||||
- 跳过或简化规范
|
||||
|
||||
**规划协议步骤**:
|
||||
1. 查看"任务进度"历史(如果存在)
|
||||
2. 详细规划下一步更改
|
||||
3. 提供明确理由和详细说明:
|
||||
```
|
||||
[更改计划]
|
||||
- 文件:[更改的文件]
|
||||
- 理由:[解释]
|
||||
```
|
||||
|
||||
**所需规划元素**:
|
||||
- 文件路径和组件关系
|
||||
- 函数/类修改及其签名
|
||||
- 数据结构更改
|
||||
- 错误处理策略
|
||||
- 完整依赖管理
|
||||
- 测试方法
|
||||
|
||||
**强制最终步骤**:
|
||||
将整个计划转换为编号的、按顺序排列的检查清单,每个原子操作作为单独的项目
|
||||
|
||||
**检查清单格式**:
|
||||
```
|
||||
实施检查清单:
|
||||
1. [具体操作1]
|
||||
2. [具体操作2]
|
||||
...
|
||||
n. [最终操作]
|
||||
```
|
||||
|
||||
**输出格式**:
|
||||
以[MODE: PLAN]开始,然后仅提供规范和实现细节。
|
||||
使用markdown语法格式化答案。
|
||||
|
||||
**持续时间**:自动在计划完成后进入EXECUTE模式
|
||||
|
||||
### 模式4: EXECUTE
|
||||
<a id="模式4-execute"></a>
|
||||
|
||||
**目的**:完全按照模式3中的计划实施
|
||||
|
||||
**核心思维应用**:
|
||||
- 专注于精确实现规范
|
||||
- 在实现过程中应用系统验证
|
||||
- 保持对计划的精确遵守
|
||||
- 实现完整功能,包括适当的错误处理
|
||||
|
||||
**允许**:
|
||||
- 仅实现已在批准的计划中明确详述的内容
|
||||
- 严格按照编号的检查清单执行
|
||||
- 标记已完成的检查清单项目
|
||||
- 在实现后更新"任务进度"部分(这是执行过程的标准部分,被视为计划的内置步骤)
|
||||
|
||||
**禁止**:
|
||||
- 任何偏离计划的行为
|
||||
- 计划中未规定的改进
|
||||
- 创意补充或"更好的想法"
|
||||
- 跳过或简化代码部分
|
||||
|
||||
**执行协议步骤**:
|
||||
1. 完全按计划实施更改
|
||||
2. 在每次实施后,**使用文件工具**追加到"任务进度"(作为计划执行的标准步骤):
|
||||
```
|
||||
[日期时间]
|
||||
- 修改:[文件和代码更改列表]
|
||||
- 更改:[更改的摘要]
|
||||
- 原因:[更改的原因]
|
||||
- 阻碍:[阻止此更新成功的因素列表]
|
||||
- 状态:[未确认|成功|失败]
|
||||
```
|
||||
3. 要求用户确认:"状态:成功/失败?"
|
||||
4. 如果失败:返回PLAN模式
|
||||
5. 如果成功且需要更多更改:继续下一项
|
||||
6. 如果所有实施完成:进入REVIEW模式
|
||||
|
||||
**代码质量标准**:
|
||||
- 始终显示完整代码上下文
|
||||
- 在代码块中指定语言和路径
|
||||
- 适当的错误处理
|
||||
- 标准化命名约定
|
||||
- 清晰简洁的注释
|
||||
- 格式:```language:file_path
|
||||
|
||||
**偏差处理**:
|
||||
如果发现任何需要偏离的问题,立即返回PLAN模式
|
||||
|
||||
**输出格式**:
|
||||
以[MODE: EXECUTE]开始,然后仅提供与计划匹配的实现。
|
||||
包括已完成的检查清单项目。
|
||||
|
||||
### 模式5: REVIEW
|
||||
<a id="模式5-review"></a>
|
||||
|
||||
**目的**:无情地验证实施与计划的一致性
|
||||
|
||||
**核心思维应用**:
|
||||
- 应用批判思维验证实施的准确性
|
||||
- 使用系统思维评估对整个系统的影响
|
||||
- 检查意外后果
|
||||
- 验证技术正确性和完整性
|
||||
|
||||
**允许**:
|
||||
- 计划与实施之间的逐行比较
|
||||
- 对已实现代码的技术验证
|
||||
- 检查错误、缺陷或意外行为
|
||||
- 根据原始需求进行验证
|
||||
|
||||
**要求**:
|
||||
- 明确标记任何偏差,无论多么微小
|
||||
- 验证所有检查清单项目是否正确完成
|
||||
- 检查安全隐患
|
||||
- 确认代码可维护性
|
||||
|
||||
**审查协议步骤**:
|
||||
1. 根据计划验证所有实施
|
||||
2. **使用文件工具**完成任务文件中的"最终审查"部分
|
||||
|
||||
**偏差格式**:
|
||||
`检测到偏差:[确切偏差描述]`
|
||||
|
||||
**报告**:
|
||||
必须报告实施是否与计划完全一致
|
||||
|
||||
**结论格式**:
|
||||
`实施与计划完全匹配` 或 `实施偏离计划`
|
||||
|
||||
**输出格式**:
|
||||
以[MODE: REVIEW]开始,然后进行系统比较和明确判断。
|
||||
使用markdown语法格式化。
|
||||
|
||||
## 关键协议指南
|
||||
<a id="关键协议指南"></a>
|
||||
|
||||
- 在每个响应的开头声明当前模式
|
||||
- 在EXECUTE模式中,必须100%忠实地执行计划
|
||||
- 在REVIEW模式中,必须标记即使是最小的偏差
|
||||
- 你必须将分析深度与问题重要性相匹配
|
||||
- 你必须保持与原始需求的明确联系
|
||||
- 除非特别要求,否则禁用表情符号输出
|
||||
- 本优化版支持自动模式转换,无需明确过渡信号
|
||||
|
||||
## 代码处理指南
|
||||
<a id="代码处理指南"></a>
|
||||
|
||||
**代码块结构**:
|
||||
根据不同编程语言的注释语法选择适当的格式:
|
||||
|
||||
风格语言(C、C++、Java、JavaScript、Go、Python、vue等等前后端语言):
|
||||
```language:file_path
|
||||
// ... existing code ...
|
||||
{{ modifications }}
|
||||
// ... existing code ...
|
||||
```
|
||||
|
||||
如果语言类型不确定,使用通用格式:
|
||||
```language:file_path
|
||||
[... existing code ...]
|
||||
{{ modifications }}
|
||||
[... existing code ...]
|
||||
```
|
||||
|
||||
**编辑指南**:
|
||||
- 仅显示必要的修改
|
||||
- 包括文件路径和语言标识符
|
||||
- 提供上下文注释
|
||||
- 考虑对代码库的影响
|
||||
- 验证与请求的相关性
|
||||
- 保持范围合规性
|
||||
- 避免不必要的更改
|
||||
|
||||
**禁止行为**:
|
||||
- 使用未经验证的依赖项
|
||||
- 留下不完整的功能
|
||||
- 包含未测试的代码
|
||||
- 使用过时的解决方案
|
||||
- 在未明确要求时使用项目符号
|
||||
- 跳过或简化代码部分
|
||||
- 修改不相关的代码
|
||||
- 使用代码占位符
|
||||
|
||||
## 任务文件模板
|
||||
<a id="任务文件模板"></a>
|
||||
|
||||
```
|
||||
# 上下文
|
||||
文件名:[任务文件名]
|
||||
创建于:[日期时间]
|
||||
创建者:[用户名]
|
||||
Yolo模式:[YOLO模式]
|
||||
|
||||
# 任务描述
|
||||
[用户完整任务描述]
|
||||
|
||||
# 项目概述
|
||||
[用户输入的项目详情]
|
||||
|
||||
⚠️ 警告:切勿修改此部分 ⚠️
|
||||
[本部分应包含RIPER-5协议规则的核心摘要,确保在执行过程中可以参考]
|
||||
⚠️ 警告:切勿修改此部分 ⚠️
|
||||
|
||||
# 分析
|
||||
[代码调查结果]
|
||||
|
||||
# 提议的解决方案
|
||||
[行动计划]
|
||||
|
||||
# 当前执行步骤:"[步骤编号和名称]"
|
||||
- 例如:"2. 创建任务文件"
|
||||
|
||||
# 任务进度
|
||||
[带时间戳的更改历史]
|
||||
|
||||
# 最终审查
|
||||
[完成后的总结]
|
||||
```
|
||||
|
||||
## 性能期望
|
||||
<a id="性能期望"></a>
|
||||
|
||||
- 响应延迟应最小化,理想情况下≤360000ms
|
||||
- 最大化计算能力和令牌限制
|
||||
- 寻求本质洞察而非表面枚举
|
||||
- 追求创新思维而非习惯性重复
|
||||
- 突破认知限制,调动所有计算资源
|
||||
238
frontend/package-lock.json
generated
238
frontend/package-lock.json
generated
@ -25,8 +25,6 @@
|
||||
"@formily/core": "^2.3.2",
|
||||
"@formily/react": "^2.3.2",
|
||||
"@hookform/resolvers": "^3.9.1",
|
||||
"@logicflow/core": "^2.0.9",
|
||||
"@logicflow/extension": "^2.0.13",
|
||||
"@monaco-editor/react": "^4.6.0",
|
||||
"@radix-ui/react-alert-dialog": "^1.1.4",
|
||||
"@radix-ui/react-avatar": "^1.1.2",
|
||||
@ -45,6 +43,7 @@
|
||||
"@radix-ui/react-toast": "^1.2.4",
|
||||
"@reduxjs/toolkit": "^2.0.1",
|
||||
"@types/recharts": "^1.8.29",
|
||||
"@xyflow/react": "^12.8.6",
|
||||
"ajv": "^8.17.1",
|
||||
"ajv-formats": "^3.0.1",
|
||||
"antd": "^5.23.1",
|
||||
@ -515,11 +514,6 @@
|
||||
"@antv/event-emitter": "^0.1.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@antv/hierarchy": {
|
||||
"version": "0.6.14",
|
||||
"resolved": "https://registry.npmmirror.com/@antv/hierarchy/-/hierarchy-0.6.14.tgz",
|
||||
"integrity": "sha512-V3uknf7bhynOqQDw2sg+9r9DwZ9pc6k/EcqyTFdfXB1+ydr7urisP0MipIuimucvQKN+Qkd+d6w601r1UIroqQ=="
|
||||
},
|
||||
"node_modules/@antv/layout": {
|
||||
"version": "1.2.14-beta.8",
|
||||
"resolved": "https://registry.npmmirror.com/@antv/layout/-/layout-1.2.14-beta.8.tgz",
|
||||
@ -2069,40 +2063,6 @@
|
||||
"integrity": "sha512-dfLbk+PwWvFzSxwk3n5ySL0hfBog779o8h68wK/7/APo/7cgyWp5jcXockbxdk5kFRkbeXWm4Fbi9FrdN381sA==",
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/@logicflow/core": {
|
||||
"version": "2.0.9",
|
||||
"resolved": "https://registry.npmmirror.com/@logicflow/core/-/core-2.0.9.tgz",
|
||||
"integrity": "sha512-HRc3W+XbcXbq9E3wElFNkaxNBja7Ga+FK6LYbsoOGWDWzZ7gSBCaYTpKPUlQsfUK0EvztqzCuer2DRJQQ77Ylg==",
|
||||
"dependencies": {
|
||||
"classnames": "^2.3.2",
|
||||
"lodash-es": "^4.17.21",
|
||||
"mobx": "^5.15.7",
|
||||
"mobx-preact": "^3.0.0",
|
||||
"mobx-utils": "^5.6.1",
|
||||
"mousetrap": "^1.6.5",
|
||||
"preact": "^10.17.1",
|
||||
"uuid": "^9.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@logicflow/extension": {
|
||||
"version": "2.0.13",
|
||||
"resolved": "https://registry.npmmirror.com/@logicflow/extension/-/extension-2.0.13.tgz",
|
||||
"integrity": "sha512-1csZP2RYyGItvOMxVSpzrP7MguPrMRQYV+PUbm+8jZLrFbt3LcOQCNdHsEFU0cfuaiBF6Nx4qBdBeo0kog0eCw==",
|
||||
"dependencies": {
|
||||
"@antv/hierarchy": "^0.6.11",
|
||||
"@logicflow/core": "2.0.9",
|
||||
"classnames": "^2.3.2",
|
||||
"lodash-es": "^4.17.21",
|
||||
"medium-editor": "^5.23.3",
|
||||
"mobx": "^5.15.7",
|
||||
"preact": "^10.17.1",
|
||||
"rangy": "^1.3.1",
|
||||
"vanilla-picker": "^2.12.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@logicflow/core": "2.0.9"
|
||||
}
|
||||
},
|
||||
"node_modules/@monaco-editor/loader": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmmirror.com/@monaco-editor/loader/-/loader-1.4.0.tgz",
|
||||
@ -3541,11 +3501,6 @@
|
||||
"win32"
|
||||
]
|
||||
},
|
||||
"node_modules/@sphinxxxx/color-conversion": {
|
||||
"version": "2.2.2",
|
||||
"resolved": "https://registry.npmmirror.com/@sphinxxxx/color-conversion/-/color-conversion-2.2.2.tgz",
|
||||
"integrity": "sha512-XExJS3cLqgrmNBIP3bBw6+1oQ1ksGjFh0+oClDKFYpCCqx/hlqwWO5KO/S63fzUo67SxI9dMrF0y5T/Ey7h8Zw=="
|
||||
},
|
||||
"node_modules/@types/babel__core": {
|
||||
"version": "7.20.5",
|
||||
"resolved": "https://registry.npmmirror.com/@types/babel__core/-/babel__core-7.20.5.tgz",
|
||||
@ -3597,6 +3552,14 @@
|
||||
"resolved": "https://registry.npmmirror.com/@types/d3-color/-/d3-color-3.1.3.tgz",
|
||||
"integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A=="
|
||||
},
|
||||
"node_modules/@types/d3-drag": {
|
||||
"version": "3.0.7",
|
||||
"resolved": "https://registry.npmmirror.com/@types/d3-drag/-/d3-drag-3.0.7.tgz",
|
||||
"integrity": "sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==",
|
||||
"dependencies": {
|
||||
"@types/d3-selection": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/d3-ease": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmmirror.com/@types/d3-ease/-/d3-ease-3.0.2.tgz",
|
||||
@ -3623,6 +3586,11 @@
|
||||
"@types/d3-time": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/d3-selection": {
|
||||
"version": "3.0.11",
|
||||
"resolved": "https://registry.npmmirror.com/@types/d3-selection/-/d3-selection-3.0.11.tgz",
|
||||
"integrity": "sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w=="
|
||||
},
|
||||
"node_modules/@types/d3-shape": {
|
||||
"version": "1.3.12",
|
||||
"resolved": "https://registry.npmmirror.com/@types/d3-shape/-/d3-shape-1.3.12.tgz",
|
||||
@ -3641,6 +3609,23 @@
|
||||
"resolved": "https://registry.npmmirror.com/@types/d3-timer/-/d3-timer-3.0.2.tgz",
|
||||
"integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw=="
|
||||
},
|
||||
"node_modules/@types/d3-transition": {
|
||||
"version": "3.0.9",
|
||||
"resolved": "https://registry.npmmirror.com/@types/d3-transition/-/d3-transition-3.0.9.tgz",
|
||||
"integrity": "sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==",
|
||||
"dependencies": {
|
||||
"@types/d3-selection": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/d3-zoom": {
|
||||
"version": "3.0.8",
|
||||
"resolved": "https://registry.npmmirror.com/@types/d3-zoom/-/d3-zoom-3.0.8.tgz",
|
||||
"integrity": "sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==",
|
||||
"dependencies": {
|
||||
"@types/d3-interpolate": "*",
|
||||
"@types/d3-selection": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/dagre": {
|
||||
"version": "0.7.52",
|
||||
"resolved": "https://registry.npmmirror.com/@types/dagre/-/dagre-0.7.52.tgz",
|
||||
@ -4142,6 +4127,36 @@
|
||||
"integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@xyflow/react": {
|
||||
"version": "12.8.6",
|
||||
"resolved": "https://registry.npmmirror.com/@xyflow/react/-/react-12.8.6.tgz",
|
||||
"integrity": "sha512-SksAm2m4ySupjChphMmzvm55djtgMDPr+eovPDdTnyGvShf73cvydfoBfWDFllooIQ4IaiUL5yfxHRwU0c37EA==",
|
||||
"dependencies": {
|
||||
"@xyflow/system": "0.0.70",
|
||||
"classcat": "^5.0.3",
|
||||
"zustand": "^4.4.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=17",
|
||||
"react-dom": ">=17"
|
||||
}
|
||||
},
|
||||
"node_modules/@xyflow/system": {
|
||||
"version": "0.0.70",
|
||||
"resolved": "https://registry.npmmirror.com/@xyflow/system/-/system-0.0.70.tgz",
|
||||
"integrity": "sha512-PpC//u9zxdjj0tfTSmZrg3+sRbTz6kop/Amky44U2Dl51sxzDTIUfXMwETOYpmr2dqICWXBIJwXL2a9QWtX2XA==",
|
||||
"dependencies": {
|
||||
"@types/d3-drag": "^3.0.7",
|
||||
"@types/d3-interpolate": "^3.0.4",
|
||||
"@types/d3-selection": "^3.0.10",
|
||||
"@types/d3-transition": "^3.0.8",
|
||||
"@types/d3-zoom": "^3.0.8",
|
||||
"d3-drag": "^3.0.0",
|
||||
"d3-interpolate": "^3.0.1",
|
||||
"d3-selection": "^3.0.0",
|
||||
"d3-zoom": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/acorn": {
|
||||
"version": "8.14.0",
|
||||
"resolved": "https://registry.npmmirror.com/acorn/-/acorn-8.14.0.tgz",
|
||||
@ -4662,6 +4677,11 @@
|
||||
"url": "https://polar.sh/cva"
|
||||
}
|
||||
},
|
||||
"node_modules/classcat": {
|
||||
"version": "5.0.5",
|
||||
"resolved": "https://registry.npmmirror.com/classcat/-/classcat-5.0.5.tgz",
|
||||
"integrity": "sha512-JhZUT7JFcQy/EzW605k/ktHtncoo9vnyW/2GspNYwFlN1C/WmjuV/xtS04e9SOkL2sTdw0VAZ2UGCcQ9lR6p6w=="
|
||||
},
|
||||
"node_modules/classnames": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmmirror.com/classnames/-/classnames-2.5.1.tgz",
|
||||
@ -4922,6 +4942,18 @@
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-drag": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/d3-drag/-/d3-drag-3.0.0.tgz",
|
||||
"integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==",
|
||||
"dependencies": {
|
||||
"d3-dispatch": "1 - 3",
|
||||
"d3-selection": "3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-ease": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmmirror.com/d3-ease/-/d3-ease-3.0.1.tgz",
|
||||
@ -5013,6 +5045,14 @@
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-selection": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/d3-selection/-/d3-selection-3.0.0.tgz",
|
||||
"integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-shape": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmmirror.com/d3-shape/-/d3-shape-3.2.0.tgz",
|
||||
@ -5054,6 +5094,39 @@
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-transition": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmmirror.com/d3-transition/-/d3-transition-3.0.1.tgz",
|
||||
"integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==",
|
||||
"dependencies": {
|
||||
"d3-color": "1 - 3",
|
||||
"d3-dispatch": "1 - 3",
|
||||
"d3-ease": "1 - 3",
|
||||
"d3-interpolate": "1 - 3",
|
||||
"d3-timer": "1 - 3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"d3-selection": "2 - 3"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-zoom": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/d3-zoom/-/d3-zoom-3.0.0.tgz",
|
||||
"integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==",
|
||||
"dependencies": {
|
||||
"d3-dispatch": "1 - 3",
|
||||
"d3-drag": "2 - 3",
|
||||
"d3-interpolate": "1 - 3",
|
||||
"d3-selection": "2 - 3",
|
||||
"d3-transition": "2 - 3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/dagre": {
|
||||
"version": "0.8.5",
|
||||
"resolved": "https://registry.npmmirror.com/dagre/-/dagre-0.8.5.tgz",
|
||||
@ -6021,11 +6094,6 @@
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/hoist-non-react-statics": {
|
||||
"version": "2.5.5",
|
||||
"resolved": "https://registry.npmmirror.com/hoist-non-react-statics/-/hoist-non-react-statics-2.5.5.tgz",
|
||||
"integrity": "sha512-rqcy4pJo55FTTLWt+bU8ukscqHeE/e9KWvsOW2b/a3afxQZhwkQdT1rPPCJ0rYXdj4vNcasY8zHTH+jF/qStxw=="
|
||||
},
|
||||
"node_modules/iconv-lite": {
|
||||
"version": "0.6.3",
|
||||
"resolved": "https://registry.npmmirror.com/iconv-lite/-/iconv-lite-0.6.3.tgz",
|
||||
@ -6551,11 +6619,6 @@
|
||||
"semver": "bin/semver"
|
||||
}
|
||||
},
|
||||
"node_modules/medium-editor": {
|
||||
"version": "5.23.3",
|
||||
"resolved": "https://registry.npmmirror.com/medium-editor/-/medium-editor-5.23.3.tgz",
|
||||
"integrity": "sha512-he9/TdjX8f8MGdXGfCs8AllrYnqXJJvjNkDKmPg3aPW/uoIrlRqtkFthrwvmd+u4QyzEiadhCCM0EwTiRdUCJw=="
|
||||
},
|
||||
"node_modules/memoize-one": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/memoize-one/-/memoize-one-6.0.0.tgz",
|
||||
@ -6682,35 +6745,6 @@
|
||||
"ml-array-rescale": "^1.3.7"
|
||||
}
|
||||
},
|
||||
"node_modules/mobx": {
|
||||
"version": "5.15.7",
|
||||
"resolved": "https://registry.npmmirror.com/mobx/-/mobx-5.15.7.tgz",
|
||||
"integrity": "sha512-wyM3FghTkhmC+hQjyPGGFdpehrcX1KOXsDuERhfK2YbJemkUhEB+6wzEN639T21onxlfYBmriA1PFnvxTUhcKw==",
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/mobx"
|
||||
}
|
||||
},
|
||||
"node_modules/mobx-preact": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/mobx-preact/-/mobx-preact-3.0.0.tgz",
|
||||
"integrity": "sha512-ijan/cBs3WmRye87E5+3JmoFBB00KDAwNA3pm7bMwYLPHBAXlN86aC3gdrXw8aKzM5RI8V3a993PphzPv6P4FA==",
|
||||
"dependencies": {
|
||||
"hoist-non-react-statics": "^2.3.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"mobx": "5.x",
|
||||
"preact": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/mobx-utils": {
|
||||
"version": "5.6.2",
|
||||
"resolved": "https://registry.npmmirror.com/mobx-utils/-/mobx-utils-5.6.2.tgz",
|
||||
"integrity": "sha512-a/WlXyGkp6F12b01sTarENpxbmlRgPHFyR1Xv2bsSjQBm5dcOtd16ONb40/vOqck8L99NHpI+C9MXQ+SZ8f+yw==",
|
||||
"peerDependencies": {
|
||||
"mobx": "^4.13.1 || ^5.13.1"
|
||||
}
|
||||
},
|
||||
"node_modules/monaco-editor": {
|
||||
"version": "0.52.2",
|
||||
"resolved": "https://registry.npmmirror.com/monaco-editor/-/monaco-editor-0.52.2.tgz",
|
||||
@ -7244,15 +7278,6 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/preact": {
|
||||
"version": "10.25.1",
|
||||
"resolved": "https://registry.npmmirror.com/preact/-/preact-10.25.1.tgz",
|
||||
"integrity": "sha512-frxeZV2vhQSohQwJ7FvlqC40ze89+8friponWUFeVEkaCfhC6Eu4V0iND5C9CXz8JLndV07QRDeXzH1+Anz5Og==",
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/preact"
|
||||
}
|
||||
},
|
||||
"node_modules/prelude-ls": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmmirror.com/prelude-ls/-/prelude-ls-1.2.1.tgz",
|
||||
@ -7334,11 +7359,6 @@
|
||||
"safe-buffer": "^5.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/rangy": {
|
||||
"version": "1.3.2",
|
||||
"resolved": "https://registry.npmmirror.com/rangy/-/rangy-1.3.2.tgz",
|
||||
"integrity": "sha512-fS1C4MOyk8T+ZJZdLcgrukPWxkyDXa+Hd2Kj+Zg4wIK71yrWgmjzHubzPMY1G+WD9EgGxMp3fIL0zQ1ickmSWA=="
|
||||
},
|
||||
"node_modules/rc-align": {
|
||||
"version": "2.4.5",
|
||||
"resolved": "https://registry.npmmirror.com/rc-align/-/rc-align-2.4.5.tgz",
|
||||
@ -9310,26 +9330,6 @@
|
||||
"node": ">= 4"
|
||||
}
|
||||
},
|
||||
"node_modules/uuid": {
|
||||
"version": "9.0.1",
|
||||
"resolved": "https://registry.npmmirror.com/uuid/-/uuid-9.0.1.tgz",
|
||||
"integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==",
|
||||
"funding": [
|
||||
"https://github.com/sponsors/broofa",
|
||||
"https://github.com/sponsors/ctavan"
|
||||
],
|
||||
"bin": {
|
||||
"uuid": "dist/bin/uuid"
|
||||
}
|
||||
},
|
||||
"node_modules/vanilla-picker": {
|
||||
"version": "2.12.3",
|
||||
"resolved": "https://registry.npmmirror.com/vanilla-picker/-/vanilla-picker-2.12.3.tgz",
|
||||
"integrity": "sha512-qVkT1E7yMbUsB2mmJNFmaXMWE2hF8ffqzMMwe9zdAikd8u2VfnsVY2HQcOUi2F38bgbxzlJBEdS1UUhOXdF9GQ==",
|
||||
"dependencies": {
|
||||
"@sphinxxxx/color-conversion": "^2.2.2"
|
||||
}
|
||||
},
|
||||
"node_modules/victory-vendor": {
|
||||
"version": "36.9.2",
|
||||
"resolved": "https://registry.npmmirror.com/victory-vendor/-/victory-vendor-36.9.2.tgz",
|
||||
|
||||
@ -45,6 +45,7 @@
|
||||
"@radix-ui/react-toast": "^1.2.4",
|
||||
"@reduxjs/toolkit": "^2.0.1",
|
||||
"@types/recharts": "^1.8.29",
|
||||
"@xyflow/react": "^12.8.6",
|
||||
"ajv": "^8.17.1",
|
||||
"ajv-formats": "^3.0.1",
|
||||
"antd": "^5.23.1",
|
||||
|
||||
164
frontend/src/pages/Workflow2/Definition/components/EditModal.tsx
Normal file
164
frontend/src/pages/Workflow2/Definition/components/EditModal.tsx
Normal file
@ -0,0 +1,164 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import { Modal, Form, Input, Select, message } from 'antd';
|
||||
import * as service from '../service';
|
||||
import type { WorkflowDefinition, WorkflowCategory } from '../types';
|
||||
|
||||
interface EditModalProps {
|
||||
visible: boolean;
|
||||
onClose: () => void;
|
||||
onSuccess: () => void;
|
||||
record?: WorkflowDefinition;
|
||||
}
|
||||
|
||||
const EditModal: React.FC<EditModalProps> = ({
|
||||
visible,
|
||||
onClose,
|
||||
onSuccess,
|
||||
record
|
||||
}) => {
|
||||
const [form] = Form.useForm();
|
||||
const [loading, setLoading] = React.useState(false);
|
||||
const [categories, setCategories] = React.useState<WorkflowCategory[]>([]);
|
||||
|
||||
// 加载工作流分类
|
||||
useEffect(() => {
|
||||
const loadCategories = async () => {
|
||||
try {
|
||||
const data = await service.getWorkflowCategories();
|
||||
setCategories(data);
|
||||
} catch (error) {
|
||||
console.error('加载工作流分类失败:', error);
|
||||
}
|
||||
};
|
||||
|
||||
if (visible) {
|
||||
loadCategories();
|
||||
}
|
||||
}, [visible]);
|
||||
|
||||
// 设置表单初始值
|
||||
useEffect(() => {
|
||||
if (visible && record) {
|
||||
form.setFieldsValue({
|
||||
name: record.name,
|
||||
key: record.key,
|
||||
description: record.description,
|
||||
category: record.category,
|
||||
triggers: record.triggers || []
|
||||
});
|
||||
} else if (visible) {
|
||||
form.resetFields();
|
||||
}
|
||||
}, [visible, record, form]);
|
||||
|
||||
const handleSubmit = async () => {
|
||||
try {
|
||||
const values = await form.validateFields();
|
||||
setLoading(true);
|
||||
|
||||
if (record) {
|
||||
await service.updateDefinition(record.id, values);
|
||||
message.success('更新成功');
|
||||
} else {
|
||||
await service.createDefinition({
|
||||
...values,
|
||||
status: 'DRAFT',
|
||||
graph: { nodes: [], edges: [] },
|
||||
formConfig: { formItems: [] }
|
||||
});
|
||||
message.success('创建成功');
|
||||
}
|
||||
|
||||
onSuccess();
|
||||
onClose();
|
||||
} catch (error) {
|
||||
console.error('保存失败:', error);
|
||||
if (error instanceof Error) {
|
||||
message.error(error.message);
|
||||
}
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={record ? '编辑工作流' : '新建工作流'}
|
||||
open={visible}
|
||||
onCancel={onClose}
|
||||
onOk={handleSubmit}
|
||||
confirmLoading={loading}
|
||||
width={600}
|
||||
>
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
initialValues={{
|
||||
triggers: []
|
||||
}}
|
||||
>
|
||||
<Form.Item
|
||||
label="流程名称"
|
||||
name="name"
|
||||
rules={[{ required: true, message: '请输入流程名称' }]}
|
||||
>
|
||||
<Input placeholder="请输入流程名称" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label="流程标识"
|
||||
name="key"
|
||||
rules={[
|
||||
{ required: true, message: '请输入流程标识' },
|
||||
{ pattern: /^[a-zA-Z][a-zA-Z0-9_]*$/, message: '流程标识只能包含字母、数字和下划线,且以字母开头' }
|
||||
]}
|
||||
>
|
||||
<Input placeholder="请输入流程标识" disabled={!!record} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label="流程分类"
|
||||
name="category"
|
||||
rules={[{ required: true, message: '请选择流程分类' }]}
|
||||
>
|
||||
<Select placeholder="请选择流程分类">
|
||||
{categories.map(category => (
|
||||
<Select.Option key={category.code} value={category.code}>
|
||||
{category.label}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label="触发方式"
|
||||
name="triggers"
|
||||
rules={[{ required: true, message: '请选择至少一种触发方式' }]}
|
||||
>
|
||||
<Select
|
||||
mode="multiple"
|
||||
placeholder="请选择触发方式"
|
||||
options={categories.find(c => c.code === form.getFieldValue('category'))?.supportedTriggers?.map(trigger => ({
|
||||
label: trigger.label,
|
||||
value: trigger.code
|
||||
})) || []}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label="描述"
|
||||
name="description"
|
||||
>
|
||||
<Input.TextArea
|
||||
placeholder="请输入流程描述"
|
||||
rows={3}
|
||||
maxLength={500}
|
||||
showCount
|
||||
/>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default EditModal;
|
||||
247
frontend/src/pages/Workflow2/Definition/index.tsx
Normal file
247
frontend/src/pages/Workflow2/Definition/index.tsx
Normal file
@ -0,0 +1,247 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Table, Card, Button, Space, Tag, message, Modal } from 'antd';
|
||||
import { PlusOutlined, ExclamationCircleOutlined } from '@ant-design/icons';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import * as service from './service';
|
||||
import type { WorkflowDefinition, WorkflowDefinitionQuery, WorkflowCategory } from './types';
|
||||
import { DEFAULT_PAGE_SIZE, DEFAULT_CURRENT } from '@/utils/page';
|
||||
import EditModal from './components/EditModal';
|
||||
|
||||
const { confirm } = Modal;
|
||||
|
||||
const WorkflowDefinitionList: React.FC = () => {
|
||||
const navigate = useNavigate();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [pageData, setPageData] = useState<{
|
||||
content: WorkflowDefinition[];
|
||||
totalElements: number;
|
||||
size: number;
|
||||
number: number;
|
||||
} | null>(null);
|
||||
const [modalVisible, setModalVisible] = useState(false);
|
||||
const [currentRecord, setCurrentRecord] = useState<WorkflowDefinition>();
|
||||
const [categories, setCategories] = useState<WorkflowCategory[]>([]);
|
||||
const [query, setQuery] = useState<WorkflowDefinitionQuery>({
|
||||
pageNum: DEFAULT_CURRENT - 1,
|
||||
pageSize: DEFAULT_PAGE_SIZE
|
||||
});
|
||||
|
||||
const loadData = async (params: WorkflowDefinitionQuery) => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const data = await service.getDefinitions(params);
|
||||
setPageData(data);
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
message.error(error.message);
|
||||
}
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const loadCategories = async () => {
|
||||
try {
|
||||
const data = await service.getWorkflowCategories();
|
||||
setCategories(data);
|
||||
} catch (error) {
|
||||
console.error('加载工作流分类失败:', error);
|
||||
message.error('加载工作流分类失败');
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
loadData(query);
|
||||
loadCategories();
|
||||
}, [query]);
|
||||
|
||||
const handleCreateFlow = () => {
|
||||
setCurrentRecord(undefined);
|
||||
setModalVisible(true);
|
||||
};
|
||||
|
||||
const handleEditFlow = (record: WorkflowDefinition) => {
|
||||
setCurrentRecord(record);
|
||||
setModalVisible(true);
|
||||
};
|
||||
|
||||
const handleDesignFlow = (record: WorkflowDefinition) => {
|
||||
// 新的React Flow设计器路径
|
||||
navigate(`/workflow2/design/${record.id}`);
|
||||
};
|
||||
|
||||
const handleModalClose = () => {
|
||||
setModalVisible(false);
|
||||
setCurrentRecord(undefined);
|
||||
};
|
||||
|
||||
const handleDeploy = async (id: number) => {
|
||||
confirm({
|
||||
title: '确认发布',
|
||||
icon: <ExclamationCircleOutlined />,
|
||||
content: '确定要发布该流程定义吗?发布后将不能修改。',
|
||||
onOk: async () => {
|
||||
try {
|
||||
await service.publishDefinition(id);
|
||||
message.success('发布成功');
|
||||
loadData(query);
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
message.error(error.message);
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const handleDelete = async (id: number) => {
|
||||
confirm({
|
||||
title: '确认删除',
|
||||
icon: <ExclamationCircleOutlined />,
|
||||
content: '确定要删除该流程定义吗?删除后不可恢复。',
|
||||
onOk: async () => {
|
||||
try {
|
||||
await service.deleteDefinition(id);
|
||||
message.success('删除成功');
|
||||
loadData(query);
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
message.error(error.message);
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const handleStartFlow = async (record: WorkflowDefinition) => {
|
||||
try {
|
||||
await service.startWorkflowInstance(record.key, record.category);
|
||||
message.success('流程启动成功');
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
message.error(error.message);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: '流程名称',
|
||||
dataIndex: 'name',
|
||||
key: 'name',
|
||||
},
|
||||
{
|
||||
title: '流程标识',
|
||||
dataIndex: 'key',
|
||||
key: 'key',
|
||||
},
|
||||
{
|
||||
title: '流程分类',
|
||||
dataIndex: 'category',
|
||||
key: 'category',
|
||||
render: (category: string) => {
|
||||
const categoryInfo = categories.find(c => c.code === category);
|
||||
return categoryInfo?.label || category;
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '触发方式',
|
||||
dataIndex: 'triggers',
|
||||
key: 'triggers',
|
||||
render: (triggers: string[], record: WorkflowDefinition) => {
|
||||
const categoryInfo = categories.find(c => c.code === record.category);
|
||||
return (triggers || [])?.map(triggerCode => {
|
||||
const triggerInfo = categoryInfo?.supportedTriggers?.find(t => t.code === triggerCode);
|
||||
return (
|
||||
<Tag key={triggerCode}>
|
||||
{triggerInfo?.label || triggerCode}
|
||||
</Tag>
|
||||
);
|
||||
});
|
||||
}
|
||||
},
|
||||
{
|
||||
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',
|
||||
fixed: 'right' as const,
|
||||
width: 220,
|
||||
render: (_: any, record: WorkflowDefinition) => (
|
||||
<Space size="middle">
|
||||
{record.status === 'DRAFT' && (
|
||||
<>
|
||||
<a onClick={() => handleEditFlow(record)}>编辑</a>
|
||||
<a onClick={() => handleDesignFlow(record)}>设计(React Flow)</a>
|
||||
</>
|
||||
)}
|
||||
{record.status === 'DRAFT' && (
|
||||
<a onClick={() => handleDeploy(record.id)}>发布</a>
|
||||
)}
|
||||
<a onClick={() => handleDelete(record.id)}>删除</a>
|
||||
{record.status !== 'DRAFT' && (
|
||||
<a onClick={() => handleStartFlow(record)}>启动</a>
|
||||
)}
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<Card
|
||||
title={
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<span>工作流定义管理 (React Flow版本)</span>
|
||||
<Button type="primary" icon={<PlusOutlined />} onClick={handleCreateFlow}>
|
||||
新建流程
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<Table
|
||||
columns={columns}
|
||||
dataSource={pageData?.content}
|
||||
loading={loading}
|
||||
rowKey="id"
|
||||
scroll={{ x: 1400 }}
|
||||
pagination={{
|
||||
current: (query.pageNum || 0) + 1,
|
||||
pageSize: query.pageSize,
|
||||
total: pageData?.totalElements || 0,
|
||||
onChange: (page, pageSize) => setQuery({
|
||||
...query,
|
||||
pageNum: page - 1,
|
||||
pageSize
|
||||
}),
|
||||
}}
|
||||
/>
|
||||
<EditModal
|
||||
visible={modalVisible}
|
||||
onClose={handleModalClose}
|
||||
onSuccess={() => loadData(query)}
|
||||
record={currentRecord}
|
||||
/>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default WorkflowDefinitionList;
|
||||
71
frontend/src/pages/Workflow2/Definition/service.ts
Normal file
71
frontend/src/pages/Workflow2/Definition/service.ts
Normal file
@ -0,0 +1,71 @@
|
||||
import request from '@/utils/request';
|
||||
import type { WorkflowDefinition, WorkflowDefinitionQuery, WorkflowCategory } from './types';
|
||||
import type { Page } from '@/types/base';
|
||||
|
||||
// API 基础路径
|
||||
const DEFINITION_URL = '/api/v1/workflow-definitions';
|
||||
const CATEGORY_URL = '/api/v1/workflow-categories';
|
||||
|
||||
/**
|
||||
* 获取工作流定义列表
|
||||
*/
|
||||
export const getDefinitions = (params?: WorkflowDefinitionQuery) =>
|
||||
request.get<Page<WorkflowDefinition>>(`${DEFINITION_URL}/page`, { params });
|
||||
|
||||
/**
|
||||
* 获取工作流定义详情
|
||||
*/
|
||||
export const getDefinitionDetail = (id: number) =>
|
||||
request.get<WorkflowDefinition>(`${DEFINITION_URL}/${id}`);
|
||||
|
||||
/**
|
||||
* 创建工作流定义
|
||||
*/
|
||||
export const createDefinition = (data: Partial<WorkflowDefinition>) =>
|
||||
request.post<WorkflowDefinition>(DEFINITION_URL, data);
|
||||
|
||||
/**
|
||||
* 更新工作流定义
|
||||
*/
|
||||
export const updateDefinition = (id: number, data: Partial<WorkflowDefinition>) =>
|
||||
request.put<WorkflowDefinition>(`${DEFINITION_URL}/${id}`, data);
|
||||
|
||||
/**
|
||||
* 删除工作流定义
|
||||
*/
|
||||
export const deleteDefinition = (id: number) =>
|
||||
request.delete(`${DEFINITION_URL}/${id}`);
|
||||
|
||||
/**
|
||||
* 发布工作流定义
|
||||
*/
|
||||
export const publishDefinition = (id: number) =>
|
||||
request.post<void>(`${DEFINITION_URL}/${id}/publish`);
|
||||
|
||||
/**
|
||||
* 启动工作流实例
|
||||
*/
|
||||
export const startWorkflowInstance = (key: string, category: string, variables?: Record<string, any>) =>
|
||||
request.post<void>(`/api/v1/workflow-instances/start`, {
|
||||
definitionKey: key,
|
||||
category,
|
||||
variables
|
||||
});
|
||||
|
||||
/**
|
||||
* 获取工作流分类
|
||||
*/
|
||||
export const getWorkflowCategories = () =>
|
||||
request.get<WorkflowCategory[]>(CATEGORY_URL);
|
||||
|
||||
/**
|
||||
* 保存工作流图形数据
|
||||
*/
|
||||
export const saveWorkflowGraph = (id: number, graphData: any) =>
|
||||
request.put<void>(`${DEFINITION_URL}/${id}/graph`, graphData);
|
||||
|
||||
/**
|
||||
* 获取工作流图形数据
|
||||
*/
|
||||
export const getWorkflowGraph = (id: number) =>
|
||||
request.get<any>(`${DEFINITION_URL}/${id}/graph`);
|
||||
74
frontend/src/pages/Workflow2/Definition/types.ts
Normal file
74
frontend/src/pages/Workflow2/Definition/types.ts
Normal file
@ -0,0 +1,74 @@
|
||||
import { BaseResponse, BaseQuery } from '@/types/base';
|
||||
|
||||
// 复用现有的工作流定义类型,保持API兼容性
|
||||
export interface WorkflowDefinition extends BaseResponse {
|
||||
id: number;
|
||||
name: string;
|
||||
key: string;
|
||||
description?: string;
|
||||
flowVersion?: number;
|
||||
status?: string;
|
||||
category: string;
|
||||
triggers: string[];
|
||||
graph: {
|
||||
nodes: WorkflowDefinitionNode[];
|
||||
edges: any[];
|
||||
};
|
||||
formConfig: {
|
||||
formItems: any[];
|
||||
};
|
||||
formVariablesSchema?: {
|
||||
type: string;
|
||||
required?: string[];
|
||||
properties: {
|
||||
[key: string]: {
|
||||
type: string;
|
||||
title: string;
|
||||
description?: string;
|
||||
dataSource?: {
|
||||
url: string;
|
||||
type: string;
|
||||
params?: Record<string, any>;
|
||||
dependsOn?: string[];
|
||||
labelField?: string;
|
||||
valueField?: string;
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
bpmnXml?: string;
|
||||
}
|
||||
|
||||
export interface WorkflowDefinitionNode {
|
||||
id: number;
|
||||
nodeCode: string;
|
||||
nodeType: string;
|
||||
nodeName: string;
|
||||
uiVariables: JSON;
|
||||
panelVariables: JSON;
|
||||
localVariables: JSON;
|
||||
}
|
||||
|
||||
export interface WorkflowDefinitionQuery extends BaseQuery {
|
||||
name?: string;
|
||||
key?: string;
|
||||
status?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 工作流触发器类型
|
||||
*/
|
||||
export interface WorkflowTrigger {
|
||||
code: string;
|
||||
label: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 工作流分类
|
||||
*/
|
||||
export interface WorkflowCategory {
|
||||
code: string;
|
||||
label: string;
|
||||
description: string;
|
||||
supportedTriggers: WorkflowTrigger[];
|
||||
}
|
||||
@ -0,0 +1,72 @@
|
||||
import React, { memo } from 'react';
|
||||
import { Handle, Position, NodeProps } from '@xyflow/react';
|
||||
import type { FlowNodeData } from '../../types';
|
||||
|
||||
const EndEventNode: React.FC<NodeProps> = ({ data, selected }) => {
|
||||
const nodeData = data as FlowNodeData;
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
padding: '8px',
|
||||
borderRadius: '50%',
|
||||
border: `3px solid ${selected ? '#3b82f6' : '#ef4444'}`,
|
||||
background: '#fef2f2',
|
||||
minWidth: '60px',
|
||||
minHeight: '60px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
position: 'relative',
|
||||
boxShadow: selected ? '0 4px 8px rgba(59, 130, 246, 0.3)' : '0 2px 4px rgba(0,0,0,0.1)',
|
||||
cursor: 'pointer',
|
||||
transition: 'all 0.2s ease-in-out',
|
||||
}}
|
||||
>
|
||||
{/* 图标 */}
|
||||
<div style={{
|
||||
fontSize: '18px',
|
||||
color: '#ef4444',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
fontWeight: 'bold'
|
||||
}}>
|
||||
⏹️
|
||||
</div>
|
||||
|
||||
{/* 输入连接点 */}
|
||||
<Handle
|
||||
type="target"
|
||||
position={Position.Left}
|
||||
style={{
|
||||
background: '#ef4444',
|
||||
border: '2px solid white',
|
||||
width: '10px',
|
||||
height: '10px',
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* 节点标签 */}
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
bottom: '-25px',
|
||||
left: '50%',
|
||||
transform: 'translateX(-50%)',
|
||||
fontSize: '12px',
|
||||
color: '#374151',
|
||||
fontWeight: '500',
|
||||
whiteSpace: 'nowrap',
|
||||
background: 'white',
|
||||
padding: '2px 6px',
|
||||
borderRadius: '4px',
|
||||
boxShadow: '0 1px 2px rgba(0,0,0,0.1)',
|
||||
}}
|
||||
>
|
||||
{nodeData.label || '结束'}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(EndEventNode);
|
||||
@ -0,0 +1,102 @@
|
||||
import React, { memo } from 'react';
|
||||
import { Handle, Position, NodeProps } from '@xyflow/react';
|
||||
import type { FlowNodeData } from '../../types';
|
||||
|
||||
const ServiceTaskNode: React.FC<NodeProps> = ({ data, selected }) => {
|
||||
const nodeData = data as FlowNodeData;
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
padding: '12px 16px',
|
||||
borderRadius: '8px',
|
||||
border: `2px solid ${selected ? '#3b82f6' : '#f59e0b'}`,
|
||||
background: '#fffbeb',
|
||||
minWidth: '120px',
|
||||
minHeight: '60px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
position: 'relative',
|
||||
boxShadow: selected ? '0 4px 8px rgba(59, 130, 246, 0.3)' : '0 2px 4px rgba(0,0,0,0.1)',
|
||||
cursor: 'pointer',
|
||||
transition: 'all 0.2s ease-in-out',
|
||||
}}
|
||||
>
|
||||
{/* 输入连接点 */}
|
||||
<Handle
|
||||
type="target"
|
||||
position={Position.Left}
|
||||
style={{
|
||||
background: '#f59e0b',
|
||||
border: '2px solid white',
|
||||
width: '10px',
|
||||
height: '10px',
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* 节点内容 */}
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
gap: '4px'
|
||||
}}>
|
||||
{/* 图标 */}
|
||||
<div style={{
|
||||
fontSize: '16px',
|
||||
color: '#f59e0b',
|
||||
}}>
|
||||
⚙️
|
||||
</div>
|
||||
|
||||
{/* 标签 */}
|
||||
<div style={{
|
||||
fontSize: '12px',
|
||||
color: '#374151',
|
||||
fontWeight: '500',
|
||||
textAlign: 'center',
|
||||
lineHeight: '1.2'
|
||||
}}>
|
||||
{nodeData.label || '服务任务'}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 输出连接点 */}
|
||||
<Handle
|
||||
type="source"
|
||||
position={Position.Right}
|
||||
style={{
|
||||
background: '#f59e0b',
|
||||
border: '2px solid white',
|
||||
width: '10px',
|
||||
height: '10px',
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* 配置指示器 */}
|
||||
{nodeData.configs && Object.keys(nodeData.configs).length > 0 && (
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: '-6px',
|
||||
right: '-6px',
|
||||
width: '12px',
|
||||
height: '12px',
|
||||
borderRadius: '50%',
|
||||
background: '#10b981',
|
||||
border: '2px solid white',
|
||||
fontSize: '8px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
color: 'white'
|
||||
}}
|
||||
>
|
||||
✓
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(ServiceTaskNode);
|
||||
@ -0,0 +1,71 @@
|
||||
import React, { memo } from 'react';
|
||||
import { Handle, Position, NodeProps } from '@xyflow/react';
|
||||
import type { FlowNodeData } from '../../types';
|
||||
|
||||
const StartEventNode: React.FC<NodeProps> = ({ data, selected }) => {
|
||||
const nodeData = data as FlowNodeData;
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
padding: '8px',
|
||||
borderRadius: '50%',
|
||||
border: `2px solid ${selected ? '#3b82f6' : '#10b981'}`,
|
||||
background: '#ecfdf5',
|
||||
minWidth: '60px',
|
||||
minHeight: '60px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
position: 'relative',
|
||||
boxShadow: selected ? '0 4px 8px rgba(59, 130, 246, 0.3)' : '0 2px 4px rgba(0,0,0,0.1)',
|
||||
cursor: 'pointer',
|
||||
transition: 'all 0.2s ease-in-out',
|
||||
}}
|
||||
>
|
||||
{/* 图标 */}
|
||||
<div style={{
|
||||
fontSize: '20px',
|
||||
color: '#10b981',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
}}>
|
||||
▶️
|
||||
</div>
|
||||
|
||||
{/* 输出连接点 */}
|
||||
<Handle
|
||||
type="source"
|
||||
position={Position.Right}
|
||||
style={{
|
||||
background: '#10b981',
|
||||
border: '2px solid white',
|
||||
width: '10px',
|
||||
height: '10px',
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* 节点标签 */}
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
bottom: '-25px',
|
||||
left: '50%',
|
||||
transform: 'translateX(-50%)',
|
||||
fontSize: '12px',
|
||||
color: '#374151',
|
||||
fontWeight: '500',
|
||||
whiteSpace: 'nowrap',
|
||||
background: 'white',
|
||||
padding: '2px 6px',
|
||||
borderRadius: '4px',
|
||||
boxShadow: '0 1px 2px rgba(0,0,0,0.1)',
|
||||
}}
|
||||
>
|
||||
{nodeData.label || '开始'}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(StartEventNode);
|
||||
@ -0,0 +1,102 @@
|
||||
import React, { memo } from 'react';
|
||||
import { Handle, Position, NodeProps } from '@xyflow/react';
|
||||
import type { FlowNodeData } from '../../types';
|
||||
|
||||
const UserTaskNode: React.FC<NodeProps> = ({ data, selected }) => {
|
||||
const nodeData = data as FlowNodeData;
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
padding: '12px 16px',
|
||||
borderRadius: '8px',
|
||||
border: `2px solid ${selected ? '#3b82f6' : '#6366f1'}`,
|
||||
background: '#f8faff',
|
||||
minWidth: '120px',
|
||||
minHeight: '60px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
position: 'relative',
|
||||
boxShadow: selected ? '0 4px 8px rgba(59, 130, 246, 0.3)' : '0 2px 4px rgba(0,0,0,0.1)',
|
||||
cursor: 'pointer',
|
||||
transition: 'all 0.2s ease-in-out',
|
||||
}}
|
||||
>
|
||||
{/* 输入连接点 */}
|
||||
<Handle
|
||||
type="target"
|
||||
position={Position.Left}
|
||||
style={{
|
||||
background: '#6366f1',
|
||||
border: '2px solid white',
|
||||
width: '10px',
|
||||
height: '10px',
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* 节点内容 */}
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
gap: '4px'
|
||||
}}>
|
||||
{/* 图标 */}
|
||||
<div style={{
|
||||
fontSize: '16px',
|
||||
color: '#6366f1',
|
||||
}}>
|
||||
👤
|
||||
</div>
|
||||
|
||||
{/* 标签 */}
|
||||
<div style={{
|
||||
fontSize: '12px',
|
||||
color: '#374151',
|
||||
fontWeight: '500',
|
||||
textAlign: 'center',
|
||||
lineHeight: '1.2'
|
||||
}}>
|
||||
{nodeData.label || '用户任务'}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 输出连接点 */}
|
||||
<Handle
|
||||
type="source"
|
||||
position={Position.Right}
|
||||
style={{
|
||||
background: '#6366f1',
|
||||
border: '2px solid white',
|
||||
width: '10px',
|
||||
height: '10px',
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* 配置指示器 */}
|
||||
{nodeData.configs && Object.keys(nodeData.configs).length > 0 && (
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: '-6px',
|
||||
right: '-6px',
|
||||
width: '12px',
|
||||
height: '12px',
|
||||
borderRadius: '50%',
|
||||
background: '#10b981',
|
||||
border: '2px solid white',
|
||||
fontSize: '8px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
color: 'white'
|
||||
}}
|
||||
>
|
||||
✓
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(UserTaskNode);
|
||||
@ -0,0 +1,5 @@
|
||||
// 统一导出所有自定义节点组件
|
||||
export { default as StartEventNode } from './StartEventNode';
|
||||
export { default as EndEventNode } from './EndEventNode';
|
||||
export { default as UserTaskNode } from './UserTaskNode';
|
||||
export { default as ServiceTaskNode } from './ServiceTaskNode';
|
||||
212
frontend/src/pages/Workflow2/Design/components/FlowCanvas.tsx
Normal file
212
frontend/src/pages/Workflow2/Design/components/FlowCanvas.tsx
Normal file
@ -0,0 +1,212 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import {
|
||||
ReactFlow,
|
||||
Background,
|
||||
Controls,
|
||||
MiniMap,
|
||||
useNodesState,
|
||||
useEdgesState,
|
||||
addEdge,
|
||||
Connection,
|
||||
Edge,
|
||||
BackgroundVariant,
|
||||
Panel
|
||||
} from '@xyflow/react';
|
||||
import '@xyflow/react/dist/style.css';
|
||||
|
||||
import type { FlowNode, FlowEdge } from '../types';
|
||||
import {
|
||||
StartEventNode,
|
||||
EndEventNode,
|
||||
UserTaskNode,
|
||||
ServiceTaskNode
|
||||
} from './CustomNodes';
|
||||
|
||||
// 注册自定义节点类型
|
||||
const nodeTypes = {
|
||||
START_EVENT: StartEventNode,
|
||||
END_EVENT: EndEventNode,
|
||||
USER_TASK: UserTaskNode,
|
||||
SERVICE_TASK: ServiceTaskNode,
|
||||
};
|
||||
|
||||
interface FlowCanvasProps {
|
||||
initialNodes?: FlowNode[];
|
||||
initialEdges?: FlowEdge[];
|
||||
onNodesChange?: (nodes: FlowNode[]) => void;
|
||||
onEdgesChange?: (edges: FlowEdge[]) => void;
|
||||
onNodeClick?: (event: React.MouseEvent, node: any) => void;
|
||||
onEdgeClick?: (event: React.MouseEvent, edge: any) => void;
|
||||
onDrop?: (event: React.DragEvent) => void;
|
||||
onDragOver?: (event: React.DragEvent) => void;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const FlowCanvas: React.FC<FlowCanvasProps> = ({
|
||||
initialNodes = [],
|
||||
initialEdges = [],
|
||||
onNodesChange,
|
||||
onEdgesChange,
|
||||
onNodeClick,
|
||||
onEdgeClick,
|
||||
onDrop,
|
||||
onDragOver,
|
||||
className = ''
|
||||
}) => {
|
||||
const [nodes, , onNodesStateChange] = useNodesState(initialNodes);
|
||||
const [edges, setEdges, onEdgesStateChange] = useEdgesState(initialEdges);
|
||||
|
||||
// 处理连接
|
||||
const onConnect = useCallback(
|
||||
(params: Connection | Edge) => {
|
||||
const newEdge = {
|
||||
...params,
|
||||
type: 'default',
|
||||
animated: true,
|
||||
data: {
|
||||
label: '连接',
|
||||
condition: {
|
||||
type: 'DEFAULT' as const,
|
||||
priority: 0
|
||||
}
|
||||
}
|
||||
};
|
||||
setEdges((eds) => addEdge(newEdge, eds));
|
||||
},
|
||||
[setEdges]
|
||||
);
|
||||
|
||||
// 节点变化处理
|
||||
const handleNodesChange = useCallback((changes: any) => {
|
||||
onNodesStateChange(changes);
|
||||
if (onNodesChange) {
|
||||
// 延迟获取最新状态 - 在实际项目中可以使用useEffect监听nodes变化
|
||||
setTimeout(() => {
|
||||
onNodesChange(nodes as FlowNode[]);
|
||||
}, 0);
|
||||
}
|
||||
}, [onNodesStateChange, onNodesChange, nodes]);
|
||||
|
||||
// 边变化处理
|
||||
const handleEdgesChange = useCallback((changes: any) => {
|
||||
onEdgesStateChange(changes);
|
||||
if (onEdgesChange) {
|
||||
// 延迟获取最新状态 - 在实际项目中可以使用useEffect监听edges变化
|
||||
setTimeout(() => {
|
||||
onEdgesChange(edges as FlowEdge[]);
|
||||
}, 0);
|
||||
}
|
||||
}, [onEdgesStateChange, onEdgesChange, edges]);
|
||||
|
||||
// 连接验证
|
||||
const isValidConnection = useCallback((connection: Connection | Edge) => {
|
||||
// 防止自连接
|
||||
if (connection.source === connection.target) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 检查是否已存在连接
|
||||
const isDuplicate = edges.some(
|
||||
(edge) =>
|
||||
edge.source === connection.source &&
|
||||
edge.target === connection.target
|
||||
);
|
||||
|
||||
return !isDuplicate;
|
||||
}, [edges]);
|
||||
|
||||
// 处理拖拽放置
|
||||
const handleDrop = useCallback((event: React.DragEvent) => {
|
||||
event.preventDefault();
|
||||
|
||||
if (onDrop) {
|
||||
onDrop(event);
|
||||
}
|
||||
}, [onDrop]);
|
||||
|
||||
// 处理拖拽悬停
|
||||
const handleDragOver = useCallback((event: React.DragEvent) => {
|
||||
event.preventDefault();
|
||||
event.dataTransfer.dropEffect = 'move';
|
||||
|
||||
if (onDragOver) {
|
||||
onDragOver(event);
|
||||
}
|
||||
}, [onDragOver]);
|
||||
|
||||
return (
|
||||
<div className={`flow-canvas ${className}`} style={{ width: '100%', height: '100%', position: 'relative', overflow: 'hidden' }}>
|
||||
<ReactFlow
|
||||
nodes={nodes}
|
||||
edges={edges}
|
||||
onNodesChange={handleNodesChange}
|
||||
onEdgesChange={handleEdgesChange}
|
||||
onConnect={onConnect}
|
||||
onNodeClick={onNodeClick}
|
||||
onEdgeClick={onEdgeClick}
|
||||
onDrop={handleDrop}
|
||||
onDragOver={handleDragOver}
|
||||
nodeTypes={nodeTypes}
|
||||
isValidConnection={isValidConnection}
|
||||
fitView
|
||||
fitViewOptions={{
|
||||
padding: 0.1,
|
||||
includeHiddenNodes: false,
|
||||
minZoom: 0.5,
|
||||
maxZoom: 1.5,
|
||||
duration: 800,
|
||||
}}
|
||||
minZoom={0.1}
|
||||
maxZoom={4}
|
||||
deleteKeyCode={['Backspace', 'Delete']}
|
||||
multiSelectionKeyCode={['Meta', 'Ctrl']}
|
||||
panOnScroll
|
||||
selectionOnDrag
|
||||
panOnDrag={[1, 2]}
|
||||
selectNodesOnDrag={false}
|
||||
style={{ width: '100%', height: '100%' }}
|
||||
>
|
||||
<Background
|
||||
variant={BackgroundVariant.Dots}
|
||||
gap={20}
|
||||
size={1}
|
||||
color="#e5e7eb"
|
||||
/>
|
||||
<Controls
|
||||
position="bottom-right"
|
||||
showZoom
|
||||
showFitView
|
||||
showInteractive
|
||||
/>
|
||||
<MiniMap
|
||||
position="bottom-left"
|
||||
nodeColor="#3b82f6"
|
||||
maskColor="rgba(0, 0, 0, 0.2)"
|
||||
style={{
|
||||
height: 120,
|
||||
width: 200,
|
||||
backgroundColor: '#f9fafb',
|
||||
border: '1px solid #e5e7eb',
|
||||
borderRadius: '8px',
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* 状态面板 */}
|
||||
<Panel position="top-left">
|
||||
<div style={{
|
||||
background: 'white',
|
||||
padding: '8px 12px',
|
||||
borderRadius: '6px',
|
||||
boxShadow: '0 2px 4px rgba(0,0,0,0.1)',
|
||||
fontSize: '12px',
|
||||
color: '#6b7280'
|
||||
}}>
|
||||
节点: {nodes.length}, 连接: {edges.length}
|
||||
</div>
|
||||
</Panel>
|
||||
</ReactFlow>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default FlowCanvas;
|
||||
248
frontend/src/pages/Workflow2/Design/components/NodePanel.tsx
Normal file
248
frontend/src/pages/Workflow2/Design/components/NodePanel.tsx
Normal file
@ -0,0 +1,248 @@
|
||||
import React from 'react';
|
||||
import { Card, Divider } from 'antd';
|
||||
import { NodeType, NodeCategory, type NodeDefinitionData } from '../types';
|
||||
|
||||
// 节点定义数据
|
||||
const nodeDefinitions: NodeDefinitionData[] = [
|
||||
{
|
||||
nodeCode: 'start_event',
|
||||
nodeName: '开始事件',
|
||||
nodeType: NodeType.START_EVENT,
|
||||
category: NodeCategory.EVENT,
|
||||
description: '工作流开始节点',
|
||||
icon: '▶️',
|
||||
color: '#10b981'
|
||||
},
|
||||
{
|
||||
nodeCode: 'end_event',
|
||||
nodeName: '结束事件',
|
||||
nodeType: NodeType.END_EVENT,
|
||||
category: NodeCategory.EVENT,
|
||||
description: '工作流结束节点',
|
||||
icon: '⏹️',
|
||||
color: '#ef4444'
|
||||
},
|
||||
{
|
||||
nodeCode: 'user_task',
|
||||
nodeName: '用户任务',
|
||||
nodeType: NodeType.USER_TASK,
|
||||
category: NodeCategory.TASK,
|
||||
description: '需要用户手动处理的任务',
|
||||
icon: '👤',
|
||||
color: '#6366f1'
|
||||
},
|
||||
{
|
||||
nodeCode: 'service_task',
|
||||
nodeName: '服务任务',
|
||||
nodeType: NodeType.SERVICE_TASK,
|
||||
category: NodeCategory.TASK,
|
||||
description: '自动执行的服务任务',
|
||||
icon: '⚙️',
|
||||
color: '#f59e0b'
|
||||
},
|
||||
{
|
||||
nodeCode: 'script_task',
|
||||
nodeName: '脚本任务',
|
||||
nodeType: NodeType.SCRIPT_TASK,
|
||||
category: NodeCategory.TASK,
|
||||
description: '执行脚本代码的任务',
|
||||
icon: '📜',
|
||||
color: '#8b5cf6'
|
||||
},
|
||||
{
|
||||
nodeCode: 'deploy_node',
|
||||
nodeName: '部署节点',
|
||||
nodeType: NodeType.DEPLOY_NODE,
|
||||
category: NodeCategory.TASK,
|
||||
description: '应用部署任务',
|
||||
icon: '🚀',
|
||||
color: '#06b6d4'
|
||||
},
|
||||
{
|
||||
nodeCode: 'jenkins_build',
|
||||
nodeName: 'Jenkins构建',
|
||||
nodeType: NodeType.JENKINS_BUILD,
|
||||
category: NodeCategory.TASK,
|
||||
description: 'Jenkins构建任务',
|
||||
icon: '🔨',
|
||||
color: '#dc2626'
|
||||
},
|
||||
{
|
||||
nodeCode: 'gateway_node',
|
||||
nodeName: '网关节点',
|
||||
nodeType: NodeType.GATEWAY_NODE,
|
||||
category: NodeCategory.GATEWAY,
|
||||
description: '条件分支网关',
|
||||
icon: '💎',
|
||||
color: '#7c3aed'
|
||||
}
|
||||
];
|
||||
|
||||
interface NodePanelProps {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const NodePanel: React.FC<NodePanelProps> = ({ className = '' }) => {
|
||||
// 按分类分组节点
|
||||
const nodesByCategory = nodeDefinitions.reduce((acc, node) => {
|
||||
if (!acc[node.category]) {
|
||||
acc[node.category] = [];
|
||||
}
|
||||
acc[node.category].push(node);
|
||||
return acc;
|
||||
}, {} as Record<NodeCategory, NodeDefinitionData[]>);
|
||||
|
||||
// 拖拽开始处理
|
||||
const handleDragStart = (event: React.DragEvent, nodeDefinition: NodeDefinitionData) => {
|
||||
event.dataTransfer.setData('application/reactflow', JSON.stringify({
|
||||
nodeType: nodeDefinition.nodeType,
|
||||
nodeDefinition
|
||||
}));
|
||||
event.dataTransfer.effectAllowed = 'move';
|
||||
};
|
||||
|
||||
// 渲染节点项
|
||||
const renderNodeItem = (nodeDefinition: NodeDefinitionData) => (
|
||||
<div
|
||||
key={nodeDefinition.nodeCode}
|
||||
draggable
|
||||
onDragStart={(e) => handleDragStart(e, nodeDefinition)}
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '8px',
|
||||
padding: '8px 12px',
|
||||
borderRadius: '6px',
|
||||
border: '1px solid #e5e7eb',
|
||||
background: 'white',
|
||||
cursor: 'grab',
|
||||
transition: 'all 0.2s ease-in-out',
|
||||
marginBottom: '6px'
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
e.currentTarget.style.background = '#f9fafb';
|
||||
e.currentTarget.style.borderColor = nodeDefinition.color;
|
||||
e.currentTarget.style.transform = 'translateX(2px)';
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.currentTarget.style.background = 'white';
|
||||
e.currentTarget.style.borderColor = '#e5e7eb';
|
||||
e.currentTarget.style.transform = 'translateX(0)';
|
||||
}}
|
||||
onDragStart={(e) => {
|
||||
e.currentTarget.style.cursor = 'grabbing';
|
||||
handleDragStart(e, nodeDefinition);
|
||||
}}
|
||||
onDragEnd={(e) => {
|
||||
e.currentTarget.style.cursor = 'grab';
|
||||
}}
|
||||
>
|
||||
<div style={{
|
||||
fontSize: '16px',
|
||||
width: '20px',
|
||||
textAlign: 'center'
|
||||
}}>
|
||||
{nodeDefinition.icon}
|
||||
</div>
|
||||
<div>
|
||||
<div style={{
|
||||
fontSize: '13px',
|
||||
fontWeight: '500',
|
||||
color: '#374151',
|
||||
lineHeight: '1.2'
|
||||
}}>
|
||||
{nodeDefinition.nodeName}
|
||||
</div>
|
||||
<div style={{
|
||||
fontSize: '11px',
|
||||
color: '#6b7280',
|
||||
lineHeight: '1.2',
|
||||
marginTop: '2px'
|
||||
}}>
|
||||
{nodeDefinition.description}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
// 分类标题映射
|
||||
const categoryTitles = {
|
||||
[NodeCategory.EVENT]: '🎯 事件节点',
|
||||
[NodeCategory.TASK]: '📋 任务节点',
|
||||
[NodeCategory.GATEWAY]: '🔀 网关节点',
|
||||
[NodeCategory.CONTAINER]: '📦 容器节点'
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={`node-panel ${className}`} style={{
|
||||
width: '260px',
|
||||
height: '100%',
|
||||
background: '#f8fafc',
|
||||
borderRight: '1px solid #e5e7eb',
|
||||
overflow: 'hidden',
|
||||
display: 'flex',
|
||||
flexDirection: 'column'
|
||||
}}>
|
||||
<div style={{
|
||||
padding: '16px',
|
||||
background: 'white',
|
||||
borderBottom: '1px solid #e5e7eb'
|
||||
}}>
|
||||
<h3 style={{
|
||||
margin: 0,
|
||||
fontSize: '14px',
|
||||
fontWeight: '600',
|
||||
color: '#374151'
|
||||
}}>
|
||||
节点面板
|
||||
</h3>
|
||||
<p style={{
|
||||
margin: '4px 0 0 0',
|
||||
fontSize: '12px',
|
||||
color: '#6b7280'
|
||||
}}>
|
||||
拖拽节点到画布创建工作流
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div style={{
|
||||
flex: '1',
|
||||
overflow: 'auto',
|
||||
padding: '12px'
|
||||
}}>
|
||||
{Object.entries(nodesByCategory).map(([category, nodes]) => (
|
||||
<div key={category} style={{ marginBottom: '16px' }}>
|
||||
<div style={{
|
||||
fontSize: '12px',
|
||||
fontWeight: '600',
|
||||
color: '#4b5563',
|
||||
marginBottom: '8px',
|
||||
padding: '4px 0',
|
||||
borderBottom: '1px solid #e5e7eb'
|
||||
}}>
|
||||
{categoryTitles[category as NodeCategory]}
|
||||
</div>
|
||||
{nodes.map(renderNodeItem)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* 使用提示 */}
|
||||
<div style={{
|
||||
padding: '12px',
|
||||
background: '#f1f5f9',
|
||||
borderTop: '1px solid #e5e7eb',
|
||||
fontSize: '11px',
|
||||
color: '#64748b',
|
||||
lineHeight: '1.4'
|
||||
}}>
|
||||
💡 提示:
|
||||
<br />• 拖拽节点到画布创建
|
||||
<br />• 双击节点进行配置
|
||||
<br />• 连接节点创建流程
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default NodePanel;
|
||||
@ -0,0 +1,234 @@
|
||||
import React from 'react';
|
||||
import { Button, Space, Divider, Tooltip } from 'antd';
|
||||
import {
|
||||
SaveOutlined,
|
||||
UndoOutlined,
|
||||
RedoOutlined,
|
||||
CopyOutlined,
|
||||
ScissorOutlined,
|
||||
DeleteOutlined,
|
||||
ZoomInOutlined,
|
||||
ZoomOutOutlined,
|
||||
ExpandOutlined,
|
||||
ArrowLeftOutlined,
|
||||
SelectOutlined
|
||||
} from '@ant-design/icons';
|
||||
|
||||
interface WorkflowToolbarProps {
|
||||
title?: string;
|
||||
onSave?: () => void;
|
||||
onUndo?: () => void;
|
||||
onRedo?: () => void;
|
||||
onCopy?: () => void;
|
||||
onCut?: () => void;
|
||||
onPaste?: () => void;
|
||||
onDelete?: () => void;
|
||||
onZoomIn?: () => void;
|
||||
onZoomOut?: () => void;
|
||||
onFitView?: () => void;
|
||||
onSelectAll?: () => void;
|
||||
onBack?: () => void;
|
||||
canUndo?: boolean;
|
||||
canRedo?: boolean;
|
||||
zoom?: number;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const WorkflowToolbar: React.FC<WorkflowToolbarProps> = ({
|
||||
title = '工作流设计器',
|
||||
onSave,
|
||||
onUndo,
|
||||
onRedo,
|
||||
onCopy,
|
||||
onCut,
|
||||
onPaste,
|
||||
onDelete,
|
||||
onZoomIn,
|
||||
onZoomOut,
|
||||
onFitView,
|
||||
onSelectAll,
|
||||
onBack,
|
||||
canUndo = false,
|
||||
canRedo = false,
|
||||
zoom = 1,
|
||||
className = ''
|
||||
}) => {
|
||||
return (
|
||||
<div
|
||||
className={`workflow-toolbar ${className}`}
|
||||
style={{
|
||||
height: '56px',
|
||||
background: 'white',
|
||||
borderBottom: '1px solid #e5e7eb',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
padding: '0 16px',
|
||||
boxShadow: '0 1px 2px rgba(0, 0, 0, 0.05)'
|
||||
}}
|
||||
>
|
||||
{/* 左侧:标题和返回按钮 */}
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
|
||||
<Tooltip title="返回列表">
|
||||
<Button
|
||||
type="text"
|
||||
icon={<ArrowLeftOutlined />}
|
||||
onClick={onBack}
|
||||
style={{ color: '#6b7280' }}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Divider type="vertical" />
|
||||
<div>
|
||||
<h2 style={{
|
||||
margin: 0,
|
||||
fontSize: '16px',
|
||||
fontWeight: '600',
|
||||
color: '#374151'
|
||||
}}>
|
||||
{title}
|
||||
</h2>
|
||||
<div style={{
|
||||
fontSize: '12px',
|
||||
color: '#6b7280',
|
||||
marginTop: '2px'
|
||||
}}>
|
||||
基于 React Flow 的工作流设计器
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 中间:主要操作按钮 */}
|
||||
<div style={{ display: 'flex', alignItems: 'center' }}>
|
||||
<Space size={4}>
|
||||
{/* 保存 */}
|
||||
<Tooltip title="保存工作流">
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<SaveOutlined />}
|
||||
onClick={onSave}
|
||||
size="small"
|
||||
>
|
||||
保存
|
||||
</Button>
|
||||
</Tooltip>
|
||||
|
||||
<Divider type="vertical" />
|
||||
|
||||
{/* 撤销/重做 */}
|
||||
<Tooltip title="撤销">
|
||||
<Button
|
||||
type="text"
|
||||
icon={<UndoOutlined />}
|
||||
onClick={onUndo}
|
||||
disabled={!canUndo}
|
||||
size="small"
|
||||
/>
|
||||
</Tooltip>
|
||||
<Tooltip title="重做">
|
||||
<Button
|
||||
type="text"
|
||||
icon={<RedoOutlined />}
|
||||
onClick={onRedo}
|
||||
disabled={!canRedo}
|
||||
size="small"
|
||||
/>
|
||||
</Tooltip>
|
||||
|
||||
<Divider type="vertical" />
|
||||
|
||||
{/* 编辑操作 */}
|
||||
<Tooltip title="复制">
|
||||
<Button
|
||||
type="text"
|
||||
icon={<CopyOutlined />}
|
||||
onClick={onCopy}
|
||||
size="small"
|
||||
/>
|
||||
</Tooltip>
|
||||
<Tooltip title="剪切">
|
||||
<Button
|
||||
type="text"
|
||||
icon={<ScissorOutlined />}
|
||||
onClick={onCut}
|
||||
size="small"
|
||||
/>
|
||||
</Tooltip>
|
||||
<Tooltip title="删除选中">
|
||||
<Button
|
||||
type="text"
|
||||
icon={<DeleteOutlined />}
|
||||
onClick={onDelete}
|
||||
size="small"
|
||||
style={{ color: '#ef4444' }}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Tooltip title="全选">
|
||||
<Button
|
||||
type="text"
|
||||
icon={<SelectOutlined />}
|
||||
onClick={onSelectAll}
|
||||
size="small"
|
||||
/>
|
||||
</Tooltip>
|
||||
|
||||
<Divider type="vertical" />
|
||||
|
||||
{/* 视图操作 */}
|
||||
<Tooltip title="放大">
|
||||
<Button
|
||||
type="text"
|
||||
icon={<ZoomInOutlined />}
|
||||
onClick={onZoomIn}
|
||||
size="small"
|
||||
/>
|
||||
</Tooltip>
|
||||
<Tooltip title="缩小">
|
||||
<Button
|
||||
type="text"
|
||||
icon={<ZoomOutOutlined />}
|
||||
onClick={onZoomOut}
|
||||
size="small"
|
||||
/>
|
||||
</Tooltip>
|
||||
<Tooltip title="适应视图">
|
||||
<Button
|
||||
type="text"
|
||||
icon={<ExpandOutlined />}
|
||||
onClick={onFitView}
|
||||
size="small"
|
||||
/>
|
||||
</Tooltip>
|
||||
</Space>
|
||||
</div>
|
||||
|
||||
{/* 右侧:状态信息 */}
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '12px',
|
||||
fontSize: '12px',
|
||||
color: '#6b7280'
|
||||
}}>
|
||||
<div style={{
|
||||
background: '#f3f4f6',
|
||||
padding: '4px 8px',
|
||||
borderRadius: '4px',
|
||||
fontFamily: 'monospace'
|
||||
}}>
|
||||
缩放: {Math.round(zoom * 100)}%
|
||||
</div>
|
||||
<div style={{
|
||||
background: '#ecfdf5',
|
||||
color: '#065f46',
|
||||
padding: '4px 8px',
|
||||
borderRadius: '4px',
|
||||
fontWeight: '500'
|
||||
}}>
|
||||
React Flow
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default WorkflowToolbar;
|
||||
267
frontend/src/pages/Workflow2/Design/index.less
Normal file
267
frontend/src/pages/Workflow2/Design/index.less
Normal file
@ -0,0 +1,267 @@
|
||||
// 工作流设计器 - React Flow版本
|
||||
// 变量定义
|
||||
@header-height: 64px;
|
||||
@toolbar-height: 56px;
|
||||
@sidebar-width: 260px;
|
||||
@container-padding: 24px;
|
||||
|
||||
// 主容器 - 覆盖父容器样式
|
||||
.workflow-design-container {
|
||||
// 使用负边距抵消父容器的padding
|
||||
margin: -@container-padding;
|
||||
// 精确计算高度:视口高度 - header高度
|
||||
height: calc(100vh - @header-height);
|
||||
// 补偿左右padding
|
||||
width: calc(100% + @container-padding * 2);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
background: #ffffff;
|
||||
position: relative;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
|
||||
// React Flow 样式覆盖
|
||||
.react-flow {
|
||||
width: 100% !important;
|
||||
height: 100% !important;
|
||||
|
||||
&__renderer {
|
||||
width: 100% !important;
|
||||
height: 100% !important;
|
||||
}
|
||||
|
||||
// 自定义控制按钮样式
|
||||
&__controls {
|
||||
button {
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
border: 1px solid #e5e7eb;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
transition: all 0.2s ease-in-out;
|
||||
|
||||
&:hover {
|
||||
background: #f9fafb;
|
||||
border-color: #3b82f6;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 小地图样式
|
||||
&__minimap {
|
||||
background: #f9fafb;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
// 内容区域
|
||||
.workflow-content-area {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
min-height: 0;
|
||||
|
||||
// 节点面板区域
|
||||
.node-panel {
|
||||
width: @sidebar-width;
|
||||
background: #f8fafc;
|
||||
border-right: 1px solid #e5e7eb;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
box-shadow: 2px 0 4px rgba(0, 0, 0, 0.05);
|
||||
|
||||
// 面板头部
|
||||
.panel-header {
|
||||
padding: 16px;
|
||||
background: #ffffff;
|
||||
border-bottom: 1px solid #e5e7eb;
|
||||
|
||||
h3 {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #374151;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 4px 0 0 0;
|
||||
font-size: 12px;
|
||||
color: #6b7280;
|
||||
}
|
||||
}
|
||||
|
||||
// 节点分组
|
||||
.node-group {
|
||||
margin-bottom: 16px;
|
||||
|
||||
.group-title {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: #4b5563;
|
||||
margin-bottom: 8px;
|
||||
padding: 4px 0;
|
||||
border-bottom: 1px solid #e5e7eb;
|
||||
}
|
||||
|
||||
// 节点项
|
||||
.node-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 8px 12px;
|
||||
border-radius: 6px;
|
||||
border: 1px solid #e5e7eb;
|
||||
background: #ffffff;
|
||||
cursor: grab;
|
||||
transition: all 0.2s ease-in-out;
|
||||
margin-bottom: 6px;
|
||||
|
||||
&:hover {
|
||||
background: #f9fafb;
|
||||
border-color: var(--node-color, #3b82f6);
|
||||
transform: translateX(2px);
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
&:active {
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
.node-icon {
|
||||
font-size: 16px;
|
||||
width: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.node-info {
|
||||
.node-name {
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: #374151;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.node-desc {
|
||||
font-size: 11px;
|
||||
color: #6b7280;
|
||||
line-height: 1.2;
|
||||
margin-top: 2px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 使用提示
|
||||
.panel-tips {
|
||||
padding: 12px;
|
||||
background: #f1f5f9;
|
||||
border-top: 1px solid #e5e7eb;
|
||||
font-size: 11px;
|
||||
color: #64748b;
|
||||
line-height: 1.4;
|
||||
}
|
||||
}
|
||||
|
||||
// 画布区域
|
||||
.workflow-canvas-area {
|
||||
flex: 1;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
background: #f8fafc;
|
||||
|
||||
// 画布状态面板
|
||||
.canvas-status {
|
||||
position: absolute;
|
||||
top: 12px;
|
||||
left: 12px;
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
padding: 8px 12px;
|
||||
border-radius: 6px;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
font-size: 12px;
|
||||
color: #6b7280;
|
||||
z-index: 10;
|
||||
backdrop-filter: blur(4px);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 响应式适配
|
||||
@media (max-width: 1024px) {
|
||||
.workflow-content-area .node-panel {
|
||||
width: 220px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.workflow-content-area {
|
||||
flex-direction: column;
|
||||
|
||||
.node-panel {
|
||||
width: 100%;
|
||||
height: 200px;
|
||||
border-right: none;
|
||||
border-bottom: 1px solid #e5e7eb;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 工具栏特殊样式
|
||||
.workflow-toolbar {
|
||||
height: @toolbar-height;
|
||||
background: #ffffff;
|
||||
border-bottom: 1px solid #e5e7eb;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 16px;
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
|
||||
flex-shrink: 0;
|
||||
|
||||
.toolbar-section {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.toolbar-title {
|
||||
h2 {
|
||||
margin: 0;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #374151;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
font-size: 12px;
|
||||
color: #6b7280;
|
||||
margin-top: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
.toolbar-status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
font-size: 12px;
|
||||
color: #6b7280;
|
||||
|
||||
.status-item {
|
||||
background: #f3f4f6;
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
font-family: 'Monaco', 'Menlo', monospace;
|
||||
}
|
||||
|
||||
.react-flow-badge {
|
||||
background: #ecfdf5;
|
||||
color: #065f46;
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
}
|
||||
293
frontend/src/pages/Workflow2/Design/index.tsx
Normal file
293
frontend/src/pages/Workflow2/Design/index.tsx
Normal file
@ -0,0 +1,293 @@
|
||||
import React, { useState, useCallback, useRef, useEffect } from 'react';
|
||||
import { useParams, useNavigate } from 'react-router-dom';
|
||||
import { message } from 'antd';
|
||||
import { ReactFlowProvider, useReactFlow } from '@xyflow/react';
|
||||
|
||||
import WorkflowToolbar from './components/WorkflowToolbar';
|
||||
import NodePanel from './components/NodePanel';
|
||||
import FlowCanvas from './components/FlowCanvas';
|
||||
import type { FlowNode, FlowEdge, DragNodeData } from './types';
|
||||
import { NodeType } from './types';
|
||||
|
||||
// 样式
|
||||
import '@xyflow/react/dist/style.css';
|
||||
import './index.less';
|
||||
|
||||
const WorkflowDesignInner: React.FC = () => {
|
||||
const { id } = useParams<{ id: string }>();
|
||||
const navigate = useNavigate();
|
||||
const {
|
||||
getNodes,
|
||||
setNodes,
|
||||
getEdges,
|
||||
setEdges,
|
||||
screenToFlowPosition,
|
||||
fitView,
|
||||
zoomIn,
|
||||
zoomOut,
|
||||
getZoom
|
||||
} = useReactFlow();
|
||||
|
||||
const [workflowTitle, setWorkflowTitle] = useState('新建工作流');
|
||||
const reactFlowWrapper = useRef<HTMLDivElement>(null);
|
||||
|
||||
// 自动适应视图
|
||||
useEffect(() => {
|
||||
// 延迟执行fitView以确保节点已渲染
|
||||
const timer = setTimeout(() => {
|
||||
fitView({
|
||||
padding: 0.1,
|
||||
duration: 800,
|
||||
minZoom: 0.5,
|
||||
maxZoom: 1.5
|
||||
});
|
||||
}, 100);
|
||||
|
||||
return () => clearTimeout(timer);
|
||||
}, [fitView]);
|
||||
|
||||
// 初始化示例节点 - 优化位置和布局
|
||||
const initialNodes: FlowNode[] = [
|
||||
{
|
||||
id: '1',
|
||||
type: 'START_EVENT',
|
||||
position: { x: 250, y: 50 },
|
||||
data: {
|
||||
label: '开始',
|
||||
nodeType: NodeType.START_EVENT,
|
||||
category: 'EVENT' as any,
|
||||
icon: '▶️',
|
||||
color: '#10b981'
|
||||
}
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
type: 'USER_TASK',
|
||||
position: { x: 200, y: 150 },
|
||||
data: {
|
||||
label: '用户审批',
|
||||
nodeType: NodeType.USER_TASK,
|
||||
category: 'TASK' as any,
|
||||
icon: '👤',
|
||||
color: '#6366f1'
|
||||
}
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
type: 'END_EVENT',
|
||||
position: { x: 250, y: 250 },
|
||||
data: {
|
||||
label: '结束',
|
||||
nodeType: NodeType.END_EVENT,
|
||||
category: 'EVENT' as any,
|
||||
icon: '⏹️',
|
||||
color: '#ef4444'
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
const initialEdges: FlowEdge[] = [
|
||||
{
|
||||
id: 'e1-2',
|
||||
source: '1',
|
||||
target: '2',
|
||||
type: 'default',
|
||||
animated: true,
|
||||
data: {
|
||||
label: '提交',
|
||||
condition: {
|
||||
type: 'DEFAULT',
|
||||
priority: 0
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'e2-3',
|
||||
source: '2',
|
||||
target: '3',
|
||||
type: 'default',
|
||||
animated: true,
|
||||
data: {
|
||||
label: '通过',
|
||||
condition: {
|
||||
type: 'DEFAULT',
|
||||
priority: 0
|
||||
}
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
// 工具栏事件处理
|
||||
const handleSave = useCallback(() => {
|
||||
const nodes = getNodes();
|
||||
const edges = getEdges();
|
||||
console.log('保存工作流:', { nodes, edges });
|
||||
message.success('工作流保存成功!');
|
||||
}, [getNodes, getEdges]);
|
||||
|
||||
const handleBack = useCallback(() => {
|
||||
navigate('/workflow2/definition');
|
||||
}, [navigate]);
|
||||
|
||||
const handleUndo = useCallback(() => {
|
||||
console.log('撤销操作');
|
||||
message.info('撤销功能开发中...');
|
||||
}, []);
|
||||
|
||||
const handleRedo = useCallback(() => {
|
||||
console.log('重做操作');
|
||||
message.info('重做功能开发中...');
|
||||
}, []);
|
||||
|
||||
const handleCopy = useCallback(() => {
|
||||
const selectedNodes = getNodes().filter(node => node.selected);
|
||||
if (selectedNodes.length > 0) {
|
||||
console.log('复制节点:', selectedNodes);
|
||||
message.success(`已复制 ${selectedNodes.length} 个节点`);
|
||||
} else {
|
||||
message.warning('请先选择要复制的节点');
|
||||
}
|
||||
}, [getNodes]);
|
||||
|
||||
const handleDelete = useCallback(() => {
|
||||
const selectedNodes = getNodes().filter(node => node.selected);
|
||||
const selectedEdges = getEdges().filter(edge => edge.selected);
|
||||
|
||||
if (selectedNodes.length > 0 || selectedEdges.length > 0) {
|
||||
setNodes(nodes => nodes.filter(node => !node.selected));
|
||||
setEdges(edges => edges.filter(edge => !edge.selected));
|
||||
message.success(`已删除 ${selectedNodes.length} 个节点和 ${selectedEdges.length} 条连接`);
|
||||
} else {
|
||||
message.warning('请先选择要删除的元素');
|
||||
}
|
||||
}, [getNodes, getEdges, setNodes, setEdges]);
|
||||
|
||||
const handleSelectAll = useCallback(() => {
|
||||
setNodes(nodes => nodes.map(node => ({ ...node, selected: true })));
|
||||
setEdges(edges => edges.map(edge => ({ ...edge, selected: true })));
|
||||
message.info('已全选所有元素');
|
||||
}, [setNodes, setEdges]);
|
||||
|
||||
const handleFitView = useCallback(() => {
|
||||
fitView({ padding: 0.2, duration: 800 });
|
||||
}, [fitView]);
|
||||
|
||||
const handleZoomIn = useCallback(() => {
|
||||
zoomIn({ duration: 300 });
|
||||
}, [zoomIn]);
|
||||
|
||||
const handleZoomOut = useCallback(() => {
|
||||
zoomOut({ duration: 300 });
|
||||
}, [zoomOut]);
|
||||
|
||||
// 处理节点拖拽放置
|
||||
const handleDrop = useCallback((event: React.DragEvent) => {
|
||||
event.preventDefault();
|
||||
|
||||
const reactFlowBounds = reactFlowWrapper.current?.getBoundingClientRect();
|
||||
if (!reactFlowBounds) return;
|
||||
|
||||
const dragData = event.dataTransfer.getData('application/reactflow');
|
||||
if (!dragData) return;
|
||||
|
||||
try {
|
||||
const { nodeType, nodeDefinition }: DragNodeData = JSON.parse(dragData);
|
||||
|
||||
const position = screenToFlowPosition({
|
||||
x: event.clientX - reactFlowBounds.left,
|
||||
y: event.clientY - reactFlowBounds.top,
|
||||
});
|
||||
|
||||
const newNode: FlowNode = {
|
||||
id: `${nodeType}-${Date.now()}`,
|
||||
type: nodeType,
|
||||
position,
|
||||
data: {
|
||||
label: nodeDefinition.nodeName,
|
||||
nodeType,
|
||||
category: nodeDefinition.category,
|
||||
icon: nodeDefinition.icon,
|
||||
color: nodeDefinition.color,
|
||||
nodeDefinition
|
||||
}
|
||||
};
|
||||
|
||||
setNodes(nodes => [...nodes, newNode]);
|
||||
message.success(`已添加 ${nodeDefinition.nodeName} 节点`);
|
||||
} catch (error) {
|
||||
console.error('解析拖拽数据失败:', error);
|
||||
message.error('添加节点失败');
|
||||
}
|
||||
}, [screenToFlowPosition, setNodes]);
|
||||
|
||||
// 处理节点点击
|
||||
const handleNodeClick = useCallback((event: React.MouseEvent, node: FlowNode) => {
|
||||
console.log('节点点击:', node);
|
||||
message.info(`点击了节点: ${node.data.label}`);
|
||||
}, []);
|
||||
|
||||
// 处理边点击
|
||||
const handleEdgeClick = useCallback((event: React.MouseEvent, edge: FlowEdge) => {
|
||||
console.log('边点击:', edge);
|
||||
message.info(`点击了连接: ${edge.data?.label || '连接'}`);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="workflow-design-container"
|
||||
style={{
|
||||
// 确保覆盖父容器的overflow设置
|
||||
overflow: 'hidden'
|
||||
}}
|
||||
>
|
||||
{/* 工具栏 */}
|
||||
<WorkflowToolbar
|
||||
title={workflowTitle}
|
||||
onSave={handleSave}
|
||||
onBack={handleBack}
|
||||
onUndo={handleUndo}
|
||||
onRedo={handleRedo}
|
||||
onCopy={handleCopy}
|
||||
onDelete={handleDelete}
|
||||
onSelectAll={handleSelectAll}
|
||||
onZoomIn={handleZoomIn}
|
||||
onZoomOut={handleZoomOut}
|
||||
onFitView={handleFitView}
|
||||
canUndo={false}
|
||||
canRedo={false}
|
||||
zoom={getZoom()}
|
||||
/>
|
||||
|
||||
{/* 主要内容区域 */}
|
||||
<div className="workflow-content-area">
|
||||
{/* 节点面板 */}
|
||||
<NodePanel />
|
||||
|
||||
{/* 画布区域 */}
|
||||
<div
|
||||
ref={reactFlowWrapper}
|
||||
className="workflow-canvas-area"
|
||||
>
|
||||
<FlowCanvas
|
||||
initialNodes={initialNodes}
|
||||
initialEdges={initialEdges}
|
||||
onNodeClick={handleNodeClick}
|
||||
onEdgeClick={handleEdgeClick}
|
||||
onDrop={handleDrop}
|
||||
className="workflow-canvas"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const WorkflowDesign: React.FC = () => {
|
||||
return (
|
||||
<ReactFlowProvider>
|
||||
<WorkflowDesignInner />
|
||||
</ReactFlowProvider>
|
||||
);
|
||||
};
|
||||
|
||||
export default WorkflowDesign;
|
||||
117
frontend/src/pages/Workflow2/Design/types.ts
Normal file
117
frontend/src/pages/Workflow2/Design/types.ts
Normal file
@ -0,0 +1,117 @@
|
||||
import type { Node, Edge } from '@xyflow/react';
|
||||
|
||||
// 节点类型枚举 - 与原有系统保持一致
|
||||
export enum NodeType {
|
||||
START_EVENT = 'START_EVENT',
|
||||
END_EVENT = 'END_EVENT',
|
||||
USER_TASK = 'USER_TASK',
|
||||
SERVICE_TASK = 'SERVICE_TASK',
|
||||
SCRIPT_TASK = 'SCRIPT_TASK',
|
||||
DEPLOY_NODE = 'DEPLOY_NODE',
|
||||
JENKINS_BUILD = 'JENKINS_BUILD',
|
||||
GATEWAY_NODE = 'GATEWAY_NODE',
|
||||
SUB_PROCESS = 'SUB_PROCESS',
|
||||
CALL_ACTIVITY = 'CALL_ACTIVITY'
|
||||
}
|
||||
|
||||
// 节点分类
|
||||
export enum NodeCategory {
|
||||
EVENT = 'EVENT',
|
||||
TASK = 'TASK',
|
||||
GATEWAY = 'GATEWAY',
|
||||
CONTAINER = 'CONTAINER'
|
||||
}
|
||||
|
||||
// 节点定义数据结构
|
||||
export interface NodeDefinitionData {
|
||||
nodeCode: string;
|
||||
nodeName: string;
|
||||
nodeType: NodeType;
|
||||
category: NodeCategory;
|
||||
description?: string;
|
||||
icon: string;
|
||||
color: string;
|
||||
}
|
||||
|
||||
// React Flow 节点数据 - 添加索引签名以满足React Flow的类型约束
|
||||
export interface FlowNodeData extends Record<string, unknown> {
|
||||
label: string;
|
||||
nodeType: NodeType;
|
||||
category: NodeCategory;
|
||||
icon: string;
|
||||
color: string;
|
||||
// 节点配置数据
|
||||
configs?: Record<string, any>;
|
||||
inputMapping?: Record<string, any>;
|
||||
outputMapping?: Record<string, any>;
|
||||
// 原始节点定义
|
||||
nodeDefinition?: NodeDefinitionData;
|
||||
}
|
||||
|
||||
// React Flow 边数据 - 添加索引签名以满足React Flow的类型约束
|
||||
export interface FlowEdgeData extends Record<string, unknown> {
|
||||
label?: string;
|
||||
condition?: {
|
||||
type: 'EXPRESSION' | 'SCRIPT' | 'DEFAULT';
|
||||
expression?: string;
|
||||
script?: string;
|
||||
priority: number;
|
||||
};
|
||||
}
|
||||
|
||||
// 扩展的React Flow节点类型
|
||||
export type FlowNode = Node<FlowNodeData>;
|
||||
|
||||
// 扩展的React Flow边类型
|
||||
export type FlowEdge = Edge<FlowEdgeData>;
|
||||
|
||||
// 工作流画布数据
|
||||
export interface WorkflowFlowData {
|
||||
nodes: FlowNode[];
|
||||
edges: FlowEdge[];
|
||||
}
|
||||
|
||||
// 节点配置模态框属性
|
||||
export interface NodeConfigModalProps {
|
||||
visible: boolean;
|
||||
node: FlowNode | null;
|
||||
onCancel: () => void;
|
||||
onOk: (node: FlowNode, updatedData: Partial<FlowNodeData>) => void;
|
||||
}
|
||||
|
||||
// 边配置模态框属性
|
||||
export interface EdgeConfigModalProps {
|
||||
visible: boolean;
|
||||
edge: FlowEdge | null;
|
||||
onCancel: () => void;
|
||||
onOk: (edge: FlowEdge, updatedData: Partial<FlowEdgeData>) => void;
|
||||
}
|
||||
|
||||
// 工具栏操作类型
|
||||
export interface ToolbarActions {
|
||||
onSave: () => void;
|
||||
onUndo: () => void;
|
||||
onRedo: () => void;
|
||||
onZoomIn: () => void;
|
||||
onZoomOut: () => void;
|
||||
onFitView: () => void;
|
||||
onCopy: () => void;
|
||||
onPaste: () => void;
|
||||
onDelete: () => void;
|
||||
onBack: () => void;
|
||||
}
|
||||
|
||||
// 画布状态
|
||||
export interface CanvasState {
|
||||
zoom: number;
|
||||
canUndo: boolean;
|
||||
canRedo: boolean;
|
||||
selectedNodes: FlowNode[];
|
||||
selectedEdges: FlowEdge[];
|
||||
}
|
||||
|
||||
// 节点拖拽数据
|
||||
export interface DragNodeData {
|
||||
nodeType: NodeType;
|
||||
nodeDefinition: NodeDefinitionData;
|
||||
}
|
||||
24
frontend/src/pages/Workflow2/Instance/index.tsx
Normal file
24
frontend/src/pages/Workflow2/Instance/index.tsx
Normal file
@ -0,0 +1,24 @@
|
||||
import React from 'react';
|
||||
import { Card } from 'antd';
|
||||
|
||||
const WorkflowInstance: React.FC = () => {
|
||||
return (
|
||||
<Card title="工作流实例管理 (React Flow版本)">
|
||||
<div style={{
|
||||
textAlign: 'center',
|
||||
color: '#6b7280',
|
||||
padding: '40px',
|
||||
fontSize: '16px'
|
||||
}}>
|
||||
🚧 工作流实例管理功能开发中...
|
||||
<br />
|
||||
<br />
|
||||
<span style={{ fontSize: '14px' }}>
|
||||
此模块将复用现有的实例管理API,提供React Flow版本的实例查看功能
|
||||
</span>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default WorkflowInstance;
|
||||
24
frontend/src/pages/Workflow2/Monitor/index.tsx
Normal file
24
frontend/src/pages/Workflow2/Monitor/index.tsx
Normal file
@ -0,0 +1,24 @@
|
||||
import React from 'react';
|
||||
import { Card } from 'antd';
|
||||
|
||||
const WorkflowMonitor: React.FC = () => {
|
||||
return (
|
||||
<Card title="工作流监控 (React Flow版本)">
|
||||
<div style={{
|
||||
textAlign: 'center',
|
||||
color: '#6b7280',
|
||||
padding: '40px',
|
||||
fontSize: '16px'
|
||||
}}>
|
||||
📊 工作流监控功能开发中...
|
||||
<br />
|
||||
<br />
|
||||
<span style={{ fontSize: '14px' }}>
|
||||
此模块将提供实时的工作流执行监控和统计分析功能
|
||||
</span>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default WorkflowMonitor;
|
||||
@ -45,6 +45,10 @@ const JenkinsManagerList = lazy(() => import('../pages/Deploy/JenkinsManager/Lis
|
||||
const GitManagerList = lazy(() => import('../pages/Deploy/GitManager/List'));
|
||||
const External = lazy(() => import('../pages/Deploy/External'));
|
||||
|
||||
// Workflow2 React Flow 版本
|
||||
const Workflow2DefinitionList = lazy(() => import('../pages/Workflow2/Definition'));
|
||||
const Workflow2Design = lazy(() => import('../pages/Workflow2/Design'));
|
||||
|
||||
// 创建路由
|
||||
const router = createBrowserRouter([
|
||||
{
|
||||
@ -224,6 +228,27 @@ const router = createBrowserRouter([
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
path: 'workflow2',
|
||||
children: [
|
||||
{
|
||||
path: 'definition',
|
||||
element: (
|
||||
<Suspense fallback={<LoadingComponent/>}>
|
||||
<Workflow2DefinitionList/>
|
||||
</Suspense>
|
||||
)
|
||||
},
|
||||
{
|
||||
path: 'design/:id',
|
||||
element: (
|
||||
<Suspense fallback={<LoadingComponent/>}>
|
||||
<Workflow2Design/>
|
||||
</Suspense>
|
||||
)
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
path: '*',
|
||||
element: <Navigate to="/dashboard"/>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user