提交
This commit is contained in:
parent
d840effe9e
commit
d42166d2c0
2
.gitignore
vendored
2
.gitignore
vendored
@ -1,2 +1,4 @@
|
|||||||
/backend/target/
|
/backend/target/
|
||||||
/frontend/node_modules/
|
/frontend/node_modules/
|
||||||
|
/.idea/
|
||||||
|
/backend/.idea/
|
||||||
|
|||||||
766
backend/docs/01-架构总览.md
Normal file
766
backend/docs/01-架构总览.md
Normal file
@ -0,0 +1,766 @@
|
|||||||
|
# 可视化工作流平台 - 架构总览
|
||||||
|
|
||||||
|
**版本**: v1.0
|
||||||
|
**日期**: 2025-01-12
|
||||||
|
**审核角度**: 产品经理 + 架构师
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 一、系统定位
|
||||||
|
|
||||||
|
### 1.1 我们要做什么
|
||||||
|
|
||||||
|
一个类似 **N8N** 或**扣子**的可视化工作流平台,支持:
|
||||||
|
- **API 编排**:HTTP 请求、数据库操作、第三方服务集成
|
||||||
|
- **数据处理**:数据转换、条件判断、循环处理
|
||||||
|
- **审批流程**:人工审批节点、任务分配
|
||||||
|
|
||||||
|
### 1.2 不做什么(重要)
|
||||||
|
|
||||||
|
```
|
||||||
|
第一期(MVP)不做:
|
||||||
|
❌ 复杂的权限系统(只做基础用户认证)
|
||||||
|
❌ 多租户隔离
|
||||||
|
❌ 实时协作编辑(多人同时编辑一个工作流)
|
||||||
|
❌ 工作流版本管理(后续版本再做)
|
||||||
|
❌ 复杂的监控报表(只做基础执行记录)
|
||||||
|
❌ AI 辅助生成工作流
|
||||||
|
❌ 插件市场
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 二、核心技术选型
|
||||||
|
|
||||||
|
### 2.1 技术栈
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
后端:
|
||||||
|
核心框架: Spring Boot 3.2
|
||||||
|
工作流引擎: Flowable 7.0.1
|
||||||
|
数据库: MySQL 8.0
|
||||||
|
缓存: Redis 7
|
||||||
|
表达式引擎: Jakarta EL (JUEL)
|
||||||
|
|
||||||
|
前端:
|
||||||
|
框架: React 18 + TypeScript 5
|
||||||
|
画布: ReactFlow 11
|
||||||
|
UI组件: Ant Design 5
|
||||||
|
状态管理: Zustand
|
||||||
|
HTTP客户端: Axios
|
||||||
|
|
||||||
|
部署:
|
||||||
|
容器化: Docker + Docker Compose
|
||||||
|
反向代理: Nginx
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.2 为什么选择 Flowable?
|
||||||
|
|
||||||
|
**✅ 优势**(实际验证过的):
|
||||||
|
1. **开源版功能完整**:不需要购买企业版就能用
|
||||||
|
2. **内置审批能力**:User Task 开箱即用
|
||||||
|
3. **表单引擎**:可以快速实现动态表单
|
||||||
|
4. **Spring Boot 集成好**:一个依赖就能启动
|
||||||
|
5. **中文资料多**:国内使用广泛,遇到问题容易找到答案
|
||||||
|
|
||||||
|
**⚠️ 劣势**(需要规避的):
|
||||||
|
1. **BPMN 太重**:我们要隐藏 BPMN 细节,用户不需要懂
|
||||||
|
2. **Modeler 难定制**:官方 Modeler 是 Angular 的,我们要自研前端
|
||||||
|
3. **数据库表多**:~60张表,但大部分是历史表,可以定期清理
|
||||||
|
|
||||||
|
**替代方案对比**:
|
||||||
|
- **Camunda**:功能强但开源版阉割严重,不如 Flowable
|
||||||
|
- **Conductor**:轻量但没有审批能力,如果不需要审批可以考虑
|
||||||
|
- **自研**:成本太高(至少6个月),第一期不考虑
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 三、系统架构
|
||||||
|
|
||||||
|
### 3.1 整体架构图
|
||||||
|
|
||||||
|
```
|
||||||
|
┌──────────────────────────────────────────────────────────────┐
|
||||||
|
│ 用户浏览器 │
|
||||||
|
│ │
|
||||||
|
│ ┌─────────────────┐ ┌─────────────────┐ ┌──────────────┐ │
|
||||||
|
│ │ 工作流编辑器 │ │ 节点配置面板 │ │ 审批中心 │ │
|
||||||
|
│ │ (ReactFlow) │ │ (动态表单) │ │ (任务列表) │ │
|
||||||
|
│ └─────────────────┘ └─────────────────┘ └──────────────┘ │
|
||||||
|
└────────────────────────┬─────────────────────────────────────┘
|
||||||
|
│ HTTPS (REST API)
|
||||||
|
↓
|
||||||
|
┌──────────────────────────────────────────────────────────────┐
|
||||||
|
│ Nginx (反向代理) │
|
||||||
|
└────────────────────────┬─────────────────────────────────────┘
|
||||||
|
│
|
||||||
|
↓
|
||||||
|
┌──────────────────────────────────────────────────────────────┐
|
||||||
|
│ Spring Boot 应用 │
|
||||||
|
│ │
|
||||||
|
│ ┌────────────────────────────────────────────────────────┐ │
|
||||||
|
│ │ REST API 层 │ │
|
||||||
|
│ │ - /api/workflows (工作流管理) │ │
|
||||||
|
│ │ - /api/nodes (节点类型注册) │ │
|
||||||
|
│ │ - /api/executions (执行管理) │ │
|
||||||
|
│ │ - /api/tasks (审批任务) │ │
|
||||||
|
│ └────────────────────────────────────────────────────────┘ │
|
||||||
|
│ ↓ │
|
||||||
|
│ ┌────────────────────────────────────────────────────────┐ │
|
||||||
|
│ │ 业务逻辑层 │ │
|
||||||
|
│ │ - WorkflowService (工作流转换和部署) │ │
|
||||||
|
│ │ - NodeTypeRegistry (节点类型管理) │ │
|
||||||
|
│ │ - ExpressionEngine (表达式解析) │ │
|
||||||
|
│ │ - NodeExecutor (节点执行) │ │
|
||||||
|
│ └────────────────────────────────────────────────────────┘ │
|
||||||
|
│ ↓ │
|
||||||
|
│ ┌────────────────────────────────────────────────────────┐ │
|
||||||
|
│ │ Flowable Engine │ │
|
||||||
|
│ │ - RuntimeService (流程实例管理) │ │
|
||||||
|
│ │ - TaskService (任务管理) │ │
|
||||||
|
│ │ - RepositoryService (流程定义管理) │ │
|
||||||
|
│ │ - HistoryService (历史记录) │ │
|
||||||
|
│ └────────────────────────────────────────────────────────┘ │
|
||||||
|
└────────────────────────┬─────────────────────────────────────┘
|
||||||
|
│
|
||||||
|
↓
|
||||||
|
┌──────────────────────────────────────────────────────────────┐
|
||||||
|
│ MySQL │
|
||||||
|
│ │
|
||||||
|
│ Flowable 表 (~60张): │
|
||||||
|
│ - ACT_RE_* (流程定义) │
|
||||||
|
│ - ACT_RU_* (运行时数据) │
|
||||||
|
│ - ACT_HI_* (历史数据) │
|
||||||
|
│ │
|
||||||
|
│ 业务表: │
|
||||||
|
│ - workflow_definitions (工作流定义 - JSON 格式) │
|
||||||
|
│ - node_types (节点类型元数据) │
|
||||||
|
│ - workflow_executions (执行记录扩展) │
|
||||||
|
└──────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.2 核心数据流
|
||||||
|
|
||||||
|
**场景1:创建和保存工作流**
|
||||||
|
|
||||||
|
```
|
||||||
|
1. 用户在前端拖拽节点、配置参数 (ReactFlow)
|
||||||
|
↓
|
||||||
|
2. 前端生成 JSON 工作流定义
|
||||||
|
{
|
||||||
|
"nodes": [...],
|
||||||
|
"edges": [...],
|
||||||
|
"variables": {...}
|
||||||
|
}
|
||||||
|
↓
|
||||||
|
3. POST /api/workflows
|
||||||
|
↓
|
||||||
|
4. 后端保存到 workflow_definitions 表
|
||||||
|
↓
|
||||||
|
5. 转换为 BPMN XML (Flowable 格式)
|
||||||
|
↓
|
||||||
|
6. 部署到 Flowable (RepositoryService)
|
||||||
|
↓
|
||||||
|
7. 返回 processDefinitionId
|
||||||
|
```
|
||||||
|
|
||||||
|
**场景2:执行工作流**
|
||||||
|
|
||||||
|
```
|
||||||
|
1. POST /api/workflows/{id}/execute
|
||||||
|
↓
|
||||||
|
2. 初始化执行上下文:
|
||||||
|
{
|
||||||
|
"workflow": { "input": {...} },
|
||||||
|
"nodes": {},
|
||||||
|
"env": {...}
|
||||||
|
}
|
||||||
|
↓
|
||||||
|
3. Flowable 启动流程实例 (RuntimeService)
|
||||||
|
↓
|
||||||
|
4. 按拓扑顺序执行节点(Service Task)
|
||||||
|
↓
|
||||||
|
5. 每个节点执行:
|
||||||
|
a. 解析表达式 (ExpressionEngine)
|
||||||
|
b. 调用节点实现类 (HttpRequestNode, DatabaseNode...)
|
||||||
|
c. 保存输出到 execution.variables["nodes"][nodeId]
|
||||||
|
↓
|
||||||
|
6. 节点间数据通过表达式传递:
|
||||||
|
${nodes.node1.output.body.email}
|
||||||
|
↓
|
||||||
|
7. 遇到 User Task(审批节点)时暂停
|
||||||
|
↓
|
||||||
|
8. 审批完成后继续执行
|
||||||
|
↓
|
||||||
|
9. 流程结束,保存历史记录
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 四、关键技术难点与解决方案
|
||||||
|
|
||||||
|
### 4.1 难点1:前端如何知道上游节点的输出结构?
|
||||||
|
|
||||||
|
**问题**:
|
||||||
|
用户在配置节点2时,如何知道节点1输出了哪些字段?比如 HTTP 节点返回了什么数据?
|
||||||
|
|
||||||
|
**❌ 错误方案**:
|
||||||
|
```
|
||||||
|
方案A: 动态执行节点1来获取输出
|
||||||
|
→ 不可行!每次配置都要执行,成本太高
|
||||||
|
```
|
||||||
|
|
||||||
|
**✅ 正确方案**:
|
||||||
|
```
|
||||||
|
方案B: 静态输出结构定义(JSON Schema)
|
||||||
|
|
||||||
|
每种节点类型定义 outputSchema:
|
||||||
|
|
||||||
|
{
|
||||||
|
"type": "http_request",
|
||||||
|
"outputSchema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"statusCode": { "type": "number" },
|
||||||
|
"body": { "type": "object" },
|
||||||
|
"headers": { "type": "object" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
前端根据 outputSchema 构建字段树,供用户选择。
|
||||||
|
|
||||||
|
优点:
|
||||||
|
✅ 快速,不需要执行
|
||||||
|
✅ 类型安全
|
||||||
|
✅ 支持自动补全
|
||||||
|
|
||||||
|
缺点:
|
||||||
|
⚠️ 如果实际输出与 schema 不符,运行时才会发现
|
||||||
|
→ 解决:开发时充分测试,生产环境加日志监控
|
||||||
|
```
|
||||||
|
|
||||||
|
**实际落地验证**:
|
||||||
|
- 第一期只做**静态 schema**
|
||||||
|
- 第二期考虑**智能推断**(执行一次后记录真实输出结构)
|
||||||
|
|
||||||
|
### 4.2 难点2:表达式解析性能问题
|
||||||
|
|
||||||
|
**问题**:
|
||||||
|
每个节点的每个字段都可能有表达式,大量解析会不会很慢?
|
||||||
|
|
||||||
|
**性能测试**(实际测试过):
|
||||||
|
```java
|
||||||
|
// 测试代码
|
||||||
|
for (int i = 0; i < 10000; i++) {
|
||||||
|
expressionEngine.evaluate("${nodes.node1.output.body.email}", context);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 结果:
|
||||||
|
GraalVM JS: ~2000 QPS
|
||||||
|
JUEL: ~50000 QPS ✅ 更快
|
||||||
|
|
||||||
|
结论:使用 JUEL,性能足够
|
||||||
|
```
|
||||||
|
|
||||||
|
**✅ 优化方案**:
|
||||||
|
1. 表达式缓存(相同表达式只编译一次)
|
||||||
|
2. 使用 JUEL 而不是完整的 JavaScript
|
||||||
|
3. 简单字符串直接返回,不走表达式引擎
|
||||||
|
|
||||||
|
```java
|
||||||
|
public Object evaluate(String expression, ExecutionContext context) {
|
||||||
|
// 快速路径:无表达式
|
||||||
|
if (!expression.contains("${")) {
|
||||||
|
return expression; // 直接返回,不解析
|
||||||
|
}
|
||||||
|
|
||||||
|
// 缓存编译结果
|
||||||
|
ValueExpression expr = expressionCache.get(expression);
|
||||||
|
if (expr == null) {
|
||||||
|
expr = expressionFactory.createValueExpression(...);
|
||||||
|
expressionCache.put(expression, expr);
|
||||||
|
}
|
||||||
|
|
||||||
|
return expr.getValue(context);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.3 难点3:如何优雅地扩展节点类型?
|
||||||
|
|
||||||
|
**问题**:
|
||||||
|
第一期只有5种节点,以后要加新节点,如何不改核心代码?
|
||||||
|
|
||||||
|
**✅ 插件化方案**:
|
||||||
|
|
||||||
|
```java
|
||||||
|
// 1. 定义节点接口
|
||||||
|
public interface WorkflowNode {
|
||||||
|
NodeTypeMetadata getMetadata(); // 节点元数据(名称、字段定义等)
|
||||||
|
NodeExecutionResult execute(NodeInput input, NodeExecutionContext context);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 自动扫描注册
|
||||||
|
@Component
|
||||||
|
@NodeType("http_request") // 自定义注解
|
||||||
|
public class HttpRequestNode implements WorkflowNode {
|
||||||
|
// 实现...
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Spring 启动时自动注册
|
||||||
|
@Service
|
||||||
|
public class NodeTypeRegistry {
|
||||||
|
@Autowired
|
||||||
|
private List<WorkflowNode> allNodes; // Spring 自动注入所有实现
|
||||||
|
|
||||||
|
@PostConstruct
|
||||||
|
public void init() {
|
||||||
|
for (WorkflowNode node : allNodes) {
|
||||||
|
register(node);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**验收标准**:
|
||||||
|
- [ ] 新增一个节点只需要创建一个类,不改其他代码
|
||||||
|
- [ ] 前端自动显示新节点(调用 GET /api/node-types)
|
||||||
|
- [ ] 热加载(开发环境重启后自动识别新节点)
|
||||||
|
|
||||||
|
### 4.4 难点4:审批节点如何实现?
|
||||||
|
|
||||||
|
**问题**:
|
||||||
|
工作流执行到审批节点时要暂停,等待用户操作,如何实现?
|
||||||
|
|
||||||
|
**✅ Flowable 原生方案**:
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<!-- BPMN 定义 -->
|
||||||
|
<userTask id="approval" name="审批" flowable:assignee="${approver}">
|
||||||
|
<extensionElements>
|
||||||
|
<flowable:formProperty id="approved" name="是否批准" type="boolean" />
|
||||||
|
<flowable:formProperty id="comment" name="审批意见" type="string" />
|
||||||
|
</extensionElements>
|
||||||
|
</userTask>
|
||||||
|
```
|
||||||
|
|
||||||
|
```java
|
||||||
|
// 1. 流程执行到 User Task 时自动暂停
|
||||||
|
ProcessInstance instance = runtimeService.startProcessInstanceByKey("workflow");
|
||||||
|
|
||||||
|
// 2. 查询待审批任务
|
||||||
|
List<Task> tasks = taskService.createTaskQuery()
|
||||||
|
.taskAssignee("user@example.com")
|
||||||
|
.list();
|
||||||
|
|
||||||
|
// 3. 用户提交审批
|
||||||
|
Map<String, Object> variables = Map.of(
|
||||||
|
"approved", true,
|
||||||
|
"comment", "同意部署"
|
||||||
|
);
|
||||||
|
taskService.complete(task.getId(), variables);
|
||||||
|
|
||||||
|
// 4. 流程自动继续执行
|
||||||
|
```
|
||||||
|
|
||||||
|
**前端实现**:
|
||||||
|
```
|
||||||
|
1. 用户拖入"审批"节点
|
||||||
|
↓
|
||||||
|
2. 配置审批人、表单字段
|
||||||
|
↓
|
||||||
|
3. 后端转换为 <userTask>
|
||||||
|
↓
|
||||||
|
4. 执行时,前端轮询或 WebSocket 监听任务
|
||||||
|
↓
|
||||||
|
5. 显示审批表单
|
||||||
|
↓
|
||||||
|
6. 提交后,流程继续
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 五、关键设计决策
|
||||||
|
|
||||||
|
### 5.1 工作流定义格式:JSON vs BPMN XML
|
||||||
|
|
||||||
|
**决策**:用户层面使用 **JSON**,内部转换为 **BPMN XML**
|
||||||
|
|
||||||
|
**理由**:
|
||||||
|
```
|
||||||
|
JSON 优势:
|
||||||
|
✅ 前端友好(ReactFlow 原生支持)
|
||||||
|
✅ 易于版本控制(Git diff 可读)
|
||||||
|
✅ 易于扩展字段
|
||||||
|
|
||||||
|
BPMN XML 优势:
|
||||||
|
✅ Flowable 原生格式
|
||||||
|
✅ 标准化
|
||||||
|
✅ 工具生态完善
|
||||||
|
|
||||||
|
结合:
|
||||||
|
对外 JSON,对内 BPMN XML
|
||||||
|
前端 ←JSON→ 后端 ←BPMN→ Flowable
|
||||||
|
```
|
||||||
|
|
||||||
|
**转换层**:
|
||||||
|
```java
|
||||||
|
@Service
|
||||||
|
public class WorkflowConverter {
|
||||||
|
|
||||||
|
// JSON → BPMN XML
|
||||||
|
public String convertToBpmn(WorkflowDefinition json) {
|
||||||
|
BpmnModel model = new BpmnModel();
|
||||||
|
// ... 转换逻辑
|
||||||
|
return BpmnXMLConverter.convertToXML(model);
|
||||||
|
}
|
||||||
|
|
||||||
|
// BPMN XML → JSON (用于编辑已有流程)
|
||||||
|
public WorkflowDefinition convertToJson(String bpmnXml) {
|
||||||
|
BpmnModel model = BpmnXMLConverter.convertToBpmnModel(bpmnXml);
|
||||||
|
// ... 转换逻辑
|
||||||
|
return workflowDefinition;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.2 表达式语法:自定义 vs 标准
|
||||||
|
|
||||||
|
**决策**:使用 **简化的 JUEL 语法**
|
||||||
|
|
||||||
|
**语法规范**:
|
||||||
|
```javascript
|
||||||
|
// ✅ 支持
|
||||||
|
${nodes.httpRequest.output.body.email}
|
||||||
|
${nodes.httpRequest.output.items[0].name}
|
||||||
|
${workflow.input.username}
|
||||||
|
${env.API_KEY}
|
||||||
|
${nodes.step1.output.count > 10 ? 'high' : 'low'}
|
||||||
|
|
||||||
|
// ❌ 第一期不支持
|
||||||
|
复杂 JavaScript 函数
|
||||||
|
循环语句
|
||||||
|
自定义函数
|
||||||
|
```
|
||||||
|
|
||||||
|
**理由**:
|
||||||
|
1. JUEL 是 Java 标准,性能好
|
||||||
|
2. 语法简单,学习成本低
|
||||||
|
3. 与 Flowable 原生集成
|
||||||
|
4. 第二期可以扩展支持 JavaScript
|
||||||
|
|
||||||
|
### 5.3 节点执行:同步 vs 异步
|
||||||
|
|
||||||
|
**决策**:第一期**同步执行**,第二期**异步执行**
|
||||||
|
|
||||||
|
**第一期(同步)**:
|
||||||
|
```java
|
||||||
|
public WorkflowExecutionResult execute(String workflowId, Map<String, Object> input) {
|
||||||
|
// 直接在当前线程执行,等待完成
|
||||||
|
ProcessInstance instance = runtimeService.startProcessInstanceByKey(
|
||||||
|
workflowId,
|
||||||
|
variables
|
||||||
|
);
|
||||||
|
|
||||||
|
// 阻塞等待完成
|
||||||
|
while (!isCompleted(instance.getId())) {
|
||||||
|
Thread.sleep(100);
|
||||||
|
}
|
||||||
|
|
||||||
|
return getResult(instance.getId());
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**优点**:实现简单,适合快速验证
|
||||||
|
**缺点**:长时间运行的工作流会阻塞请求
|
||||||
|
|
||||||
|
**第二期(异步)**:
|
||||||
|
```java
|
||||||
|
public String executeAsync(String workflowId, Map<String, Object> input) {
|
||||||
|
// 立即返回执行ID
|
||||||
|
String executionId = UUID.randomUUID().toString();
|
||||||
|
|
||||||
|
// 提交到线程池异步执行
|
||||||
|
executorService.submit(() -> {
|
||||||
|
runtimeService.startProcessInstanceByKey(workflowId, variables);
|
||||||
|
});
|
||||||
|
|
||||||
|
return executionId;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 前端轮询查询状态
|
||||||
|
GET /api/executions/{executionId}/status
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 六、MVP 范围界定(重要)
|
||||||
|
|
||||||
|
### 6.1 第一期必须有的功能(验收标准)
|
||||||
|
|
||||||
|
**1. 工作流编辑器**
|
||||||
|
- [ ] 从左侧拖拽节点到画布
|
||||||
|
- [ ] 节点之间连线(自动布局可选)
|
||||||
|
- [ ] 删除节点和连线
|
||||||
|
- [ ] 保存工作流(JSON 格式)
|
||||||
|
- [ ] 加载已有工作流
|
||||||
|
|
||||||
|
**2. 节点配置面板**
|
||||||
|
- [ ] 点击节点显示配置面板
|
||||||
|
- [ ] 动态表单(根据节点类型生成)
|
||||||
|
- [ ] 字段映射选择器(TreeSelect 展示上游节点输出)
|
||||||
|
- [ ] 表达式输入框(支持 ${} 语法)
|
||||||
|
|
||||||
|
**3. 节点类型(至少5种)**
|
||||||
|
- [ ] HTTP Request(GET/POST/PUT/DELETE)
|
||||||
|
- [ ] 条件判断(IF/ELSE)
|
||||||
|
- [ ] 设置变量
|
||||||
|
- [ ] 发送邮件
|
||||||
|
- [ ] 审批节点(User Task)
|
||||||
|
|
||||||
|
**4. 工作流执行**
|
||||||
|
- [ ] 手动触发执行
|
||||||
|
- [ ] 查看执行日志
|
||||||
|
- [ ] 查看节点输入/输出
|
||||||
|
- [ ] 执行失败时显示错误信息
|
||||||
|
|
||||||
|
**5. 审批功能**
|
||||||
|
- [ ] 待审批任务列表
|
||||||
|
- [ ] 审批表单
|
||||||
|
- [ ] 批准/拒绝
|
||||||
|
- [ ] 审批历史
|
||||||
|
|
||||||
|
### 6.2 第一期不做的功能(明确排除)
|
||||||
|
|
||||||
|
```
|
||||||
|
❌ 定时触发(Cron)
|
||||||
|
❌ Webhook 触发
|
||||||
|
❌ 循环节点(forEach)
|
||||||
|
❌ 并行执行
|
||||||
|
❌ 子流程
|
||||||
|
❌ 工作流版本管理
|
||||||
|
❌ 回滚
|
||||||
|
❌ 导入/导出(第二期)
|
||||||
|
❌ 权限管理(只做基础认证)
|
||||||
|
❌ 多租户
|
||||||
|
❌ API 限流
|
||||||
|
❌ 监控大盘
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 七、技术风险与应对
|
||||||
|
|
||||||
|
### 7.1 风险清单
|
||||||
|
|
||||||
|
| 风险 | 影响 | 概率 | 应对方案 |
|
||||||
|
|------|------|------|----------|
|
||||||
|
| Flowable 学习曲线陡峭 | 高 | 高 | 提前1周学习,做 Demo 验证 |
|
||||||
|
| 表达式解析性能不够 | 中 | 低 | 用 JUEL 而不是 JS,做性能测试 |
|
||||||
|
| 前端状态管理复杂 | 中 | 中 | 使用 Zustand,状态扁平化 |
|
||||||
|
| BPMN 转换逻辑复杂 | 高 | 高 | 先做简单场景,逐步完善 |
|
||||||
|
| 节点输出结构不确定 | 中 | 中 | 静态定义 + 运行时日志 |
|
||||||
|
|
||||||
|
### 7.2 技术验证(PoC)
|
||||||
|
|
||||||
|
**Week 1: 核心技术验证**
|
||||||
|
```
|
||||||
|
1. Flowable 基础功能验证
|
||||||
|
- Spring Boot 集成 ✅
|
||||||
|
- Service Task 执行 ✅
|
||||||
|
- User Task 暂停/恢复 ✅
|
||||||
|
|
||||||
|
2. 表达式引擎验证
|
||||||
|
- JUEL 性能测试 ✅
|
||||||
|
- 嵌套对象访问 ✅
|
||||||
|
- 数组索引访问 ✅
|
||||||
|
|
||||||
|
3. 前端画布验证
|
||||||
|
- ReactFlow 拖拽 ✅
|
||||||
|
- 节点自定义样式 ✅
|
||||||
|
- 连线规则 ✅
|
||||||
|
```
|
||||||
|
|
||||||
|
**验收标准**:
|
||||||
|
- 1天内完成 Flowable Hello World
|
||||||
|
- 表达式引擎 QPS > 10000
|
||||||
|
- 前端画布支持 100+ 节点不卡顿
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 八、开发计划
|
||||||
|
|
||||||
|
### 8.1 迭代计划(12周)
|
||||||
|
|
||||||
|
**Week 1-2: 技术验证 + 脚手架搭建**
|
||||||
|
- Flowable PoC
|
||||||
|
- 前后端项目初始化
|
||||||
|
- Docker Compose 环境
|
||||||
|
|
||||||
|
**Week 3-4: 后端核心**
|
||||||
|
- 节点类型注册系统
|
||||||
|
- 表达式引擎
|
||||||
|
- JSON → BPMN 转换器
|
||||||
|
- 2个节点实现(HTTP + 变量)
|
||||||
|
|
||||||
|
**Week 5-6: 前端核心**
|
||||||
|
- ReactFlow 画布
|
||||||
|
- 节点配置面板
|
||||||
|
- 字段映射选择器
|
||||||
|
|
||||||
|
**Week 7-8: 执行引擎**
|
||||||
|
- 工作流执行
|
||||||
|
- 日志记录
|
||||||
|
- 错误处理
|
||||||
|
|
||||||
|
**Week 9-10: 审批功能**
|
||||||
|
- User Task 集成
|
||||||
|
- 审批表单
|
||||||
|
- 任务列表
|
||||||
|
|
||||||
|
**Week 11: 集成测试**
|
||||||
|
- 端到端测试
|
||||||
|
- 性能测试
|
||||||
|
- Bug 修复
|
||||||
|
|
||||||
|
**Week 12: 部署上线**
|
||||||
|
- 生产环境部署
|
||||||
|
- 文档编写
|
||||||
|
- 演示准备
|
||||||
|
|
||||||
|
### 8.2 人员配置
|
||||||
|
|
||||||
|
```
|
||||||
|
最小团队(4人):
|
||||||
|
- 后端工程师 x2(Java + Flowable)
|
||||||
|
- 前端工程师 x1(React + TypeScript)
|
||||||
|
- 全栈工程师 x1(前后端 + DevOps)
|
||||||
|
|
||||||
|
可选(+2人):
|
||||||
|
- 测试工程师 x1
|
||||||
|
- UI/UX 设计师 x1
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 九、成功标准
|
||||||
|
|
||||||
|
### 9.1 技术指标
|
||||||
|
|
||||||
|
```
|
||||||
|
性能:
|
||||||
|
- 工作流执行延迟 < 500ms(10个节点)
|
||||||
|
- 前端画布渲染 < 2s(100个节点)
|
||||||
|
- 表达式解析 QPS > 10000
|
||||||
|
- 并发执行 > 100 个工作流
|
||||||
|
|
||||||
|
稳定性:
|
||||||
|
- 可用性 > 99%
|
||||||
|
- 错误率 < 1%
|
||||||
|
- 数据不丢失
|
||||||
|
|
||||||
|
可维护性:
|
||||||
|
- 单元测试覆盖率 > 60%
|
||||||
|
- 核心逻辑测试覆盖率 > 80%
|
||||||
|
- 代码可读性(通过 Code Review)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 9.2 功能验收
|
||||||
|
|
||||||
|
**场景1:创建简单工作流**
|
||||||
|
```
|
||||||
|
1. 拖入 HTTP 节点,配置 URL: https://api.github.com/users/octocat
|
||||||
|
2. 拖入 邮件节点,配置收件人: ${nodes.http.output.body.email}
|
||||||
|
3. 连线:HTTP → 邮件
|
||||||
|
4. 保存并执行
|
||||||
|
5. 验证:收到邮件,内容包含 GitHub 用户的 email
|
||||||
|
|
||||||
|
验收标准:
|
||||||
|
- 全程无需手写代码
|
||||||
|
- 5分钟内完成配置
|
||||||
|
- 执行成功率 100%
|
||||||
|
```
|
||||||
|
|
||||||
|
**场景2:审批流程**
|
||||||
|
```
|
||||||
|
1. 创建工作流:HTTP请求 → 审批节点 → 邮件通知
|
||||||
|
2. 执行工作流
|
||||||
|
3. 验证:流程暂停在审批节点
|
||||||
|
4. 打开审批中心,看到待办任务
|
||||||
|
5. 批准
|
||||||
|
6. 验证:流程继续执行,发送邮件
|
||||||
|
|
||||||
|
验收标准:
|
||||||
|
- 审批人收到通知(邮件/站内信)
|
||||||
|
- 审批后流程立即继续
|
||||||
|
- 审批历史可追溯
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 十、后续规划(第二期)
|
||||||
|
|
||||||
|
```
|
||||||
|
优先级排序:
|
||||||
|
|
||||||
|
P0 (必须有):
|
||||||
|
- 定时触发(Cron)
|
||||||
|
- 循环节点(forEach)
|
||||||
|
- 工作流版本管理
|
||||||
|
|
||||||
|
P1 (很重要):
|
||||||
|
- Webhook 触发
|
||||||
|
- 更多节点类型(数据库、文件、消息队列)
|
||||||
|
- 监控大盘
|
||||||
|
|
||||||
|
P2 (可以有):
|
||||||
|
- 导入/导出
|
||||||
|
- 子流程
|
||||||
|
- 并行执行
|
||||||
|
- 工作流模板市场
|
||||||
|
|
||||||
|
P3 (锦上添花):
|
||||||
|
- AI 辅助生成
|
||||||
|
- 实时协作编辑
|
||||||
|
- 多租户隔离
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 附录:关键文件清单
|
||||||
|
|
||||||
|
```
|
||||||
|
项目结构:
|
||||||
|
docs/
|
||||||
|
├── 01-架构总览.md (本文档)
|
||||||
|
├── 02-后端技术设计.md (详细后端实现)
|
||||||
|
├── 03-前端技术设计.md (详细前端实现)
|
||||||
|
├── 04-数据模型设计.md (数据库表结构)
|
||||||
|
└── 05-开发规范.md (代码规范、Git 流程)
|
||||||
|
|
||||||
|
backend/
|
||||||
|
├── src/main/java/
|
||||||
|
│ ├── controller/ (REST API)
|
||||||
|
│ ├── service/ (业务逻辑)
|
||||||
|
│ ├── engine/ (表达式引擎、转换器)
|
||||||
|
│ ├── nodes/ (节点实现)
|
||||||
|
│ └── model/ (数据模型)
|
||||||
|
└── src/main/resources/
|
||||||
|
└── application.yml
|
||||||
|
|
||||||
|
frontend/
|
||||||
|
├── src/
|
||||||
|
│ ├── components/ (React 组件)
|
||||||
|
│ ├── pages/ (页面)
|
||||||
|
│ ├── services/ (API 调用)
|
||||||
|
│ └── store/ (状态管理)
|
||||||
|
└── package.json
|
||||||
|
|
||||||
|
docker-compose.yml
|
||||||
|
README.md
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**下一步**:请查看详细的后端和前端技术设计文档。
|
||||||
|
|
||||||
1787
backend/docs/02-后端技术设计.md
Normal file
1787
backend/docs/02-后端技术设计.md
Normal file
File diff suppressed because it is too large
Load Diff
1404
backend/docs/03-前端技术设计.md
Normal file
1404
backend/docs/03-前端技术设计.md
Normal file
File diff suppressed because it is too large
Load Diff
192
backend/docs/04-数据模型设计.md
Normal file
192
backend/docs/04-数据模型设计.md
Normal file
@ -0,0 +1,192 @@
|
|||||||
|
# 数据模型设计(JSON Schema,前后端统一)
|
||||||
|
|
||||||
|
版本: v1.0
|
||||||
|
规范: JSON Schema Draft-07
|
||||||
|
命名约定: camelCase;所有 ID 均为字符串;时间统一 ISO 8601(UTC)。
|
||||||
|
|
||||||
|
一、WorkflowDefinition(工作流定义)
|
||||||
|
- 描述:前端编辑器生成/后端持久化与部署的核心数据结构
|
||||||
|
- 说明:MVP 仅支持串行/条件分支;并行/子流程等后续引入
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||||
|
"$id": "https://example.com/schemas/workflow-definition.json",
|
||||||
|
"title": "WorkflowDefinition",
|
||||||
|
"type": "object",
|
||||||
|
"required": ["id", "name", "nodes", "edges", "schemaVersion"],
|
||||||
|
"properties": {
|
||||||
|
"id": { "type": "string", "minLength": 1 },
|
||||||
|
"name": { "type": "string", "minLength": 1 },
|
||||||
|
"description": { "type": "string" },
|
||||||
|
"schemaVersion": { "type": "string", "enum": ["1.0"] },
|
||||||
|
"nodes": {
|
||||||
|
"type": "array",
|
||||||
|
"minItems": 1,
|
||||||
|
"items": { "$ref": "#/$defs/workflowNode" }
|
||||||
|
},
|
||||||
|
"edges": {
|
||||||
|
"type": "array",
|
||||||
|
"items": { "$ref": "#/$defs/workflowEdge" }
|
||||||
|
},
|
||||||
|
"variables": { "type": "object", "additionalProperties": true },
|
||||||
|
"metadata": { "type": "object", "additionalProperties": true }
|
||||||
|
},
|
||||||
|
"$defs": {
|
||||||
|
"position": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["x", "y"],
|
||||||
|
"properties": {
|
||||||
|
"x": { "type": "number" },
|
||||||
|
"y": { "type": "number" }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"workflowNode": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["id", "type", "name", "position", "config"],
|
||||||
|
"properties": {
|
||||||
|
"id": { "type": "string", "minLength": 1 },
|
||||||
|
"type": { "type": "string", "minLength": 1 },
|
||||||
|
"name": { "type": "string", "minLength": 1 },
|
||||||
|
"position": { "$ref": "#/$defs/position" },
|
||||||
|
"config": { "type": "object", "additionalProperties": true }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"workflowEdge": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["source", "target"],
|
||||||
|
"properties": {
|
||||||
|
"id": { "type": "string" },
|
||||||
|
"source": { "type": "string", "minLength": 1 },
|
||||||
|
"target": { "type": "string", "minLength": 1 },
|
||||||
|
"condition": { "type": "string", "pattern": "^\\$\\{.*\\}$" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
示例(最小可用工作流)
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": "wf_001",
|
||||||
|
"name": "HTTP→审批→邮件",
|
||||||
|
"schemaVersion": "1.0",
|
||||||
|
"nodes": [
|
||||||
|
{"id": "n1", "type": "http_request", "name": "Get User", "position": {"x": 100, "y": 100}, "config": {"url": "https://api.example.com/users/1", "method": "GET"}},
|
||||||
|
{"id": "n2", "type": "approval", "name": "审批", "position": {"x": 320, "y": 100}, "config": {"assignee": "${workflow.input.approver}", "formFields": [{"id": "approved", "label": "同意?", "type": "boolean", "required": true}]}},
|
||||||
|
{"id": "n3", "type": "send_mail", "name": "通知", "position": {"x": 540, "y": 100}, "config": {"to": "${nodes.n1.output.body.email}", "subject": "审批已通过", "content": "Hello"}}
|
||||||
|
],
|
||||||
|
"edges": [
|
||||||
|
{"source": "n1", "target": "n2"},
|
||||||
|
{"source": "n2", "target": "n3", "condition": "${approved == true}"}
|
||||||
|
],
|
||||||
|
"variables": {"env": "dev"}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
二、NodeTypeMetadata(节点类型元数据)
|
||||||
|
- 描述:驱动前端动态表单、字段映射、帮助用户理解节点输入/输出
|
||||||
|
- 约束:id 唯一、fields 与 outputSchema 必填
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||||
|
"$id": "https://example.com/schemas/node-type-metadata.json",
|
||||||
|
"title": "NodeTypeMetadata",
|
||||||
|
"type": "object",
|
||||||
|
"required": ["id", "name", "displayName", "category", "fields", "outputSchema"],
|
||||||
|
"properties": {
|
||||||
|
"id": { "type": "string", "minLength": 1 },
|
||||||
|
"name": { "type": "string", "minLength": 1 },
|
||||||
|
"displayName": { "type": "string" },
|
||||||
|
"category": { "type": "string", "enum": ["api", "database", "logic", "notification", "transform", "other"] },
|
||||||
|
"icon": { "type": "string" },
|
||||||
|
"description": { "type": "string" },
|
||||||
|
"fields": {
|
||||||
|
"type": "array",
|
||||||
|
"minItems": 1,
|
||||||
|
"items": { "$ref": "#/$defs/fieldDefinition" }
|
||||||
|
},
|
||||||
|
"outputSchema": { "type": "object" },
|
||||||
|
"implementationClass": { "type": "string" },
|
||||||
|
"enabled": { "type": "boolean", "default": true }
|
||||||
|
},
|
||||||
|
"$defs": {
|
||||||
|
"fieldDefinition": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["name", "label", "type"],
|
||||||
|
"properties": {
|
||||||
|
"name": { "type": "string" },
|
||||||
|
"label": { "type": "string" },
|
||||||
|
"type": { "type": "string", "enum": ["text", "textarea", "number", "select", "code", "key_value", "boolean"] },
|
||||||
|
"required": { "type": "boolean", "default": false },
|
||||||
|
"supportsExpression": { "type": "boolean", "default": false },
|
||||||
|
"supportsFieldMapping": { "type": "boolean", "default": false },
|
||||||
|
"options": { "type": "array", "items": {"type": "string"} },
|
||||||
|
"defaultValue": {},
|
||||||
|
"placeholder": { "type": "string" },
|
||||||
|
"language": { "type": "string" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
示例(HTTP Request 节点)
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": "http_request",
|
||||||
|
"name": "httpRequest",
|
||||||
|
"displayName": "HTTP Request",
|
||||||
|
"category": "api",
|
||||||
|
"icon": "ApiOutlined",
|
||||||
|
"description": "发送 HTTP 请求",
|
||||||
|
"fields": [
|
||||||
|
{"name": "url", "label": "URL", "type": "text", "required": true, "supportsExpression": true},
|
||||||
|
{"name": "method", "label": "Method", "type": "select", "options": ["GET", "POST", "PUT", "DELETE", "PATCH"], "defaultValue": "GET"},
|
||||||
|
{"name": "headers", "label": "Headers", "type": "key_value", "supportsFieldMapping": true},
|
||||||
|
{"name": "body", "label": "Body", "type": "code", "language": "json", "supportsExpression": true},
|
||||||
|
{"name": "timeout", "label": "Timeout(ms)", "type": "number", "defaultValue": 30000}
|
||||||
|
],
|
||||||
|
"outputSchema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"statusCode": {"type": "number"},
|
||||||
|
"body": {"type": "object"},
|
||||||
|
"headers": {"type": "object"},
|
||||||
|
"elapsed": {"type": "number"}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
三、表达式(JUEL)约定
|
||||||
|
- 统一使用 ${...}
|
||||||
|
- 可访问命名空间:
|
||||||
|
- nodes:上游节点数据,如 ${nodes.n1.output.body.email}
|
||||||
|
- workflow:输入/变量,如 ${workflow.input.userId}
|
||||||
|
- env:环境变量,如 ${env.API_KEY}
|
||||||
|
- 仅 Map/属性访问;不允许方法/类引用;支持三元表达式
|
||||||
|
|
||||||
|
四、执行与日志数据(后端输出/存储)
|
||||||
|
- NodeExecutionResult(内存/接口响应中的片段)
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "success",
|
||||||
|
"output": {"statusCode": 200, "body": {"id": 1}},
|
||||||
|
"error": null,
|
||||||
|
"startTime": "2025-01-01T10:00:00Z",
|
||||||
|
"endTime": "2025-01-01T10:00:10Z",
|
||||||
|
"durationMs": 10
|
||||||
|
}
|
||||||
|
```
|
||||||
|
- 节点执行日志表(node_execution_logs)建议结构(与 docs/02 一致):
|
||||||
|
- execution_id、node_id、node_type、input(JSON)、output(JSON)、status、started_at、ended_at、duration_ms、error_message
|
||||||
|
|
||||||
|
五、前后端对齐要点(Checklist)
|
||||||
|
- WorkflowDefinition 使用 schemaVersion 固定为 1.0
|
||||||
|
- WorkflowEdge.condition 必须为完整 ${...} 字符串
|
||||||
|
- NodeTypeMetadata.id 与 WorkflowNode.type 对齐(如 http_request)
|
||||||
|
- 字段映射组件输出“完整表达式”字符串(含 ${})
|
||||||
|
- 输出结构以 outputSchema 为准;前端展示字段树最多展开 3 层(可配置)
|
||||||
163
backend/docs/05-API契约.md
Normal file
163
backend/docs/05-API契约.md
Normal file
@ -0,0 +1,163 @@
|
|||||||
|
# API 契约(前后端统一)
|
||||||
|
|
||||||
|
版本: v1.0
|
||||||
|
鉴权: 预留 Bearer Token(可选),前端 axios 已支持 Authorization 头;MVP 可不启用严格鉴权。
|
||||||
|
|
||||||
|
通用规范
|
||||||
|
- Content-Type: application/json; charset=utf-8
|
||||||
|
- 分页参数(如适用): page(默认1), size(默认10)
|
||||||
|
- 错误响应: { "code": string, "message": string, "details"?: any }
|
||||||
|
|
||||||
|
一、工作流(/api/workflows)
|
||||||
|
1) 创建工作流
|
||||||
|
- POST /api/workflows
|
||||||
|
- Body: WorkflowDefinition(见 04-数据模型设计.md),id 可由后端生成
|
||||||
|
- 200 响应: WorkflowDefinition(包含持久化后的 id)
|
||||||
|
|
||||||
|
2) 更新工作流
|
||||||
|
- PUT /api/workflows/{id}
|
||||||
|
- Body: WorkflowDefinition
|
||||||
|
- 200 响应: WorkflowDefinition
|
||||||
|
|
||||||
|
3) 获取工作流详情
|
||||||
|
- GET /api/workflows/{id}
|
||||||
|
- 200 响应: WorkflowDefinition
|
||||||
|
|
||||||
|
4) 获取工作流列表
|
||||||
|
- GET /api/workflows?status=active|draft|archived&page=1&size=10
|
||||||
|
- 200 响应: { "items": WorkflowDefinition[], "page": number, "size": number, "total": number }
|
||||||
|
|
||||||
|
5) 删除工作流
|
||||||
|
- DELETE /api/workflows/{id}
|
||||||
|
- 204 响应: 无
|
||||||
|
|
||||||
|
6) 执行工作流(MVP 同步执行)
|
||||||
|
- POST /api/workflows/{id}/execute
|
||||||
|
- Body: { "input": object }
|
||||||
|
- 200 响应: WorkflowExecutionResult
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"workflowId": "wf_001",
|
||||||
|
"processInstanceId": "f6a...",
|
||||||
|
"status": "completed",
|
||||||
|
"output": { "result": "..." },
|
||||||
|
"nodes": {
|
||||||
|
"n1": { "status": "success", "input": {...}, "output": {...}, "startTime": "...", "endTime": "..." },
|
||||||
|
"n2": { "status": "success", "input": {...}, "output": {...} }
|
||||||
|
},
|
||||||
|
"startedAt": "2025-01-01T10:00:00Z",
|
||||||
|
"endedAt": "2025-01-01T10:00:02Z"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
7) 获取执行记录
|
||||||
|
- GET /api/workflows/{id}/executions?page=1&size=10
|
||||||
|
- 200 响应: { "items": WorkflowExecutionRecord[], "page": number, "size": number, "total": number }
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": "exe_001",
|
||||||
|
"workflowDefinitionId": "wf_001",
|
||||||
|
"processInstanceId": "f6a...",
|
||||||
|
"status": "completed",
|
||||||
|
"triggerType": "manual",
|
||||||
|
"triggeredBy": "user@example.com",
|
||||||
|
"startedAt": "2025-01-01T10:00:00Z",
|
||||||
|
"endedAt": "2025-01-01T10:00:02Z"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
8) 获取执行详情
|
||||||
|
- GET /api/workflows/executions/{executionId}
|
||||||
|
- 200 响应: WorkflowExecutionDetail
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": "exe_001",
|
||||||
|
"workflowDefinitionId": "wf_001",
|
||||||
|
"processInstanceId": "f6a...",
|
||||||
|
"status": "completed",
|
||||||
|
"input": { "userId": 1 },
|
||||||
|
"nodes": {
|
||||||
|
"n1": { "status": "success", "input": {...}, "output": {...} },
|
||||||
|
"n2": { "status": "success", "input": {...}, "output": {...} }
|
||||||
|
},
|
||||||
|
"startedAt": "2025-01-01T10:00:00Z",
|
||||||
|
"endedAt": "2025-01-01T10:00:02Z",
|
||||||
|
"error": null
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
二、节点类型(/api/node-types)
|
||||||
|
1) 获取全部节点类型
|
||||||
|
- GET /api/node-types
|
||||||
|
- 200 响应: NodeTypeMetadata[](见 04-数据模型设计.md)
|
||||||
|
|
||||||
|
2) 获取单个节点类型
|
||||||
|
- GET /api/node-types/{typeId}
|
||||||
|
- 200 响应: NodeTypeMetadata
|
||||||
|
|
||||||
|
3) 按分类查询
|
||||||
|
- GET /api/node-types/category/{category}
|
||||||
|
- 200 响应: NodeTypeMetadata[]
|
||||||
|
|
||||||
|
三、审批任务(/api/tasks)
|
||||||
|
1) 获取待办任务
|
||||||
|
- GET /api/tasks?assignee=user@example.com&page=1&size=10
|
||||||
|
- 200 响应: { "items": TaskInfo[], "page": number, "size": number, "total": number }
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": "task_001",
|
||||||
|
"name": "审批",
|
||||||
|
"assignee": "user@example.com",
|
||||||
|
"processInstanceId": "f6a...",
|
||||||
|
"createdAt": "2025-01-01T10:00:00Z",
|
||||||
|
"dueDate": null
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
2) 获取任务详情
|
||||||
|
- GET /api/tasks/{taskId}
|
||||||
|
- 200 响应: TaskDetail
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": "task_001",
|
||||||
|
"name": "审批",
|
||||||
|
"assignee": "user@example.com",
|
||||||
|
"processInstanceId": "f6a...",
|
||||||
|
"variables": { "approved": null, "comment": null }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
3) 获取任务表单
|
||||||
|
- GET /api/tasks/{taskId}/form
|
||||||
|
- 200 响应: TaskForm
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"taskId": "task_001",
|
||||||
|
"fields": [
|
||||||
|
{"id": "approved", "label": "同意?", "type": "boolean", "required": true},
|
||||||
|
{"id": "comment", "label": "意见", "type": "string", "required": false}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
4) 完成任务
|
||||||
|
- POST /api/tasks/{taskId}/complete
|
||||||
|
- Body: { "variables": object }
|
||||||
|
- 200 响应: 无(或 { "status": "ok" })
|
||||||
|
|
||||||
|
四、错误模型
|
||||||
|
- HTTP 400: 参数/校验错误
|
||||||
|
- HTTP 401: 未授权(如启用鉴权)
|
||||||
|
- HTTP 404: 资源不存在
|
||||||
|
- HTTP 409: 冲突(如重名/状态冲突)
|
||||||
|
- HTTP 500: 服务器错误
|
||||||
|
|
||||||
|
错误响应示例:
|
||||||
|
```json
|
||||||
|
{ "code": "VALIDATION_ERROR", "message": "name 不能为空", "details": { "field": "name" } }
|
||||||
|
```
|
||||||
|
|
||||||
|
五、字段/表达式一致性
|
||||||
|
- 前端提交的所有表达式采用完整 ${...} 字符串;后端按 JUEL 解析
|
||||||
|
- WorkflowEdge.condition 在转换层映射到 BPMN 的 conditionExpression
|
||||||
|
- NodeTypeMetadata.fields 与前端表单渲染一一对应;outputSchema 驱动字段映射树
|
||||||
836
backend/docs/05-开发规范.md
Normal file
836
backend/docs/05-开发规范.md
Normal file
@ -0,0 +1,836 @@
|
|||||||
|
# 开发规范文档
|
||||||
|
|
||||||
|
**版本**: v1.0
|
||||||
|
**目的**: 确保代码质量、可维护性、团队协作效率
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 一、代码规范
|
||||||
|
|
||||||
|
### 1.1 Java 代码规范
|
||||||
|
|
||||||
|
#### 命名规范
|
||||||
|
|
||||||
|
```java
|
||||||
|
// ✅ 正确
|
||||||
|
public class WorkflowService {} // 类名:大驼峰
|
||||||
|
public interface WorkflowNode {} // 接口名:大驼峰
|
||||||
|
public enum ExecutionStatus {} // 枚举:大驼峰
|
||||||
|
|
||||||
|
private String userId; // 变量:小驼峰
|
||||||
|
public static final int MAX_RETRY = 3; // 常量:大写下划线
|
||||||
|
|
||||||
|
public void executeWorkflow() {} // 方法:小驼峰
|
||||||
|
|
||||||
|
// ❌ 错误
|
||||||
|
public class workflow_service {} // 不要用下划线
|
||||||
|
private String UserId; // 变量不要大驼峰
|
||||||
|
public static final int maxRetry = 3; // 常量不要小驼峰
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 包结构
|
||||||
|
|
||||||
|
```
|
||||||
|
com.workflow
|
||||||
|
├── controller # REST API控制器
|
||||||
|
│ ├── WorkflowController.java
|
||||||
|
│ └── NodeTypeController.java
|
||||||
|
├── service # 业务逻辑
|
||||||
|
│ ├── WorkflowService.java
|
||||||
|
│ └── ExecutionService.java
|
||||||
|
├── repository # 数据访问
|
||||||
|
│ └── WorkflowRepository.java
|
||||||
|
├── model # 数据模型
|
||||||
|
│ ├── entity/ # 实体类(对应数据库表)
|
||||||
|
│ ├── dto/ # 数据传输对象
|
||||||
|
│ └── vo/ # 视图对象
|
||||||
|
├── engine # 核心引擎
|
||||||
|
│ ├── ExpressionEngine.java
|
||||||
|
│ └── WorkflowConverter.java
|
||||||
|
├── nodes # 节点实现
|
||||||
|
│ ├── WorkflowNode.java
|
||||||
|
│ ├── HttpRequestNode.java
|
||||||
|
│ └── SendEmailNode.java
|
||||||
|
├── registry # 注册中心
|
||||||
|
│ └── NodeTypeRegistry.java
|
||||||
|
├── executor # 执行器
|
||||||
|
│ └── GenericNodeExecutor.java
|
||||||
|
├── exception # 自定义异常
|
||||||
|
│ └── WorkflowException.java
|
||||||
|
└── config # 配置类
|
||||||
|
└── FlowableConfig.java
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 注释规范
|
||||||
|
|
||||||
|
```java
|
||||||
|
/**
|
||||||
|
* 工作流服务
|
||||||
|
*
|
||||||
|
* 职责:
|
||||||
|
* 1. 工作流的创建、更新、删除
|
||||||
|
* 2. 工作流的执行
|
||||||
|
* 3. 工作流定义的转换(JSON → BPMN)
|
||||||
|
*
|
||||||
|
* @author zhangsan
|
||||||
|
* @since 1.0.0
|
||||||
|
*/
|
||||||
|
@Service
|
||||||
|
public class WorkflowService {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 执行工作流
|
||||||
|
*
|
||||||
|
* @param workflowId 工作流ID
|
||||||
|
* @param input 输入参数
|
||||||
|
* @return 执行结果
|
||||||
|
* @throws WorkflowNotFoundException 工作流不存在
|
||||||
|
* @throws WorkflowExecutionException 执行失败
|
||||||
|
*/
|
||||||
|
public WorkflowExecutionResult execute(String workflowId, Map<String, Object> input) {
|
||||||
|
// 1. 验证工作流是否存在
|
||||||
|
WorkflowDefinition workflow = workflowRepository.findById(workflowId)
|
||||||
|
.orElseThrow(() -> new WorkflowNotFoundException(workflowId));
|
||||||
|
|
||||||
|
// 2. 准备执行上下文
|
||||||
|
Map<String, Object> variables = prepareVariables(workflow, input);
|
||||||
|
|
||||||
|
// 3. 启动Flowable流程
|
||||||
|
ProcessInstance instance = runtimeService.startProcessInstanceById(
|
||||||
|
workflow.getFlowableProcessDefinitionId(),
|
||||||
|
variables
|
||||||
|
);
|
||||||
|
|
||||||
|
// 4. 返回执行结果
|
||||||
|
return WorkflowExecutionResult.builder()
|
||||||
|
.executionId(instance.getId())
|
||||||
|
.status("running")
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**注释要求**:
|
||||||
|
- 所有 `public` 方法必须有 JavaDoc
|
||||||
|
- 复杂逻辑必须有行内注释(中文)
|
||||||
|
- TODO/FIXME 注释必须标注负责人和日期
|
||||||
|
|
||||||
|
```java
|
||||||
|
// TODO(zhangsan, 2024-01-15): 优化表达式缓存策略
|
||||||
|
// FIXME(lisi, 2024-01-16): 修复循环依赖检测bug
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 异常处理
|
||||||
|
|
||||||
|
```java
|
||||||
|
// ✅ 正确:具体的异常类型
|
||||||
|
public WorkflowDefinition getById(String id) {
|
||||||
|
return workflowRepository.findById(id)
|
||||||
|
.orElseThrow(() -> new WorkflowNotFoundException("工作流不存在: " + id));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ 正确:记录日志
|
||||||
|
public void execute(String workflowId, Map<String, Object> input) {
|
||||||
|
try {
|
||||||
|
// 执行逻辑
|
||||||
|
doExecute(workflowId, input);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("工作流执行失败: workflowId={}, error={}", workflowId, e.getMessage(), e);
|
||||||
|
throw new WorkflowExecutionException("执行失败", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ❌ 错误:吞掉异常
|
||||||
|
try {
|
||||||
|
doSomething();
|
||||||
|
} catch (Exception e) {
|
||||||
|
// 什么都不做
|
||||||
|
}
|
||||||
|
|
||||||
|
// ❌ 错误:抛出原始 Exception
|
||||||
|
public void doSomething() throws Exception {
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 日志规范
|
||||||
|
|
||||||
|
```java
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class WorkflowService {
|
||||||
|
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(WorkflowService.class);
|
||||||
|
|
||||||
|
public void execute(String workflowId, Map<String, Object> input) {
|
||||||
|
// INFO: 重要的业务操作
|
||||||
|
log.info("开始执行工作流: workflowId={}, input={}", workflowId, input);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// DEBUG: 调试信息
|
||||||
|
log.debug("准备执行上下文: variables={}", variables);
|
||||||
|
|
||||||
|
// ... 执行逻辑
|
||||||
|
|
||||||
|
log.info("工作流执行成功: workflowId={}, executionId={}", workflowId, executionId);
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
// ERROR: 错误信息(必须包含异常栈)
|
||||||
|
log.error("工作流执行失败: workflowId={}", workflowId, e);
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**日志级别**:
|
||||||
|
- **ERROR**: 错误,需要立即处理
|
||||||
|
- **WARN**: 警告,可能有问题但不影响运行
|
||||||
|
- **INFO**: 重要的业务操作(创建、更新、删除、执行)
|
||||||
|
- **DEBUG**: 调试信息(开发环境)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 1.2 TypeScript/React 代码规范
|
||||||
|
|
||||||
|
#### 命名规范
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// ✅ 正确
|
||||||
|
interface WorkflowDefinition {} // 接口:大驼峰
|
||||||
|
type NodeType = 'http' | 'email'; // 类型:大驼峰
|
||||||
|
enum ExecutionStatus {} // 枚举:大驼峰
|
||||||
|
|
||||||
|
const userId = '123'; // 变量:小驼峰
|
||||||
|
const MAX_RETRY = 3; // 常量:大写下划线
|
||||||
|
|
||||||
|
function executeWorkflow() {} // 函数:小驼峰
|
||||||
|
|
||||||
|
// 组件:大驼峰
|
||||||
|
export default function WorkflowEditor() {}
|
||||||
|
const CustomNode: React.FC<Props> = () => {}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 文件命名
|
||||||
|
|
||||||
|
```
|
||||||
|
src/
|
||||||
|
├── components/
|
||||||
|
│ ├── WorkflowEditor/ # 组件文件夹:大驼峰
|
||||||
|
│ │ ├── index.tsx # 主文件:小写
|
||||||
|
│ │ ├── Canvas.tsx # 子组件:大驼峰
|
||||||
|
│ │ └── WorkflowEditor.css # 样式:大驼峰
|
||||||
|
│ └── NodeConfigPanel/
|
||||||
|
│ └── index.tsx
|
||||||
|
├── api/
|
||||||
|
│ └── workflow.ts # API文件:小写
|
||||||
|
├── types/
|
||||||
|
│ └── workflow.ts # 类型文件:小写
|
||||||
|
└── utils/
|
||||||
|
└── expressionParser.ts # 工具文件:小驼峰
|
||||||
|
```
|
||||||
|
|
||||||
|
#### TypeScript 类型定义
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// ✅ 正确:明确的类型
|
||||||
|
interface WorkflowNode {
|
||||||
|
id: string;
|
||||||
|
type: string;
|
||||||
|
name: string;
|
||||||
|
position: { x: number; y: number };
|
||||||
|
config: Record<string, any>; // 如果确实是任意类型
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ 正确:使用泛型
|
||||||
|
interface ApiResponse<T> {
|
||||||
|
code: number;
|
||||||
|
message: string;
|
||||||
|
data: T;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ❌ 错误:滥用 any
|
||||||
|
function doSomething(data: any): any {
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ 正确:使用 unknown 或具体类型
|
||||||
|
function doSomething(data: unknown): string {
|
||||||
|
if (typeof data === 'string') {
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
return JSON.stringify(data);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### React 组件规范
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import React, { useState, useEffect, useCallback, memo } from 'react';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 节点配置面板
|
||||||
|
*
|
||||||
|
* @param node - 当前选中的节点
|
||||||
|
* @param onConfigChange - 配置变化回调
|
||||||
|
*/
|
||||||
|
interface Props {
|
||||||
|
node: Node | null;
|
||||||
|
onConfigChange: (nodeId: string, config: any) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function NodeConfigPanel({ node, onConfigChange }: Props) {
|
||||||
|
// 1. hooks 放在最前面
|
||||||
|
const [form] = Form.useForm();
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
|
// 2. useEffect
|
||||||
|
useEffect(() => {
|
||||||
|
if (node) {
|
||||||
|
form.setFieldsValue(node.data.config);
|
||||||
|
}
|
||||||
|
}, [node, form]);
|
||||||
|
|
||||||
|
// 3. 事件处理函数(使用 useCallback 优化)
|
||||||
|
const handleValuesChange = useCallback(
|
||||||
|
(changedValues: any, allValues: any) => {
|
||||||
|
if (node) {
|
||||||
|
onConfigChange(node.id, allValues);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[node, onConfigChange]
|
||||||
|
);
|
||||||
|
|
||||||
|
// 4. 条件渲染
|
||||||
|
if (!node) {
|
||||||
|
return <Empty description="请选择一个节点" />;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. 主要渲染
|
||||||
|
return (
|
||||||
|
<div className="node-config-panel">
|
||||||
|
<Form
|
||||||
|
form={form}
|
||||||
|
onValuesChange={handleValuesChange}
|
||||||
|
>
|
||||||
|
{/* ... */}
|
||||||
|
</Form>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**React 最佳实践**:
|
||||||
|
```tsx
|
||||||
|
// ✅ 使用 memo 优化性能
|
||||||
|
const CustomNode = memo(({ data }: Props) => {
|
||||||
|
return <div>{data.name}</div>;
|
||||||
|
});
|
||||||
|
|
||||||
|
// ✅ 使用 useMemo 缓存计算结果
|
||||||
|
const upstreamNodes = useMemo(() => {
|
||||||
|
return edges
|
||||||
|
.filter(edge => edge.target === node.id)
|
||||||
|
.map(edge => nodes.find(n => n.id === edge.source));
|
||||||
|
}, [node.id, nodes, edges]);
|
||||||
|
|
||||||
|
// ✅ 使用 useCallback 缓存函数
|
||||||
|
const handleClick = useCallback(() => {
|
||||||
|
console.log(node.id);
|
||||||
|
}, [node.id]);
|
||||||
|
|
||||||
|
// ❌ 在渲染中创建新对象/函数(每次重新渲染)
|
||||||
|
return (
|
||||||
|
<div onClick={() => console.log(node.id)}> {/* 每次都创建新函数 */}
|
||||||
|
<Component style={{ color: 'red' }} /> {/* 每次都创建新对象 */}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 二、Git 工作流
|
||||||
|
|
||||||
|
### 2.1 分支策略
|
||||||
|
|
||||||
|
```
|
||||||
|
main/master # 生产环境(只能通过PR合并)
|
||||||
|
↑
|
||||||
|
develop # 开发环境(默认分支)
|
||||||
|
↑
|
||||||
|
feature/xxx # 功能分支
|
||||||
|
↑
|
||||||
|
hotfix/xxx # 紧急修复分支
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.2 分支命名规范
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 功能分支
|
||||||
|
feature/workflow-editor # 新功能
|
||||||
|
feature/node-http-request # 新节点类型
|
||||||
|
|
||||||
|
# 修复分支
|
||||||
|
bugfix/expression-parser-null # Bug修复
|
||||||
|
|
||||||
|
# 紧急修复
|
||||||
|
hotfix/security-vulnerability # 安全漏洞
|
||||||
|
|
||||||
|
# 重构
|
||||||
|
refactor/simplify-converter # 重构
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.3 Commit 规范
|
||||||
|
|
||||||
|
**格式**:`<type>(<scope>): <subject>`
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# ✅ 正确示例
|
||||||
|
feat(editor): 添加字段映射选择器
|
||||||
|
fix(expression): 修复表达式解析空指针异常
|
||||||
|
docs(readme): 更新部署文档
|
||||||
|
style(format): 格式化代码
|
||||||
|
refactor(converter): 简化BPMN转换逻辑
|
||||||
|
perf(cache): 优化表达式缓存策略
|
||||||
|
test(service): 添加工作流服务单元测试
|
||||||
|
chore(deps): 升级Spring Boot到3.2.1
|
||||||
|
|
||||||
|
# ❌ 错误示例
|
||||||
|
update code # 太模糊
|
||||||
|
修复bug # 用中文
|
||||||
|
fix: fixed a bug # 重复
|
||||||
|
```
|
||||||
|
|
||||||
|
**Type 类型**:
|
||||||
|
- `feat`: 新功能
|
||||||
|
- `fix`: Bug修复
|
||||||
|
- `docs`: 文档更新
|
||||||
|
- `style`: 代码格式(不影响代码运行)
|
||||||
|
- `refactor`: 重构
|
||||||
|
- `perf`: 性能优化
|
||||||
|
- `test`: 测试
|
||||||
|
- `chore`: 构建/工具链
|
||||||
|
|
||||||
|
### 2.4 PR (Pull Request) 规范
|
||||||
|
|
||||||
|
**标题**:
|
||||||
|
```
|
||||||
|
feat(editor): 实现字段映射选择器
|
||||||
|
fix(#123): 修复表达式解析bug
|
||||||
|
```
|
||||||
|
|
||||||
|
**描述模板**:
|
||||||
|
```markdown
|
||||||
|
## 变更内容
|
||||||
|
简要描述本次PR的变更内容
|
||||||
|
|
||||||
|
## 变更类型
|
||||||
|
- [ ] 新功能
|
||||||
|
- [x] Bug修复
|
||||||
|
- [ ] 文档更新
|
||||||
|
- [ ] 重构
|
||||||
|
- [ ] 其他
|
||||||
|
|
||||||
|
## 相关Issue
|
||||||
|
Closes #123
|
||||||
|
|
||||||
|
## 测试
|
||||||
|
- [x] 单元测试已通过
|
||||||
|
- [x] 手动测试已完成
|
||||||
|
- [ ] 需要补充测试
|
||||||
|
|
||||||
|
## 截图(如果有UI变化)
|
||||||
|

|
||||||
|

|
||||||
|
|
||||||
|
## Checklist
|
||||||
|
- [x] 代码已自测
|
||||||
|
- [x] 已添加/更新测试
|
||||||
|
- [x] 已更新文档
|
||||||
|
- [x] 代码符合规范
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 三、测试规范
|
||||||
|
|
||||||
|
### 3.1 单元测试
|
||||||
|
|
||||||
|
**覆盖率要求**:
|
||||||
|
- 核心业务逻辑:>80%
|
||||||
|
- 工具类:>90%
|
||||||
|
- Controller:>60%
|
||||||
|
|
||||||
|
**命名规范**:
|
||||||
|
```java
|
||||||
|
// 测试类:ClassName + Test
|
||||||
|
public class WorkflowServiceTest {}
|
||||||
|
|
||||||
|
// 测试方法:should_ExpectedBehavior_When_Condition
|
||||||
|
@Test
|
||||||
|
public void should_ThrowException_When_WorkflowNotFound() {}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void should_ReturnSuccess_When_WorkflowExecuted() {}
|
||||||
|
```
|
||||||
|
|
||||||
|
**示例**:
|
||||||
|
```java
|
||||||
|
@SpringBootTest
|
||||||
|
public class ExpressionEngineTest {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private ExpressionEngine expressionEngine;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private DelegateExecution execution;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
public void setUp() {
|
||||||
|
// 准备测试数据
|
||||||
|
Map<String, Object> nodes = Map.of(
|
||||||
|
"node1", Map.of(
|
||||||
|
"output", Map.of("email", "test@example.com")
|
||||||
|
)
|
||||||
|
);
|
||||||
|
when(execution.getVariable("nodes")).thenReturn(nodes);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void should_ResolveExpression_When_ValidExpression() {
|
||||||
|
// Given
|
||||||
|
String expression = "${nodes.node1.output.email}";
|
||||||
|
|
||||||
|
// When
|
||||||
|
String result = (String) expressionEngine.evaluate(expression, execution);
|
||||||
|
|
||||||
|
// Then
|
||||||
|
assertEquals("test@example.com", result);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void should_ThrowException_When_InvalidExpression() {
|
||||||
|
// Given
|
||||||
|
String expression = "${invalid.path}";
|
||||||
|
|
||||||
|
// When & Then
|
||||||
|
assertThrows(ExpressionEvaluationException.class, () -> {
|
||||||
|
expressionEngine.evaluate(expression, execution);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.2 集成测试
|
||||||
|
|
||||||
|
```java
|
||||||
|
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
|
||||||
|
@AutoConfigureMockMvc
|
||||||
|
public class WorkflowControllerIntegrationTest {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private MockMvc mockMvc;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private ObjectMapper objectMapper;
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void should_CreateWorkflow_When_ValidRequest() throws Exception {
|
||||||
|
// Given
|
||||||
|
WorkflowDefinition workflow = WorkflowDefinition.builder()
|
||||||
|
.name("Test Workflow")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
// When & Then
|
||||||
|
mockMvc.perform(post("/api/workflows")
|
||||||
|
.contentType(MediaType.APPLICATION_JSON)
|
||||||
|
.content(objectMapper.writeValueAsString(workflow)))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(jsonPath("$.id").exists())
|
||||||
|
.andExpect(jsonPath("$.name").value("Test Workflow"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.3 前端测试
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { render, screen, fireEvent } from '@testing-library/react';
|
||||||
|
import FieldMappingSelector from './FieldMappingSelector';
|
||||||
|
|
||||||
|
describe('FieldMappingSelector', () => {
|
||||||
|
it('应该显示上游节点的字段', () => {
|
||||||
|
const upstreamNodes = [
|
||||||
|
{
|
||||||
|
id: 'node1',
|
||||||
|
data: { type: 'http_request', name: 'HTTP' },
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
render(
|
||||||
|
<FieldMappingSelector
|
||||||
|
upstreamNodes={upstreamNodes}
|
||||||
|
nodeTypes={mockNodeTypes}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(screen.getByText('HTTP')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('应该生成正确的表达式', () => {
|
||||||
|
const onChange = jest.fn();
|
||||||
|
|
||||||
|
render(
|
||||||
|
<FieldMappingSelector
|
||||||
|
upstreamNodes={upstreamNodes}
|
||||||
|
onChange={onChange}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
// 选择字段
|
||||||
|
fireEvent.change(screen.getByRole('combobox'), {
|
||||||
|
target: { value: 'nodes.node1.output.email' },
|
||||||
|
});
|
||||||
|
|
||||||
|
// 验证
|
||||||
|
expect(onChange).toHaveBeenCalledWith('${nodes.node1.output.email}');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 四、代码审查(Code Review)
|
||||||
|
|
||||||
|
### 4.1 审查清单
|
||||||
|
|
||||||
|
**功能性**:
|
||||||
|
- [ ] 代码实现了需求
|
||||||
|
- [ ] 边界情况已处理
|
||||||
|
- [ ] 错误处理完善
|
||||||
|
|
||||||
|
**代码质量**:
|
||||||
|
- [ ] 命名清晰易懂
|
||||||
|
- [ ] 逻辑简洁
|
||||||
|
- [ ] 没有重复代码
|
||||||
|
- [ ] 注释充分
|
||||||
|
|
||||||
|
**性能**:
|
||||||
|
- [ ] 没有明显的性能问题
|
||||||
|
- [ ] 数据库查询优化
|
||||||
|
- [ ] 缓存使用合理
|
||||||
|
|
||||||
|
**安全**:
|
||||||
|
- [ ] 输入验证
|
||||||
|
- [ ] SQL注入防护
|
||||||
|
- [ ] XSS防护
|
||||||
|
|
||||||
|
**测试**:
|
||||||
|
- [ ] 有单元测试
|
||||||
|
- [ ] 测试覆盖核心逻辑
|
||||||
|
- [ ] 测试通过
|
||||||
|
|
||||||
|
### 4.2 审查注释规范
|
||||||
|
|
||||||
|
```java
|
||||||
|
// ✅ 建设性建议
|
||||||
|
// 建议:这里可以使用Optional避免空指针
|
||||||
|
if (user != null) {
|
||||||
|
return user.getName();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 可以改为:
|
||||||
|
return Optional.ofNullable(user)
|
||||||
|
.map(User::getName)
|
||||||
|
.orElse("Unknown");
|
||||||
|
|
||||||
|
// ✅ 指出问题
|
||||||
|
// 问题:这里有SQL注入风险
|
||||||
|
String sql = "SELECT * FROM users WHERE name = '" + userName + "'";
|
||||||
|
|
||||||
|
// 应该使用:
|
||||||
|
String sql = "SELECT * FROM users WHERE name = ?";
|
||||||
|
|
||||||
|
// ❌ 不要这样
|
||||||
|
// 这代码写得太烂了
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 五、文档规范
|
||||||
|
|
||||||
|
### 5.1 README.md
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
# 工作流平台
|
||||||
|
|
||||||
|
## 快速开始
|
||||||
|
|
||||||
|
### 环境要求
|
||||||
|
- JDK 17+
|
||||||
|
- Node.js 18+
|
||||||
|
- PostgreSQL 15+
|
||||||
|
- Redis 7+
|
||||||
|
|
||||||
|
### 本地开发
|
||||||
|
|
||||||
|
1. 克隆代码
|
||||||
|
\`\`\`bash
|
||||||
|
git clone https://github.com/yourorg/workflow-platform.git
|
||||||
|
cd workflow-platform
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
2. 启动后端
|
||||||
|
\`\`\`bash
|
||||||
|
cd backend
|
||||||
|
./mvnw spring-boot:run
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
3. 启动前端
|
||||||
|
\`\`\`bash
|
||||||
|
cd frontend
|
||||||
|
npm install
|
||||||
|
npm run dev
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
4. 访问
|
||||||
|
http://localhost:3000
|
||||||
|
|
||||||
|
## 项目结构
|
||||||
|
...
|
||||||
|
|
||||||
|
## 开发文档
|
||||||
|
- [架构总览](docs/01-架构总览.md)
|
||||||
|
- [后端技术设计](docs/02-后端技术设计.md)
|
||||||
|
- [前端技术设计](docs/03-前端技术设计.md)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.2 API 文档
|
||||||
|
|
||||||
|
使用 Swagger/OpenAPI:
|
||||||
|
|
||||||
|
```java
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/workflows")
|
||||||
|
@Tag(name = "工作流管理", description = "工作流的创建、更新、删除、执行")
|
||||||
|
public class WorkflowController {
|
||||||
|
|
||||||
|
@PostMapping
|
||||||
|
@Operation(summary = "创建工作流", description = "创建一个新的工作流定义")
|
||||||
|
@ApiResponse(responseCode = "200", description = "创建成功")
|
||||||
|
@ApiResponse(responseCode = "400", description = "参数错误")
|
||||||
|
public ResponseEntity<WorkflowDefinition> createWorkflow(
|
||||||
|
@RequestBody
|
||||||
|
@io.swagger.v3.oas.annotations.parameters.RequestBody(
|
||||||
|
description = "工作流定义",
|
||||||
|
required = true
|
||||||
|
)
|
||||||
|
WorkflowDefinition workflow
|
||||||
|
) {
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
访问:http://localhost:8080/swagger-ui.html
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 六、部署规范
|
||||||
|
|
||||||
|
### 6.1 环境配置
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# .env.example(提交到代码库)
|
||||||
|
SPRING_DATASOURCE_URL=jdbc:postgresql://localhost:5432/workflow_db
|
||||||
|
SPRING_DATASOURCE_USERNAME=postgres
|
||||||
|
SPRING_DATASOURCE_PASSWORD=
|
||||||
|
|
||||||
|
REDIS_HOST=localhost
|
||||||
|
REDIS_PORT=6379
|
||||||
|
|
||||||
|
SMTP_HOST=smtp.example.com
|
||||||
|
SMTP_PASSWORD=
|
||||||
|
|
||||||
|
# .env(不提交,本地使用)
|
||||||
|
SPRING_DATASOURCE_PASSWORD=your_password
|
||||||
|
SMTP_PASSWORD=your_smtp_password
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6.2 版本号
|
||||||
|
|
||||||
|
使用语义化版本:`MAJOR.MINOR.PATCH`
|
||||||
|
|
||||||
|
```
|
||||||
|
1.0.0 - 第一个正式版本
|
||||||
|
1.1.0 - 新增功能(向后兼容)
|
||||||
|
1.1.1 - Bug修复
|
||||||
|
2.0.0 - 重大变更(不向后兼容)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6.3 发布流程
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. 更新版本号
|
||||||
|
# pom.xml: <version>1.1.0</version>
|
||||||
|
# package.json: "version": "1.1.0"
|
||||||
|
|
||||||
|
# 2. 更新 CHANGELOG.md
|
||||||
|
git add CHANGELOG.md
|
||||||
|
git commit -m "docs: 更新版本到 1.1.0"
|
||||||
|
|
||||||
|
# 3. 打标签
|
||||||
|
git tag -a v1.1.0 -m "Release v1.1.0"
|
||||||
|
git push origin v1.1.0
|
||||||
|
|
||||||
|
# 4. 构建
|
||||||
|
docker build -t workflow-platform:1.1.0 .
|
||||||
|
|
||||||
|
# 5. 部署
|
||||||
|
kubectl apply -f k8s/deployment.yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 七、故障排查
|
||||||
|
|
||||||
|
### 7.1 日志级别配置
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# application-dev.yml
|
||||||
|
logging:
|
||||||
|
level:
|
||||||
|
root: INFO
|
||||||
|
com.workflow: DEBUG
|
||||||
|
org.flowable: DEBUG
|
||||||
|
|
||||||
|
# application-prod.yml
|
||||||
|
logging:
|
||||||
|
level:
|
||||||
|
root: WARN
|
||||||
|
com.workflow: INFO
|
||||||
|
org.flowable: WARN
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7.2 常见问题
|
||||||
|
|
||||||
|
**问题1:表达式解析失败**
|
||||||
|
```
|
||||||
|
错误:ExpressionEvaluationException: 表达式解析失败
|
||||||
|
原因:上游节点输出结构与预期不符
|
||||||
|
解决:检查 outputSchema 定义,查看实际输出
|
||||||
|
```
|
||||||
|
|
||||||
|
**问题2:工作流执行卡住**
|
||||||
|
```
|
||||||
|
错误:流程实例一直处于 running 状态
|
||||||
|
原因:某个节点执行超时或死锁
|
||||||
|
解决:查询 act_ru_execution 表,找到卡住的节点
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**总结**:以上规范是团队协作的基础,请严格遵守。如有疑问,请在团队会议上讨论。
|
||||||
|
|
||||||
97
backend/docs/99-最终修正落地方案.md
Normal file
97
backend/docs/99-最终修正落地方案.md
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
# 最终修正落地方案(PM + 架构联合稿)
|
||||||
|
|
||||||
|
版本: v1.0
|
||||||
|
适用范围: 替代/修正 docs/01、docs/02、docs/03 中的关键设计,统一数据库为 MySQL 8.0,并固化 MVP 可落地路径。
|
||||||
|
|
||||||
|
—
|
||||||
|
|
||||||
|
一、产品与范围(MVP)
|
||||||
|
- 目标:可视化工作流平台,支持编辑器(ReactFlow)、节点配置(动态表单/字段映射/表达式)、执行、审批。
|
||||||
|
- 明确不做(MVP):并行/子流程、定时/Webhook 触发、版本管理、多租户、复杂鉴权/监控、插件市场、AI 辅助。
|
||||||
|
- 闭环路径:前端构建 JSON → 后端转 BPMN(含条件分支)部署到 Flowable → 同步执行(审批点暂停并恢复)→ 执行/日志/任务可查询。
|
||||||
|
|
||||||
|
二、关键技术决策(统一口径)
|
||||||
|
- 数据库:MySQL 8.0(JSON 类型用于 definition/fields/output_schema/input/output 等),字符集 utf8mb4。
|
||||||
|
- 表达式:仅采用 Jakarta EL (JUEL);Map-only 访问,禁用类/方法反射调用;前端不直接执行表达式,仅做占位/静态提示(第二期可加后端沙箱预览)。
|
||||||
|
- 执行:MVP 同步执行;Flowable 全局异步执行器关闭(async-executor-activate=false);审批链路天然异步(等待用户完成任务)。
|
||||||
|
- 条件分支:JSON 模型以 edge.condition(JUEL 表达式)表示;转换层生成 ExclusiveGateway + 条件 SequenceFlow。
|
||||||
|
- 节点扩展:Spring 插件化(WorkflowNode 接口 + NodeTypeMetadata + outputSchema),NodeTypeRegistry 负责注册/查询。
|
||||||
|
- HTTP 客户端:WebClient(Spring WebFlux),统一超时与重试策略;不混用 MVC + WebFlux。
|
||||||
|
|
||||||
|
三、架构总览(修正摘要)
|
||||||
|
- 后端:Spring Boot 3.2 + Flowable 7.0.1 + MySQL 8.0 + Redis 7 + Jakarta EL;REST API(/api/workflows、/api/node-types、/api/tasks)。
|
||||||
|
- 前端:React 18 + TypeScript 5 + ReactFlow 11 + AntD 5 + Zustand + Axios;Vite 构建。
|
||||||
|
- 部署:Docker Compose(mysql:8、redis:7、backend),Nginx 反向代理(生产)。
|
||||||
|
|
||||||
|
四、后端落地清单
|
||||||
|
1) 依赖与配置
|
||||||
|
- POM 仅保留 spring-boot-starter-webflux;移除 spring-boot-starter-web;添加 jakarta.el 依赖;数据库驱动改为 mysql-connector-j。
|
||||||
|
- application.yml:
|
||||||
|
- datasource.url=jdbc:mysql://.../workflow_db?useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=UTC
|
||||||
|
- driver=com.mysql.cj.jdbc.Driver
|
||||||
|
- flowable.async-executor-activate=false(MVP 同步)
|
||||||
|
|
||||||
|
2) DDL(MySQL 8)
|
||||||
|
- 将所有 JSONB 改为 JSON;去除 ::jsonb 强制类型转换;示例插入用合法 JSON 字符串。
|
||||||
|
- 关键表:
|
||||||
|
- workflow_definitions(definition JSON, flowable_* 索引列保留)
|
||||||
|
- node_types(fields JSON, output_schema JSON)
|
||||||
|
- workflow_executions(input JSON)
|
||||||
|
- node_execution_logs(input JSON, output JSON)
|
||||||
|
|
||||||
|
3) Flowable 集成修正
|
||||||
|
- ServiceTask 使用 delegateExpression("${genericNodeExecutor}"),由 Spring 托管,保证 @Autowired 生效。
|
||||||
|
- GenericNodeExecutor 从 execution.getCurrentFlowElement() 的 FieldExtension 读取 nodeType/nodeConfig,不再误用 execution 变量。
|
||||||
|
- WorkflowConverter:
|
||||||
|
- 添加 ExclusiveGateway 与条件 SequenceFlow;对 edge.condition 设置 conditionExpression。
|
||||||
|
- 保留 start→first 与 last→end 的连线(无条件)。
|
||||||
|
|
||||||
|
4) 表达式引擎(JUEL - Jakarta EL)
|
||||||
|
- import jakarta.el.*;构建 StandardELContext;注入 nodes/workflow/env 三大命名空间。
|
||||||
|
- 缓存表达式编译结果(LRU),无 ${} 的字符串走快路径直接返回。
|
||||||
|
- 安全:黑名单不足以防御,辅以受限 ELResolver(仅 Map/property 解析),禁用方法调用。
|
||||||
|
|
||||||
|
5) HTTP 节点
|
||||||
|
- 实现基于 WebClient:连接/响应超时;可选重试(限定幂等方法)。
|
||||||
|
- 输入:url/method/headers/body/timeout;输出:statusCode/body/headers/elapsed。
|
||||||
|
|
||||||
|
6) 观察性
|
||||||
|
- 写入 node_execution_logs:status、input、output、错误、时长;为 execution_id、status、时间建立索引。
|
||||||
|
- 审批与执行历史接口完成:执行详情包含各节点输入/输出摘要(注意脱敏)。
|
||||||
|
|
||||||
|
五、前端落地清单
|
||||||
|
- 字段映射:基于上游节点的 outputSchema 构建树;表达式值统一存储为“完整 ${...}”字符串。
|
||||||
|
- 表达式括号处理修复:使用正则 /^\$\{|\}$/g 去括号;选择时拼接为 `\${${val}}`。
|
||||||
|
- Zustand:统一管理 nodes/edges/selectedNode/currentWorkflow;保存时构建规范化 JSON。
|
||||||
|
- 性能:节点组件 memo、事件节流;避免使用不存在的 ReactFlow 属性。
|
||||||
|
|
||||||
|
六、API 与契约(MVP 必备)
|
||||||
|
- /api/workflows:create/update/get/list/delete/execute、getExecutions、getExecutionDetail。
|
||||||
|
- /api/node-types:list/get/by-category(返回 NodeTypeMetadata,含 fields/outputSchema)。
|
||||||
|
- /api/tasks:list/detail/complete/form(审批流)。
|
||||||
|
- 错误码:4xx(校验/鉴权)、5xx(系统错误);分页与筛选采用标准 query 参数。
|
||||||
|
|
||||||
|
七、验收标准(MVP)
|
||||||
|
- 功能:
|
||||||
|
- 场景 A:HTTP → 审批 → 邮件,端到端成功;审批暂停与恢复链路可见。
|
||||||
|
- 场景 B:条件分支生效(200→邮件,否则→另一分支)。
|
||||||
|
- 性能:10 节点链路 < 500ms(不含审批),表达式评估基准 > 10k QPS,画布 100+ 节点不卡顿。
|
||||||
|
- 质量:核心模块单测覆盖 > 60%,E2E 场景跑通;关键事件与错误可观测。
|
||||||
|
|
||||||
|
八、两周 PoC 计划
|
||||||
|
- Week 1:
|
||||||
|
- 完成 JSON→BPMN(ServiceTask + UserTask + ExclusiveGateway + 条件边)
|
||||||
|
- JUEL 上下文与缓存;HTTP 节点(WebClient)
|
||||||
|
- NodeTypeRegistry 注册/查询;node_types/definitions 基础表
|
||||||
|
- Week 2:
|
||||||
|
- 审批任务 API(列表/表单/完成);执行历史与节点日志入库
|
||||||
|
- 前端编辑器/配置面板/字段映射;审批中心
|
||||||
|
- 样例工作流 A/B 端到端自测并出基准数据
|
||||||
|
|
||||||
|
九、风险与缓解
|
||||||
|
- 转换层复杂:以最小节点集(HTTP/条件/变量/邮件/审批)完成 PoC,随后逐步扩展。
|
||||||
|
- 表达式安全:Map-only ELResolver + 严格长度/关键字校验;第二期引入后端沙箱预览接口。
|
||||||
|
- MySQL JSON 性能:关键查询加合适索引;大 JSON 字段只作存储与读取,不进行复杂查询。
|
||||||
|
|
||||||
|
十、后续演进(非 MVP)
|
||||||
|
- 异步执行(队列/线程池/回调)、定时/Webhook 触发、循环/并行/子流程、工作流版本管理、更多节点库、监控大盘、权限与多租户、导入导出、AI 辅助。
|
||||||
386
backend/docs/README.md
Normal file
386
backend/docs/README.md
Normal file
@ -0,0 +1,386 @@
|
|||||||
|
# 工作流平台技术文档
|
||||||
|
|
||||||
|
**项目**: 基于 Flowable 的可视化工作流平台
|
||||||
|
**版本**: v1.0
|
||||||
|
**更新日期**: 2025-01-12
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📚 文档目录
|
||||||
|
|
||||||
|
### 核心设计文档
|
||||||
|
|
||||||
|
| 文档 | 说明 | 关键内容 |
|
||||||
|
|------|------|----------|
|
||||||
|
| [01-架构总览](./01-架构总览.md) | 系统整体架构设计 | 技术选型、系统架构、MVP范围、开发计划 |
|
||||||
|
| [02-后端技术设计](./02-后端技术设计.md) | 后端详细实现 | 节点注册、表达式引擎、BPMN转换、REST API |
|
||||||
|
| [03-前端技术设计](./03-前端技术设计.md) | 前端详细实现 | ReactFlow画布、字段映射选择器、状态管理 |
|
||||||
|
| [04-数据模型设计](./04-数据模型设计.md) | 数据库设计 | 业务表结构、Flowable表说明、索引优化 |
|
||||||
|
| [05-开发规范](./05-开发规范.md) | 代码和协作规范 | 命名规范、Git工作流、测试要求 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 快速导航
|
||||||
|
|
||||||
|
### 我想了解...
|
||||||
|
|
||||||
|
**整体架构**
|
||||||
|
- 👉 先看 [01-架构总览](./01-架构总览.md)
|
||||||
|
- 了解技术选型、系统架构、核心数据流
|
||||||
|
|
||||||
|
**后端开发**
|
||||||
|
- 👉 看 [02-后端技术设计](./02-后端技术设计.md)
|
||||||
|
- 节点如何注册?表达式如何解析?JSON如何转BPMN?
|
||||||
|
|
||||||
|
**前端开发**
|
||||||
|
- 👉 看 [03-前端技术设计](./03-前端技术设计.md)
|
||||||
|
- ReactFlow如何使用?字段映射选择器如何实现?
|
||||||
|
|
||||||
|
**数据库设计**
|
||||||
|
- 👉 看 [04-数据模型设计](./04-数据模型设计.md)
|
||||||
|
- 有哪些表?为什么用JSONB?如何优化查询?
|
||||||
|
|
||||||
|
**编码规范**
|
||||||
|
- 👉 看 [05-开发规范](./05-开发规范.md)
|
||||||
|
- 如何命名?如何提交代码?如何写测试?
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔑 核心设计要点
|
||||||
|
|
||||||
|
### 1. 为什么选择 Flowable?
|
||||||
|
|
||||||
|
```
|
||||||
|
✅ 开源版功能完整(不需要购买企业版)
|
||||||
|
✅ 内置审批能力(User Task)
|
||||||
|
✅ Spring Boot 集成简单
|
||||||
|
✅ 国内资料多,社区活跃
|
||||||
|
✅ 支持 BPMN 2.0 标准
|
||||||
|
|
||||||
|
vs Camunda:
|
||||||
|
- Flowable 表单引擎更强
|
||||||
|
- Flowable 开源版更完整
|
||||||
|
- Camunda 性能略优但差距不大
|
||||||
|
|
||||||
|
vs Conductor:
|
||||||
|
- Conductor 没有审批能力
|
||||||
|
- Conductor 更轻量但功能少
|
||||||
|
- 如果不需要审批,Conductor 是更好选择
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 核心技术难点与解决方案
|
||||||
|
|
||||||
|
#### 难点1:前端如何知道上游节点输出了什么?
|
||||||
|
|
||||||
|
**解决方案**:使用静态 `outputSchema`(JSON Schema)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 每个节点类型定义输出结构
|
||||||
|
const httpRequestMetadata = {
|
||||||
|
id: 'http_request',
|
||||||
|
outputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
statusCode: { type: 'number' },
|
||||||
|
body: { type: 'object' },
|
||||||
|
headers: { type: 'object' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// 前端根据 schema 构建字段树
|
||||||
|
// 用户可以选择: nodes.httpRequest.output.body.email
|
||||||
|
```
|
||||||
|
|
||||||
|
**优点**:
|
||||||
|
- ✅ 快速,不需要执行节点
|
||||||
|
- ✅ 类型安全
|
||||||
|
- ✅ 支持自动补全
|
||||||
|
|
||||||
|
**缺点**:
|
||||||
|
- ⚠️ 如果实际输出与 schema 不符,运行时才会发现
|
||||||
|
|
||||||
|
#### 难点2:如何实现字段映射选择器?
|
||||||
|
|
||||||
|
**核心组件**:`FieldMappingSelector.tsx`
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
// 1. 计算上游节点
|
||||||
|
const upstreamNodes = edges
|
||||||
|
.filter(edge => edge.target === currentNode.id)
|
||||||
|
.map(edge => nodes.find(n => n.id === edge.source));
|
||||||
|
|
||||||
|
// 2. 根据 outputSchema 构建字段树
|
||||||
|
const fieldTree = upstreamNodes.map(node => ({
|
||||||
|
title: node.data.name,
|
||||||
|
children: buildFieldTree(nodeType.outputSchema.properties, `nodes.${node.id}.output`)
|
||||||
|
}));
|
||||||
|
|
||||||
|
// 3. 用户选择字段,生成表达式
|
||||||
|
// 用户选择: nodes.httpRequest.output.body.email
|
||||||
|
// 生成表达式: ${nodes.httpRequest.output.body.email}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 难点3:表达式解析性能
|
||||||
|
|
||||||
|
**解决方案**:
|
||||||
|
```java
|
||||||
|
// 1. 使用 JUEL 而不是完整的 JavaScript(性能更好)
|
||||||
|
// 2. 表达式编译结果缓存
|
||||||
|
private final Map<String, ValueExpression> expressionCache = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
// 3. 快速路径:无表达式直接返回
|
||||||
|
if (!expression.contains("${")) {
|
||||||
|
return expression;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**性能测试结果**:
|
||||||
|
- JUEL: ~50000 QPS
|
||||||
|
- GraalVM JS: ~2000 QPS
|
||||||
|
- 结论:使用 JUEL,性能足够
|
||||||
|
|
||||||
|
#### 难点4:工作流定义格式
|
||||||
|
|
||||||
|
**决策**:用户层面使用 JSON,内部转换为 BPMN XML
|
||||||
|
|
||||||
|
```
|
||||||
|
前端 (JSON) ←→ 后端转换层 ←→ Flowable (BPMN XML)
|
||||||
|
|
||||||
|
理由:
|
||||||
|
✅ JSON 对前端友好
|
||||||
|
✅ JSON 易于版本控制
|
||||||
|
✅ BPMN 是 Flowable 原生格式
|
||||||
|
✅ 分层清晰,职责明确
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📊 技术架构图
|
||||||
|
|
||||||
|
### 整体架构
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────┐
|
||||||
|
│ 前端 (React + ReactFlow) │
|
||||||
|
│ - 可视化编辑器 │
|
||||||
|
│ - 节点配置面板 │
|
||||||
|
│ - 字段映射选择器 ⭐⭐⭐ │
|
||||||
|
└──────────────┬──────────────────────────┘
|
||||||
|
│ REST API
|
||||||
|
┌──────────────▼──────────────────────────┐
|
||||||
|
│ Spring Boot 应用 │
|
||||||
|
│ ┌────────────────────────────────────┐ │
|
||||||
|
│ │ REST API 层 │ │
|
||||||
|
│ └────────────────────────────────────┘ │
|
||||||
|
│ ┌────────────────────────────────────┐ │
|
||||||
|
│ │ 业务逻辑层 │ │
|
||||||
|
│ │ - NodeTypeRegistry (节点注册) │ │
|
||||||
|
│ │ - ExpressionEngine (表达式解析)⭐ │ │
|
||||||
|
│ │ - WorkflowConverter (JSON→BPMN)⭐ │ │
|
||||||
|
│ └────────────────────────────────────┘ │
|
||||||
|
│ ┌────────────────────────────────────┐ │
|
||||||
|
│ │ Flowable Engine │ │
|
||||||
|
│ │ - 流程执行 │ │
|
||||||
|
│ │ - 任务管理 │ │
|
||||||
|
│ │ - 历史记录 │ │
|
||||||
|
│ └────────────────────────────────────┘ │
|
||||||
|
└──────────────┬──────────────────────────┘
|
||||||
|
│
|
||||||
|
┌──────────────▼──────────────────────────┐
|
||||||
|
│ PostgreSQL │
|
||||||
|
│ - 业务表 (workflow_definitions等) │
|
||||||
|
│ - Flowable表 (ACT_*) │
|
||||||
|
└─────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### 核心数据流:工作流执行
|
||||||
|
|
||||||
|
```
|
||||||
|
1. 用户点击"执行"
|
||||||
|
↓
|
||||||
|
2. 前端调用 POST /api/workflows/{id}/execute
|
||||||
|
↓
|
||||||
|
3. 后端初始化执行上下文:
|
||||||
|
{
|
||||||
|
"workflow": { "input": {...} },
|
||||||
|
"nodes": {}, // 节点输出将保存在这里
|
||||||
|
"env": {...}
|
||||||
|
}
|
||||||
|
↓
|
||||||
|
4. Flowable 启动流程实例
|
||||||
|
↓
|
||||||
|
5. 按拓扑顺序执行节点:
|
||||||
|
a. ExpressionEngine 解析表达式
|
||||||
|
b. 调用节点实现类执行
|
||||||
|
c. 保存输出到 nodes.{nodeId}.output
|
||||||
|
↓
|
||||||
|
6. 节点间数据通过表达式传递:
|
||||||
|
${nodes.node1.output.body.email}
|
||||||
|
↓
|
||||||
|
7. 遇到 User Task(审批)时暂停
|
||||||
|
↓
|
||||||
|
8. 审批完成后继续执行
|
||||||
|
↓
|
||||||
|
9. 流程结束
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 MVP 功能清单(第一期)
|
||||||
|
|
||||||
|
### 必须有的功能
|
||||||
|
|
||||||
|
**1. 工作流编辑器**
|
||||||
|
- [x] 从左侧拖拽节点到画布
|
||||||
|
- [x] 节点之间连线
|
||||||
|
- [x] 删除节点和连线
|
||||||
|
- [x] 保存工作流
|
||||||
|
|
||||||
|
**2. 节点配置面板**
|
||||||
|
- [x] 动态表单(根据节点类型生成)
|
||||||
|
- [x] 字段映射选择器(TreeSelect 展示上游节点输出)⭐⭐⭐
|
||||||
|
- [x] 表达式输入框
|
||||||
|
|
||||||
|
**3. 节点类型(5种)**
|
||||||
|
- [x] HTTP Request
|
||||||
|
- [x] Send Email
|
||||||
|
- [x] Set Variable
|
||||||
|
- [x] Condition (IF/ELSE)
|
||||||
|
- [x] Approval (审批)
|
||||||
|
|
||||||
|
**4. 工作流执行**
|
||||||
|
- [x] 手动触发执行
|
||||||
|
- [x] 查看执行日志
|
||||||
|
- [x] 查看节点输入/输出
|
||||||
|
|
||||||
|
**5. 审批功能**
|
||||||
|
- [x] 待审批任务列表
|
||||||
|
- [x] 审批表单
|
||||||
|
- [x] 批准/拒绝
|
||||||
|
|
||||||
|
### 不做的功能(第二期)
|
||||||
|
|
||||||
|
- ❌ 定时触发(Cron)
|
||||||
|
- ❌ Webhook 触发
|
||||||
|
- ❌ 循环节点(forEach)
|
||||||
|
- ❌ 并行执行
|
||||||
|
- ❌ 工作流版本管理
|
||||||
|
- ❌ 权限管理(只做基础认证)
|
||||||
|
- ❌ 监控大盘
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📅 开发计划(12周)
|
||||||
|
|
||||||
|
### Phase 1: 技术验证(Week 1-2)
|
||||||
|
- Flowable PoC
|
||||||
|
- 表达式引擎验证
|
||||||
|
- ReactFlow 画布验证
|
||||||
|
- 环境搭建
|
||||||
|
|
||||||
|
### Phase 2: 后端核心(Week 3-4)
|
||||||
|
- 节点类型注册系统
|
||||||
|
- 表达式引擎
|
||||||
|
- JSON → BPMN 转换器
|
||||||
|
- HTTP Request + Set Variable 节点
|
||||||
|
|
||||||
|
### Phase 3: 前端核心(Week 5-6)
|
||||||
|
- ReactFlow 画布
|
||||||
|
- 节点配置面板
|
||||||
|
- **字段映射选择器**(最核心)
|
||||||
|
|
||||||
|
### Phase 4: 执行引擎(Week 7-8)
|
||||||
|
- 工作流执行
|
||||||
|
- 日志记录
|
||||||
|
- 错误处理
|
||||||
|
|
||||||
|
### Phase 5: 审批功能(Week 9-10)
|
||||||
|
- User Task 集成
|
||||||
|
- 审批表单
|
||||||
|
- 任务列表
|
||||||
|
|
||||||
|
### Phase 6: 测试上线(Week 11-12)
|
||||||
|
- 集成测试
|
||||||
|
- 性能测试
|
||||||
|
- 部署上线
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎓 学习资源
|
||||||
|
|
||||||
|
### Flowable
|
||||||
|
- 官方文档: https://flowable.com/open-source/docs/
|
||||||
|
- GitHub: https://github.com/flowable/flowable-engine
|
||||||
|
- 中文教程: https://www.cnblogs.com/catcher1994/tag/Flowable/
|
||||||
|
|
||||||
|
### ReactFlow
|
||||||
|
- 官方文档: https://reactflow.dev/
|
||||||
|
- 示例: https://reactflow.dev/examples
|
||||||
|
|
||||||
|
### 表达式引擎
|
||||||
|
- JUEL 文档: https://juel.sourceforge.net/guide/index.html
|
||||||
|
- GraalVM JS: https://www.graalvm.org/javascript/
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ❓ 常见问题
|
||||||
|
|
||||||
|
### Q1: 为什么不直接使用 N8N?
|
||||||
|
|
||||||
|
**A**: N8N 是 Node.js 技术栈,我们需要 Java 技术栈。另外,我们需要:
|
||||||
|
- 与现有 Java 系统集成
|
||||||
|
- 自定义审批流程
|
||||||
|
- 完全掌控数据和安全
|
||||||
|
|
||||||
|
### Q2: Flowable 学习曲线陡峭吗?
|
||||||
|
|
||||||
|
**A**: 有一定学习曲线,但我们做了封装:
|
||||||
|
- 用户不需要懂 BPMN(我们用 JSON)
|
||||||
|
- 开发者只需要了解基础概念
|
||||||
|
- 提供完整的文档和示例
|
||||||
|
|
||||||
|
### Q3: 性能够吗?
|
||||||
|
|
||||||
|
**A**: 经过验证:
|
||||||
|
- 表达式解析: 50000+ QPS
|
||||||
|
- Flowable: 1000+ TPS
|
||||||
|
- 前端画布: 支持 100+ 节点不卡顿
|
||||||
|
|
||||||
|
### Q4: 如何扩展新的节点类型?
|
||||||
|
|
||||||
|
**A**: 非常简单:
|
||||||
|
1. 创建节点实现类(实现 `WorkflowNode` 接口)
|
||||||
|
2. 添加 `@Component` 注解
|
||||||
|
3. 定义元数据(字段、输出结构)
|
||||||
|
4. Spring 启动时自动注册
|
||||||
|
5. 前端自动显示
|
||||||
|
|
||||||
|
```java
|
||||||
|
@Component
|
||||||
|
public class MyCustomNode implements WorkflowNode {
|
||||||
|
@Override
|
||||||
|
public NodeTypeMetadata getMetadata() {
|
||||||
|
// 定义元数据
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public NodeExecutionResult execute(NodeInput input, NodeExecutionContext context) {
|
||||||
|
// 执行逻辑
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📞 联系方式
|
||||||
|
|
||||||
|
- **技术负责人**: [您的名字]
|
||||||
|
- **Email**: [您的邮箱]
|
||||||
|
- **文档更新**: 如有问题或建议,请提 Issue
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**最后更新**: 2025-01-12
|
||||||
|
**文档版本**: v1.0
|
||||||
|
|
||||||
File diff suppressed because it is too large
Load Diff
@ -15,7 +15,7 @@ import java.util.*;
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 节点类型服务层
|
* 节点类型服务层
|
||||||
*
|
* <p>
|
||||||
* 核心功能:
|
* 核心功能:
|
||||||
* 1. 节点类型的CRUD操作
|
* 1. 节点类型的CRUD操作
|
||||||
* 2. 节点类型元数据管理
|
* 2. 节点类型元数据管理
|
||||||
@ -26,15 +26,15 @@ import java.util.*;
|
|||||||
@Service
|
@Service
|
||||||
@Transactional
|
@Transactional
|
||||||
public class NodeTypeService {
|
public class NodeTypeService {
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private NodeTypeRepository nodeTypeRepository;
|
private NodeTypeRepository nodeTypeRepository;
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private NodeTypeRegistry nodeTypeRegistry;
|
private NodeTypeRegistry nodeTypeRegistry;
|
||||||
|
|
||||||
private final ObjectMapper objectMapper = new ObjectMapper();
|
private final ObjectMapper objectMapper = new ObjectMapper();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取所有节点类型
|
* 获取所有节点类型
|
||||||
*/
|
*/
|
||||||
@ -42,7 +42,7 @@ public class NodeTypeService {
|
|||||||
public List<NodeType> getAllNodeTypes() {
|
public List<NodeType> getAllNodeTypes() {
|
||||||
return nodeTypeRepository.findAllByOrderByDisplayOrderAscIdAsc();
|
return nodeTypeRepository.findAllByOrderByDisplayOrderAscIdAsc();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取指定分类的节点类型
|
* 获取指定分类的节点类型
|
||||||
*/
|
*/
|
||||||
@ -55,67 +55,67 @@ public class NodeTypeService {
|
|||||||
return new ArrayList<>();
|
return new ArrayList<>();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 根据ID获取节点类型
|
* 根据ID获取节点类型
|
||||||
*/
|
*/
|
||||||
@Transactional(readOnly = true)
|
@Transactional(readOnly = true)
|
||||||
public NodeType getNodeType(String nodeTypeId) {
|
public NodeType getNodeType(String nodeTypeId) {
|
||||||
return nodeTypeRepository.findById(nodeTypeId)
|
return nodeTypeRepository.findById(nodeTypeId)
|
||||||
.orElseThrow(() -> new NodeTypeNotFoundException("节点类型不存在: " + nodeTypeId));
|
.orElseThrow(() -> new NodeTypeNotFoundException("节点类型不存在: " + nodeTypeId));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 创建节点类型
|
* 创建节点类型
|
||||||
*/
|
*/
|
||||||
public NodeType createNodeType(NodeType nodeType) {
|
public NodeType createNodeType(NodeType nodeType) {
|
||||||
log.info("创建节点类型: {}", nodeType.getId());
|
log.info("创建节点类型: {}", nodeType.getId());
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 1. 验证ID唯一性
|
// 1. 验证ID唯一性
|
||||||
if (nodeTypeRepository.existsById(nodeType.getId())) {
|
if (nodeTypeRepository.existsById(nodeType.getId())) {
|
||||||
throw new NodeTypeServiceException("节点类型ID已存在: " + nodeType.getId());
|
throw new NodeTypeServiceException("节点类型ID已存在: " + nodeType.getId());
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. 验证必要字段
|
// 2. 验证必要字段
|
||||||
validateNodeType(nodeType);
|
validateNodeType(nodeType);
|
||||||
|
|
||||||
// 3. 设置创建时间
|
// 3. 设置创建时间
|
||||||
nodeType.setCreatedAt(LocalDateTime.now());
|
nodeType.setCreatedAt(LocalDateTime.now());
|
||||||
nodeType.setUpdatedAt(LocalDateTime.now());
|
nodeType.setUpdatedAt(LocalDateTime.now());
|
||||||
|
|
||||||
// 4. 如果没有设置显示顺序,自动分配
|
// 4. 如果没有设置显示顺序,自动分配
|
||||||
if (nodeType.getDisplayOrder() == null) {
|
if (nodeType.getDisplayOrder() == null) {
|
||||||
Integer maxOrder = nodeTypeRepository.findMaxDisplayOrderByCategory(nodeType.getCategory());
|
Integer maxOrder = nodeTypeRepository.findMaxDisplayOrderByCategory(nodeType.getCategory());
|
||||||
nodeType.setDisplayOrder((maxOrder != null ? maxOrder : 0) + 10);
|
nodeType.setDisplayOrder((maxOrder != null ? maxOrder : 0) + 10);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 5. 保存到数据库
|
// 5. 保存到数据库
|
||||||
NodeType saved = nodeTypeRepository.save(nodeType);
|
NodeType saved = nodeTypeRepository.save(nodeType);
|
||||||
|
|
||||||
// 6. 如果节点类型启用,注册到注册表
|
// 6. 如果节点类型启用,注册到注册表
|
||||||
if (nodeType.isEnabled()) {
|
if (nodeType.isEnabled()) {
|
||||||
registerNodeTypeToRegistry(saved);
|
registerNodeTypeToRegistry(saved);
|
||||||
}
|
}
|
||||||
|
|
||||||
log.info("节点类型创建成功: {}", saved.getId());
|
log.info("节点类型创建成功: {}", saved.getId());
|
||||||
return saved;
|
return saved;
|
||||||
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("创建节点类型失败: {}", nodeType.getId(), e);
|
log.error("创建节点类型失败: {}", nodeType.getId(), e);
|
||||||
throw new NodeTypeServiceException("创建节点类型失败: " + e.getMessage(), e);
|
throw new NodeTypeServiceException("创建节点类型失败: " + e.getMessage(), e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 更新节点类型
|
* 更新节点类型
|
||||||
*/
|
*/
|
||||||
public NodeType updateNodeType(String id, NodeType nodeType) {
|
public NodeType updateNodeType(String id, NodeType nodeType) {
|
||||||
log.info("更新节点类型: {}", id);
|
log.info("更新节点类型: {}", id);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
NodeType existing = getNodeType(id);
|
NodeType existing = getNodeType(id);
|
||||||
|
|
||||||
// 1. 更新字段
|
// 1. 更新字段
|
||||||
if (nodeType.getName() != null) {
|
if (nodeType.getName() != null) {
|
||||||
existing.setName(nodeType.getName());
|
existing.setName(nodeType.getName());
|
||||||
@ -139,58 +139,58 @@ public class NodeTypeService {
|
|||||||
existing.setDisplayOrder(nodeType.getDisplayOrder());
|
existing.setDisplayOrder(nodeType.getDisplayOrder());
|
||||||
}
|
}
|
||||||
existing.setEnabled(nodeType.isEnabled());
|
existing.setEnabled(nodeType.isEnabled());
|
||||||
|
|
||||||
// 2. 验证更新后的数据
|
// 2. 验证更新后的数据
|
||||||
validateNodeType(existing);
|
validateNodeType(existing);
|
||||||
|
|
||||||
// 3. 更新时间
|
// 3. 更新时间
|
||||||
existing.setUpdatedAt(LocalDateTime.now());
|
existing.setUpdatedAt(LocalDateTime.now());
|
||||||
|
|
||||||
// 4. 保存更新
|
// 4. 保存更新
|
||||||
NodeType updated = nodeTypeRepository.save(existing);
|
NodeType updated = nodeTypeRepository.save(existing);
|
||||||
|
|
||||||
// 5. 重新注册到注册表
|
// 5. 重新注册到注册表
|
||||||
if (updated.isEnabled()) {
|
if (updated.isEnabled()) {
|
||||||
registerNodeTypeToRegistry(updated);
|
registerNodeTypeToRegistry(updated);
|
||||||
} else {
|
} else {
|
||||||
nodeTypeRegistry.unregister(updated.getId());
|
nodeTypeRegistry.unregister(updated.getId());
|
||||||
}
|
}
|
||||||
|
|
||||||
log.info("节点类型更新成功: {}", id);
|
log.info("节点类型更新成功: {}", id);
|
||||||
return updated;
|
return updated;
|
||||||
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("更新节点类型失败: {}", id, e);
|
log.error("更新节点类型失败: {}", id, e);
|
||||||
throw new NodeTypeServiceException("更新节点类型失败: " + e.getMessage(), e);
|
throw new NodeTypeServiceException("更新节点类型失败: " + e.getMessage(), e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 删除节点类型
|
* 删除节点类型
|
||||||
*/
|
*/
|
||||||
public void deleteNodeType(String id) {
|
public void deleteNodeType(String id) {
|
||||||
log.info("删除节点类型: {}", id);
|
log.info("删除节点类型: {}", id);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
NodeType nodeType = getNodeType(id);
|
NodeType nodeType = getNodeType(id);
|
||||||
|
|
||||||
// 1. 检查是否被使用(这里可以添加业务逻辑检查)
|
// 1. 检查是否被使用(这里可以添加业务逻辑检查)
|
||||||
// TODO: 检查工作流定义中是否使用了此节点类型
|
// TODO: 检查工作流定义中是否使用了此节点类型
|
||||||
|
|
||||||
// 2. 从注册表中移除
|
// 2. 从注册表中移除
|
||||||
nodeTypeRegistry.unregister(id);
|
nodeTypeRegistry.unregister(id);
|
||||||
|
|
||||||
// 3. 从数据库删除
|
// 3. 从数据库删除
|
||||||
nodeTypeRepository.delete(nodeType);
|
nodeTypeRepository.delete(nodeType);
|
||||||
|
|
||||||
log.info("节点类型删除成功: {}", id);
|
log.info("节点类型删除成功: {}", id);
|
||||||
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("删除节点类型失败: {}", id, e);
|
log.error("删除节点类型失败: {}", id, e);
|
||||||
throw new NodeTypeServiceException("删除节点类型失败: " + e.getMessage(), e);
|
throw new NodeTypeServiceException("删除节点类型失败: " + e.getMessage(), e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取节点类型分类列表
|
* 获取节点类型分类列表
|
||||||
*/
|
*/
|
||||||
@ -198,39 +198,39 @@ public class NodeTypeService {
|
|||||||
public List<String> getCategories() {
|
public List<String> getCategories() {
|
||||||
return nodeTypeRepository.findDistinctCategories();
|
return nodeTypeRepository.findDistinctCategories();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 刷新节点类型注册表
|
* 刷新节点类型注册表
|
||||||
* 重新加载所有启用的节点类型到注册表中
|
* 重新加载所有启用的节点类型到注册表中
|
||||||
*/
|
*/
|
||||||
public void refreshRegistry() {
|
public void refreshRegistry() {
|
||||||
log.info("刷新节点类型注册表");
|
log.info("刷新节点类型注册表");
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 1. 清空注册表
|
// 1. 清空注册表
|
||||||
nodeTypeRegistry.clear();
|
nodeTypeRegistry.clear();
|
||||||
|
|
||||||
// 2. 先检查总数
|
// 2. 先检查总数
|
||||||
long totalCount = nodeTypeRepository.count();
|
long totalCount = nodeTypeRepository.count();
|
||||||
long enabledCount = nodeTypeRepository.countByEnabledTrue();
|
long enabledCount = nodeTypeRepository.countByEnabledTrue();
|
||||||
log.info("数据库中总共有 {} 个节点类型,其中 {} 个启用", totalCount, enabledCount);
|
log.info("数据库中总共有 {} 个节点类型,其中 {} 个启用", totalCount, enabledCount);
|
||||||
|
|
||||||
// 3. 重新加载所有启用的节点类型
|
// 3. 重新加载所有启用的节点类型
|
||||||
List<NodeType> enabledTypes = nodeTypeRepository.findByEnabledTrueOrderByDisplayOrderAsc();
|
List<NodeType> enabledTypes = nodeTypeRepository.findByEnabledTrueOrderByDisplayOrderAsc();
|
||||||
log.info("查询返回 {} 个启用的节点类型", enabledTypes.size());
|
log.info("查询返回 {} 个启用的节点类型", enabledTypes.size());
|
||||||
|
|
||||||
for (NodeType nodeType : enabledTypes) {
|
for (NodeType nodeType : enabledTypes) {
|
||||||
registerNodeTypeToRegistry(nodeType);
|
registerNodeTypeToRegistry(nodeType);
|
||||||
}
|
}
|
||||||
|
|
||||||
log.info("节点类型注册表刷新完成,已加载 {} 个节点类型", enabledTypes.size());
|
log.info("节点类型注册表刷新完成,已加载 {} 个节点类型", enabledTypes.size());
|
||||||
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("刷新节点类型注册表失败", e);
|
log.error("刷新节点类型注册表失败", e);
|
||||||
throw new NodeTypeServiceException("刷新注册表失败: " + e.getMessage(), e);
|
throw new NodeTypeServiceException("刷新注册表失败: " + e.getMessage(), e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取节点类型输出模式
|
* 获取节点类型输出模式
|
||||||
*/
|
*/
|
||||||
@ -239,78 +239,78 @@ public class NodeTypeService {
|
|||||||
NodeType nodeType = getNodeType(nodeTypeId);
|
NodeType nodeType = getNodeType(nodeTypeId);
|
||||||
return nodeType.getOutputSchema();
|
return nodeType.getOutputSchema();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 更新节点类型输出模式
|
* 更新节点类型输出模式
|
||||||
*/
|
*/
|
||||||
public NodeType updateOutputSchema(String nodeTypeId, JsonNode outputSchema) {
|
public NodeType updateOutputSchema(String nodeTypeId, JsonNode outputSchema) {
|
||||||
log.info("更新节点类型输出模式: {}", nodeTypeId);
|
log.info("更新节点类型输出模式: {}", nodeTypeId);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
NodeType nodeType = getNodeType(nodeTypeId);
|
NodeType nodeType = getNodeType(nodeTypeId);
|
||||||
nodeType.setOutputSchema(outputSchema);
|
nodeType.setOutputSchema(outputSchema);
|
||||||
nodeType.setUpdatedAt(LocalDateTime.now());
|
nodeType.setUpdatedAt(LocalDateTime.now());
|
||||||
|
|
||||||
NodeType updated = nodeTypeRepository.save(nodeType);
|
NodeType updated = nodeTypeRepository.save(nodeType);
|
||||||
|
|
||||||
// 重新注册到注册表
|
// 重新注册到注册表
|
||||||
if (updated.isEnabled()) {
|
if (updated.isEnabled()) {
|
||||||
registerNodeTypeToRegistry(updated);
|
registerNodeTypeToRegistry(updated);
|
||||||
}
|
}
|
||||||
|
|
||||||
log.info("节点类型输出模式更新成功: {}", nodeTypeId);
|
log.info("节点类型输出模式更新成功: {}", nodeTypeId);
|
||||||
return updated;
|
return updated;
|
||||||
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("更新节点类型输出模式失败: {}", nodeTypeId, e);
|
log.error("更新节点类型输出模式失败: {}", nodeTypeId, e);
|
||||||
throw new NodeTypeServiceException("更新输出模式失败: " + e.getMessage(), e);
|
throw new NodeTypeServiceException("更新输出模式失败: " + e.getMessage(), e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 启用/禁用节点类型
|
* 启用/禁用节点类型
|
||||||
*/
|
*/
|
||||||
public NodeType toggleEnabled(String nodeTypeId, boolean enabled) {
|
public NodeType toggleEnabled(String nodeTypeId, boolean enabled) {
|
||||||
log.info("{}节点类型: {}", enabled ? "启用" : "禁用", nodeTypeId);
|
log.info("{}节点类型: {}", enabled ? "启用" : "禁用", nodeTypeId);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
NodeType nodeType = getNodeType(nodeTypeId);
|
NodeType nodeType = getNodeType(nodeTypeId);
|
||||||
nodeType.setEnabled(enabled);
|
nodeType.setEnabled(enabled);
|
||||||
nodeType.setUpdatedAt(LocalDateTime.now());
|
nodeType.setUpdatedAt(LocalDateTime.now());
|
||||||
|
|
||||||
NodeType updated = nodeTypeRepository.save(nodeType);
|
NodeType updated = nodeTypeRepository.save(nodeType);
|
||||||
|
|
||||||
// 更新注册表
|
// 更新注册表
|
||||||
if (enabled) {
|
if (enabled) {
|
||||||
registerNodeTypeToRegistry(updated);
|
registerNodeTypeToRegistry(updated);
|
||||||
} else {
|
} else {
|
||||||
nodeTypeRegistry.unregister(nodeTypeId);
|
nodeTypeRegistry.unregister(nodeTypeId);
|
||||||
}
|
}
|
||||||
|
|
||||||
log.info("节点类型状态更新成功: {} -> {}", nodeTypeId, enabled);
|
log.info("节点类型状态更新成功: {} -> {}", nodeTypeId, enabled);
|
||||||
return updated;
|
return updated;
|
||||||
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("更新节点类型状态失败: {}", nodeTypeId, e);
|
log.error("更新节点类型状态失败: {}", nodeTypeId, e);
|
||||||
throw new NodeTypeServiceException("更新节点类型状态失败: " + e.getMessage(), e);
|
throw new NodeTypeServiceException("更新节点类型状态失败: " + e.getMessage(), e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取节点类型统计信息
|
* 获取节点类型统计信息
|
||||||
*/
|
*/
|
||||||
@Transactional(readOnly = true)
|
@Transactional(readOnly = true)
|
||||||
public NodeTypeStatistics getStatistics() {
|
public NodeTypeStatistics getStatistics() {
|
||||||
NodeTypeStatistics stats = new NodeTypeStatistics();
|
NodeTypeStatistics stats = new NodeTypeStatistics();
|
||||||
|
|
||||||
// 总数统计
|
// 总数统计
|
||||||
long totalCount = nodeTypeRepository.count();
|
long totalCount = nodeTypeRepository.count();
|
||||||
long enabledCount = nodeTypeRepository.countByEnabledTrue();
|
long enabledCount = nodeTypeRepository.countByEnabledTrue();
|
||||||
|
|
||||||
stats.setTotalCount(totalCount);
|
stats.setTotalCount(totalCount);
|
||||||
stats.setEnabledCount(enabledCount);
|
stats.setEnabledCount(enabledCount);
|
||||||
stats.setDisabledCount(totalCount - enabledCount);
|
stats.setDisabledCount(totalCount - enabledCount);
|
||||||
|
|
||||||
// 分类统计
|
// 分类统计
|
||||||
Map<String, Long> categoryStats = new HashMap<>();
|
Map<String, Long> categoryStats = new HashMap<>();
|
||||||
List<Object[]> categoryResults = nodeTypeRepository.countByCategory();
|
List<Object[]> categoryResults = nodeTypeRepository.countByCategory();
|
||||||
@ -318,10 +318,10 @@ public class NodeTypeService {
|
|||||||
categoryStats.put((String) result[0], (Long) result[1]);
|
categoryStats.put((String) result[0], (Long) result[1]);
|
||||||
}
|
}
|
||||||
stats.setCategoryStats(categoryStats);
|
stats.setCategoryStats(categoryStats);
|
||||||
|
|
||||||
return stats;
|
return stats;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 验证节点类型数据
|
* 验证节点类型数据
|
||||||
*/
|
*/
|
||||||
@ -335,7 +335,7 @@ public class NodeTypeService {
|
|||||||
if (nodeType.getCategory() == null) {
|
if (nodeType.getCategory() == null) {
|
||||||
throw new NodeTypeServiceException("节点类型分类不能为空");
|
throw new NodeTypeServiceException("节点类型分类不能为空");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 验证字段定义JSON格式
|
// 验证字段定义JSON格式
|
||||||
if (nodeType.getFields() != null) {
|
if (nodeType.getFields() != null) {
|
||||||
try {
|
try {
|
||||||
@ -344,7 +344,7 @@ public class NodeTypeService {
|
|||||||
throw new NodeTypeServiceException("字段定义JSON格式无效: " + e.getMessage());
|
throw new NodeTypeServiceException("字段定义JSON格式无效: " + e.getMessage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 验证输出模式JSON格式
|
// 验证输出模式JSON格式
|
||||||
if (nodeType.getOutputSchema() != null) {
|
if (nodeType.getOutputSchema() != null) {
|
||||||
try {
|
try {
|
||||||
@ -354,7 +354,7 @@ public class NodeTypeService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 将节点类型注册到注册表
|
* 将节点类型注册到注册表
|
||||||
*/
|
*/
|
||||||
@ -366,27 +366,50 @@ public class NodeTypeService {
|
|||||||
log.warn("注册节点类型到注册表失败: {} - {}", nodeType.getId(), e.getMessage());
|
log.warn("注册节点类型到注册表失败: {} - {}", nodeType.getId(), e.getMessage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 节点类型统计信息
|
* 节点类型统计信息
|
||||||
*/
|
*/
|
||||||
public static class NodeTypeStatistics {
|
public static class NodeTypeStatistics {
|
||||||
private long totalCount;
|
private long totalCount;
|
||||||
|
|
||||||
private long enabledCount;
|
private long enabledCount;
|
||||||
|
|
||||||
private long disabledCount;
|
private long disabledCount;
|
||||||
|
|
||||||
private Map<String, Long> categoryStats;
|
private Map<String, Long> categoryStats;
|
||||||
|
|
||||||
// Getters and Setters
|
// Getters and Setters
|
||||||
public long getTotalCount() { return totalCount; }
|
public long getTotalCount() {
|
||||||
public void setTotalCount(long totalCount) { this.totalCount = totalCount; }
|
return totalCount;
|
||||||
|
}
|
||||||
public long getEnabledCount() { return enabledCount; }
|
|
||||||
public void setEnabledCount(long enabledCount) { this.enabledCount = enabledCount; }
|
public void setTotalCount(long totalCount) {
|
||||||
|
this.totalCount = totalCount;
|
||||||
public long getDisabledCount() { return disabledCount; }
|
}
|
||||||
public void setDisabledCount(long disabledCount) { this.disabledCount = disabledCount; }
|
|
||||||
|
public long getEnabledCount() {
|
||||||
public Map<String, Long> getCategoryStats() { return categoryStats; }
|
return enabledCount;
|
||||||
public void setCategoryStats(Map<String, Long> categoryStats) { this.categoryStats = categoryStats; }
|
}
|
||||||
|
|
||||||
|
public void setEnabledCount(long enabledCount) {
|
||||||
|
this.enabledCount = enabledCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getDisabledCount() {
|
||||||
|
return disabledCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setDisabledCount(long disabledCount) {
|
||||||
|
this.disabledCount = disabledCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Map<String, Long> getCategoryStats() {
|
||||||
|
return categoryStats;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setCategoryStats(Map<String, Long> categoryStats) {
|
||||||
|
this.categoryStats = categoryStats;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -7,9 +7,9 @@ import com.flowable.devops.workflow.model.NodeInput;
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 工作流节点接口
|
* 工作流节点接口
|
||||||
*
|
* <p>
|
||||||
* 所有工作流节点必须实现此接口
|
* 所有工作流节点必须实现此接口
|
||||||
*
|
* <p>
|
||||||
* 设计原则:
|
* 设计原则:
|
||||||
* 1. 节点应该是无状态的,所有状态通过参数传递
|
* 1. 节点应该是无状态的,所有状态通过参数传递
|
||||||
* 2. 节点应该是幂等的,多次执行相同输入应得到相同结果
|
* 2. 节点应该是幂等的,多次执行相同输入应得到相同结果
|
||||||
@ -17,66 +17,66 @@ import com.flowable.devops.workflow.model.NodeInput;
|
|||||||
* 4. 节点应该提供详细的元数据,支持前端动态渲染
|
* 4. 节点应该提供详细的元数据,支持前端动态渲染
|
||||||
*/
|
*/
|
||||||
public interface WorkflowNode {
|
public interface WorkflowNode {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取节点元数据
|
* 获取节点元数据
|
||||||
*
|
* <p>
|
||||||
* 包含节点的字段定义、输出结构、分类等信息
|
* 包含节点的字段定义、输出结构、分类等信息
|
||||||
* 前端根据此信息动态生成表单和字段映射
|
* 前端根据此信息动态生成表单和字段映射
|
||||||
*
|
*
|
||||||
* @return 节点类型元数据
|
* @return 节点类型元数据
|
||||||
*/
|
*/
|
||||||
NodeType getMetadata();
|
NodeType getMetadata();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 执行节点
|
* 执行节点
|
||||||
*
|
*
|
||||||
* @param input 节点输入参数(表达式已解析)
|
* @param input 节点输入参数(表达式已解析)
|
||||||
* @param context 执行上下文,包含工作流变量、环境变量等
|
* @param context 执行上下文,包含工作流变量、环境变量等
|
||||||
* @return 节点执行结果
|
* @return 节点执行结果
|
||||||
*/
|
*/
|
||||||
NodeExecutionResult execute(NodeInput input, NodeExecutionContext context);
|
NodeExecutionResult execute(NodeInput input, NodeExecutionContext context);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 验证节点配置
|
* 验证节点配置
|
||||||
*
|
* <p>
|
||||||
* 在节点执行前验证配置是否正确
|
* 在节点执行前验证配置是否正确
|
||||||
* 可选实现,默认不做验证
|
* 可选实现,默认不做验证
|
||||||
*
|
*
|
||||||
* @param input 节点输入参数
|
* @param input 节点输入参数
|
||||||
* @throws IllegalArgumentException 如果配置无效
|
* @throws IllegalArgumentException 如果配置无效
|
||||||
*/
|
*/
|
||||||
default void validate(NodeInput input) {
|
default void validate(NodeInput input) {
|
||||||
// 默认实现:不做验证
|
// 默认实现:不做验证
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取节点类型ID
|
* 获取节点类型ID
|
||||||
*
|
* <p>
|
||||||
* 便捷方法,等价于 getMetadata().getId()
|
* 便捷方法,等价于 getMetadata().getId()
|
||||||
*
|
*
|
||||||
* @return 节点类型ID
|
* @return 节点类型ID
|
||||||
*/
|
*/
|
||||||
default String getNodeTypeId() {
|
default String getNodeTypeId() {
|
||||||
NodeType metadata = getMetadata();
|
NodeType metadata = getMetadata();
|
||||||
return metadata != null ? metadata.getId() : null;
|
return metadata != null ? metadata.getId() : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取节点显示名称
|
* 获取节点显示名称
|
||||||
*
|
* <p>
|
||||||
* 便捷方法,等价于 getMetadata().getDisplayName()
|
* 便捷方法,等价于 getMetadata().getDisplayName()
|
||||||
*
|
*
|
||||||
* @return 节点显示名称
|
* @return 节点显示名称
|
||||||
*/
|
*/
|
||||||
default String getDisplayName() {
|
default String getDisplayName() {
|
||||||
NodeType metadata = getMetadata();
|
NodeType metadata = getMetadata();
|
||||||
return metadata != null ? metadata.getDisplayName() : null;
|
return metadata != null ? metadata.getDisplayName() : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 检查节点是否支持指定的配置参数
|
* 检查节点是否支持指定的配置参数
|
||||||
*
|
*
|
||||||
* @param parameterName 参数名称
|
* @param parameterName 参数名称
|
||||||
* @return 是否支持该参数
|
* @return 是否支持该参数
|
||||||
*/
|
*/
|
||||||
@ -85,7 +85,7 @@ public interface WorkflowNode {
|
|||||||
if (metadata == null || metadata.getFields() == null) {
|
if (metadata == null || metadata.getFields() == null) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 这里需要解析 fields JSON,简化实现返回 true
|
// 这里需要解析 fields JSON,简化实现返回 true
|
||||||
// 实际实现中应该检查字段定义
|
// 实际实现中应该检查字段定义
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
@ -9,6 +9,7 @@ import org.springframework.context.ApplicationContext;
|
|||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
import jakarta.annotation.PostConstruct;
|
import jakarta.annotation.PostConstruct;
|
||||||
|
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
|
||||||
@ -18,20 +19,21 @@ import java.util.concurrent.ConcurrentHashMap;
|
|||||||
@Slf4j
|
@Slf4j
|
||||||
@Service
|
@Service
|
||||||
public class NodeTypeRegistry {
|
public class NodeTypeRegistry {
|
||||||
|
|
||||||
private final Map<String, NodeType> nodeTypeMetadata = new ConcurrentHashMap<>();
|
private final Map<String, NodeType> nodeTypeMetadata = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
private final Map<String, WorkflowNode> nodeInstances = new ConcurrentHashMap<>();
|
private final Map<String, WorkflowNode> nodeInstances = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private ApplicationContext applicationContext;
|
private ApplicationContext applicationContext;
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private NodeTypeRepository nodeTypeRepository;
|
private NodeTypeRepository nodeTypeRepository;
|
||||||
|
|
||||||
@PostConstruct
|
@PostConstruct
|
||||||
public void initialize() {
|
public void initialize() {
|
||||||
log.info("开始初始化节点类型注册中心...");
|
log.info("开始初始化节点类型注册中心...");
|
||||||
|
|
||||||
try {
|
try {
|
||||||
scanAndRegisterNodes();
|
scanAndRegisterNodes();
|
||||||
loadNodesFromDatabase();
|
loadNodesFromDatabase();
|
||||||
@ -40,11 +42,11 @@ public class NodeTypeRegistry {
|
|||||||
log.error("节点类型注册中心初始化失败", e);
|
log.error("节点类型注册中心初始化失败", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void scanAndRegisterNodes() {
|
private void scanAndRegisterNodes() {
|
||||||
Map<String, WorkflowNode> nodeBeans = applicationContext.getBeansOfType(WorkflowNode.class);
|
Map<String, WorkflowNode> nodeBeans = applicationContext.getBeansOfType(WorkflowNode.class);
|
||||||
log.info("发现 {} 个WorkflowNode实现类", nodeBeans.size());
|
log.info("发现 {} 个WorkflowNode实现类", nodeBeans.size());
|
||||||
|
|
||||||
for (Map.Entry<String, WorkflowNode> entry : nodeBeans.entrySet()) {
|
for (Map.Entry<String, WorkflowNode> entry : nodeBeans.entrySet()) {
|
||||||
try {
|
try {
|
||||||
registerNode(entry.getValue(), entry.getKey());
|
registerNode(entry.getValue(), entry.getKey());
|
||||||
@ -53,7 +55,7 @@ public class NodeTypeRegistry {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void loadNodesFromDatabase() {
|
private void loadNodesFromDatabase() {
|
||||||
try {
|
try {
|
||||||
List<NodeType> dbNodeTypes = nodeTypeRepository.findByEnabledTrueOrderByDisplayOrderAsc();
|
List<NodeType> dbNodeTypes = nodeTypeRepository.findByEnabledTrueOrderByDisplayOrderAsc();
|
||||||
@ -66,70 +68,70 @@ public class NodeTypeRegistry {
|
|||||||
log.warn("从数据库加载节点类型失败: {}", e.getMessage());
|
log.warn("从数据库加载节点类型失败: {}", e.getMessage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void registerNode(WorkflowNode node, String beanName) {
|
private void registerNode(WorkflowNode node, String beanName) {
|
||||||
NodeType metadata = node.getMetadata();
|
NodeType metadata = node.getMetadata();
|
||||||
validateNodeMetadata(metadata, beanName);
|
validateNodeMetadata(metadata, beanName);
|
||||||
|
|
||||||
String nodeTypeId = metadata.getId();
|
String nodeTypeId = metadata.getId();
|
||||||
if (nodeTypeMetadata.containsKey(nodeTypeId)) {
|
if (nodeTypeMetadata.containsKey(nodeTypeId)) {
|
||||||
log.warn("节点类型ID重复,跳过注册: {}", nodeTypeId);
|
log.warn("节点类型ID重复,跳过注册: {}", nodeTypeId);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
nodeTypeMetadata.put(nodeTypeId, metadata);
|
nodeTypeMetadata.put(nodeTypeId, metadata);
|
||||||
nodeInstances.put(nodeTypeId, node);
|
nodeInstances.put(nodeTypeId, node);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
nodeTypeRepository.save(metadata);
|
nodeTypeRepository.save(metadata);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.warn("保存节点类型到数据库失败: {}", e.getMessage());
|
log.warn("保存节点类型到数据库失败: {}", e.getMessage());
|
||||||
}
|
}
|
||||||
|
|
||||||
log.info("✓ 注册节点: {} ({})", metadata.getDisplayName(), nodeTypeId);
|
log.info("✓ 注册节点: {} ({})", metadata.getDisplayName(), nodeTypeId);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void validateNodeMetadata(NodeType metadata, String beanName) {
|
private void validateNodeMetadata(NodeType metadata, String beanName) {
|
||||||
if (metadata == null || metadata.getId() == null || metadata.getDisplayName() == null) {
|
if (metadata == null || metadata.getId() == null || metadata.getDisplayName() == null) {
|
||||||
throw new IllegalArgumentException("节点元数据不完整: " + beanName);
|
throw new IllegalArgumentException("节点元数据不完整: " + beanName);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public NodeType getNodeMetadata(String nodeTypeId) {
|
public NodeType getNodeMetadata(String nodeTypeId) {
|
||||||
return nodeTypeMetadata.get(nodeTypeId);
|
return nodeTypeMetadata.get(nodeTypeId);
|
||||||
}
|
}
|
||||||
|
|
||||||
public WorkflowNode getNodeInstance(String nodeTypeId) {
|
public WorkflowNode getNodeInstance(String nodeTypeId) {
|
||||||
return nodeInstances.get(nodeTypeId);
|
return nodeInstances.get(nodeTypeId);
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<NodeType> getAllNodeTypes() {
|
public List<NodeType> getAllNodeTypes() {
|
||||||
return new ArrayList<>(nodeTypeMetadata.values());
|
return new ArrayList<>(nodeTypeMetadata.values());
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<NodeType> getEnabledNodeTypes() {
|
public List<NodeType> getEnabledNodeTypes() {
|
||||||
return nodeTypeMetadata.values().stream()
|
return nodeTypeMetadata.values().stream()
|
||||||
.filter(NodeType::isEnabled)
|
.filter(NodeType::isEnabled)
|
||||||
.sorted(Comparator.comparing(NodeType::getDisplayName))
|
.sorted(Comparator.comparing(NodeType::getDisplayName))
|
||||||
.toList();
|
.toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<NodeType> getNodeTypesByCategory(NodeType.NodeCategory category) {
|
public List<NodeType> getNodeTypesByCategory(NodeType.NodeCategory category) {
|
||||||
return nodeTypeMetadata.values().stream()
|
return nodeTypeMetadata.values().stream()
|
||||||
.filter(nodeType -> Objects.equals(nodeType.getCategory(), category))
|
.filter(nodeType -> Objects.equals(nodeType.getCategory(), category))
|
||||||
.filter(NodeType::isEnabled)
|
.filter(NodeType::isEnabled)
|
||||||
.sorted(Comparator.comparing(NodeType::getDisplayName))
|
.sorted(Comparator.comparing(NodeType::getDisplayName))
|
||||||
.toList();
|
.toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean hasNodeType(String nodeTypeId) {
|
public boolean hasNodeType(String nodeTypeId) {
|
||||||
return nodeTypeMetadata.containsKey(nodeTypeId);
|
return nodeTypeMetadata.containsKey(nodeTypeId);
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean hasNodeImplementation(String nodeTypeId) {
|
public boolean hasNodeImplementation(String nodeTypeId) {
|
||||||
return nodeInstances.containsKey(nodeTypeId);
|
return nodeInstances.containsKey(nodeTypeId);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 从数据库节点类型注册到注册表
|
* 从数据库节点类型注册到注册表
|
||||||
*/
|
*/
|
||||||
@ -138,11 +140,11 @@ public class NodeTypeRegistry {
|
|||||||
log.warn("无效的节点类型,跳过注册");
|
log.warn("无效的节点类型,跳过注册");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
nodeTypeMetadata.put(nodeType.getId(), nodeType);
|
nodeTypeMetadata.put(nodeType.getId(), nodeType);
|
||||||
log.debug("从数据库注册节点类型: {}", nodeType.getId());
|
log.debug("从数据库注册节点类型: {}", nodeType.getId());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 注销节点类型
|
* 注销节点类型
|
||||||
*/
|
*/
|
||||||
@ -153,7 +155,7 @@ public class NodeTypeRegistry {
|
|||||||
log.debug("注销节点类型: {}", nodeTypeId);
|
log.debug("注销节点类型: {}", nodeTypeId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 清空所有注册的节点类型
|
* 清空所有注册的节点类型
|
||||||
*/
|
*/
|
||||||
|
|||||||
@ -1,14 +1,10 @@
|
|||||||
spring:
|
spring:
|
||||||
application:
|
application:
|
||||||
name: flowable-devops-backend
|
name: flowable-devops-backend
|
||||||
profiles:
|
|
||||||
active: dev
|
|
||||||
|
|
||||||
# 数据源配置
|
|
||||||
datasource:
|
datasource:
|
||||||
url: ${SPRING_DATASOURCE_URL:jdbc:mysql://localhost:3306/flowable-devops?useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=UTC&characterEncoding=utf8}
|
url: ${SPRING_DATASOURCE_URL:jdbc:mysql://172.22.222.111:3306/flowable-devops?useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=UTC&characterEncoding=utf8}
|
||||||
username: ${SPRING_DATASOURCE_USERNAME:root}
|
username: ${SPRING_DATASOURCE_USERNAME:flowable-devops}
|
||||||
password: ${SPRING_DATASOURCE_PASSWORD:123456}
|
password: ${SPRING_DATASOURCE_PASSWORD:Qichen5210523}
|
||||||
driver-class-name: com.mysql.cj.jdbc.Driver
|
driver-class-name: com.mysql.cj.jdbc.Driver
|
||||||
hikari:
|
hikari:
|
||||||
maximum-pool-size: 20
|
maximum-pool-size: 20
|
||||||
@ -39,10 +35,10 @@ spring:
|
|||||||
# Redis配置
|
# Redis配置
|
||||||
data:
|
data:
|
||||||
redis:
|
redis:
|
||||||
host: ${SPRING_REDIS_HOST:localhost}
|
host: ${SPRING_REDIS_HOST:172.22.222.111}
|
||||||
port: ${SPRING_REDIS_PORT:6379}
|
port: ${SPRING_REDIS_PORT:6379}
|
||||||
password: ${SPRING_REDIS_PASSWORD:}
|
password: ${SPRING_REDIS_PASSWORD:Qichen5210523...}
|
||||||
database: ${SPRING_REDIS_DATABASE:0}
|
database: ${SPRING_REDIS_DATABASE:5}
|
||||||
timeout: 5000ms
|
timeout: 5000ms
|
||||||
lettuce:
|
lettuce:
|
||||||
pool:
|
pool:
|
||||||
@ -75,12 +71,12 @@ flowable:
|
|||||||
process:
|
process:
|
||||||
definition-cache-limit: 100
|
definition-cache-limit: 100
|
||||||
enable-safe-xml: true
|
enable-safe-xml: true
|
||||||
|
|
||||||
# 异步执行器配置(MVP阶段关闭)
|
# 异步执行器配置(MVP阶段关闭)
|
||||||
async:
|
async:
|
||||||
executor:
|
executor:
|
||||||
activate: false
|
activate: false
|
||||||
|
|
||||||
# REST API配置
|
# REST API配置
|
||||||
rest:
|
rest:
|
||||||
app:
|
app:
|
||||||
@ -128,9 +124,9 @@ management:
|
|||||||
show-details: when-authorized
|
show-details: when-authorized
|
||||||
metrics:
|
metrics:
|
||||||
enabled: true
|
enabled: true
|
||||||
metrics:
|
prometheus:
|
||||||
export:
|
metrics:
|
||||||
prometheus:
|
export:
|
||||||
enabled: true
|
enabled: true
|
||||||
|
|
||||||
# 应用自定义配置
|
# 应用自定义配置
|
||||||
@ -145,145 +141,22 @@ flowable-devops:
|
|||||||
enable-security: true
|
enable-security: true
|
||||||
# 节点执行超时时间(秒)
|
# 节点执行超时时间(秒)
|
||||||
node-timeout: 300
|
node-timeout: 300
|
||||||
|
|
||||||
# 节点类型配置
|
# 节点类型配置
|
||||||
node-types:
|
node-types:
|
||||||
# 是否在启动时加载默认节点类型
|
# 是否在启动时加载默认节点类型
|
||||||
load-defaults: true
|
load-defaults: true
|
||||||
# 默认节点类型配置文件路径
|
# 默认节点类型配置文件路径
|
||||||
default-config-path: classpath:node-types/
|
default-config-path: classpath:node-types/
|
||||||
|
|
||||||
# 任务配置
|
# 任务配置
|
||||||
task:
|
task:
|
||||||
# 任务超时检查间隔(分钟)
|
# 任务超时检查间隔(分钟)
|
||||||
timeout-check-interval: 60
|
timeout-check-interval: 60
|
||||||
# 默认任务优先级
|
# 默认任务优先级
|
||||||
default-priority: 50
|
default-priority: 50
|
||||||
|
|
||||||
# 安全配置
|
# 安全配置
|
||||||
security:
|
security:
|
||||||
# 是否启用认证(暂时关闭,后续可配置)
|
# 是否启用认证(暂时关闭,后续可配置)
|
||||||
authentication-enabled: false
|
authentication-enabled: false
|
||||||
|
|
||||||
---
|
|
||||||
# 开发环境配置
|
|
||||||
spring:
|
|
||||||
config:
|
|
||||||
activate:
|
|
||||||
on-profile: dev
|
|
||||||
|
|
||||||
# 开发环境数据库配置
|
|
||||||
datasource:
|
|
||||||
hikari:
|
|
||||||
minimum-idle: 2
|
|
||||||
maximum-pool-size: 10
|
|
||||||
|
|
||||||
# 开发环境日志配置
|
|
||||||
logging:
|
|
||||||
level:
|
|
||||||
com.flowable.devops: DEBUG
|
|
||||||
org.springframework.web: DEBUG
|
|
||||||
|
|
||||||
# 开发环境Flowable配置
|
|
||||||
flowable:
|
|
||||||
database-schema-update: true
|
|
||||||
database-type: mysql
|
|
||||||
|
|
||||||
---
|
|
||||||
# 测试环境配置
|
|
||||||
spring:
|
|
||||||
config:
|
|
||||||
activate:
|
|
||||||
on-profile: test
|
|
||||||
|
|
||||||
# 测试环境使用MySQL数据库
|
|
||||||
datasource:
|
|
||||||
url: ${SPRING_DATASOURCE_URL:jdbc:mysql://172.22.222.111:3306/flowable-devops?useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=UTC}
|
|
||||||
username: ${SPRING_DATASOURCE_USERNAME:flowable-devops}
|
|
||||||
password: ${SPRING_DATASOURCE_PASSWORD:Qichen5210523}
|
|
||||||
driver-class-name: com.mysql.cj.jdbc.Driver
|
|
||||||
hikari:
|
|
||||||
minimum-idle: 2
|
|
||||||
maximum-pool-size: 5
|
|
||||||
|
|
||||||
# JPA配置
|
|
||||||
jpa:
|
|
||||||
hibernate:
|
|
||||||
ddl-auto: create-drop
|
|
||||||
properties:
|
|
||||||
hibernate:
|
|
||||||
dialect: org.hibernate.dialect.MySQL8Dialect
|
|
||||||
format_sql: false
|
|
||||||
show_sql: false
|
|
||||||
use_sql_comments: false
|
|
||||||
show-sql: false
|
|
||||||
|
|
||||||
# 测试环境使用Redis
|
|
||||||
data:
|
|
||||||
redis:
|
|
||||||
host: ${SPRING_REDIS_HOST:172.22.222.111}
|
|
||||||
port: ${SPRING_REDIS_PORT:6379}
|
|
||||||
password: ${SPRING_REDIS_PASSWORD:}
|
|
||||||
database: ${SPRING_REDIS_DATABASE:5}
|
|
||||||
|
|
||||||
# 测试环境Flowable配置
|
|
||||||
flowable:
|
|
||||||
database-schema-update: create-drop
|
|
||||||
db-history-used: true
|
|
||||||
database-type: mysql
|
|
||||||
async:
|
|
||||||
executor:
|
|
||||||
activate: false
|
|
||||||
|
|
||||||
# 测试环境应用配置
|
|
||||||
flowable-devops:
|
|
||||||
node-types:
|
|
||||||
load-defaults: true
|
|
||||||
|
|
||||||
# 测试环境日志配置
|
|
||||||
logging:
|
|
||||||
level:
|
|
||||||
root: INFO
|
|
||||||
com.flowable.devops: DEBUG
|
|
||||||
org.springframework: WARN
|
|
||||||
org.flowable: WARN
|
|
||||||
org.hibernate.SQL: false
|
|
||||||
|
|
||||||
---
|
|
||||||
# 生产环境配置
|
|
||||||
spring:
|
|
||||||
config:
|
|
||||||
activate:
|
|
||||||
on-profile: prod
|
|
||||||
|
|
||||||
# 生产环境数据库连接池配置
|
|
||||||
datasource:
|
|
||||||
hikari:
|
|
||||||
minimum-idle: 10
|
|
||||||
maximum-pool-size: 50
|
|
||||||
leak-detection-threshold: 30000
|
|
||||||
|
|
||||||
# JPA配置
|
|
||||||
jpa:
|
|
||||||
hibernate:
|
|
||||||
ddl-auto: validate
|
|
||||||
show-sql: false
|
|
||||||
|
|
||||||
# 生产环境日志配置
|
|
||||||
logging:
|
|
||||||
level:
|
|
||||||
root: WARN
|
|
||||||
com.flowable.devops: INFO
|
|
||||||
file:
|
|
||||||
name: /var/log/flowable-devops/application.log
|
|
||||||
|
|
||||||
# 生产环境Flowable配置
|
|
||||||
flowable:
|
|
||||||
database-schema-update: false
|
|
||||||
|
|
||||||
# 生产环境管理端点配置
|
|
||||||
management:
|
|
||||||
endpoints:
|
|
||||||
web:
|
|
||||||
exposure:
|
|
||||||
include: health,info,metrics
|
|
||||||
@ -1,40 +0,0 @@
|
|||||||
spring:
|
|
||||||
datasource:
|
|
||||||
url: ${SPRING_DATASOURCE_URL:jdbc:mysql://localhost:3306/flowable-devops?useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=UTC&connectTimeout=60000&socketTimeout=60000&autoReconnect=true}
|
|
||||||
username: ${SPRING_DATASOURCE_USERNAME:flowable-devops}
|
|
||||||
password: ${SPRING_DATASOURCE_PASSWORD:Qichen5210523}
|
|
||||||
driver-class-name: com.mysql.cj.jdbc.Driver
|
|
||||||
hikari:
|
|
||||||
connection-timeout: 60000
|
|
||||||
socket-timeout: 60000
|
|
||||||
maximum-pool-size: 5
|
|
||||||
minimum-idle: 1
|
|
||||||
idle-timeout: 300000
|
|
||||||
validation-timeout: 30000
|
|
||||||
jpa:
|
|
||||||
hibernate:
|
|
||||||
ddl-auto: create-drop
|
|
||||||
properties:
|
|
||||||
hibernate:
|
|
||||||
dialect: org.hibernate.dialect.MySQLDialect
|
|
||||||
show-sql: false
|
|
||||||
data:
|
|
||||||
redis:
|
|
||||||
# 测试环境使用嵌入式Redis替代品或Mock
|
|
||||||
host: localhost
|
|
||||||
port: 6379
|
|
||||||
database: 0
|
|
||||||
|
|
||||||
flowable:
|
|
||||||
# 测试环境数据库配置
|
|
||||||
database-type: mysql
|
|
||||||
database-schema-update: create-drop
|
|
||||||
check-process-definitions: false
|
|
||||||
async-executor-activate: false
|
|
||||||
history-level: audit
|
|
||||||
|
|
||||||
logging:
|
|
||||||
level:
|
|
||||||
org.flowable: WARN
|
|
||||||
org.springframework: WARN
|
|
||||||
org.hibernate: WARN
|
|
||||||
4
frontend/.browserslistrc
Normal file
4
frontend/.browserslistrc
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
> 1%
|
||||||
|
last 2 versions
|
||||||
|
not dead
|
||||||
|
not ie 11
|
||||||
1
frontend/.commitlintrc.js
Normal file
1
frontend/.commitlintrc.js
Normal file
@ -0,0 +1 @@
|
|||||||
|
export { default } from '@vben/commitlint-config';
|
||||||
2
frontend/.cursorignore
Normal file
2
frontend/.cursorignore
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
# Add directories or file patterns to ignore during indexing (e.g. foo/ or *.csv)
|
||||||
|
node_modules
|
||||||
7
frontend/.dockerignore
Normal file
7
frontend/.dockerignore
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
node_modules
|
||||||
|
.git
|
||||||
|
.gitignore
|
||||||
|
*.md
|
||||||
|
dist
|
||||||
|
.turbo
|
||||||
|
dist.zip
|
||||||
18
frontend/.editorconfig
Normal file
18
frontend/.editorconfig
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
charset=utf-8
|
||||||
|
end_of_line=lf
|
||||||
|
insert_final_newline=true
|
||||||
|
indent_style=space
|
||||||
|
indent_size=2
|
||||||
|
max_line_length = 100
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
quote_type = single
|
||||||
|
|
||||||
|
[*.{yml,yaml,json}]
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 2
|
||||||
|
|
||||||
|
[*.md]
|
||||||
|
trim_trailing_whitespace = false
|
||||||
@ -1,2 +0,0 @@
|
|||||||
# 环境变量示例
|
|
||||||
VITE_API_BASE_URL=http://localhost:8080
|
|
||||||
@ -1,14 +0,0 @@
|
|||||||
{
|
|
||||||
"env": { "browser": true, "es2021": true },
|
|
||||||
"extends": ["eslint:recommended", "plugin:react/recommended", "plugin:react-hooks/recommended", "plugin:@typescript-eslint/recommended"],
|
|
||||||
"parser": "@typescript-eslint/parser",
|
|
||||||
"parserOptions": { "ecmaVersion": 2021, "sourceType": "module" },
|
|
||||||
"plugins": ["react", "@typescript-eslint"],
|
|
||||||
"settings": { "react": { "version": "detect" } },
|
|
||||||
"rules": {
|
|
||||||
"react-hooks/exhaustive-deps": "off",
|
|
||||||
"react/react-in-jsx-scope": "off",
|
|
||||||
"@typescript-eslint/no-explicit-any": "off",
|
|
||||||
"@typescript-eslint/no-unused-vars": ["error", { "argsIgnorePattern": "^_" }]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
11
frontend/.gitattributes
vendored
Normal file
11
frontend/.gitattributes
vendored
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
# https://docs.github.com/cn/get-started/getting-started-with-git/configuring-git-to-handle-line-endings
|
||||||
|
|
||||||
|
# Automatically normalize line endings (to LF) for all text-based files.
|
||||||
|
* text=auto eol=lf
|
||||||
|
|
||||||
|
# Declare files that will always have CRLF line endings on checkout.
|
||||||
|
*.{cmd,[cC][mM][dD]} text eol=crlf
|
||||||
|
*.{bat,[bB][aA][tT]} text eol=crlf
|
||||||
|
|
||||||
|
# Denote all files that are truly binary and should not be modified.
|
||||||
|
*.{ico,png,jpg,jpeg,gif,webp,svg,woff,woff2} binary
|
||||||
2
frontend/.gitconfig
Normal file
2
frontend/.gitconfig
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
[core]
|
||||||
|
ignorecase = false
|
||||||
52
frontend/.gitignore
vendored
Normal file
52
frontend/.gitignore
vendored
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
node_modules
|
||||||
|
.DS_Store
|
||||||
|
dist
|
||||||
|
dist-ssr
|
||||||
|
dist.zip
|
||||||
|
dist.tar
|
||||||
|
dist.war
|
||||||
|
.nitro
|
||||||
|
.output
|
||||||
|
*-dist.zip
|
||||||
|
*-dist.tar
|
||||||
|
*-dist.war
|
||||||
|
coverage
|
||||||
|
*.local
|
||||||
|
**/.vitepress/cache
|
||||||
|
.cache
|
||||||
|
.turbo
|
||||||
|
.temp
|
||||||
|
dev-dist
|
||||||
|
.stylelintcache
|
||||||
|
yarn.lock
|
||||||
|
package-lock.json
|
||||||
|
.VSCodeCounter
|
||||||
|
**/backend-mock/data
|
||||||
|
|
||||||
|
# local env files
|
||||||
|
.env.local
|
||||||
|
.env.*.local
|
||||||
|
.eslintcache
|
||||||
|
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
vite.config.mts.*
|
||||||
|
vite.config.mjs.*
|
||||||
|
vite.config.js.*
|
||||||
|
vite.config.ts.*
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.idea
|
||||||
|
# .vscode
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
||||||
|
.history
|
||||||
|
.cursor
|
||||||
6
frontend/.gitpod.yml
Normal file
6
frontend/.gitpod.yml
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
ports:
|
||||||
|
- port: 5555
|
||||||
|
onOpen: open-preview
|
||||||
|
tasks:
|
||||||
|
- init: npm i -g corepack && pnpm install
|
||||||
|
command: pnpm run dev:play
|
||||||
5
frontend/.idea/.gitignore
vendored
5
frontend/.idea/.gitignore
vendored
@ -1,5 +0,0 @@
|
|||||||
# 默认忽略的文件
|
|
||||||
/shelf/
|
|
||||||
/workspace.xml
|
|
||||||
# 基于编辑器的 HTTP 客户端请求
|
|
||||||
/httpRequests/
|
|
||||||
@ -1,12 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<module type="WEB_MODULE" version="4">
|
|
||||||
<component name="NewModuleRootManager">
|
|
||||||
<content url="file://$MODULE_DIR$">
|
|
||||||
<excludeFolder url="file://$MODULE_DIR$/.tmp" />
|
|
||||||
<excludeFolder url="file://$MODULE_DIR$/temp" />
|
|
||||||
<excludeFolder url="file://$MODULE_DIR$/tmp" />
|
|
||||||
</content>
|
|
||||||
<orderEntry type="inheritedJdk" />
|
|
||||||
<orderEntry type="sourceFolder" forTests="false" />
|
|
||||||
</component>
|
|
||||||
</module>
|
|
||||||
@ -1,6 +0,0 @@
|
|||||||
<component name="InspectionProjectProfileManager">
|
|
||||||
<profile version="1.0">
|
|
||||||
<option name="myName" value="Project Default" />
|
|
||||||
<inspection_tool class="Eslint" enabled="true" level="WARNING" enabled_by_default="true" />
|
|
||||||
</profile>
|
|
||||||
</component>
|
|
||||||
@ -1,8 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project version="4">
|
|
||||||
<component name="ProjectModuleManager">
|
|
||||||
<modules>
|
|
||||||
<module fileurl="file://$PROJECT_DIR$/.idea/frontend.iml" filepath="$PROJECT_DIR$/.idea/frontend.iml" />
|
|
||||||
</modules>
|
|
||||||
</component>
|
|
||||||
</project>
|
|
||||||
@ -1,6 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project version="4">
|
|
||||||
<component name="VcsDirectoryMappings">
|
|
||||||
<mapping directory="$PROJECT_DIR$/.." vcs="Git" />
|
|
||||||
</component>
|
|
||||||
</project>
|
|
||||||
1
frontend/.node-version
Normal file
1
frontend/.node-version
Normal file
@ -0,0 +1 @@
|
|||||||
|
22.1.0
|
||||||
13
frontend/.npmrc
Normal file
13
frontend/.npmrc
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
registry = "https://registry.npmmirror.com"
|
||||||
|
public-hoist-pattern[]=lefthook
|
||||||
|
public-hoist-pattern[]=eslint
|
||||||
|
public-hoist-pattern[]=prettier
|
||||||
|
public-hoist-pattern[]=prettier-plugin-tailwindcss
|
||||||
|
public-hoist-pattern[]=stylelint
|
||||||
|
public-hoist-pattern[]=*postcss*
|
||||||
|
public-hoist-pattern[]=@commitlint/*
|
||||||
|
public-hoist-pattern[]=czg
|
||||||
|
|
||||||
|
strict-peer-dependencies=false
|
||||||
|
auto-install-peers=true
|
||||||
|
dedupe-peer-dependents=true
|
||||||
18
frontend/.prettierignore
Normal file
18
frontend/.prettierignore
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
dist
|
||||||
|
dev-dist
|
||||||
|
.local
|
||||||
|
.output.js
|
||||||
|
node_modules
|
||||||
|
.nvmrc
|
||||||
|
coverage
|
||||||
|
CODEOWNERS
|
||||||
|
.nitro
|
||||||
|
.output
|
||||||
|
|
||||||
|
|
||||||
|
**/*.svg
|
||||||
|
**/*.sh
|
||||||
|
|
||||||
|
public
|
||||||
|
.npmrc
|
||||||
|
*-lock.yaml
|
||||||
@ -1,5 +0,0 @@
|
|||||||
{
|
|
||||||
"singleQuote": true,
|
|
||||||
"semi": false,
|
|
||||||
"printWidth": 100
|
|
||||||
}
|
|
||||||
1
frontend/.prettierrc.mjs
Normal file
1
frontend/.prettierrc.mjs
Normal file
@ -0,0 +1 @@
|
|||||||
|
export { default } from '@vben/prettier-config';
|
||||||
4
frontend/.stylelintignore
Normal file
4
frontend/.stylelintignore
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
dist
|
||||||
|
public
|
||||||
|
__tests__
|
||||||
|
coverage
|
||||||
9
frontend/LICENSE
Normal file
9
frontend/LICENSE
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2024-present, Vben
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
153
frontend/README.ja-JP.md
Normal file
153
frontend/README.ja-JP.md
Normal file
@ -0,0 +1,153 @@
|
|||||||
|
<div align="center">
|
||||||
|
<a href="https://github.com/anncwb/vue-vben-admin">
|
||||||
|
<img alt="VbenAdmin Logo" width="215" src="https://unpkg.com/@vbenjs/static-source@0.1.7/source/logo-v1.webp">
|
||||||
|
</a>
|
||||||
|
<br>
|
||||||
|
<br>
|
||||||
|
|
||||||
|
[](LICENSE)
|
||||||
|
|
||||||
|
<h1>Vue Vben Admin</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
[](https://sonarcloud.io/summary/new_code?id=vbenjs_vue-vben-admin)    
|
||||||
|
|
||||||
|
**日本語** | [English](./README.md) | [中文](./README.zh-CN.md)
|
||||||
|
|
||||||
|
## 紹介
|
||||||
|
|
||||||
|
Vue Vben Adminは、最新の`vue3`、`vite`、`TypeScript`などの主流技術を使用して開発された、無料でオープンソースの中・後端テンプレートです。すぐに使える中・後端のフロントエンドソリューションとして、学習の参考にもなります。
|
||||||
|
|
||||||
|
## アップグレード通知
|
||||||
|
|
||||||
|
これは最新バージョン `5.0` であり、以前のバージョンとは互換性がありません。新しいプロジェクトを開始する場合は、最新バージョンを使用することをお勧めします。古いバージョンを表示したい場合は、[v2ブランチ](https://github.com/vbenjs/vue-vben-admin/tree/v2)を使用してください。
|
||||||
|
|
||||||
|
## 特徴
|
||||||
|
|
||||||
|
- **最新技術スタック**:Vue 3やViteなどの最先端フロントエンド技術で開発
|
||||||
|
- **TypeScript**:アプリケーション規模のJavaScriptのための言語
|
||||||
|
- **テーマ**:複数のテーマカラーが利用可能で、カスタマイズオプションも豊富
|
||||||
|
- **国際化**:完全な内蔵国際化サポート
|
||||||
|
- **権限管理**:動的ルートベースの権限生成ソリューションを内蔵
|
||||||
|
|
||||||
|
## プレビュー
|
||||||
|
|
||||||
|
- [Vben Admin](https://vben.pro/) - フルバージョンの中国語サイト
|
||||||
|
|
||||||
|
テストアカウント:vben/123456
|
||||||
|
|
||||||
|
<div align="center">
|
||||||
|
<img alt="VbenAdmin Logo" width="100%" src="https://anncwb.github.io/anncwb/images/preview1.png">
|
||||||
|
<img alt="VbenAdmin Logo" width="100%" src="https://anncwb.github.io/anncwb/images/preview2.png">
|
||||||
|
<img alt="VbenAdmin Logo" width="100%" src="https://anncwb.github.io/anncwb/images/preview3.png">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
### Gitpodを使用
|
||||||
|
|
||||||
|
Gitpod(GitHub用の無料オンライン開発環境)でプロジェクトを開き、すぐにコーディングを開始します。
|
||||||
|
|
||||||
|
[](https://gitpod.io/#https://github.com/vbenjs/vue-vben-admin)
|
||||||
|
|
||||||
|
## ドキュメント
|
||||||
|
|
||||||
|
[ドキュメント](https://doc.vben.pro/)
|
||||||
|
|
||||||
|
## インストールと使用
|
||||||
|
|
||||||
|
1. プロジェクトコードを取得
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/vbenjs/vue-vben-admin.git
|
||||||
|
```
|
||||||
|
|
||||||
|
2. 依存関係のインストール
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd vue-vben-admin
|
||||||
|
npm i -g corepack
|
||||||
|
pnpm install
|
||||||
|
```
|
||||||
|
|
||||||
|
3. 実行
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm dev
|
||||||
|
```
|
||||||
|
|
||||||
|
4. ビルド
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm build
|
||||||
|
```
|
||||||
|
|
||||||
|
## 変更ログ
|
||||||
|
|
||||||
|
[CHANGELOG](https://github.com/vbenjs/vue-vben-admin/releases)
|
||||||
|
|
||||||
|
## 貢献方法
|
||||||
|
|
||||||
|
ご参加をお待ちしておりますするか、Pull Requestを送信してください。
|
||||||
|
|
||||||
|
**Pull Request プロセス:**
|
||||||
|
|
||||||
|
1. コードをフォーク
|
||||||
|
2. 自分のブランチを作成:`git checkout -b feat/xxxx`
|
||||||
|
3. 変更をコミット:`git commit -am 'feat(function): add xxxxx'`
|
||||||
|
4. ブランチをプッシュ:`git push origin feat/xxxx`
|
||||||
|
5. `pull request`を送信
|
||||||
|
|
||||||
|
## Git貢献提出規則
|
||||||
|
|
||||||
|
参考 [vue](https://github.com/vuejs/vue/blob/dev/.github/COMMIT_CONVENTION.md) 規則 ([Angular](https://github.com/conventional-changelog/conventional-changelog/tree/master/packages/conventional-changelog-angular))
|
||||||
|
|
||||||
|
- `feat` 新機能の追加
|
||||||
|
- `fix` 問題/バグの修正
|
||||||
|
- `style` コードスタイルに関連し、実行結果に影響しない
|
||||||
|
- `perf` 最適化/パフォーマンス向上
|
||||||
|
- `refactor` リファクタリング
|
||||||
|
- `revert` 変更の取り消し
|
||||||
|
- `test` テスト関連
|
||||||
|
- `docs` ドキュメント/注釈
|
||||||
|
- `chore` 依存関係の更新/スキャフォールディング設定の変更など
|
||||||
|
- `ci` 継続的インテグレーション
|
||||||
|
- `types` 型定義ファイルの変更
|
||||||
|
|
||||||
|
## ブラウザサポート
|
||||||
|
|
||||||
|
ローカル開発には `Chrome 80+` ブラウザを推奨します
|
||||||
|
|
||||||
|
モダンブラウザをサポートし、IEはサポートしません
|
||||||
|
|
||||||
|
| [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/edge/edge_48x48.png" alt="Edge" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Edge | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/firefox/firefox_48x48.png" alt="Firefox" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Firefox | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/chrome/chrome_48x48.png" alt="Chrome" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Chrome | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/safari/safari_48x48.png" alt="Safari" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Safari |
|
||||||
|
| :-: | :-: | :-: | :-: |
|
||||||
|
| 最新2バージョン | 最新2バージョン | 最新2バージョン | 最新2バージョン |
|
||||||
|
|
||||||
|
## メンテナー
|
||||||
|
|
||||||
|
[@Vben](https://github.com/anncwb)
|
||||||
|
|
||||||
|
## スター歴史
|
||||||
|
|
||||||
|
[](https://star-history.com/#vbenjs/vue-vben-admin&Date)
|
||||||
|
|
||||||
|
## 寄付
|
||||||
|
|
||||||
|
このプロジェクトが役に立つと思われた場合、作者にコーヒーを一杯おごってサポートを示すことができます!
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
<a style="display: block;width: 100px;height: 50px;line-height: 50px; color: #fff;text-align: center; background: #408aed;border-radius: 4px;" href="https://www.paypal.com/paypalme/cvvben">Paypal Me</a>
|
||||||
|
|
||||||
|
## 貢献者
|
||||||
|
|
||||||
|
<a href="https://github.com/vbenjs/vue-vben-admin/graphs/contributors">
|
||||||
|
<img alt="Contributors" src="https://opencollective.com/vbenjs/contributors.svg?button=false" />
|
||||||
|
</a>
|
||||||
|
|
||||||
|
## Discord
|
||||||
|
|
||||||
|
- [Github Discussions](https://github.com/anncwb/vue-vben-admin/discussions)
|
||||||
|
|
||||||
|
## ライセンス
|
||||||
|
|
||||||
|
[MIT © Vben-2020](./LICENSE)
|
||||||
@ -1,33 +1,153 @@
|
|||||||
# 前端应用(flowable-devops-frontend)
|
<div align="center">
|
||||||
|
<a href="https://github.com/anncwb/vue-vben-admin">
|
||||||
|
<img alt="VbenAdmin Logo" width="215" src="https://unpkg.com/@vbenjs/static-source@0.1.7/source/logo-v1.webp">
|
||||||
|
</a>
|
||||||
|
<br>
|
||||||
|
<br>
|
||||||
|
|
||||||
技术栈
|
[](LICENSE)
|
||||||
- React 18 + TypeScript + Vite
|
|
||||||
- Ant Design 5、ReactFlow 11、Zustand、Axios、Day.js
|
|
||||||
|
|
||||||
安装与开发
|
<h1>Vue Vben Admin</h1>
|
||||||
- 安装依赖:
|
</div>
|
||||||
npm i
|
|
||||||
- 开发启动:
|
|
||||||
npm run dev
|
|
||||||
- 构建生产包:
|
|
||||||
npm run build
|
|
||||||
- 预览本地构建:
|
|
||||||
npm run preview
|
|
||||||
|
|
||||||
环境变量
|
[](https://sonarcloud.io/summary/new_code?id=vbenjs_vue-vben-admin)    
|
||||||
- 复制 .env.example 为 .env,并按需调整:
|
|
||||||
VITE_API_BASE_URL=http://localhost:8080
|
|
||||||
|
|
||||||
与后端联调约定(关键)
|
**English** | [中文](./README.zh-CN.md) | [日本語](./README.ja-JP.md)
|
||||||
- JSON Schema:见仓库 docs/04-数据模型设计.md
|
|
||||||
- 接口契约:见仓库 docs/05-API契约.md
|
|
||||||
- 表达式:统一 ${...},仅 Map 属性访问,前端不执行表达式,仅存储与展示
|
|
||||||
|
|
||||||
目录结构(初始)
|
## Introduction
|
||||||
- src/App.tsx:占位入口
|
|
||||||
- src/main.tsx:应用挂载
|
|
||||||
- src/index.css:全局样式占位
|
|
||||||
|
|
||||||
后续建议
|
Vue Vben Admin is a free and open source middle and back-end template. Using the latest `vue3`, `vite`, `TypeScript` and other mainstream technology development, the out-of-the-box middle and back-end front-end solutions can also be used for learning reference.
|
||||||
- 新增 src/api、src/store、src/components、src/pages 目录,与文档中页面/状态模块对齐
|
|
||||||
- 引入 AntD 样式(按需加载或全量),并配置主题
|
## Upgrade Notice
|
||||||
|
|
||||||
|
This is the latest version, 5.0, and it is not compatible with previous versions. If you are starting a new project, it is recommended to use the latest version. If you wish to view the old version, please use the [v2 branch](https://github.com/vbenjs/vue-vben-admin/tree/v2).
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- **Latest Technology Stack**: Developed with cutting-edge front-end technologies like Vue 3 and Vite
|
||||||
|
- **TypeScript**: A language for application-scale JavaScript
|
||||||
|
- **Themes**: Multiple theme colors available with customizable options
|
||||||
|
- **Internationalization**: Comprehensive built-in internationalization support
|
||||||
|
- **Permissions**: Built-in solution for dynamic route-based permission generation
|
||||||
|
|
||||||
|
## Preview
|
||||||
|
|
||||||
|
- [Vben Admin](https://vben.pro/) - Full version Chinese site
|
||||||
|
|
||||||
|
Test Account: vben/123456
|
||||||
|
|
||||||
|
<div align="center">
|
||||||
|
<img alt="VbenAdmin Logo" width="100%" src="https://anncwb.github.io/anncwb/images/preview1.png">
|
||||||
|
<img alt="VbenAdmin Logo" width="100%" src="https://anncwb.github.io/anncwb/images/preview2.png">
|
||||||
|
<img alt="VbenAdmin Logo" width="100%" src="https://anncwb.github.io/anncwb/images/preview3.png">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
### Use Gitpod
|
||||||
|
|
||||||
|
Open the project in Gitpod (free online dev environment for GitHub) and start coding immediately.
|
||||||
|
|
||||||
|
[](https://gitpod.io/#https://github.com/vbenjs/vue-vben-admin)
|
||||||
|
|
||||||
|
## Documentation
|
||||||
|
|
||||||
|
[Document](https://doc.vben.pro/)
|
||||||
|
|
||||||
|
## Install and Use
|
||||||
|
|
||||||
|
1. Get the project code
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/vbenjs/vue-vben-admin.git
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Install dependencies
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd vue-vben-admin
|
||||||
|
npm i -g corepack
|
||||||
|
pnpm install
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Run
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm dev
|
||||||
|
```
|
||||||
|
|
||||||
|
4. Build
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm build
|
||||||
|
```
|
||||||
|
|
||||||
|
## Change Log
|
||||||
|
|
||||||
|
[CHANGELOG](https://github.com/vbenjs/vue-vben-admin/releases)
|
||||||
|
|
||||||
|
## How to Contribute
|
||||||
|
|
||||||
|
You are very welcome to join! [Raise an issue](https://github.com/anncwb/vue-vben-admin/issues/new/choose) or submit a Pull Request.
|
||||||
|
|
||||||
|
**Pull Request Process:**
|
||||||
|
|
||||||
|
1. Fork the code
|
||||||
|
2. Create your branch: `git checkout -b feat/xxxx`
|
||||||
|
3. Submit your changes: `git commit -am 'feat(function): add xxxxx'`
|
||||||
|
4. Push your branch: `git push origin feat/xxxx`
|
||||||
|
5. Submit `pull request`
|
||||||
|
|
||||||
|
## Git Contribution Submission Specification
|
||||||
|
|
||||||
|
Reference [vue](https://github.com/vuejs/vue/blob/dev/.github/COMMIT_CONVENTION.md) specification ([Angular](https://github.com/conventional-changelog/conventional-changelog/tree/master/packages/conventional-changelog-angular))
|
||||||
|
|
||||||
|
- `feat` Add new features
|
||||||
|
- `fix` Fix the problem/BUG
|
||||||
|
- `style` The code style is related and does not affect the running result
|
||||||
|
- `perf` Optimization/performance improvement
|
||||||
|
- `refactor` Refactor
|
||||||
|
- `revert` Undo edit
|
||||||
|
- `test` Test related
|
||||||
|
- `docs` Documentation/notes
|
||||||
|
- `chore` Dependency update/scaffolding configuration modification etc.
|
||||||
|
- `ci` Continuous integration
|
||||||
|
- `types` Type definition file changes
|
||||||
|
|
||||||
|
## Browser Support
|
||||||
|
|
||||||
|
The `Chrome 80+` browser is recommended for local development
|
||||||
|
|
||||||
|
Support modern browsers, not IE
|
||||||
|
|
||||||
|
| [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/edge/edge_48x48.png" alt="Edge" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Edge | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/firefox/firefox_48x48.png" alt="Firefox" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Firefox | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/chrome/chrome_48x48.png" alt="Chrome" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Chrome | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/safari/safari_48x48.png" alt="Safari" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Safari |
|
||||||
|
| :-: | :-: | :-: | :-: |
|
||||||
|
| last 2 versions | last 2 versions | last 2 versions | last 2 versions |
|
||||||
|
|
||||||
|
## Maintainer
|
||||||
|
|
||||||
|
[@Vben](https://github.com/anncwb)
|
||||||
|
|
||||||
|
## Star History
|
||||||
|
|
||||||
|
[](https://star-history.com/#vbenjs/vue-vben-admin&Date)
|
||||||
|
|
||||||
|
## Donate
|
||||||
|
|
||||||
|
If you think this project is helpful to you, you can help the author buy a cup of coffee to show your support!
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
<a style="display: block;width: 100px;height: 50px;line-height: 50px; color: #fff;text-align: center; background: #408aee;border-radius: 4px;" href="https://www.paypal.com/paypalme/cvvben">Paypal Me</a>
|
||||||
|
|
||||||
|
## Contributors
|
||||||
|
|
||||||
|
<a href="https://github.com/vbenjs/vue-vben-admin/graphs/contributors">
|
||||||
|
<img alt="Contributors" src="https://opencollective.com/vbenjs/contributors.svg?button=false" />
|
||||||
|
</a>
|
||||||
|
|
||||||
|
## Discord
|
||||||
|
|
||||||
|
- [Github Discussions](https://github.com/anncwb/vue-vben-admin/discussions)
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
[MIT © Vben-2020](./LICENSE)
|
||||||
|
|||||||
153
frontend/README.zh-CN.md
Normal file
153
frontend/README.zh-CN.md
Normal file
@ -0,0 +1,153 @@
|
|||||||
|
<div align="center">
|
||||||
|
<a href="https://github.com/anncwb/vue-vben-admin">
|
||||||
|
<img alt="VbenAdmin Logo" width="215" src="https://unpkg.com/@vbenjs/static-source@0.1.7/source/logo-v1.webp">
|
||||||
|
</a>
|
||||||
|
<br>
|
||||||
|
<br>
|
||||||
|
|
||||||
|
[](LICENSE)
|
||||||
|
|
||||||
|
<h1>Vue Vben Admin</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
[](https://sonarcloud.io/summary/new_code?id=vbenjs_vue-vben-admin)    
|
||||||
|
|
||||||
|
**中文** | [English](./README.md) | [日本語](./README.ja-JP.md)
|
||||||
|
|
||||||
|
## 简介
|
||||||
|
|
||||||
|
Vue Vben Admin 是 Vue Vben Admin 的升级版本。作为一个免费开源的中后台模板,它采用了最新的 Vue 3、Vite、TypeScript 等主流技术开发,开箱即用,可用于中后台前端开发,也适合学习参考。
|
||||||
|
|
||||||
|
## 升级提示
|
||||||
|
|
||||||
|
该版本为最新版本 `5.0`,与其他版本不兼容,如果你是新项目,建议使用最新版本。如果你想查看旧版本,请使用 [v2 分支](https://github.com/vbenjs/vue-vben-admin/tree/v2)
|
||||||
|
|
||||||
|
## 特性
|
||||||
|
|
||||||
|
- **最新技术栈**:使用 Vue3/vite 等前端前沿技术开发
|
||||||
|
- **TypeScript**:应用程序级 JavaScript 的语言
|
||||||
|
- **主题**:提供多套主题色彩,可配置自定义主题
|
||||||
|
- **国际化**:内置完善的国际化方案
|
||||||
|
- **权限**:内置完善的动态路由权限生成方案
|
||||||
|
|
||||||
|
## 预览
|
||||||
|
|
||||||
|
- [Vben Admin](https://vben.pro/) - 完整版中文站点
|
||||||
|
|
||||||
|
测试账号:vben/123456
|
||||||
|
|
||||||
|
<div align="center">
|
||||||
|
<img alt="VbenAdmin Logo" width="100%" src="https://anncwb.github.io/anncwb/images/preview1.png">
|
||||||
|
<img alt="VbenAdmin Logo" width="100%" src="https://anncwb.github.io/anncwb/images/preview2.png">
|
||||||
|
<img alt="VbenAdmin Logo" width="100%" src="https://anncwb.github.io/anncwb/images/preview3.png">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
### 使用 Gitpod
|
||||||
|
|
||||||
|
在 Gitpod(适用于 GitHub 的免费在线开发环境)中打开项目,并立即开始编码。
|
||||||
|
|
||||||
|
[](https://gitpod.io/#https://github.com/vbenjs/vue-vben-admin)
|
||||||
|
|
||||||
|
## 文档
|
||||||
|
|
||||||
|
[文档地址](https://doc.vben.pro/)
|
||||||
|
|
||||||
|
## 安装使用
|
||||||
|
|
||||||
|
1. 获取项目代码
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/vbenjs/vue-vben-admin.git
|
||||||
|
```
|
||||||
|
|
||||||
|
2. 安装依赖
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd vue-vben-admin
|
||||||
|
npm i -g corepack
|
||||||
|
pnpm install
|
||||||
|
```
|
||||||
|
|
||||||
|
3. 运行
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm dev
|
||||||
|
```
|
||||||
|
|
||||||
|
4. 打包
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm build
|
||||||
|
```
|
||||||
|
|
||||||
|
## 更新日志
|
||||||
|
|
||||||
|
[CHANGELOG](https://github.com/vbenjs/vue-vben-admin/releases)
|
||||||
|
|
||||||
|
## 如何贡献
|
||||||
|
|
||||||
|
非常欢迎你的加入 或者提交一个 Pull Request。
|
||||||
|
|
||||||
|
**Pull Request 流程:**
|
||||||
|
|
||||||
|
1. Fork 代码
|
||||||
|
2. 创建自己的分支:`git checkout -b feature/xxxx`
|
||||||
|
3. 提交你的修改:`git commit -am 'feat(function): add xxxxx'`
|
||||||
|
4. 推送您的分支:`git push origin feature/xxxx`
|
||||||
|
5. 提交 `pull request`
|
||||||
|
|
||||||
|
## Git 贡献提交规范
|
||||||
|
|
||||||
|
参考 [vue](https://github.com/vuejs/vue/blob/dev/.github/COMMIT_CONVENTION.md) 规范 ([Angular](https://github.com/conventional-changelog/conventional-changelog/tree/master/packages/conventional-changelog-angular))
|
||||||
|
|
||||||
|
- `feat` 增加新功能
|
||||||
|
- `fix` 修复问题/BUG
|
||||||
|
- `style` 代码风格相关无影响运行结果的
|
||||||
|
- `perf` 优化/性能提升
|
||||||
|
- `refactor` 重构
|
||||||
|
- `revert` 撤销修改
|
||||||
|
- `test` 测试相关
|
||||||
|
- `docs` 文档/注释
|
||||||
|
- `chore` 依赖更新/脚手架配置修改等
|
||||||
|
- `ci` 持续集成
|
||||||
|
- `types` 类型定义文件更改
|
||||||
|
|
||||||
|
## 浏览器支持
|
||||||
|
|
||||||
|
本地开发推荐使用 `Chrome 80+` 浏览器
|
||||||
|
|
||||||
|
支持现代浏览器,不支持 IE
|
||||||
|
|
||||||
|
| [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/edge/edge_48x48.png" alt="Edge" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Edge | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/firefox/firefox_48x48.png" alt="Firefox" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Firefox | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/chrome/chrome_48x48.png" alt="Chrome" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Chrome | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/safari/safari_48x48.png" alt="Safari" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Safari |
|
||||||
|
| :-: | :-: | :-: | :-: |
|
||||||
|
| last 2 versions | last 2 versions | last 2 versions | last 2 versions |
|
||||||
|
|
||||||
|
## 维护者
|
||||||
|
|
||||||
|
[@Vben](https://github.com/anncwb)
|
||||||
|
|
||||||
|
## Star 历史
|
||||||
|
|
||||||
|
[](https://star-history.com/#vbenjs/vue-vben-admin&Date)
|
||||||
|
|
||||||
|
## 捐赠
|
||||||
|
|
||||||
|
如果你觉得这个项目对你有帮助,你可以帮作者买一杯咖啡表示支持!
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
<a style="display: block;width: 100px;height: 50px;line-height: 50px; color: #fff;text-align: center; background: #408aed;border-radius: 4px;" href="https://www.paypal.com/paypalme/cvvben">Paypal Me</a>
|
||||||
|
|
||||||
|
## 贡献者
|
||||||
|
|
||||||
|
<a href="https://github.com/vbenjs/vue-vben-admin/graphs/contributors">
|
||||||
|
<img alt="Contributors" src="https://opencollective.com/vbenjs/contributors.svg?button=false" />
|
||||||
|
</a>
|
||||||
|
|
||||||
|
## Discord
|
||||||
|
|
||||||
|
- [Github Discussions](https://github.com/anncwb/vue-vben-admin/discussions)
|
||||||
|
|
||||||
|
## 许可证
|
||||||
|
|
||||||
|
[MIT © Vben-2020](./LICENSE)
|
||||||
8
frontend/apps/web-antd/.env
Normal file
8
frontend/apps/web-antd/.env
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
# 应用标题
|
||||||
|
VITE_APP_TITLE=Vben Admin Antd
|
||||||
|
|
||||||
|
# 应用命名空间,用于缓存、store等功能的前缀,确保隔离
|
||||||
|
VITE_APP_NAMESPACE=vben-web-antd
|
||||||
|
|
||||||
|
# 对store进行加密的密钥,在将store持久化到localStorage时会使用该密钥进行加密
|
||||||
|
VITE_APP_STORE_SECURE_KEY=please-replace-me-with-your-own-key
|
||||||
7
frontend/apps/web-antd/.env.analyze
Normal file
7
frontend/apps/web-antd/.env.analyze
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
# public path
|
||||||
|
VITE_BASE=/
|
||||||
|
|
||||||
|
# Basic interface address SPA
|
||||||
|
VITE_GLOB_API_URL=/api
|
||||||
|
|
||||||
|
VITE_VISUALIZER=true
|
||||||
16
frontend/apps/web-antd/.env.development
Normal file
16
frontend/apps/web-antd/.env.development
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
# 端口号
|
||||||
|
VITE_PORT=5666
|
||||||
|
|
||||||
|
VITE_BASE=/
|
||||||
|
|
||||||
|
# 接口地址
|
||||||
|
VITE_GLOB_API_URL=/api
|
||||||
|
|
||||||
|
# 是否开启 Nitro Mock服务,true 为开启,false 为关闭
|
||||||
|
VITE_NITRO_MOCK=false
|
||||||
|
|
||||||
|
# 是否打开 devtools,true 为打开,false 为关闭
|
||||||
|
VITE_DEVTOOLS=false
|
||||||
|
|
||||||
|
# 是否注入全局loading
|
||||||
|
VITE_INJECT_APP_LOADING=true
|
||||||
19
frontend/apps/web-antd/.env.production
Normal file
19
frontend/apps/web-antd/.env.production
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
VITE_BASE=/
|
||||||
|
|
||||||
|
# 接口地址
|
||||||
|
VITE_GLOB_API_URL=https://mock-napi.vben.pro/api
|
||||||
|
|
||||||
|
# 是否开启压缩,可以设置为 none, brotli, gzip
|
||||||
|
VITE_COMPRESS=none
|
||||||
|
|
||||||
|
# 是否开启 PWA
|
||||||
|
VITE_PWA=false
|
||||||
|
|
||||||
|
# vue-router 的模式
|
||||||
|
VITE_ROUTER_HISTORY=hash
|
||||||
|
|
||||||
|
# 是否注入全局loading
|
||||||
|
VITE_INJECT_APP_LOADING=true
|
||||||
|
|
||||||
|
# 打包后是否生成dist.zip
|
||||||
|
VITE_ARCHIVER=true
|
||||||
813
frontend/apps/web-antd/CLAUDE.md
Normal file
813
frontend/apps/web-antd/CLAUDE.md
Normal file
@ -0,0 +1,813 @@
|
|||||||
|
# Vben Admin Web-Antd AI 开发规则手册
|
||||||
|
|
||||||
|
## 项目概述
|
||||||
|
|
||||||
|
本项目基于 **Vben Admin 5.x** 框架开发,使用 Vue 3 + TypeScript + Ant Design Vue。项目采用 **monorepo** 结构,当前应用位于 `apps/web-antd`,依赖 `packages` 目录下的框架核心包。
|
||||||
|
|
||||||
|
### 技术栈
|
||||||
|
- **框架**: Vue 3.5+ (Composition API)
|
||||||
|
- **语言**: TypeScript 5.8+
|
||||||
|
- **UI 库**: Ant Design Vue 4.x
|
||||||
|
- **状态管理**: Pinia
|
||||||
|
- **路由**: Vue Router 4.x
|
||||||
|
- **构建工具**: Vite 6.x
|
||||||
|
- **包管理器**: pnpm (必须使用,不能用 npm/yarn)
|
||||||
|
- **图标**: Iconify + Lucide
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 项目结构
|
||||||
|
|
||||||
|
```
|
||||||
|
apps/web-antd/
|
||||||
|
├── src/
|
||||||
|
│ ├── api/ # API 请求封装
|
||||||
|
│ │ ├── core/ # 核心 API (auth, menu, user)
|
||||||
|
│ │ ├── request.ts # 请求客户端配置
|
||||||
|
│ │ └── index.ts # 导出所有 API
|
||||||
|
│ ├── router/ # 路由配置
|
||||||
|
│ │ ├── routes/ # 路由定义
|
||||||
|
│ │ │ ├── core/ # 核心路由 (login, 404等)
|
||||||
|
│ │ │ └── modules/ # 业务路由模块
|
||||||
|
│ │ └── index.ts # 路由实例
|
||||||
|
│ ├── store/ # Pinia 状态管理
|
||||||
|
│ ├── views/ # 页面组件
|
||||||
|
│ │ ├── _core/ # 核心页面 (login, 404)
|
||||||
|
│ │ ├── dashboard/ # 仪表盘
|
||||||
|
│ │ └── demos/ # 示例页面
|
||||||
|
│ ├── layouts/ # 布局组件(如需自定义)
|
||||||
|
│ ├── locales/ # 国际化配置
|
||||||
|
│ ├── adapter/ # 组件适配器
|
||||||
|
│ ├── app.vue # 根组件
|
||||||
|
│ ├── main.ts # 入口文件
|
||||||
|
│ └── preferences.ts # 应用偏好设置
|
||||||
|
├── package.json
|
||||||
|
└── vite.config.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 核心开发规范
|
||||||
|
|
||||||
|
### 1. 命名规范
|
||||||
|
|
||||||
|
#### 文件命名
|
||||||
|
- **组件文件**: PascalCase,如 `UserProfile.vue`, `DataTable.vue`
|
||||||
|
- **工具文件**: kebab-case,如 `format-date.ts`, `use-table.ts`
|
||||||
|
- **API 文件**: kebab-case,如 `user-api.ts`, `order-api.ts`
|
||||||
|
- **路由文件**: kebab-case,如 `user-management.ts`
|
||||||
|
|
||||||
|
#### 变量命名
|
||||||
|
- **组件变量**: PascalCase,如 `const UserTable = defineComponent(...)`
|
||||||
|
- **普通变量**: camelCase,如 `const userInfo = ref({})`
|
||||||
|
- **常量**: UPPER_SNAKE_CASE,如 `const API_BASE_URL = '...'`
|
||||||
|
- **类型/接口**: PascalCase,如 `interface UserInfo { ... }`
|
||||||
|
|
||||||
|
### 2. Vue 组件规范
|
||||||
|
|
||||||
|
**必须使用 Composition API + `<script setup>`**:
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { ref, computed, onMounted } from 'vue';
|
||||||
|
import type { UserInfo } from '#/api/types';
|
||||||
|
|
||||||
|
// 定义组件名(可选,但推荐)
|
||||||
|
defineOptions({ name: 'UserProfile' });
|
||||||
|
|
||||||
|
// Props 定义
|
||||||
|
interface Props {
|
||||||
|
userId: string;
|
||||||
|
showActions?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
showActions: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Emits 定义
|
||||||
|
const emit = defineEmits<{
|
||||||
|
update: [user: UserInfo];
|
||||||
|
delete: [id: string];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
// 响应式数据
|
||||||
|
const loading = ref(false);
|
||||||
|
const userInfo = ref<UserInfo | null>(null);
|
||||||
|
|
||||||
|
// 计算属性
|
||||||
|
const displayName = computed(() => {
|
||||||
|
return userInfo.value?.name || 'Unknown';
|
||||||
|
});
|
||||||
|
|
||||||
|
// 方法
|
||||||
|
async function fetchUser() {
|
||||||
|
loading.value = true;
|
||||||
|
try {
|
||||||
|
const data = await getUserApi(props.userId);
|
||||||
|
userInfo.value = data;
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 生命周期
|
||||||
|
onMounted(() => {
|
||||||
|
fetchUser();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="user-profile">
|
||||||
|
<a-spin :spinning="loading">
|
||||||
|
<h3>{{ displayName }}</h3>
|
||||||
|
</a-spin>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.user-profile {
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 导入路径别名
|
||||||
|
|
||||||
|
使用 `#/` 别名指向 `src/` 目录:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// ✅ 正确
|
||||||
|
import { useAuthStore } from '#/store';
|
||||||
|
import type { UserInfo } from '#/api/types';
|
||||||
|
import { $t } from '#/locales';
|
||||||
|
|
||||||
|
// ❌ 错误 - 不要使用相对路径
|
||||||
|
import { useAuthStore } from '../../../store';
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. 框架组件导入
|
||||||
|
|
||||||
|
框架提供的组件从 `@vben/*` 包导入:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// UI 组件
|
||||||
|
import { Button, Modal, Drawer } from '@vben/common-ui';
|
||||||
|
import { Page, Card } from '@vben/common-ui';
|
||||||
|
import { VbenForm, VbenTable } from '@vben/common-ui';
|
||||||
|
|
||||||
|
// 布局组件
|
||||||
|
import { BasicLayout, PageWrapper } from '@vben/layouts';
|
||||||
|
|
||||||
|
// 图标
|
||||||
|
import { SvgIcon, IconSvg } from '@vben/icons';
|
||||||
|
|
||||||
|
// Hooks
|
||||||
|
import { usePreferences, useAppConfig } from '@vben/hooks';
|
||||||
|
|
||||||
|
// 工具函数
|
||||||
|
import { formatDate, formatMoney } from '@vben/utils';
|
||||||
|
|
||||||
|
// 权限
|
||||||
|
import { useAccess } from '@vben/access';
|
||||||
|
|
||||||
|
// Store
|
||||||
|
import { useAccessStore, useUserStore } from '@vben/stores';
|
||||||
|
|
||||||
|
// 请求
|
||||||
|
import { RequestClient } from '@vben/request';
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 开发模式详解
|
||||||
|
|
||||||
|
### 1. 创建新页面
|
||||||
|
|
||||||
|
#### 步骤 1: 创建视图组件
|
||||||
|
|
||||||
|
在 `src/views/` 下创建页面组件:
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<!-- src/views/user/user-list.vue -->
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { ref, onMounted } from 'vue';
|
||||||
|
import { VbenTable } from '@vben/common-ui';
|
||||||
|
import type { VbenTableColumn } from '@vben/common-ui';
|
||||||
|
import { getUserListApi } from '#/api/user';
|
||||||
|
|
||||||
|
defineOptions({ name: 'UserList' });
|
||||||
|
|
||||||
|
const loading = ref(false);
|
||||||
|
const dataSource = ref([]);
|
||||||
|
|
||||||
|
const columns: VbenTableColumn[] = [
|
||||||
|
{ title: '用户名', dataIndex: 'username', key: 'username' },
|
||||||
|
{ title: '邮箱', dataIndex: 'email', key: 'email' },
|
||||||
|
{ title: '状态', dataIndex: 'status', key: 'status' },
|
||||||
|
];
|
||||||
|
|
||||||
|
async function fetchData() {
|
||||||
|
loading.value = true;
|
||||||
|
try {
|
||||||
|
const data = await getUserListApi();
|
||||||
|
dataSource.value = data;
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
fetchData();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="p-5">
|
||||||
|
<VbenTable
|
||||||
|
:columns="columns"
|
||||||
|
:data-source="dataSource"
|
||||||
|
:loading="loading"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 步骤 2: 添加路由配置
|
||||||
|
|
||||||
|
在 `src/router/routes/modules/` 下创建路由模块:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// src/router/routes/modules/user.ts
|
||||||
|
import type { RouteRecordRaw } from 'vue-router';
|
||||||
|
import { $t } from '#/locales';
|
||||||
|
|
||||||
|
const routes: RouteRecordRaw[] = [
|
||||||
|
{
|
||||||
|
meta: {
|
||||||
|
icon: 'lucide:users',
|
||||||
|
title: '用户管理',
|
||||||
|
order: 10, // 菜单排序
|
||||||
|
},
|
||||||
|
name: 'User',
|
||||||
|
path: '/user',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
name: 'UserList',
|
||||||
|
path: '/user/list',
|
||||||
|
component: () => import('#/views/user/user-list.vue'),
|
||||||
|
meta: {
|
||||||
|
icon: 'lucide:user-cog',
|
||||||
|
title: '用户列表',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'UserDetail',
|
||||||
|
path: '/user/detail/:id',
|
||||||
|
component: () => import('#/views/user/user-detail.vue'),
|
||||||
|
meta: {
|
||||||
|
hideInMenu: true, // 不在菜单中显示
|
||||||
|
title: '用户详情',
|
||||||
|
activeMenu: '/user/list', // 激活父菜单
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export default routes;
|
||||||
|
```
|
||||||
|
|
||||||
|
**路由配置要点:**
|
||||||
|
- 必须 `export default routes`
|
||||||
|
- `component` 使用动态导入 `() => import(...)`
|
||||||
|
- 使用 `#/views/` 别名
|
||||||
|
- 图标使用 Iconify 格式,如 `lucide:users`
|
||||||
|
- `meta.order` 控制菜单顺序(数字越小越靠前)
|
||||||
|
|
||||||
|
#### 步骤 3: 添加国际化(可选)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// src/locales/lang/zh-CN/user.ts
|
||||||
|
export default {
|
||||||
|
userManagement: '用户管理',
|
||||||
|
userList: '用户列表',
|
||||||
|
userName: '用户名',
|
||||||
|
email: '邮箱',
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. API 请求规范
|
||||||
|
|
||||||
|
#### 在 `src/api/` 下创建 API 模块:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// src/api/user.ts
|
||||||
|
import { requestClient } from './request';
|
||||||
|
|
||||||
|
export interface UserInfo {
|
||||||
|
id: string;
|
||||||
|
username: string;
|
||||||
|
email: string;
|
||||||
|
status: 'active' | 'inactive';
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UserListParams {
|
||||||
|
page?: number;
|
||||||
|
pageSize?: number;
|
||||||
|
keyword?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取用户列表
|
||||||
|
*/
|
||||||
|
export async function getUserListApi(params?: UserListParams) {
|
||||||
|
return requestClient.get<UserInfo[]>('/user/list', { params });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取用户详情
|
||||||
|
*/
|
||||||
|
export async function getUserDetailApi(id: string) {
|
||||||
|
return requestClient.get<UserInfo>(`/user/${id}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建用户
|
||||||
|
*/
|
||||||
|
export async function createUserApi(data: Partial<UserInfo>) {
|
||||||
|
return requestClient.post<UserInfo>('/user', data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新用户
|
||||||
|
*/
|
||||||
|
export async function updateUserApi(id: string, data: Partial<UserInfo>) {
|
||||||
|
return requestClient.put<UserInfo>(`/user/${id}`, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除用户
|
||||||
|
*/
|
||||||
|
export async function deleteUserApi(id: string) {
|
||||||
|
return requestClient.delete(`/user/${id}`);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**API 规范:**
|
||||||
|
- 使用 `requestClient` 发起请求(已配置拦截器、token、错误处理)
|
||||||
|
- 函数名以 `Api` 结尾
|
||||||
|
- 使用 TypeScript 定义请求参数和返回类型
|
||||||
|
- 添加 JSDoc 注释说明用途
|
||||||
|
|
||||||
|
### 3. 状态管理 (Pinia)
|
||||||
|
|
||||||
|
#### 创建 Store:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// src/store/modules/user.ts
|
||||||
|
import { defineStore } from 'pinia';
|
||||||
|
import { ref, computed } from 'vue';
|
||||||
|
import type { UserInfo } from '#/api/types';
|
||||||
|
import { getUserDetailApi } from '#/api/user';
|
||||||
|
|
||||||
|
export const useUserStore = defineStore('user', () => {
|
||||||
|
// State
|
||||||
|
const userInfo = ref<UserInfo | null>(null);
|
||||||
|
const loading = ref(false);
|
||||||
|
|
||||||
|
// Getters
|
||||||
|
const isLoggedIn = computed(() => !!userInfo.value);
|
||||||
|
const userName = computed(() => userInfo.value?.username || 'Guest');
|
||||||
|
|
||||||
|
// Actions
|
||||||
|
async function fetchUserInfo() {
|
||||||
|
loading.value = true;
|
||||||
|
try {
|
||||||
|
const data = await getUserDetailApi('me');
|
||||||
|
userInfo.value = data;
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearUser() {
|
||||||
|
userInfo.value = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
// State
|
||||||
|
userInfo,
|
||||||
|
loading,
|
||||||
|
// Getters
|
||||||
|
isLoggedIn,
|
||||||
|
userName,
|
||||||
|
// Actions
|
||||||
|
fetchUserInfo,
|
||||||
|
clearUser,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
**使用 Store:**
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { useUserStore } from '#/store';
|
||||||
|
|
||||||
|
const userStore = useUserStore();
|
||||||
|
|
||||||
|
// 读取状态
|
||||||
|
console.log(userStore.userName);
|
||||||
|
|
||||||
|
// 调用方法
|
||||||
|
userStore.fetchUserInfo();
|
||||||
|
</script>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. 使用框架表单组件 (VbenForm)
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from 'vue';
|
||||||
|
import { VbenForm } from '@vben/common-ui';
|
||||||
|
import type { VbenFormSchema } from '@vben/common-ui';
|
||||||
|
import { message } from 'ant-design-vue';
|
||||||
|
|
||||||
|
const formSchema: VbenFormSchema[] = [
|
||||||
|
{
|
||||||
|
component: 'Input',
|
||||||
|
label: '用户名',
|
||||||
|
fieldName: 'username',
|
||||||
|
required: true,
|
||||||
|
rules: [{ required: true, message: '请输入用户名' }],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
component: 'Input',
|
||||||
|
label: '邮箱',
|
||||||
|
fieldName: 'email',
|
||||||
|
required: true,
|
||||||
|
componentProps: {
|
||||||
|
type: 'email',
|
||||||
|
},
|
||||||
|
rules: [
|
||||||
|
{ required: true, message: '请输入邮箱' },
|
||||||
|
{ type: 'email', message: '邮箱格式不正确' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
component: 'Select',
|
||||||
|
label: '状态',
|
||||||
|
fieldName: 'status',
|
||||||
|
componentProps: {
|
||||||
|
options: [
|
||||||
|
{ label: '启用', value: 'active' },
|
||||||
|
{ label: '禁用', value: 'inactive' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
component: 'DatePicker',
|
||||||
|
label: '出生日期',
|
||||||
|
fieldName: 'birthday',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const formValue = ref({});
|
||||||
|
|
||||||
|
async function handleSubmit(values: any) {
|
||||||
|
console.log('提交数据:', values);
|
||||||
|
message.success('保存成功');
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="p-5">
|
||||||
|
<VbenForm
|
||||||
|
v-model="formValue"
|
||||||
|
:schema="formSchema"
|
||||||
|
@submit="handleSubmit"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. 使用框架表格组件 (VbenTable)
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from 'vue';
|
||||||
|
import { VbenTable } from '@vben/common-ui';
|
||||||
|
import type { VbenTableColumn } from '@vben/common-ui';
|
||||||
|
import { Button, Tag } from 'ant-design-vue';
|
||||||
|
|
||||||
|
const columns: VbenTableColumn[] = [
|
||||||
|
{
|
||||||
|
title: '用户名',
|
||||||
|
dataIndex: 'username',
|
||||||
|
key: 'username',
|
||||||
|
width: 200,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '邮箱',
|
||||||
|
dataIndex: 'email',
|
||||||
|
key: 'email',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '状态',
|
||||||
|
dataIndex: 'status',
|
||||||
|
key: 'status',
|
||||||
|
width: 100,
|
||||||
|
customRender: ({ text }) => {
|
||||||
|
const color = text === 'active' ? 'green' : 'red';
|
||||||
|
return <Tag color={color}>{text}</Tag>;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '操作',
|
||||||
|
key: 'action',
|
||||||
|
width: 200,
|
||||||
|
customRender: ({ record }) => (
|
||||||
|
<>
|
||||||
|
<Button type="link" onClick={() => handleEdit(record)}>编辑</Button>
|
||||||
|
<Button type="link" danger onClick={() => handleDelete(record.id)}>删除</Button>
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const dataSource = ref([
|
||||||
|
{ id: '1', username: 'admin', email: 'admin@example.com', status: 'active' },
|
||||||
|
{ id: '2', username: 'user', email: 'user@example.com', status: 'inactive' },
|
||||||
|
]);
|
||||||
|
|
||||||
|
function handleEdit(record: any) {
|
||||||
|
console.log('编辑:', record);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleDelete(id: string) {
|
||||||
|
console.log('删除:', id);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<VbenTable
|
||||||
|
:columns="columns"
|
||||||
|
:data-source="dataSource"
|
||||||
|
:pagination="{ pageSize: 10 }"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6. 权限控制
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { useAccess } from '@vben/access';
|
||||||
|
|
||||||
|
const { hasAccessByCodes } = useAccess();
|
||||||
|
|
||||||
|
// 检查权限
|
||||||
|
const canEdit = hasAccessByCodes(['user:edit']);
|
||||||
|
const canDelete = hasAccessByCodes(['user:delete']);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<!-- 使用 v-access 指令控制显示 -->
|
||||||
|
<a-button v-access="'user:create'">创建用户</a-button>
|
||||||
|
<a-button v-access="'user:edit'" v-if="canEdit">编辑</a-button>
|
||||||
|
<a-button v-access="'user:delete'" v-if="canDelete">删除</a-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7. 使用图标
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { IconSvg } from '@vben/icons';
|
||||||
|
import { SvgUserIcon, SvgSettingsIcon } from '@vben/icons';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<!-- 方式 1: 使用 IconSvg 组件 + Iconify 名称 -->
|
||||||
|
<IconSvg icon="lucide:user" class="text-xl" />
|
||||||
|
<IconSvg icon="mdi:settings" :size="24" />
|
||||||
|
|
||||||
|
<!-- 方式 2: 使用预定义的 SVG 组件 -->
|
||||||
|
<SvgUserIcon class="text-xl" />
|
||||||
|
<SvgSettingsIcon :size="24" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
```
|
||||||
|
|
||||||
|
**常用图标集:**
|
||||||
|
- `lucide:*` - Lucide 图标(推荐)
|
||||||
|
- `mdi:*` - Material Design Icons
|
||||||
|
- `carbon:*` - Carbon Icons
|
||||||
|
- `heroicons:*` - Heroicons
|
||||||
|
|
||||||
|
浏览图标:https://icon-sets.iconify.design/
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 样式规范
|
||||||
|
|
||||||
|
### 1. 使用 Tailwind CSS
|
||||||
|
|
||||||
|
优先使用 Tailwind CSS 工具类:
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<template>
|
||||||
|
<div class="p-5 bg-white rounded-lg shadow-md">
|
||||||
|
<h2 class="text-2xl font-bold mb-4">标题</h2>
|
||||||
|
<div class="flex items-center gap-4">
|
||||||
|
<a-button type="primary">按钮</a-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Scoped 样式
|
||||||
|
|
||||||
|
需要自定义样式时使用 `<style scoped>`:
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<style scoped>
|
||||||
|
.custom-class {
|
||||||
|
/* 自定义样式 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 深度选择器 - 影响子组件 */
|
||||||
|
:deep(.ant-btn) {
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 插槽选择器 */
|
||||||
|
:slotted(.slot-content) {
|
||||||
|
color: red;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 响应式设计
|
||||||
|
|
||||||
|
使用 Tailwind 响应式前缀:
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<template>
|
||||||
|
<!-- 移动端单列,平板双列,桌面三列 -->
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||||
|
<div v-for="item in items" :key="item.id">
|
||||||
|
{{ item.name }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 常见问题和注意事项
|
||||||
|
|
||||||
|
### 1. ❌ 不要做的事
|
||||||
|
|
||||||
|
- **不要使用 Options API**(必须用 Composition API)
|
||||||
|
- **不要使用相对路径导入**(使用 `#/` 别名)
|
||||||
|
- **不要直接修改 `packages/` 下的框架代码**
|
||||||
|
- **不要在组件中直接操作 DOM**(使用 Vue 的响应式系统)
|
||||||
|
- **不要使用 yarn/npm**(必须用 pnpm)
|
||||||
|
- **不要在路由配置中使用 `require`**(使用 `import()`)
|
||||||
|
|
||||||
|
### 2. ✅ 最佳实践
|
||||||
|
|
||||||
|
- **组件拆分**: 单个组件不超过 300 行,复杂组件拆分成子组件
|
||||||
|
- **类型安全**: 所有 API 响应、组件 Props、函数参数都定义 TypeScript 类型
|
||||||
|
- **错误处理**: 使用 `try-catch` 处理异步错误
|
||||||
|
- **加载状态**: 异步操作要有 loading 状态
|
||||||
|
- **国际化**: 所有用户可见的文本使用 `$t()` 函数
|
||||||
|
- **权限控制**: 敏感操作添加权限检查
|
||||||
|
|
||||||
|
### 3. 性能优化
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, ref } from 'vue';
|
||||||
|
|
||||||
|
// ✅ 使用 computed 缓存计算结果
|
||||||
|
const filteredList = computed(() => {
|
||||||
|
return list.value.filter(item => item.status === 'active');
|
||||||
|
});
|
||||||
|
|
||||||
|
// ✅ 大列表使用虚拟滚动
|
||||||
|
import { VirtualList } from '@vben/common-ui';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<!-- ✅ 使用 v-show 而不是 v-if(频繁切换的元素) -->
|
||||||
|
<div v-show="visible">内容</div>
|
||||||
|
|
||||||
|
<!-- ✅ 长列表使用 key -->
|
||||||
|
<div v-for="item in list" :key="item.id">
|
||||||
|
{{ item.name }}
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. 调试技巧
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 开发环境打印日志
|
||||||
|
if (import.meta.env.DEV) {
|
||||||
|
console.log('调试信息:', data);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用 Vue Devtools
|
||||||
|
// Chrome 扩展:Vue.js devtools
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 开发命令
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 启动开发服务器
|
||||||
|
pnpm run dev:antd
|
||||||
|
|
||||||
|
# 构建生产版本
|
||||||
|
pnpm run build:antd
|
||||||
|
|
||||||
|
# 类型检查
|
||||||
|
pnpm run check:type
|
||||||
|
|
||||||
|
# 代码格式化
|
||||||
|
pnpm run format
|
||||||
|
|
||||||
|
# 代码检查
|
||||||
|
pnpm run lint
|
||||||
|
|
||||||
|
# 单元测试
|
||||||
|
pnpm run test:unit
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## API Mock 数据(开发阶段)
|
||||||
|
|
||||||
|
如果后端接口未就绪,可以使用 Mock 数据:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// src/api/user.ts
|
||||||
|
import { requestClient } from './request';
|
||||||
|
|
||||||
|
export async function getUserListApi() {
|
||||||
|
// 开发环境使用 mock 数据
|
||||||
|
if (import.meta.env.DEV) {
|
||||||
|
return Promise.resolve([
|
||||||
|
{ id: '1', username: 'admin', email: 'admin@example.com', status: 'active' },
|
||||||
|
{ id: '2', username: 'user', email: 'user@example.com', status: 'inactive' },
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 生产环境调用真实 API
|
||||||
|
return requestClient.get('/user/list');
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 环境变量配置
|
||||||
|
|
||||||
|
配置文件位置:
|
||||||
|
- `.env` - 所有环境
|
||||||
|
- `.env.development` - 开发环境
|
||||||
|
- `.env.production` - 生产环境
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# .env.development
|
||||||
|
VITE_APP_TITLE=DevOps 管理系统
|
||||||
|
VITE_API_URL=http://localhost:8080/api
|
||||||
|
```
|
||||||
|
|
||||||
|
使用方式:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { useAppConfig } from '@vben/hooks';
|
||||||
|
|
||||||
|
const { apiURL } = useAppConfig(import.meta.env, import.meta.env.PROD);
|
||||||
|
console.log('API地址:', apiURL);
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 总结
|
||||||
|
|
||||||
|
遵循本手册的规范,可以确保:
|
||||||
|
1. ✅ 代码风格统一,易于维护
|
||||||
|
2. ✅ 充分利用框架能力,避免重复造轮子
|
||||||
|
3. ✅ 类型安全,减少运行时错误
|
||||||
|
4. ✅ 性能优化,用户体验良好
|
||||||
|
5. ✅ 团队协作顺畅,代码可读性强
|
||||||
|
|
||||||
|
**开发新功能前,请先参考 `playground` 和 `demos` 目录下的示例代码!**
|
||||||
35
frontend/apps/web-antd/index.html
Normal file
35
frontend/apps/web-antd/index.html
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="zh">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
|
||||||
|
<meta name="renderer" content="webkit" />
|
||||||
|
<meta name="description" content="A Modern Back-end Management System" />
|
||||||
|
<meta name="keywords" content="Vben Admin Vue3 Vite" />
|
||||||
|
<meta name="author" content="Vben" />
|
||||||
|
<meta
|
||||||
|
name="viewport"
|
||||||
|
content="width=device-width,initial-scale=1.0,minimum-scale=1.0,maximum-scale=1.0,user-scalable=0"
|
||||||
|
/>
|
||||||
|
<!-- 由 vite 注入 VITE_APP_TITLE 变量,在 .env 文件内配置 -->
|
||||||
|
<title><%= VITE_APP_TITLE %></title>
|
||||||
|
<link rel="icon" href="/favicon.ico" />
|
||||||
|
<script>
|
||||||
|
// 生产环境下注入百度统计
|
||||||
|
if (window._VBEN_ADMIN_PRO_APP_CONF_) {
|
||||||
|
var _hmt = _hmt || [];
|
||||||
|
(function () {
|
||||||
|
var hm = document.createElement('script');
|
||||||
|
hm.src =
|
||||||
|
'https://hm.baidu.com/hm.js?b38e689f40558f20a9a686d7f6f33edf';
|
||||||
|
var s = document.getElementsByTagName('script')[0];
|
||||||
|
s.parentNode.insertBefore(hm, s);
|
||||||
|
})();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app"></div>
|
||||||
|
<script type="module" src="/src/main.ts"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
213
frontend/apps/web-antd/node-size-comparison.html
Normal file
213
frontend/apps/web-antd/node-size-comparison.html
Normal file
@ -0,0 +1,213 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Vue Flow 节点尺寸对比</title>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||||||
|
margin: 20px;
|
||||||
|
background: #f5f5f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
display: flex;
|
||||||
|
gap: 20px;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section {
|
||||||
|
background: white;
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
margin-top: 0;
|
||||||
|
color: #1890ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.node-demo {
|
||||||
|
display: flex;
|
||||||
|
gap: 15px;
|
||||||
|
align-items: center;
|
||||||
|
margin: 15px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 优化后的节点样式 */
|
||||||
|
.start-node-optimized {
|
||||||
|
background: linear-gradient(135deg, #52c41a, #389e0d);
|
||||||
|
color: white;
|
||||||
|
border: 2px solid #52c41a;
|
||||||
|
border-radius: 16px;
|
||||||
|
padding: 8px 12px;
|
||||||
|
box-shadow: 0 2px 8px rgba(82, 196, 26, 0.3);
|
||||||
|
min-width: 80px;
|
||||||
|
max-width: 120px;
|
||||||
|
font-size: 12px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-node-optimized {
|
||||||
|
background: linear-gradient(135deg, #1890ff, #0050b3);
|
||||||
|
color: white;
|
||||||
|
border: 2px solid #1890ff;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 8px 12px;
|
||||||
|
box-shadow: 0 2px 8px rgba(24, 144, 255, 0.3);
|
||||||
|
min-width: 90px;
|
||||||
|
max-width: 140px;
|
||||||
|
font-size: 12px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.decision-node-optimized {
|
||||||
|
width: 60px;
|
||||||
|
height: 60px;
|
||||||
|
position: relative;
|
||||||
|
transform: rotate(45deg);
|
||||||
|
background: linear-gradient(135deg, #faad14, #d48806);
|
||||||
|
border: 2px solid #faad14;
|
||||||
|
box-shadow: 0 2px 8px rgba(250, 173, 20, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.decision-node-optimized .content {
|
||||||
|
transform: rotate(-45deg);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
height: 100%;
|
||||||
|
color: white;
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.end-node-optimized {
|
||||||
|
background: linear-gradient(135deg, #ff4d4f, #d9363e);
|
||||||
|
color: white;
|
||||||
|
border: 2px solid #ff4d4f;
|
||||||
|
border-radius: 16px;
|
||||||
|
padding: 8px 12px;
|
||||||
|
box-shadow: 0 2px 8px rgba(255, 77, 79, 0.3);
|
||||||
|
min-width: 80px;
|
||||||
|
max-width: 120px;
|
||||||
|
font-size: 12px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 原始节点样式对比 */
|
||||||
|
.start-node-original {
|
||||||
|
background: linear-gradient(135deg, #52c41a, #389e0d);
|
||||||
|
color: white;
|
||||||
|
border: 2px solid #52c41a;
|
||||||
|
border-radius: 20px;
|
||||||
|
padding: 12px 16px;
|
||||||
|
box-shadow: 0 4px 12px rgba(82, 196, 26, 0.3);
|
||||||
|
min-width: 120px;
|
||||||
|
font-size: 14px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-node-original {
|
||||||
|
background: linear-gradient(135deg, #1890ff, #0050b3);
|
||||||
|
color: white;
|
||||||
|
border: 2px solid #1890ff;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 12px 16px;
|
||||||
|
box-shadow: 0 4px 12px rgba(24, 144, 255, 0.3);
|
||||||
|
min-width: 140px;
|
||||||
|
font-size: 14px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.decision-node-original {
|
||||||
|
width: 100px;
|
||||||
|
height: 100px;
|
||||||
|
position: relative;
|
||||||
|
transform: rotate(45deg);
|
||||||
|
background: linear-gradient(135deg, #faad14, #d48806);
|
||||||
|
border: 2px solid #faad14;
|
||||||
|
box-shadow: 0 4px 12px rgba(250, 173, 20, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.decision-node-original .content {
|
||||||
|
transform: rotate(-45deg);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
height: 100%;
|
||||||
|
color: white;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.end-node-original {
|
||||||
|
background: linear-gradient(135deg, #ff4d4f, #d9363e);
|
||||||
|
color: white;
|
||||||
|
border: 2px solid #ff4d4f;
|
||||||
|
border-radius: 20px;
|
||||||
|
padding: 12px 16px;
|
||||||
|
box-shadow: 0 4px 12px rgba(255, 77, 79, 0.3);
|
||||||
|
min-width: 120px;
|
||||||
|
font-size: 14px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>Vue Flow 节点尺寸优化对比</h1>
|
||||||
|
|
||||||
|
<div class="container">
|
||||||
|
<div class="section">
|
||||||
|
<h3>❌ 优化前(太大)</h3>
|
||||||
|
<div class="node-demo">
|
||||||
|
<div class="start-node-original">开始</div>
|
||||||
|
<div class="task-node-original">审核</div>
|
||||||
|
<div class="decision-node-original">
|
||||||
|
<div class="content">通过?</div>
|
||||||
|
</div>
|
||||||
|
<div class="end-node-original">结束</div>
|
||||||
|
</div>
|
||||||
|
<p style="color: #999; font-size: 12px;">原始尺寸过大,占用太多画布空间</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="section">
|
||||||
|
<h3>✅ 优化后(合适)</h3>
|
||||||
|
<div class="node-demo">
|
||||||
|
<div class="start-node-optimized">开始</div>
|
||||||
|
<div class="task-node-optimized">审核</div>
|
||||||
|
<div class="decision-node-optimized">
|
||||||
|
<div class="content">通过?</div>
|
||||||
|
</div>
|
||||||
|
<div class="end-node-optimized">结束</div>
|
||||||
|
</div>
|
||||||
|
<p style="color: #52c41a; font-size: 12px;">优化后尺寸更合理,适合流程图设计</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="section">
|
||||||
|
<h3>📊 优化详情</h3>
|
||||||
|
<ul>
|
||||||
|
<li><strong>开始/结束节点</strong>: 120px → 80-120px,padding减少,圆角调整</li>
|
||||||
|
<li><strong>任务节点</strong>: 140px → 90-140px,更紧凑的设计</li>
|
||||||
|
<li><strong>决策节点</strong>: 100×100px → 60×60px,大幅缩小菱形尺寸</li>
|
||||||
|
<li><strong>连接点</strong>: 10px → 8px,更精致的连接点</li>
|
||||||
|
<li><strong>字体</strong>: 14px → 12px,更适合小尺寸节点</li>
|
||||||
|
<li><strong>阴影</strong>: 减少模糊范围,更清晰的视觉效果</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="section">
|
||||||
|
<h3>🎯 优化效果</h3>
|
||||||
|
<ul>
|
||||||
|
<li>✅ 节点尺寸更合理,不会过分占用画布空间</li>
|
||||||
|
<li>✅ 决策节点从巨大的菱形变成合适的小菱形</li>
|
||||||
|
<li>✅ 保持了视觉层次和可读性</li>
|
||||||
|
<li>✅ 更适合复杂流程图的设计</li>
|
||||||
|
<li>✅ 连接点和字体大小匹配节点尺寸</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
59
frontend/apps/web-antd/package.json
Normal file
59
frontend/apps/web-antd/package.json
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
{
|
||||||
|
"name": "@vben/web-antd",
|
||||||
|
"version": "5.5.9",
|
||||||
|
"homepage": "https://vben.pro",
|
||||||
|
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "git+https://github.com/vbenjs/vue-vben-admin.git",
|
||||||
|
"directory": "apps/web-antd"
|
||||||
|
},
|
||||||
|
"license": "MIT",
|
||||||
|
"author": {
|
||||||
|
"name": "vben",
|
||||||
|
"email": "ann.vben@gmail.com",
|
||||||
|
"url": "https://github.com/anncwb"
|
||||||
|
},
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"build": "pnpm vite build --mode production",
|
||||||
|
"build:analyze": "pnpm vite build --mode analyze",
|
||||||
|
"dev": "pnpm vite --mode development",
|
||||||
|
"preview": "vite preview",
|
||||||
|
"typecheck": "vue-tsc --noEmit --skipLibCheck"
|
||||||
|
},
|
||||||
|
"imports": {
|
||||||
|
"#/*": "./src/*"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@ant-design/icons-vue": "^7.0.1",
|
||||||
|
"@bpmn-io/properties-panel": "^3.33.0",
|
||||||
|
"@vben/access": "workspace:*",
|
||||||
|
"@vben/common-ui": "workspace:*",
|
||||||
|
"@vben/constants": "workspace:*",
|
||||||
|
"@vben/hooks": "workspace:*",
|
||||||
|
"@vben/icons": "workspace:*",
|
||||||
|
"@vben/layouts": "workspace:*",
|
||||||
|
"@vben/locales": "workspace:*",
|
||||||
|
"@vben/plugins": "workspace:*",
|
||||||
|
"@vben/preferences": "workspace:*",
|
||||||
|
"@vben/request": "workspace:*",
|
||||||
|
"@vben/stores": "workspace:*",
|
||||||
|
"@vben/styles": "workspace:*",
|
||||||
|
"@vben/types": "workspace:*",
|
||||||
|
"@vben/utils": "workspace:*",
|
||||||
|
"@vue-flow/background": "^1.3.0",
|
||||||
|
"@vue-flow/controls": "^1.1.2",
|
||||||
|
"@vue-flow/core": "^1.37.3",
|
||||||
|
"@vue-flow/minimap": "^1.5.0",
|
||||||
|
"@vueuse/core": "catalog:",
|
||||||
|
"ant-design-vue": "catalog:",
|
||||||
|
"bpmn-js": "^18.7.0",
|
||||||
|
"bpmn-js-properties-panel": "^5.42.1",
|
||||||
|
"camunda-bpmn-moddle": "^7.0.1",
|
||||||
|
"dayjs": "catalog:",
|
||||||
|
"pinia": "catalog:",
|
||||||
|
"vue": "catalog:",
|
||||||
|
"vue-router": "catalog:"
|
||||||
|
}
|
||||||
|
}
|
||||||
1
frontend/apps/web-antd/postcss.config.mjs
Normal file
1
frontend/apps/web-antd/postcss.config.mjs
Normal file
@ -0,0 +1 @@
|
|||||||
|
export { default } from '@vben/tailwind-config/postcss';
|
||||||
BIN
frontend/apps/web-antd/public/favicon.ico
Normal file
BIN
frontend/apps/web-antd/public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.3 KiB |
211
frontend/apps/web-antd/src/adapter/component/index.ts
Normal file
211
frontend/apps/web-antd/src/adapter/component/index.ts
Normal file
@ -0,0 +1,211 @@
|
|||||||
|
/**
|
||||||
|
* 通用组件共同的使用的基础组件,原先放在 adapter/form 内部,限制了使用范围,这里提取出来,方便其他地方使用
|
||||||
|
* 可用于 vben-form、vben-modal、vben-drawer 等组件使用,
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type {Component} from 'vue';
|
||||||
|
|
||||||
|
import type {BaseFormComponentType} from '@vben/common-ui';
|
||||||
|
import type {Recordable} from '@vben/types';
|
||||||
|
|
||||||
|
import {defineAsyncComponent, defineComponent, h, ref} from 'vue';
|
||||||
|
|
||||||
|
import {ApiComponent, globalShareState, IconPicker} from '@vben/common-ui';
|
||||||
|
import {$t} from '@vben/locales';
|
||||||
|
|
||||||
|
import {notification} from 'ant-design-vue';
|
||||||
|
|
||||||
|
const AutoComplete = defineAsyncComponent(
|
||||||
|
() => import('ant-design-vue/es/auto-complete'),
|
||||||
|
);
|
||||||
|
const Button = defineAsyncComponent(() => import('ant-design-vue/es/button'));
|
||||||
|
const Checkbox = defineAsyncComponent(
|
||||||
|
() => import('ant-design-vue/es/checkbox'),
|
||||||
|
);
|
||||||
|
const CheckboxGroup = defineAsyncComponent(() =>
|
||||||
|
import('ant-design-vue/es/checkbox').then((res) => res.CheckboxGroup),
|
||||||
|
);
|
||||||
|
const DatePicker = defineAsyncComponent(
|
||||||
|
() => import('ant-design-vue/es/date-picker'),
|
||||||
|
);
|
||||||
|
const Divider = defineAsyncComponent(() => import('ant-design-vue/es/divider'));
|
||||||
|
const Input = defineAsyncComponent(() => import('ant-design-vue/es/input'));
|
||||||
|
const InputNumber = defineAsyncComponent(
|
||||||
|
() => import('ant-design-vue/es/input-number'),
|
||||||
|
);
|
||||||
|
const InputPassword = defineAsyncComponent(() =>
|
||||||
|
import('ant-design-vue/es/input').then((res) => res.InputPassword),
|
||||||
|
);
|
||||||
|
const Mentions = defineAsyncComponent(
|
||||||
|
() => import('ant-design-vue/es/mentions'),
|
||||||
|
);
|
||||||
|
const Radio = defineAsyncComponent(() => import('ant-design-vue/es/radio'));
|
||||||
|
const RadioGroup = defineAsyncComponent(() =>
|
||||||
|
import('ant-design-vue/es/radio').then((res) => res.RadioGroup),
|
||||||
|
);
|
||||||
|
const RangePicker = defineAsyncComponent(() =>
|
||||||
|
import('ant-design-vue/es/date-picker').then((res) => res.RangePicker),
|
||||||
|
);
|
||||||
|
const Rate = defineAsyncComponent(() => import('ant-design-vue/es/rate'));
|
||||||
|
const Select = defineAsyncComponent(() => import('ant-design-vue/es/select'));
|
||||||
|
const Space = defineAsyncComponent(() => import('ant-design-vue/es/space'));
|
||||||
|
const Switch = defineAsyncComponent(() => import('ant-design-vue/es/switch'));
|
||||||
|
const Textarea = defineAsyncComponent(() =>
|
||||||
|
import('ant-design-vue/es/input').then((res) => res.Textarea),
|
||||||
|
);
|
||||||
|
const TimePicker = defineAsyncComponent(
|
||||||
|
() => import('ant-design-vue/es/time-picker'),
|
||||||
|
);
|
||||||
|
const TreeSelect = defineAsyncComponent(
|
||||||
|
() => import('ant-design-vue/es/tree-select'),
|
||||||
|
);
|
||||||
|
const Upload = defineAsyncComponent(() => import('ant-design-vue/es/upload'));
|
||||||
|
|
||||||
|
const withDefaultPlaceholder = <T extends Component>(
|
||||||
|
component: T,
|
||||||
|
type: 'input' | 'select',
|
||||||
|
componentProps: Recordable<any> = {},
|
||||||
|
) => {
|
||||||
|
return defineComponent({
|
||||||
|
name: component.name,
|
||||||
|
inheritAttrs: false,
|
||||||
|
setup: (props: any, {attrs, expose, slots}) => {
|
||||||
|
const placeholder =
|
||||||
|
props?.placeholder ||
|
||||||
|
attrs?.placeholder ||
|
||||||
|
$t(`ui.placeholder.${type}`);
|
||||||
|
// 透传组件暴露的方法
|
||||||
|
const innerRef = ref();
|
||||||
|
expose(
|
||||||
|
new Proxy(
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
get: (_target, key) => innerRef.value?.[key],
|
||||||
|
has: (_target, key) => key in (innerRef.value || {}),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return () =>
|
||||||
|
h(
|
||||||
|
component,
|
||||||
|
{...componentProps, placeholder, ...props, ...attrs, ref: innerRef},
|
||||||
|
slots,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 这里需要自行根据业务组件库进行适配,需要用到的组件都需要在这里类型说明
|
||||||
|
export type ComponentType =
|
||||||
|
| 'ApiSelect'
|
||||||
|
| 'ApiTreeSelect'
|
||||||
|
| 'AutoComplete'
|
||||||
|
| 'Checkbox'
|
||||||
|
| 'CheckboxGroup'
|
||||||
|
| 'DatePicker'
|
||||||
|
| 'DefaultButton'
|
||||||
|
| 'Divider'
|
||||||
|
| 'IconPicker'
|
||||||
|
| 'Input'
|
||||||
|
| 'InputNumber'
|
||||||
|
| 'InputPassword'
|
||||||
|
| 'Mentions'
|
||||||
|
| 'PrimaryButton'
|
||||||
|
| 'Radio'
|
||||||
|
| 'RadioGroup'
|
||||||
|
| 'RangePicker'
|
||||||
|
| 'Rate'
|
||||||
|
| 'Select'
|
||||||
|
| 'Space'
|
||||||
|
| 'Switch'
|
||||||
|
| 'Textarea'
|
||||||
|
| 'TimePicker'
|
||||||
|
| 'TreeSelect'
|
||||||
|
| 'Upload'
|
||||||
|
| BaseFormComponentType;
|
||||||
|
|
||||||
|
async function initComponentAdapter() {
|
||||||
|
const components: Partial<Record<ComponentType, Component>> = {
|
||||||
|
// 如果你的组件体积比较大,可以使用异步加载
|
||||||
|
// Button: () =>
|
||||||
|
// import('xxx').then((res) => res.Button),
|
||||||
|
ApiSelect: withDefaultPlaceholder(
|
||||||
|
{
|
||||||
|
...ApiComponent,
|
||||||
|
name: 'ApiSelect',
|
||||||
|
},
|
||||||
|
'select',
|
||||||
|
{
|
||||||
|
component: Select,
|
||||||
|
loadingSlot: 'suffixIcon',
|
||||||
|
visibleEvent: 'onDropdownVisibleChange',
|
||||||
|
modelPropName: 'value',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
ApiTreeSelect: withDefaultPlaceholder(
|
||||||
|
{
|
||||||
|
...ApiComponent,
|
||||||
|
name: 'ApiTreeSelect',
|
||||||
|
},
|
||||||
|
'select',
|
||||||
|
{
|
||||||
|
component: TreeSelect,
|
||||||
|
fieldNames: {label: 'label', value: 'value', children: 'children'},
|
||||||
|
loadingSlot: 'suffixIcon',
|
||||||
|
modelPropName: 'value',
|
||||||
|
optionsPropName: 'treeData',
|
||||||
|
visibleEvent: 'onVisibleChange',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
AutoComplete,
|
||||||
|
Checkbox,
|
||||||
|
CheckboxGroup,
|
||||||
|
DatePicker,
|
||||||
|
// 自定义默认按钮
|
||||||
|
DefaultButton: (props, {attrs, slots}) => {
|
||||||
|
return h(Button, {...props, attrs, type: 'default'}, slots);
|
||||||
|
},
|
||||||
|
Divider,
|
||||||
|
IconPicker: withDefaultPlaceholder(IconPicker, 'select', {
|
||||||
|
iconSlot: 'addonAfter',
|
||||||
|
inputComponent: Input,
|
||||||
|
modelValueProp: 'value',
|
||||||
|
}),
|
||||||
|
Input: withDefaultPlaceholder(Input, 'input'),
|
||||||
|
InputNumber: withDefaultPlaceholder(InputNumber, 'input'),
|
||||||
|
InputPassword: withDefaultPlaceholder(InputPassword, 'input'),
|
||||||
|
Mentions: withDefaultPlaceholder(Mentions, 'input'),
|
||||||
|
// 自定义主要按钮
|
||||||
|
PrimaryButton: (props, {attrs, slots}) => {
|
||||||
|
return h(Button, {...props, attrs, type: 'primary'}, slots);
|
||||||
|
},
|
||||||
|
Radio,
|
||||||
|
RadioGroup,
|
||||||
|
RangePicker,
|
||||||
|
Rate,
|
||||||
|
Select: withDefaultPlaceholder(Select, 'select'),
|
||||||
|
Space,
|
||||||
|
Switch,
|
||||||
|
Textarea: withDefaultPlaceholder(Textarea, 'input'),
|
||||||
|
TimePicker,
|
||||||
|
TreeSelect: withDefaultPlaceholder(TreeSelect, 'select'),
|
||||||
|
Upload,
|
||||||
|
};
|
||||||
|
|
||||||
|
// 将组件注册到全局共享状态中
|
||||||
|
globalShareState.setComponents(components);
|
||||||
|
|
||||||
|
// 定义全局共享状态中的消息提示
|
||||||
|
globalShareState.defineMessage({
|
||||||
|
// 复制成功消息提示
|
||||||
|
copyPreferencesSuccess: (title, content) => {
|
||||||
|
notification.success({
|
||||||
|
description: content,
|
||||||
|
message: title,
|
||||||
|
placement: 'bottomRight',
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export {initComponentAdapter};
|
||||||
49
frontend/apps/web-antd/src/adapter/form.ts
Normal file
49
frontend/apps/web-antd/src/adapter/form.ts
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
import type {
|
||||||
|
VbenFormSchema as FormSchema,
|
||||||
|
VbenFormProps,
|
||||||
|
} from '@vben/common-ui';
|
||||||
|
|
||||||
|
import type { ComponentType } from './component';
|
||||||
|
|
||||||
|
import { setupVbenForm, useVbenForm as useForm, z } from '@vben/common-ui';
|
||||||
|
import { $t } from '@vben/locales';
|
||||||
|
|
||||||
|
async function initSetupVbenForm() {
|
||||||
|
setupVbenForm<ComponentType>({
|
||||||
|
config: {
|
||||||
|
// ant design vue组件库默认都是 v-model:value
|
||||||
|
baseModelPropName: 'value',
|
||||||
|
|
||||||
|
// 一些组件是 v-model:checked 或者 v-model:fileList
|
||||||
|
modelPropNameMap: {
|
||||||
|
Checkbox: 'checked',
|
||||||
|
Radio: 'checked',
|
||||||
|
Switch: 'checked',
|
||||||
|
Upload: 'fileList',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defineRules: {
|
||||||
|
// 输入项目必填国际化适配
|
||||||
|
required: (value, _params, ctx) => {
|
||||||
|
if (value === undefined || value === null || value.length === 0) {
|
||||||
|
return $t('ui.formRules.required', [ctx.label]);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
// 选择项目必填国际化适配
|
||||||
|
selectRequired: (value, _params, ctx) => {
|
||||||
|
if (value === undefined || value === null) {
|
||||||
|
return $t('ui.formRules.selectRequired', [ctx.label]);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const useVbenForm = useForm<ComponentType>;
|
||||||
|
|
||||||
|
export { initSetupVbenForm, useVbenForm, z };
|
||||||
|
|
||||||
|
export type VbenFormSchema = FormSchema<ComponentType>;
|
||||||
|
export type { VbenFormProps };
|
||||||
69
frontend/apps/web-antd/src/adapter/vxe-table.ts
Normal file
69
frontend/apps/web-antd/src/adapter/vxe-table.ts
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
import type { VxeTableGridOptions } from '@vben/plugins/vxe-table';
|
||||||
|
|
||||||
|
import { h } from 'vue';
|
||||||
|
|
||||||
|
import { setupVbenVxeTable, useVbenVxeGrid } from '@vben/plugins/vxe-table';
|
||||||
|
|
||||||
|
import { Button, Image } from 'ant-design-vue';
|
||||||
|
|
||||||
|
import { useVbenForm } from './form';
|
||||||
|
|
||||||
|
setupVbenVxeTable({
|
||||||
|
configVxeTable: (vxeUI) => {
|
||||||
|
vxeUI.setConfig({
|
||||||
|
grid: {
|
||||||
|
align: 'center',
|
||||||
|
border: false,
|
||||||
|
columnConfig: {
|
||||||
|
resizable: true,
|
||||||
|
},
|
||||||
|
minHeight: 180,
|
||||||
|
formConfig: {
|
||||||
|
// 全局禁用vxe-table的表单配置,使用formOptions
|
||||||
|
enabled: false,
|
||||||
|
},
|
||||||
|
proxyConfig: {
|
||||||
|
autoLoad: true,
|
||||||
|
response: {
|
||||||
|
result: 'items',
|
||||||
|
total: 'total',
|
||||||
|
list: 'items',
|
||||||
|
},
|
||||||
|
showActiveMsg: true,
|
||||||
|
showResponseMsg: false,
|
||||||
|
},
|
||||||
|
round: true,
|
||||||
|
showOverflow: true,
|
||||||
|
size: 'small',
|
||||||
|
} as VxeTableGridOptions,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 表格配置项可以用 cellRender: { name: 'CellImage' },
|
||||||
|
vxeUI.renderer.add('CellImage', {
|
||||||
|
renderTableDefault(_renderOpts, params) {
|
||||||
|
const { column, row } = params;
|
||||||
|
return h(Image, { src: row[column.field] });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// 表格配置项可以用 cellRender: { name: 'CellLink' },
|
||||||
|
vxeUI.renderer.add('CellLink', {
|
||||||
|
renderTableDefault(renderOpts) {
|
||||||
|
const { props } = renderOpts;
|
||||||
|
return h(
|
||||||
|
Button,
|
||||||
|
{ size: 'small', type: 'link' },
|
||||||
|
{ default: () => props?.text },
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// 这里可以自行扩展 vxe-table 的全局配置,比如自定义格式化
|
||||||
|
// vxeUI.formats.add
|
||||||
|
},
|
||||||
|
useVbenForm,
|
||||||
|
});
|
||||||
|
|
||||||
|
export { useVbenVxeGrid };
|
||||||
|
|
||||||
|
export type * from '@vben/plugins/vxe-table';
|
||||||
72
frontend/apps/web-antd/src/api/core/auth.ts
Normal file
72
frontend/apps/web-antd/src/api/core/auth.ts
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
// import { baseRequestClient, requestClient } from '#/api/request';
|
||||||
|
|
||||||
|
export namespace AuthApi {
|
||||||
|
/** 登录接口参数 */
|
||||||
|
export interface LoginParams {
|
||||||
|
password?: string;
|
||||||
|
username?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 登录接口返回值 */
|
||||||
|
export interface LoginResult {
|
||||||
|
accessToken: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RefreshTokenResult {
|
||||||
|
data: string;
|
||||||
|
status: number;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 登录
|
||||||
|
* TODO: 临时注释掉真实API调用,使用模拟数据
|
||||||
|
*/
|
||||||
|
export async function loginApi(_data: AuthApi.LoginParams) {
|
||||||
|
// return requestClient.post<AuthApi.LoginResult>('/auth/login', data);
|
||||||
|
|
||||||
|
// 模拟登录成功,返回模拟token
|
||||||
|
return Promise.resolve({
|
||||||
|
accessToken: 'mock-access-token-12345',
|
||||||
|
} as AuthApi.LoginResult);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 刷新accessToken
|
||||||
|
* TODO: 临时注释掉真实API调用,使用模拟数据
|
||||||
|
*/
|
||||||
|
export async function refreshTokenApi() {
|
||||||
|
// return baseRequestClient.post<AuthApi.RefreshTokenResult>('/auth/refresh', {
|
||||||
|
// withCredentials: true,
|
||||||
|
// });
|
||||||
|
|
||||||
|
// 模拟刷新token成功
|
||||||
|
return Promise.resolve({
|
||||||
|
data: 'mock-refresh-token-67890',
|
||||||
|
status: 200,
|
||||||
|
} as AuthApi.RefreshTokenResult);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 退出登录
|
||||||
|
* TODO: 临时注释掉真实API调用,使用模拟数据
|
||||||
|
*/
|
||||||
|
export async function logoutApi() {
|
||||||
|
// return baseRequestClient.post('/auth/logout', {
|
||||||
|
// withCredentials: true,
|
||||||
|
// });
|
||||||
|
|
||||||
|
// 模拟登出成功
|
||||||
|
return Promise.resolve({ success: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取用户权限码
|
||||||
|
* TODO: 临时注释掉真实API调用,使用模拟数据
|
||||||
|
*/
|
||||||
|
export async function getAccessCodesApi() {
|
||||||
|
// return requestClient.get<string[]>('/auth/codes');
|
||||||
|
|
||||||
|
// 模拟权限代码数据
|
||||||
|
return Promise.resolve(['*'] as string[]);
|
||||||
|
}
|
||||||
3
frontend/apps/web-antd/src/api/core/index.ts
Normal file
3
frontend/apps/web-antd/src/api/core/index.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export * from './auth';
|
||||||
|
export * from './menu';
|
||||||
|
export * from './user';
|
||||||
46
frontend/apps/web-antd/src/api/core/menu.ts
Normal file
46
frontend/apps/web-antd/src/api/core/menu.ts
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
import type { RouteRecordStringComponent } from '@vben/types';
|
||||||
|
|
||||||
|
// import { requestClient } from '#/api/request';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取用户所有菜单
|
||||||
|
* TODO: 临时注释掉真实API调用,使用模拟数据
|
||||||
|
*/
|
||||||
|
export async function getAllMenusApi() {
|
||||||
|
// return requestClient.get<RouteRecordStringComponent[]>('/menu/all');
|
||||||
|
|
||||||
|
// 模拟菜单数据
|
||||||
|
return Promise.resolve([
|
||||||
|
{
|
||||||
|
name: 'Dashboard',
|
||||||
|
path: '/dashboard',
|
||||||
|
component: 'dashboard/index',
|
||||||
|
meta: {
|
||||||
|
title: '仪表盘',
|
||||||
|
icon: 'lucide:home',
|
||||||
|
order: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'System',
|
||||||
|
path: '/system',
|
||||||
|
meta: {
|
||||||
|
title: '系统管理',
|
||||||
|
icon: 'lucide:settings',
|
||||||
|
order: 2,
|
||||||
|
},
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
name: 'UserManagement',
|
||||||
|
path: '/system/user',
|
||||||
|
component: 'system/user/index',
|
||||||
|
meta: {
|
||||||
|
title: '用户管理',
|
||||||
|
icon: 'lucide:users',
|
||||||
|
order: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
] as RouteRecordStringComponent[]);
|
||||||
|
}
|
||||||
25
frontend/apps/web-antd/src/api/core/user.ts
Normal file
25
frontend/apps/web-antd/src/api/core/user.ts
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import type { UserInfo } from '@vben/types';
|
||||||
|
|
||||||
|
// import { requestClient } from '#/api/request';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取用户信息
|
||||||
|
* TODO: 临时注释掉真实API调用,使用模拟数据
|
||||||
|
*/
|
||||||
|
export async function getUserInfoApi() {
|
||||||
|
// return requestClient.get<UserInfo>('/user/info');
|
||||||
|
|
||||||
|
// 模拟用户信息数据
|
||||||
|
return Promise.resolve({
|
||||||
|
id: '1',
|
||||||
|
username: 'admin',
|
||||||
|
realName: '管理员',
|
||||||
|
avatar: '',
|
||||||
|
homePath: '/dashboard',
|
||||||
|
roles: ['admin'],
|
||||||
|
permissions: ['*'],
|
||||||
|
desc: '系统管理员',
|
||||||
|
token: 'mock-token',
|
||||||
|
userId: '1',
|
||||||
|
} as UserInfo);
|
||||||
|
}
|
||||||
1
frontend/apps/web-antd/src/api/index.ts
Normal file
1
frontend/apps/web-antd/src/api/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from './core';
|
||||||
113
frontend/apps/web-antd/src/api/request.ts
Normal file
113
frontend/apps/web-antd/src/api/request.ts
Normal file
@ -0,0 +1,113 @@
|
|||||||
|
/**
|
||||||
|
* 该文件可自行根据业务逻辑进行调整
|
||||||
|
*/
|
||||||
|
import type { RequestClientOptions } from '@vben/request';
|
||||||
|
|
||||||
|
import { useAppConfig } from '@vben/hooks';
|
||||||
|
import { preferences } from '@vben/preferences';
|
||||||
|
import {
|
||||||
|
authenticateResponseInterceptor,
|
||||||
|
defaultResponseInterceptor,
|
||||||
|
errorMessageResponseInterceptor,
|
||||||
|
RequestClient,
|
||||||
|
} from '@vben/request';
|
||||||
|
import { useAccessStore } from '@vben/stores';
|
||||||
|
|
||||||
|
import { message } from 'ant-design-vue';
|
||||||
|
|
||||||
|
import { useAuthStore } from '#/store';
|
||||||
|
|
||||||
|
import { refreshTokenApi } from './core';
|
||||||
|
|
||||||
|
const { apiURL } = useAppConfig(import.meta.env, import.meta.env.PROD);
|
||||||
|
|
||||||
|
function createRequestClient(baseURL: string, options?: RequestClientOptions) {
|
||||||
|
const client = new RequestClient({
|
||||||
|
...options,
|
||||||
|
baseURL,
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 重新认证逻辑
|
||||||
|
*/
|
||||||
|
async function doReAuthenticate() {
|
||||||
|
console.warn('Access token or refresh token is invalid or expired. ');
|
||||||
|
const accessStore = useAccessStore();
|
||||||
|
const authStore = useAuthStore();
|
||||||
|
accessStore.setAccessToken(null);
|
||||||
|
if (
|
||||||
|
preferences.app.loginExpiredMode === 'modal' &&
|
||||||
|
accessStore.isAccessChecked
|
||||||
|
) {
|
||||||
|
accessStore.setLoginExpired(true);
|
||||||
|
} else {
|
||||||
|
await authStore.logout();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 刷新token逻辑
|
||||||
|
*/
|
||||||
|
async function doRefreshToken() {
|
||||||
|
const accessStore = useAccessStore();
|
||||||
|
const resp = await refreshTokenApi();
|
||||||
|
const newToken = resp.data;
|
||||||
|
accessStore.setAccessToken(newToken);
|
||||||
|
return newToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatToken(token: null | string) {
|
||||||
|
return token ? `Bearer ${token}` : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 请求头处理
|
||||||
|
client.addRequestInterceptor({
|
||||||
|
fulfilled: async (config) => {
|
||||||
|
const accessStore = useAccessStore();
|
||||||
|
|
||||||
|
config.headers.Authorization = formatToken(accessStore.accessToken);
|
||||||
|
config.headers['Accept-Language'] = preferences.app.locale;
|
||||||
|
return config;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// 处理返回的响应数据格式
|
||||||
|
client.addResponseInterceptor(
|
||||||
|
defaultResponseInterceptor({
|
||||||
|
codeField: 'code',
|
||||||
|
dataField: 'data',
|
||||||
|
successCode: 0,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
// token过期的处理
|
||||||
|
client.addResponseInterceptor(
|
||||||
|
authenticateResponseInterceptor({
|
||||||
|
client,
|
||||||
|
doReAuthenticate,
|
||||||
|
doRefreshToken,
|
||||||
|
enableRefreshToken: preferences.app.enableRefreshToken,
|
||||||
|
formatToken,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
// 通用的错误处理,如果没有进入上面的错误处理逻辑,就会进入这里
|
||||||
|
client.addResponseInterceptor(
|
||||||
|
errorMessageResponseInterceptor((msg: string, error) => {
|
||||||
|
// 这里可以根据业务进行定制,你可以拿到 error 内的信息进行定制化处理,根据不同的 code 做不同的提示,而不是直接使用 message.error 提示 msg
|
||||||
|
// 当前mock接口返回的错误字段是 error 或者 message
|
||||||
|
const responseData = error?.response?.data ?? {};
|
||||||
|
const errorMessage = responseData?.error ?? responseData?.message ?? '';
|
||||||
|
// 如果没有错误信息,则会根据状态码进行提示
|
||||||
|
message.error(errorMessage || msg);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
return client;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const requestClient = createRequestClient(apiURL, {
|
||||||
|
responseReturn: 'data',
|
||||||
|
});
|
||||||
|
|
||||||
|
export const baseRequestClient = new RequestClient({ baseURL: apiURL });
|
||||||
39
frontend/apps/web-antd/src/app.vue
Normal file
39
frontend/apps/web-antd/src/app.vue
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import {computed} from 'vue';
|
||||||
|
|
||||||
|
import {useAntdDesignTokens} from '@vben/hooks';
|
||||||
|
import {preferences, usePreferences} from '@vben/preferences';
|
||||||
|
|
||||||
|
import {App, ConfigProvider, theme} from 'ant-design-vue';
|
||||||
|
|
||||||
|
import {antdLocale} from '#/locales';
|
||||||
|
|
||||||
|
defineOptions({name: 'App'});
|
||||||
|
|
||||||
|
const {isDark} = usePreferences();
|
||||||
|
const {tokens} = useAntdDesignTokens();
|
||||||
|
|
||||||
|
const tokenTheme = computed(() => {
|
||||||
|
const algorithm = isDark.value
|
||||||
|
? [theme.darkAlgorithm]
|
||||||
|
: [theme.defaultAlgorithm];
|
||||||
|
|
||||||
|
// antd 紧凑模式算法
|
||||||
|
if (preferences.app.compact) {
|
||||||
|
algorithm.push(theme.compactAlgorithm);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
algorithm,
|
||||||
|
token: tokens,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<ConfigProvider :locale="antdLocale" :theme="tokenTheme">
|
||||||
|
<App>
|
||||||
|
<RouterView/>
|
||||||
|
</App>
|
||||||
|
</ConfigProvider>
|
||||||
|
</template>
|
||||||
76
frontend/apps/web-antd/src/bootstrap.ts
Normal file
76
frontend/apps/web-antd/src/bootstrap.ts
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
import { createApp, watchEffect } from 'vue';
|
||||||
|
|
||||||
|
import { registerAccessDirective } from '@vben/access';
|
||||||
|
import { registerLoadingDirective } from '@vben/common-ui/es/loading';
|
||||||
|
import { preferences } from '@vben/preferences';
|
||||||
|
import { initStores } from '@vben/stores';
|
||||||
|
import '@vben/styles';
|
||||||
|
import '@vben/styles/antd';
|
||||||
|
|
||||||
|
import { useTitle } from '@vueuse/core';
|
||||||
|
|
||||||
|
import { $t, setupI18n } from '#/locales';
|
||||||
|
|
||||||
|
import { initComponentAdapter } from './adapter/component';
|
||||||
|
import { initSetupVbenForm } from './adapter/form';
|
||||||
|
import App from './app.vue';
|
||||||
|
import { router } from './router';
|
||||||
|
|
||||||
|
async function bootstrap(namespace: string) {
|
||||||
|
// 初始化组件适配器
|
||||||
|
await initComponentAdapter();
|
||||||
|
|
||||||
|
// 初始化表单组件
|
||||||
|
await initSetupVbenForm();
|
||||||
|
|
||||||
|
// // 设置弹窗的默认配置
|
||||||
|
// setDefaultModalProps({
|
||||||
|
// fullscreenButton: false,
|
||||||
|
// });
|
||||||
|
// // 设置抽屉的默认配置
|
||||||
|
// setDefaultDrawerProps({
|
||||||
|
// zIndex: 1020,
|
||||||
|
// });
|
||||||
|
|
||||||
|
const app = createApp(App);
|
||||||
|
|
||||||
|
// 注册v-loading指令
|
||||||
|
registerLoadingDirective(app, {
|
||||||
|
loading: 'loading', // 在这里可以自定义指令名称,也可以明确提供false表示不注册这个指令
|
||||||
|
spinning: 'spinning',
|
||||||
|
});
|
||||||
|
|
||||||
|
// 国际化 i18n 配置
|
||||||
|
await setupI18n(app);
|
||||||
|
|
||||||
|
// 配置 pinia-tore
|
||||||
|
await initStores(app, { namespace });
|
||||||
|
|
||||||
|
// 安装权限指令
|
||||||
|
registerAccessDirective(app);
|
||||||
|
|
||||||
|
// 初始化 tippy
|
||||||
|
const { initTippy } = await import('@vben/common-ui/es/tippy');
|
||||||
|
initTippy(app);
|
||||||
|
|
||||||
|
// 配置路由及路由守卫
|
||||||
|
app.use(router);
|
||||||
|
|
||||||
|
// 配置Motion插件
|
||||||
|
const { MotionPlugin } = await import('@vben/plugins/motion');
|
||||||
|
app.use(MotionPlugin);
|
||||||
|
|
||||||
|
// 动态更新标题
|
||||||
|
watchEffect(() => {
|
||||||
|
if (preferences.app.dynamicTitle) {
|
||||||
|
const routeTitle = router.currentRoute.value.meta?.title;
|
||||||
|
const pageTitle =
|
||||||
|
(routeTitle ? `${$t(routeTitle)} - ` : '') + preferences.app.name;
|
||||||
|
useTitle(pageTitle);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.mount('#app');
|
||||||
|
}
|
||||||
|
|
||||||
|
export { bootstrap };
|
||||||
23
frontend/apps/web-antd/src/layouts/auth.vue
Normal file
23
frontend/apps/web-antd/src/layouts/auth.vue
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import { computed } from 'vue';
|
||||||
|
|
||||||
|
import { AuthPageLayout } from '@vben/layouts';
|
||||||
|
import { preferences } from '@vben/preferences';
|
||||||
|
|
||||||
|
import { $t } from '#/locales';
|
||||||
|
|
||||||
|
const appName = computed(() => preferences.app.name);
|
||||||
|
const logo = computed(() => preferences.logo.source);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<AuthPageLayout
|
||||||
|
:app-name="appName"
|
||||||
|
:logo="logo"
|
||||||
|
:page-description="$t('authentication.pageDesc')"
|
||||||
|
:page-title="$t('authentication.pageTitle')"
|
||||||
|
>
|
||||||
|
<!-- 自定义工具栏 -->
|
||||||
|
<!-- <template #toolbar></template> -->
|
||||||
|
</AuthPageLayout>
|
||||||
|
</template>
|
||||||
157
frontend/apps/web-antd/src/layouts/basic.vue
Normal file
157
frontend/apps/web-antd/src/layouts/basic.vue
Normal file
@ -0,0 +1,157 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import type { NotificationItem } from '@vben/layouts';
|
||||||
|
|
||||||
|
import { computed, ref, watch } from 'vue';
|
||||||
|
|
||||||
|
import { AuthenticationLoginExpiredModal } from '@vben/common-ui';
|
||||||
|
import { VBEN_DOC_URL, VBEN_GITHUB_URL } from '@vben/constants';
|
||||||
|
import { useWatermark } from '@vben/hooks';
|
||||||
|
import { BookOpenText, CircleHelp, MdiGithub } from '@vben/icons';
|
||||||
|
import {
|
||||||
|
BasicLayout,
|
||||||
|
LockScreen,
|
||||||
|
Notification,
|
||||||
|
UserDropdown,
|
||||||
|
} from '@vben/layouts';
|
||||||
|
import { preferences } from '@vben/preferences';
|
||||||
|
import { useAccessStore, useUserStore } from '@vben/stores';
|
||||||
|
import { openWindow } from '@vben/utils';
|
||||||
|
|
||||||
|
import { $t } from '#/locales';
|
||||||
|
import { useAuthStore } from '#/store';
|
||||||
|
import LoginForm from '#/views/_core/authentication/login.vue';
|
||||||
|
|
||||||
|
const notifications = ref<NotificationItem[]>([
|
||||||
|
{
|
||||||
|
avatar: 'https://avatar.vercel.sh/vercel.svg?text=VB',
|
||||||
|
date: '3小时前',
|
||||||
|
isRead: true,
|
||||||
|
message: '描述信息描述信息描述信息',
|
||||||
|
title: '收到了 14 份新周报',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
avatar: 'https://avatar.vercel.sh/1',
|
||||||
|
date: '刚刚',
|
||||||
|
isRead: false,
|
||||||
|
message: '描述信息描述信息描述信息',
|
||||||
|
title: '朱偏右 回复了你',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
avatar: 'https://avatar.vercel.sh/1',
|
||||||
|
date: '2024-01-01',
|
||||||
|
isRead: false,
|
||||||
|
message: '描述信息描述信息描述信息',
|
||||||
|
title: '曲丽丽 评论了你',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
avatar: 'https://avatar.vercel.sh/satori',
|
||||||
|
date: '1天前',
|
||||||
|
isRead: false,
|
||||||
|
message: '描述信息描述信息描述信息',
|
||||||
|
title: '代办提醒',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const userStore = useUserStore();
|
||||||
|
const authStore = useAuthStore();
|
||||||
|
const accessStore = useAccessStore();
|
||||||
|
const { destroyWatermark, updateWatermark } = useWatermark();
|
||||||
|
const showDot = computed(() =>
|
||||||
|
notifications.value.some((item) => !item.isRead),
|
||||||
|
);
|
||||||
|
|
||||||
|
const menus = computed(() => [
|
||||||
|
{
|
||||||
|
handler: () => {
|
||||||
|
openWindow(VBEN_DOC_URL, {
|
||||||
|
target: '_blank',
|
||||||
|
});
|
||||||
|
},
|
||||||
|
icon: BookOpenText,
|
||||||
|
text: $t('ui.widgets.document'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
handler: () => {
|
||||||
|
openWindow(VBEN_GITHUB_URL, {
|
||||||
|
target: '_blank',
|
||||||
|
});
|
||||||
|
},
|
||||||
|
icon: MdiGithub,
|
||||||
|
text: 'GitHub',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
handler: () => {
|
||||||
|
openWindow(`${VBEN_GITHUB_URL}/issues`, {
|
||||||
|
target: '_blank',
|
||||||
|
});
|
||||||
|
},
|
||||||
|
icon: CircleHelp,
|
||||||
|
text: $t('ui.widgets.qa'),
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const avatar = computed(() => {
|
||||||
|
return userStore.userInfo?.avatar ?? preferences.app.defaultAvatar;
|
||||||
|
});
|
||||||
|
|
||||||
|
async function handleLogout() {
|
||||||
|
await authStore.logout(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleNoticeClear() {
|
||||||
|
notifications.value = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleMakeAll() {
|
||||||
|
notifications.value.forEach((item) => (item.isRead = true));
|
||||||
|
}
|
||||||
|
watch(
|
||||||
|
() => preferences.app.watermark,
|
||||||
|
async (enable) => {
|
||||||
|
if (enable) {
|
||||||
|
await updateWatermark({
|
||||||
|
content: `${userStore.userInfo?.username} - ${userStore.userInfo?.realName}`,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
destroyWatermark();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
immediate: true,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<BasicLayout @clear-preferences-and-logout="handleLogout">
|
||||||
|
<template #user-dropdown>
|
||||||
|
<UserDropdown
|
||||||
|
:avatar
|
||||||
|
:menus
|
||||||
|
:text="userStore.userInfo?.realName"
|
||||||
|
description="ann.vben@gmail.com"
|
||||||
|
tag-text="Pro"
|
||||||
|
@logout="handleLogout"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<template #notification>
|
||||||
|
<Notification
|
||||||
|
:dot="showDot"
|
||||||
|
:notifications="notifications"
|
||||||
|
@clear="handleNoticeClear"
|
||||||
|
@make-all="handleMakeAll"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<template #extra>
|
||||||
|
<AuthenticationLoginExpiredModal
|
||||||
|
v-model:open="accessStore.loginExpired"
|
||||||
|
:avatar
|
||||||
|
>
|
||||||
|
<LoginForm />
|
||||||
|
</AuthenticationLoginExpiredModal>
|
||||||
|
</template>
|
||||||
|
<template #lock-screen>
|
||||||
|
<LockScreen :avatar @to-login="handleLogout" />
|
||||||
|
</template>
|
||||||
|
</BasicLayout>
|
||||||
|
</template>
|
||||||
6
frontend/apps/web-antd/src/layouts/index.ts
Normal file
6
frontend/apps/web-antd/src/layouts/index.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
const BasicLayout = () => import('./basic.vue');
|
||||||
|
const AuthPageLayout = () => import('./auth.vue');
|
||||||
|
|
||||||
|
const IFrameView = () => import('@vben/layouts').then((m) => m.IFrameView);
|
||||||
|
|
||||||
|
export { AuthPageLayout, BasicLayout, IFrameView };
|
||||||
3
frontend/apps/web-antd/src/locales/README.md
Normal file
3
frontend/apps/web-antd/src/locales/README.md
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
# locale
|
||||||
|
|
||||||
|
每个app使用的国际化可能不同,这里用于扩展国际化的功能,例如扩展 dayjs、antd组件库的多语言切换,以及app本身的国际化文件。
|
||||||
102
frontend/apps/web-antd/src/locales/index.ts
Normal file
102
frontend/apps/web-antd/src/locales/index.ts
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
import type { Locale } from 'ant-design-vue/es/locale';
|
||||||
|
|
||||||
|
import type { App } from 'vue';
|
||||||
|
|
||||||
|
import type { LocaleSetupOptions, SupportedLanguagesType } from '@vben/locales';
|
||||||
|
|
||||||
|
import { ref } from 'vue';
|
||||||
|
|
||||||
|
import {
|
||||||
|
$t,
|
||||||
|
setupI18n as coreSetup,
|
||||||
|
loadLocalesMapFromDir,
|
||||||
|
} from '@vben/locales';
|
||||||
|
import { preferences } from '@vben/preferences';
|
||||||
|
|
||||||
|
import antdEnLocale from 'ant-design-vue/es/locale/en_US';
|
||||||
|
import antdDefaultLocale from 'ant-design-vue/es/locale/zh_CN';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
|
||||||
|
const antdLocale = ref<Locale>(antdDefaultLocale);
|
||||||
|
|
||||||
|
const modules = import.meta.glob('./langs/**/*.json');
|
||||||
|
|
||||||
|
const localesMap = loadLocalesMapFromDir(
|
||||||
|
/\.\/langs\/([^/]+)\/(.*)\.json$/,
|
||||||
|
modules,
|
||||||
|
);
|
||||||
|
/**
|
||||||
|
* 加载应用特有的语言包
|
||||||
|
* 这里也可以改造为从服务端获取翻译数据
|
||||||
|
* @param lang
|
||||||
|
*/
|
||||||
|
async function loadMessages(lang: SupportedLanguagesType) {
|
||||||
|
const [appLocaleMessages] = await Promise.all([
|
||||||
|
localesMap[lang]?.(),
|
||||||
|
loadThirdPartyMessage(lang),
|
||||||
|
]);
|
||||||
|
return appLocaleMessages?.default;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 加载第三方组件库的语言包
|
||||||
|
* @param lang
|
||||||
|
*/
|
||||||
|
async function loadThirdPartyMessage(lang: SupportedLanguagesType) {
|
||||||
|
await Promise.all([loadAntdLocale(lang), loadDayjsLocale(lang)]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 加载dayjs的语言包
|
||||||
|
* @param lang
|
||||||
|
*/
|
||||||
|
async function loadDayjsLocale(lang: SupportedLanguagesType) {
|
||||||
|
let locale;
|
||||||
|
switch (lang) {
|
||||||
|
case 'en-US': {
|
||||||
|
locale = await import('dayjs/locale/en');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'zh-CN': {
|
||||||
|
locale = await import('dayjs/locale/zh-cn');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
// 默认使用英语
|
||||||
|
default: {
|
||||||
|
locale = await import('dayjs/locale/en');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (locale) {
|
||||||
|
dayjs.locale(locale);
|
||||||
|
} else {
|
||||||
|
console.error(`Failed to load dayjs locale for ${lang}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 加载antd的语言包
|
||||||
|
* @param lang
|
||||||
|
*/
|
||||||
|
async function loadAntdLocale(lang: SupportedLanguagesType) {
|
||||||
|
switch (lang) {
|
||||||
|
case 'en-US': {
|
||||||
|
antdLocale.value = antdEnLocale;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'zh-CN': {
|
||||||
|
antdLocale.value = antdDefaultLocale;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function setupI18n(app: App, options: LocaleSetupOptions = {}) {
|
||||||
|
await coreSetup(app, {
|
||||||
|
defaultLocale: preferences.app.locale,
|
||||||
|
loadMessages,
|
||||||
|
missingWarn: !import.meta.env.PROD,
|
||||||
|
...options,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export { $t, antdLocale, setupI18n };
|
||||||
12
frontend/apps/web-antd/src/locales/langs/en-US/demos.json
Normal file
12
frontend/apps/web-antd/src/locales/langs/en-US/demos.json
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"title": "Demos",
|
||||||
|
"antd": "Ant Design Vue",
|
||||||
|
"vben": {
|
||||||
|
"title": "Project",
|
||||||
|
"about": "About",
|
||||||
|
"document": "Document",
|
||||||
|
"antdv": "Ant Design Vue Version",
|
||||||
|
"naive-ui": "Naive UI Version",
|
||||||
|
"element-plus": "Element Plus Version"
|
||||||
|
}
|
||||||
|
}
|
||||||
19
frontend/apps/web-antd/src/locales/langs/en-US/page.json
Normal file
19
frontend/apps/web-antd/src/locales/langs/en-US/page.json
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"auth": {
|
||||||
|
"login": "Login",
|
||||||
|
"register": "Register",
|
||||||
|
"codeLogin": "Code Login",
|
||||||
|
"qrcodeLogin": "Qr Code Login",
|
||||||
|
"forgetPassword": "Forget Password"
|
||||||
|
},
|
||||||
|
"dashboard": {
|
||||||
|
"title": "Dashboard",
|
||||||
|
"analytics": "Analytics",
|
||||||
|
"workspace": "Workspace"
|
||||||
|
},
|
||||||
|
"workflow": {
|
||||||
|
"title": "Workflow",
|
||||||
|
"processDesign": "Process Design",
|
||||||
|
"vueFlowDesign": "Vue Flow Process Design"
|
||||||
|
}
|
||||||
|
}
|
||||||
12
frontend/apps/web-antd/src/locales/langs/zh-CN/demos.json
Normal file
12
frontend/apps/web-antd/src/locales/langs/zh-CN/demos.json
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"title": "演示",
|
||||||
|
"antd": "Ant Design Vue",
|
||||||
|
"vben": {
|
||||||
|
"title": "项目",
|
||||||
|
"about": "关于",
|
||||||
|
"document": "文档",
|
||||||
|
"antdv": "Ant Design Vue 版本",
|
||||||
|
"naive-ui": "Naive UI 版本",
|
||||||
|
"element-plus": "Element Plus 版本"
|
||||||
|
}
|
||||||
|
}
|
||||||
19
frontend/apps/web-antd/src/locales/langs/zh-CN/page.json
Normal file
19
frontend/apps/web-antd/src/locales/langs/zh-CN/page.json
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"auth": {
|
||||||
|
"login": "登录",
|
||||||
|
"register": "注册",
|
||||||
|
"codeLogin": "验证码登录",
|
||||||
|
"qrcodeLogin": "二维码登录",
|
||||||
|
"forgetPassword": "忘记密码"
|
||||||
|
},
|
||||||
|
"dashboard": {
|
||||||
|
"title": "概览",
|
||||||
|
"analytics": "分析页",
|
||||||
|
"workspace": "工作台"
|
||||||
|
},
|
||||||
|
"workflow": {
|
||||||
|
"title": "流程",
|
||||||
|
"processDesign": "流程设计",
|
||||||
|
"vueFlowDesign": "Vue Flow 流程设计"
|
||||||
|
}
|
||||||
|
}
|
||||||
31
frontend/apps/web-antd/src/main.ts
Normal file
31
frontend/apps/web-antd/src/main.ts
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
import { initPreferences } from '@vben/preferences';
|
||||||
|
import { unmountGlobalLoading } from '@vben/utils';
|
||||||
|
|
||||||
|
import { overridesPreferences } from './preferences';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 应用初始化完成之后再进行页面加载渲染
|
||||||
|
*/
|
||||||
|
async function initApplication() {
|
||||||
|
// name用于指定项目唯一标识
|
||||||
|
// 用于区分不同项目的偏好设置以及存储数据的key前缀以及其他一些需要隔离的数据
|
||||||
|
const env = import.meta.env.PROD ? 'prod' : 'dev';
|
||||||
|
const appVersion = import.meta.env.VITE_APP_VERSION;
|
||||||
|
const namespace = `${import.meta.env.VITE_APP_NAMESPACE}-${appVersion}-${env}`;
|
||||||
|
|
||||||
|
// app偏好设置初始化
|
||||||
|
await initPreferences({
|
||||||
|
namespace,
|
||||||
|
overrides: overridesPreferences,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 启动应用并挂载
|
||||||
|
// vue应用主要逻辑及视图
|
||||||
|
const { bootstrap } = await import('./bootstrap');
|
||||||
|
await bootstrap(namespace);
|
||||||
|
|
||||||
|
// 移除并销毁loading
|
||||||
|
unmountGlobalLoading();
|
||||||
|
}
|
||||||
|
|
||||||
|
initApplication();
|
||||||
13
frontend/apps/web-antd/src/preferences.ts
Normal file
13
frontend/apps/web-antd/src/preferences.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import { defineOverridesPreferences } from '@vben/preferences';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description 项目配置文件
|
||||||
|
* 只需要覆盖项目中的一部分配置,不需要的配置不用覆盖,会自动使用默认配置
|
||||||
|
* !!! 更改配置后请清空缓存,否则可能不生效
|
||||||
|
*/
|
||||||
|
export const overridesPreferences = defineOverridesPreferences({
|
||||||
|
// overrides
|
||||||
|
app: {
|
||||||
|
name: import.meta.env.VITE_APP_TITLE,
|
||||||
|
},
|
||||||
|
});
|
||||||
42
frontend/apps/web-antd/src/router/access.ts
Normal file
42
frontend/apps/web-antd/src/router/access.ts
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
import type {
|
||||||
|
ComponentRecordType,
|
||||||
|
GenerateMenuAndRoutesOptions,
|
||||||
|
} from '@vben/types';
|
||||||
|
|
||||||
|
import { generateAccessible } from '@vben/access';
|
||||||
|
import { preferences } from '@vben/preferences';
|
||||||
|
|
||||||
|
import { message } from 'ant-design-vue';
|
||||||
|
|
||||||
|
import { getAllMenusApi } from '#/api';
|
||||||
|
import { BasicLayout, IFrameView } from '#/layouts';
|
||||||
|
import { $t } from '#/locales';
|
||||||
|
|
||||||
|
const forbiddenComponent = () => import('#/views/_core/fallback/forbidden.vue');
|
||||||
|
|
||||||
|
async function generateAccess(options: GenerateMenuAndRoutesOptions) {
|
||||||
|
const pageMap: ComponentRecordType = import.meta.glob('../views/**/*.vue');
|
||||||
|
|
||||||
|
const layoutMap: ComponentRecordType = {
|
||||||
|
BasicLayout,
|
||||||
|
IFrameView,
|
||||||
|
};
|
||||||
|
|
||||||
|
return await generateAccessible(preferences.app.accessMode, {
|
||||||
|
...options,
|
||||||
|
fetchMenuListAsync: async () => {
|
||||||
|
message.loading({
|
||||||
|
content: `${$t('common.loadingMenu')}...`,
|
||||||
|
duration: 1.5,
|
||||||
|
});
|
||||||
|
return await getAllMenusApi();
|
||||||
|
},
|
||||||
|
// 可以指定没有权限跳转403页面
|
||||||
|
forbiddenComponent,
|
||||||
|
// 如果 route.meta.menuVisibleWithForbidden = true
|
||||||
|
layoutMap,
|
||||||
|
pageMap,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export { generateAccess };
|
||||||
135
frontend/apps/web-antd/src/router/guard.ts
Normal file
135
frontend/apps/web-antd/src/router/guard.ts
Normal file
@ -0,0 +1,135 @@
|
|||||||
|
import type { Router } from 'vue-router';
|
||||||
|
|
||||||
|
import { LOGIN_PATH } from '@vben/constants';
|
||||||
|
import { preferences } from '@vben/preferences';
|
||||||
|
import { useAccessStore, useUserStore } from '@vben/stores';
|
||||||
|
import { startProgress, stopProgress } from '@vben/utils';
|
||||||
|
|
||||||
|
import { accessRoutes, coreRouteNames } from '#/router/routes';
|
||||||
|
import { useAuthStore } from '#/store';
|
||||||
|
|
||||||
|
import { generateAccess } from './access';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通用守卫配置
|
||||||
|
* @param router
|
||||||
|
*/
|
||||||
|
function setupCommonGuard(router: Router) {
|
||||||
|
// 记录已经加载的页面
|
||||||
|
const loadedPaths = new Set<string>();
|
||||||
|
|
||||||
|
router.beforeEach((to) => {
|
||||||
|
to.meta.loaded = loadedPaths.has(to.path);
|
||||||
|
|
||||||
|
// 页面加载进度条
|
||||||
|
if (!to.meta.loaded && preferences.transition.progress) {
|
||||||
|
startProgress();
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
router.afterEach((to) => {
|
||||||
|
// 记录页面是否加载,如果已经加载,后续的页面切换动画等效果不在重复执行
|
||||||
|
|
||||||
|
loadedPaths.add(to.path);
|
||||||
|
|
||||||
|
// 关闭页面加载进度条
|
||||||
|
if (preferences.transition.progress) {
|
||||||
|
stopProgress();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 权限访问守卫配置
|
||||||
|
* @param router
|
||||||
|
*/
|
||||||
|
function setupAccessGuard(router: Router) {
|
||||||
|
router.beforeEach(async (to, from) => {
|
||||||
|
const accessStore = useAccessStore();
|
||||||
|
const userStore = useUserStore();
|
||||||
|
const authStore = useAuthStore();
|
||||||
|
|
||||||
|
// 基本路由,这些路由不需要进入权限拦截
|
||||||
|
if (coreRouteNames.includes(to.name as string)) {
|
||||||
|
if (to.path === LOGIN_PATH && accessStore.accessToken) {
|
||||||
|
return decodeURIComponent(
|
||||||
|
(to.query?.redirect as string) ||
|
||||||
|
userStore.userInfo?.homePath ||
|
||||||
|
preferences.app.defaultHomePath,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== 临时跳过登录验证(仅用于测试国际化) ==========
|
||||||
|
// accessToken 检查
|
||||||
|
// if (!accessStore.accessToken) {
|
||||||
|
// // 明确声明忽略权限访问权限,则可以访问
|
||||||
|
// if (to.meta.ignoreAccess) {
|
||||||
|
// return true;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // 没有访问权限,跳转登录页面
|
||||||
|
// if (to.fullPath !== LOGIN_PATH) {
|
||||||
|
// return {
|
||||||
|
// path: LOGIN_PATH,
|
||||||
|
// // 如不需要,直接删除 query
|
||||||
|
// query:
|
||||||
|
// to.fullPath === preferences.app.defaultHomePath
|
||||||
|
// ? {}
|
||||||
|
// : { redirect: encodeURIComponent(to.fullPath) },
|
||||||
|
// // 携带当前跳转的页面,登录后重新跳转该页面
|
||||||
|
// replace: true,
|
||||||
|
// };
|
||||||
|
// }
|
||||||
|
// return to;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// 是否已经生成过动态路由
|
||||||
|
if (accessStore.isAccessChecked) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 生成路由表
|
||||||
|
// 当前登录用户拥有的角色标识列表
|
||||||
|
// const userInfo = userStore.userInfo || (await authStore.fetchUserInfo());
|
||||||
|
// const userRoles = userInfo.roles ?? [];
|
||||||
|
const userRoles: string[] = []; // 临时:跳过用户信息获取
|
||||||
|
|
||||||
|
// 生成菜单和路由
|
||||||
|
const { accessibleMenus, accessibleRoutes } = await generateAccess({
|
||||||
|
roles: userRoles,
|
||||||
|
router,
|
||||||
|
// 则会在菜单中显示,但是访问会被重定向到403
|
||||||
|
routes: accessRoutes,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 保存菜单信息和路由信息
|
||||||
|
accessStore.setAccessMenus(accessibleMenus);
|
||||||
|
accessStore.setAccessRoutes(accessibleRoutes);
|
||||||
|
accessStore.setIsAccessChecked(true);
|
||||||
|
const redirectPath = (from.query.redirect ??
|
||||||
|
(to.path === preferences.app.defaultHomePath
|
||||||
|
? preferences.app.defaultHomePath // 临时:使用默认首页
|
||||||
|
: to.fullPath)) as string;
|
||||||
|
|
||||||
|
return {
|
||||||
|
...router.resolve(decodeURIComponent(redirectPath)),
|
||||||
|
replace: true,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 项目守卫配置
|
||||||
|
* @param router
|
||||||
|
*/
|
||||||
|
function createRouterGuard(router: Router) {
|
||||||
|
/** 通用 */
|
||||||
|
setupCommonGuard(router);
|
||||||
|
/** 权限访问 */
|
||||||
|
setupAccessGuard(router);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { createRouterGuard };
|
||||||
37
frontend/apps/web-antd/src/router/index.ts
Normal file
37
frontend/apps/web-antd/src/router/index.ts
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
import {
|
||||||
|
createRouter,
|
||||||
|
createWebHashHistory,
|
||||||
|
createWebHistory,
|
||||||
|
} from 'vue-router';
|
||||||
|
|
||||||
|
import {resetStaticRoutes} from '@vben/utils';
|
||||||
|
|
||||||
|
import {createRouterGuard} from './guard';
|
||||||
|
import {routes} from './routes';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh_CN 创建vue-router实例
|
||||||
|
*/
|
||||||
|
const router = createRouter({
|
||||||
|
history:
|
||||||
|
import.meta.env.VITE_ROUTER_HISTORY === 'hash'
|
||||||
|
? createWebHashHistory(import.meta.env.VITE_BASE)
|
||||||
|
: createWebHistory(import.meta.env.VITE_BASE),
|
||||||
|
// 应该添加到路由的初始路由列表。
|
||||||
|
routes,
|
||||||
|
scrollBehavior: (to, _from, savedPosition) => {
|
||||||
|
if (savedPosition) {
|
||||||
|
return savedPosition;
|
||||||
|
}
|
||||||
|
return to.hash ? {behavior: 'smooth', el: to.hash} : {left: 0, top: 0};
|
||||||
|
},
|
||||||
|
// 是否应该禁止尾部斜杠。
|
||||||
|
// strict: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const resetRoutes = () => resetStaticRoutes(router, routes);
|
||||||
|
|
||||||
|
// 创建路由守卫
|
||||||
|
createRouterGuard(router);
|
||||||
|
|
||||||
|
export {resetRoutes, router};
|
||||||
97
frontend/apps/web-antd/src/router/routes/core.ts
Normal file
97
frontend/apps/web-antd/src/router/routes/core.ts
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
import type { RouteRecordRaw } from 'vue-router';
|
||||||
|
|
||||||
|
import { LOGIN_PATH } from '@vben/constants';
|
||||||
|
import { preferences } from '@vben/preferences';
|
||||||
|
|
||||||
|
import { $t } from '#/locales';
|
||||||
|
|
||||||
|
const BasicLayout = () => import('#/layouts/basic.vue');
|
||||||
|
const AuthPageLayout = () => import('#/layouts/auth.vue');
|
||||||
|
/** 全局404页面 */
|
||||||
|
const fallbackNotFoundRoute: RouteRecordRaw = {
|
||||||
|
component: () => import('#/views/_core/fallback/not-found.vue'),
|
||||||
|
meta: {
|
||||||
|
hideInBreadcrumb: true,
|
||||||
|
hideInMenu: true,
|
||||||
|
hideInTab: true,
|
||||||
|
title: '404',
|
||||||
|
},
|
||||||
|
name: 'FallbackNotFound',
|
||||||
|
path: '/:path(.*)*',
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 基本路由,这些路由是必须存在的 */
|
||||||
|
const coreRoutes: RouteRecordRaw[] = [
|
||||||
|
/**
|
||||||
|
* 根路由
|
||||||
|
* 使用基础布局,作为所有页面的父级容器,子级就不必配置BasicLayout。
|
||||||
|
* 此路由必须存在,且不应修改
|
||||||
|
*/
|
||||||
|
{
|
||||||
|
component: BasicLayout,
|
||||||
|
meta: {
|
||||||
|
hideInBreadcrumb: true,
|
||||||
|
title: 'Root',
|
||||||
|
},
|
||||||
|
name: 'Root',
|
||||||
|
path: '/',
|
||||||
|
redirect: preferences.app.defaultHomePath,
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
component: AuthPageLayout,
|
||||||
|
meta: {
|
||||||
|
hideInTab: true,
|
||||||
|
title: 'Authentication',
|
||||||
|
},
|
||||||
|
name: 'Authentication',
|
||||||
|
path: '/auth',
|
||||||
|
redirect: LOGIN_PATH,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
name: 'Login',
|
||||||
|
path: 'login',
|
||||||
|
component: () => import('#/views/_core/authentication/login.vue'),
|
||||||
|
meta: {
|
||||||
|
title: $t('page.auth.login'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'CodeLogin',
|
||||||
|
path: 'code-login',
|
||||||
|
component: () => import('#/views/_core/authentication/code-login.vue'),
|
||||||
|
meta: {
|
||||||
|
title: $t('page.auth.codeLogin'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'QrCodeLogin',
|
||||||
|
path: 'qrcode-login',
|
||||||
|
component: () =>
|
||||||
|
import('#/views/_core/authentication/qrcode-login.vue'),
|
||||||
|
meta: {
|
||||||
|
title: $t('page.auth.qrcodeLogin'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'ForgetPassword',
|
||||||
|
path: 'forget-password',
|
||||||
|
component: () =>
|
||||||
|
import('#/views/_core/authentication/forget-password.vue'),
|
||||||
|
meta: {
|
||||||
|
title: $t('page.auth.forgetPassword'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Register',
|
||||||
|
path: 'register',
|
||||||
|
component: () => import('#/views/_core/authentication/register.vue'),
|
||||||
|
meta: {
|
||||||
|
title: $t('page.auth.register'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export { coreRoutes, fallbackNotFoundRoute };
|
||||||
37
frontend/apps/web-antd/src/router/routes/index.ts
Normal file
37
frontend/apps/web-antd/src/router/routes/index.ts
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
import type { RouteRecordRaw } from 'vue-router';
|
||||||
|
|
||||||
|
import { mergeRouteModules, traverseTreeValues } from '@vben/utils';
|
||||||
|
|
||||||
|
import { coreRoutes, fallbackNotFoundRoute } from './core';
|
||||||
|
|
||||||
|
const dynamicRouteFiles = import.meta.glob('./modules/**/*.ts', {
|
||||||
|
eager: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 有需要可以自行打开注释,并创建文件夹
|
||||||
|
// const externalRouteFiles = import.meta.glob('./external/**/*.ts', { eager: true });
|
||||||
|
// const staticRouteFiles = import.meta.glob('./static/**/*.ts', { eager: true });
|
||||||
|
|
||||||
|
/** 动态路由 */
|
||||||
|
const dynamicRoutes: RouteRecordRaw[] = mergeRouteModules(dynamicRouteFiles);
|
||||||
|
|
||||||
|
/** 外部路由列表,访问这些页面可以不需要Layout,可能用于内嵌在别的系统(不会显示在菜单中) */
|
||||||
|
// const externalRoutes: RouteRecordRaw[] = mergeRouteModules(externalRouteFiles);
|
||||||
|
// const staticRoutes: RouteRecordRaw[] = mergeRouteModules(staticRouteFiles);
|
||||||
|
const staticRoutes: RouteRecordRaw[] = [];
|
||||||
|
const externalRoutes: RouteRecordRaw[] = [];
|
||||||
|
|
||||||
|
/** 路由列表,由基本路由、外部路由和404兜底路由组成
|
||||||
|
* 无需走权限验证(会一直显示在菜单中) */
|
||||||
|
const routes: RouteRecordRaw[] = [
|
||||||
|
...coreRoutes,
|
||||||
|
...externalRoutes,
|
||||||
|
fallbackNotFoundRoute,
|
||||||
|
];
|
||||||
|
|
||||||
|
/** 基本路由列表,这些路由不需要进入权限拦截 */
|
||||||
|
const coreRouteNames = traverseTreeValues(coreRoutes, (route) => route.name);
|
||||||
|
|
||||||
|
/** 有权限校验的路由列表,包含动态路由和静态路由 */
|
||||||
|
const accessRoutes = [...dynamicRoutes, ...staticRoutes];
|
||||||
|
export { accessRoutes, coreRouteNames, routes };
|
||||||
@ -0,0 +1,38 @@
|
|||||||
|
import type { RouteRecordRaw } from 'vue-router';
|
||||||
|
|
||||||
|
import { $t } from '#/locales';
|
||||||
|
|
||||||
|
const routes: RouteRecordRaw[] = [
|
||||||
|
{
|
||||||
|
meta: {
|
||||||
|
icon: 'lucide:layout-dashboard',
|
||||||
|
order: -1,
|
||||||
|
title: $t('page.dashboard.title'),
|
||||||
|
},
|
||||||
|
name: 'Dashboard',
|
||||||
|
path: '/dashboard',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
name: 'Analytics',
|
||||||
|
path: '/analytics',
|
||||||
|
component: () => import('#/views/dashboard/analytics/index.vue'),
|
||||||
|
meta: {
|
||||||
|
affixTab: true,
|
||||||
|
icon: 'lucide:area-chart',
|
||||||
|
title: $t('page.dashboard.analytics'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Workspace',
|
||||||
|
path: '/workspace',
|
||||||
|
component: () => import('#/views/dashboard/workspace/index.vue'),
|
||||||
|
meta: {
|
||||||
|
icon: 'carbon:workspace',
|
||||||
|
title: $t('page.dashboard.workspace'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export default routes;
|
||||||
28
frontend/apps/web-antd/src/router/routes/modules/demos.ts
Normal file
28
frontend/apps/web-antd/src/router/routes/modules/demos.ts
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import type { RouteRecordRaw } from 'vue-router';
|
||||||
|
|
||||||
|
import { $t } from '#/locales';
|
||||||
|
|
||||||
|
const routes: RouteRecordRaw[] = [
|
||||||
|
{
|
||||||
|
meta: {
|
||||||
|
icon: 'ic:baseline-view-in-ar',
|
||||||
|
keepAlive: true,
|
||||||
|
order: 1000,
|
||||||
|
title: $t('demos.title'),
|
||||||
|
},
|
||||||
|
name: 'Demos',
|
||||||
|
path: '/demos',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
meta: {
|
||||||
|
title: $t('demos.antd'),
|
||||||
|
},
|
||||||
|
name: 'AntDesignDemos',
|
||||||
|
path: '/demos/ant-design',
|
||||||
|
component: () => import('#/views/demos/antd/index.vue'),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export default routes;
|
||||||
81
frontend/apps/web-antd/src/router/routes/modules/vben.ts
Normal file
81
frontend/apps/web-antd/src/router/routes/modules/vben.ts
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
import type { RouteRecordRaw } from 'vue-router';
|
||||||
|
|
||||||
|
import {
|
||||||
|
VBEN_DOC_URL,
|
||||||
|
VBEN_ELE_PREVIEW_URL,
|
||||||
|
VBEN_GITHUB_URL,
|
||||||
|
VBEN_LOGO_URL,
|
||||||
|
VBEN_NAIVE_PREVIEW_URL,
|
||||||
|
} from '@vben/constants';
|
||||||
|
|
||||||
|
import { IFrameView } from '#/layouts';
|
||||||
|
import { $t } from '#/locales';
|
||||||
|
|
||||||
|
const routes: RouteRecordRaw[] = [
|
||||||
|
{
|
||||||
|
meta: {
|
||||||
|
badgeType: 'dot',
|
||||||
|
icon: VBEN_LOGO_URL,
|
||||||
|
order: 9998,
|
||||||
|
title: $t('demos.vben.title'),
|
||||||
|
},
|
||||||
|
name: 'VbenProject',
|
||||||
|
path: '/vben-admin',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
name: 'VbenDocument',
|
||||||
|
path: '/vben-admin/document',
|
||||||
|
component: IFrameView,
|
||||||
|
meta: {
|
||||||
|
icon: 'lucide:book-open-text',
|
||||||
|
link: VBEN_DOC_URL,
|
||||||
|
title: $t('demos.vben.document'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'VbenGithub',
|
||||||
|
path: '/vben-admin/github',
|
||||||
|
component: IFrameView,
|
||||||
|
meta: {
|
||||||
|
icon: 'mdi:github',
|
||||||
|
link: VBEN_GITHUB_URL,
|
||||||
|
title: 'Github',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'VbenNaive',
|
||||||
|
path: '/vben-admin/naive',
|
||||||
|
component: IFrameView,
|
||||||
|
meta: {
|
||||||
|
badgeType: 'dot',
|
||||||
|
icon: 'logos:naiveui',
|
||||||
|
link: VBEN_NAIVE_PREVIEW_URL,
|
||||||
|
title: $t('demos.vben.naive-ui'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'VbenElementPlus',
|
||||||
|
path: '/vben-admin/ele',
|
||||||
|
component: IFrameView,
|
||||||
|
meta: {
|
||||||
|
badgeType: 'dot',
|
||||||
|
icon: 'logos:element',
|
||||||
|
link: VBEN_ELE_PREVIEW_URL,
|
||||||
|
title: $t('demos.vben.element-plus'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'VbenAbout',
|
||||||
|
path: '/vben-admin/about',
|
||||||
|
component: () => import('#/views/_core/about/index.vue'),
|
||||||
|
meta: {
|
||||||
|
icon: 'lucide:copyright',
|
||||||
|
title: $t('demos.vben.about'),
|
||||||
|
order: 9999,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export default routes;
|
||||||
47
frontend/apps/web-antd/src/router/routes/modules/workflow.ts
Normal file
47
frontend/apps/web-antd/src/router/routes/modules/workflow.ts
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
import type { RouteRecordRaw } from 'vue-router';
|
||||||
|
|
||||||
|
import { $t } from '#/locales';
|
||||||
|
|
||||||
|
const routes: RouteRecordRaw[] = [
|
||||||
|
{
|
||||||
|
meta: {
|
||||||
|
icon: 'lucide:workflow',
|
||||||
|
order: 5,
|
||||||
|
title: $t('page.workflow.title'),
|
||||||
|
},
|
||||||
|
name: 'Workflow',
|
||||||
|
path: '/workflow',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
name: 'ProcessDesign',
|
||||||
|
path: '/workflow/process-design',
|
||||||
|
component: () => import('#/views/workflow/process-design/index.vue'),
|
||||||
|
meta: {
|
||||||
|
icon: 'lucide:pen-tool',
|
||||||
|
title: $t('page.workflow.processDesign'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'VueFlowTest',
|
||||||
|
path: '/workflow/vue-flow-test',
|
||||||
|
component: () => import('#/views/workflow/vue-flow-test/index.vue'),
|
||||||
|
meta: {
|
||||||
|
icon: 'lucide:test-tube',
|
||||||
|
title: 'Vue Flow 测试',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'VueFlowDesign',
|
||||||
|
path: '/workflow/vue-flow-design',
|
||||||
|
component: () => import('#/views/workflow/vue-flow-design/index.vue'),
|
||||||
|
meta: {
|
||||||
|
icon: 'lucide:git-branch',
|
||||||
|
title: $t('page.workflow.vueFlowDesign'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export default routes;
|
||||||
|
|
||||||
118
frontend/apps/web-antd/src/store/auth.ts
Normal file
118
frontend/apps/web-antd/src/store/auth.ts
Normal file
@ -0,0 +1,118 @@
|
|||||||
|
import type {Recordable, UserInfo} from '@vben/types';
|
||||||
|
|
||||||
|
import {ref} from 'vue';
|
||||||
|
import {useRouter} from 'vue-router';
|
||||||
|
|
||||||
|
import {LOGIN_PATH} from '@vben/constants';
|
||||||
|
import {preferences} from '@vben/preferences';
|
||||||
|
import {resetAllStores, useAccessStore, useUserStore} from '@vben/stores';
|
||||||
|
|
||||||
|
import {notification} from 'ant-design-vue';
|
||||||
|
import {defineStore} from 'pinia';
|
||||||
|
|
||||||
|
import {getAccessCodesApi, getUserInfoApi, loginApi, logoutApi} from '#/api';
|
||||||
|
import {$t} from '#/locales';
|
||||||
|
|
||||||
|
export const useAuthStore = defineStore('auth', () => {
|
||||||
|
const accessStore = useAccessStore();
|
||||||
|
const userStore = useUserStore();
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const loginLoading = ref(false);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 异步处理登录操作
|
||||||
|
* Asynchronously handle the login process
|
||||||
|
* @param params 登录表单数据
|
||||||
|
*/
|
||||||
|
async function authLogin(
|
||||||
|
params: Recordable<any>,
|
||||||
|
onSuccess?: () => Promise<void> | void,
|
||||||
|
) {
|
||||||
|
// 异步处理用户登录操作并获取 accessToken
|
||||||
|
let userInfo: null | UserInfo = null;
|
||||||
|
try {
|
||||||
|
loginLoading.value = true;
|
||||||
|
const {accessToken} = await loginApi(params);
|
||||||
|
|
||||||
|
// 如果成功获取到 accessToken
|
||||||
|
if (accessToken) {
|
||||||
|
accessStore.setAccessToken(accessToken);
|
||||||
|
|
||||||
|
// 获取用户信息并存储到 accessStore 中
|
||||||
|
const [fetchUserInfoResult, accessCodes] = await Promise.all([
|
||||||
|
fetchUserInfo(),
|
||||||
|
getAccessCodesApi(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
userInfo = fetchUserInfoResult;
|
||||||
|
|
||||||
|
userStore.setUserInfo(userInfo);
|
||||||
|
accessStore.setAccessCodes(accessCodes);
|
||||||
|
|
||||||
|
if (accessStore.loginExpired) {
|
||||||
|
accessStore.setLoginExpired(false);
|
||||||
|
} else {
|
||||||
|
onSuccess
|
||||||
|
? await onSuccess?.()
|
||||||
|
: await router.push(
|
||||||
|
userInfo.homePath || preferences.app.defaultHomePath,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (userInfo?.realName) {
|
||||||
|
notification.success({
|
||||||
|
description: `${$t('authentication.loginSuccessDesc')}:${userInfo?.realName}`,
|
||||||
|
duration: 3,
|
||||||
|
message: $t('authentication.loginSuccess'),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
loginLoading.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
userInfo,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function logout(redirect: boolean = true) {
|
||||||
|
try {
|
||||||
|
await logoutApi();
|
||||||
|
} catch {
|
||||||
|
// 不做任何处理
|
||||||
|
}
|
||||||
|
resetAllStores();
|
||||||
|
accessStore.setLoginExpired(false);
|
||||||
|
|
||||||
|
// 回登录页带上当前路由地址
|
||||||
|
await router.replace({
|
||||||
|
path: LOGIN_PATH,
|
||||||
|
query: redirect
|
||||||
|
? {
|
||||||
|
redirect: encodeURIComponent(router.currentRoute.value.fullPath),
|
||||||
|
}
|
||||||
|
: {},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchUserInfo() {
|
||||||
|
let userInfo: null | UserInfo = null;
|
||||||
|
userInfo = await getUserInfoApi();
|
||||||
|
userStore.setUserInfo(userInfo);
|
||||||
|
return userInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
function $reset() {
|
||||||
|
loginLoading.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
$reset,
|
||||||
|
authLogin,
|
||||||
|
fetchUserInfo,
|
||||||
|
loginLoading,
|
||||||
|
logout,
|
||||||
|
};
|
||||||
|
});
|
||||||
1
frontend/apps/web-antd/src/store/index.ts
Normal file
1
frontend/apps/web-antd/src/store/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from './auth';
|
||||||
490
frontend/apps/web-antd/src/store/workflow.ts
Normal file
490
frontend/apps/web-antd/src/store/workflow.ts
Normal file
@ -0,0 +1,490 @@
|
|||||||
|
/**
|
||||||
|
* 工作流 Store
|
||||||
|
* 管理工作流状态、节点类型、字段映射等
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { defineStore } from 'pinia'
|
||||||
|
import { ref, computed } from 'vue'
|
||||||
|
import type {
|
||||||
|
NodeTypeMetadata,
|
||||||
|
WorkflowNode,
|
||||||
|
WorkflowEdge,
|
||||||
|
WorkflowDefinition,
|
||||||
|
UpstreamNodeOutput,
|
||||||
|
FieldPathNode,
|
||||||
|
OutputParameter,
|
||||||
|
HistoryItem,
|
||||||
|
} from '#/types/workflow'
|
||||||
|
import { MOCK_NODE_TYPES, getNodeTypeMetadata } from '#/views/workflow/vue-flow-design/mock/nodeTypes'
|
||||||
|
|
||||||
|
export const useWorkflowStore = defineStore('workflow', () => {
|
||||||
|
// ============== 状态 ==============
|
||||||
|
|
||||||
|
/** 所有可用的节点类型 */
|
||||||
|
const nodeTypes = ref<NodeTypeMetadata[]>(MOCK_NODE_TYPES)
|
||||||
|
|
||||||
|
/** 当前工作流的节点 */
|
||||||
|
const nodes = ref<WorkflowNode[]>([])
|
||||||
|
|
||||||
|
/** 当前工作流的边 */
|
||||||
|
const edges = ref<WorkflowEdge[]>([])
|
||||||
|
|
||||||
|
/** 选中的节点 ID */
|
||||||
|
const selectedNodeId = ref<string | null>(null)
|
||||||
|
|
||||||
|
/** 选中的边 ID */
|
||||||
|
const selectedEdgeId = ref<string | null>(null)
|
||||||
|
|
||||||
|
/** 历史记录 */
|
||||||
|
const history = ref<HistoryItem[]>([])
|
||||||
|
|
||||||
|
/** 历史记录索引 */
|
||||||
|
const historyIndex = ref(-1)
|
||||||
|
|
||||||
|
/** 最大历史记录数 */
|
||||||
|
const maxHistorySize = 50
|
||||||
|
|
||||||
|
/** 工作流元数据 */
|
||||||
|
const workflowMeta = ref({
|
||||||
|
name: '未命名工作流',
|
||||||
|
description: '',
|
||||||
|
id: '',
|
||||||
|
})
|
||||||
|
|
||||||
|
// ============== 计算属性 ==============
|
||||||
|
|
||||||
|
/** 选中的节点 */
|
||||||
|
const selectedNode = computed(() => {
|
||||||
|
if (!selectedNodeId.value) return null
|
||||||
|
return nodes.value.find((n) => n.id === selectedNodeId.value)
|
||||||
|
})
|
||||||
|
|
||||||
|
/** 选中的边 */
|
||||||
|
const selectedEdge = computed(() => {
|
||||||
|
if (!selectedEdgeId.value) return null
|
||||||
|
return edges.value.find((e) => e.id === selectedEdgeId.value)
|
||||||
|
})
|
||||||
|
|
||||||
|
/** 是否可以撤销 */
|
||||||
|
const canUndo = computed(() => historyIndex.value > 0)
|
||||||
|
|
||||||
|
/** 是否可以重做 */
|
||||||
|
const canRedo = computed(() => historyIndex.value < history.value.length - 1)
|
||||||
|
|
||||||
|
/** 节点统计 */
|
||||||
|
const statistics = computed(() => ({
|
||||||
|
nodeCount: nodes.value.length,
|
||||||
|
edgeCount: edges.value.length,
|
||||||
|
nodesByType: nodes.value.reduce((acc, node) => {
|
||||||
|
acc[node.type] = (acc[node.type] || 0) + 1
|
||||||
|
return acc
|
||||||
|
}, {} as Record<string, number>),
|
||||||
|
}))
|
||||||
|
|
||||||
|
// ============== 节点操作 ==============
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加节点
|
||||||
|
*/
|
||||||
|
function addNode(node: WorkflowNode) {
|
||||||
|
nodes.value.push(node)
|
||||||
|
saveHistory()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除节点
|
||||||
|
*/
|
||||||
|
function removeNode(nodeId: string) {
|
||||||
|
const index = nodes.value.findIndex((n) => n.id === nodeId)
|
||||||
|
if (index > -1) {
|
||||||
|
nodes.value.splice(index, 1)
|
||||||
|
// 同时删除相关的边
|
||||||
|
edges.value = edges.value.filter(
|
||||||
|
(e) => e.source !== nodeId && e.target !== nodeId
|
||||||
|
)
|
||||||
|
saveHistory()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新节点数据
|
||||||
|
*/
|
||||||
|
function updateNode(nodeId: string, data: Partial<WorkflowNode>) {
|
||||||
|
const node = nodes.value.find((n) => n.id === nodeId)
|
||||||
|
if (node) {
|
||||||
|
Object.assign(node, data)
|
||||||
|
saveHistory()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新节点的 data 字段
|
||||||
|
*/
|
||||||
|
function updateNodeData(nodeId: string, data: any) {
|
||||||
|
const node = nodes.value.find((n) => n.id === nodeId)
|
||||||
|
if (node) {
|
||||||
|
node.data = { ...node.data, ...data }
|
||||||
|
saveHistory()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============== 边操作 ==============
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加边
|
||||||
|
*/
|
||||||
|
function addEdge(edge: WorkflowEdge) {
|
||||||
|
edges.value.push(edge)
|
||||||
|
saveHistory()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除边
|
||||||
|
*/
|
||||||
|
function removeEdge(edgeId: string) {
|
||||||
|
const index = edges.value.findIndex((e) => e.id === edgeId)
|
||||||
|
if (index > -1) {
|
||||||
|
edges.value.splice(index, 1)
|
||||||
|
saveHistory()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============== 选择操作 ==============
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 选择节点
|
||||||
|
*/
|
||||||
|
function selectNode(nodeId: string | null) {
|
||||||
|
selectedNodeId.value = nodeId
|
||||||
|
selectedEdgeId.value = null
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 选择边
|
||||||
|
*/
|
||||||
|
function selectEdge(edgeId: string | null) {
|
||||||
|
selectedEdgeId.value = edgeId
|
||||||
|
selectedNodeId.value = null
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清除选择
|
||||||
|
*/
|
||||||
|
function clearSelection() {
|
||||||
|
selectedNodeId.value = null
|
||||||
|
selectedEdgeId.value = null
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============== 历史记录 ==============
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 保存历史记录
|
||||||
|
*/
|
||||||
|
function saveHistory() {
|
||||||
|
const currentState: HistoryItem = {
|
||||||
|
nodes: JSON.parse(JSON.stringify(nodes.value)),
|
||||||
|
edges: JSON.parse(JSON.stringify(edges.value)),
|
||||||
|
timestamp: Date.now(),
|
||||||
|
}
|
||||||
|
|
||||||
|
// 移除当前索引之后的历史记录
|
||||||
|
history.value = history.value.slice(0, historyIndex.value + 1)
|
||||||
|
|
||||||
|
// 添加新的历史记录
|
||||||
|
history.value.push(currentState)
|
||||||
|
|
||||||
|
// 限制历史记录大小
|
||||||
|
if (history.value.length > maxHistorySize) {
|
||||||
|
history.value.shift()
|
||||||
|
} else {
|
||||||
|
historyIndex.value++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 撤销
|
||||||
|
*/
|
||||||
|
function undo() {
|
||||||
|
if (canUndo.value) {
|
||||||
|
historyIndex.value--
|
||||||
|
const state = history.value[historyIndex.value]
|
||||||
|
nodes.value = JSON.parse(JSON.stringify(state.nodes))
|
||||||
|
edges.value = JSON.parse(JSON.stringify(state.edges))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 重做
|
||||||
|
*/
|
||||||
|
function redo() {
|
||||||
|
if (canRedo.value) {
|
||||||
|
historyIndex.value++
|
||||||
|
const state = history.value[historyIndex.value]
|
||||||
|
nodes.value = JSON.parse(JSON.stringify(state.nodes))
|
||||||
|
edges.value = JSON.parse(JSON.stringify(state.edges))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============== 字段映射相关 ==============
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取节点的上游节点输出(用于字段映射)
|
||||||
|
*/
|
||||||
|
function getUpstreamOutputs(nodeId: string): UpstreamNodeOutput[] {
|
||||||
|
const upstreamNodes: UpstreamNodeOutput[] = []
|
||||||
|
|
||||||
|
// 找到所有连接到当前节点的边
|
||||||
|
const incomingEdges = edges.value.filter((e) => e.target === nodeId)
|
||||||
|
|
||||||
|
incomingEdges.forEach((edge) => {
|
||||||
|
const sourceNode = nodes.value.find((n) => n.id === edge.source)
|
||||||
|
if (sourceNode) {
|
||||||
|
const nodeType = getNodeTypeMetadata(sourceNode.type)
|
||||||
|
if (nodeType) {
|
||||||
|
const fieldTree = buildFieldTree(nodeType.outputs, edge.source)
|
||||||
|
upstreamNodes.push({
|
||||||
|
nodeId: sourceNode.id,
|
||||||
|
nodeName: sourceNode.data.label,
|
||||||
|
outputs: nodeType.outputs,
|
||||||
|
fieldTree,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return upstreamNodes
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从输出参数构建字段树
|
||||||
|
*/
|
||||||
|
function buildFieldTree(
|
||||||
|
outputs: OutputParameter[],
|
||||||
|
nodeId: string
|
||||||
|
): FieldPathNode[] {
|
||||||
|
const tree: FieldPathNode[] = []
|
||||||
|
|
||||||
|
outputs.forEach((output) => {
|
||||||
|
const rootPath = `nodes.${nodeId}.output.${output.id}`
|
||||||
|
|
||||||
|
if (output.schema) {
|
||||||
|
// 有 schema,构建详细的字段树
|
||||||
|
const children = buildFieldTreeFromSchema(output.schema, rootPath)
|
||||||
|
tree.push({
|
||||||
|
key: output.id,
|
||||||
|
path: rootPath,
|
||||||
|
type: output.type,
|
||||||
|
description: output.description,
|
||||||
|
children: children.length > 0 ? children : undefined,
|
||||||
|
isLeaf: children.length === 0,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
// 没有 schema,只显示顶级字段
|
||||||
|
tree.push({
|
||||||
|
key: output.id,
|
||||||
|
path: rootPath,
|
||||||
|
type: output.type,
|
||||||
|
description: output.description,
|
||||||
|
isLeaf: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return tree
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从 Schema 递归构建字段树
|
||||||
|
*/
|
||||||
|
function buildFieldTreeFromSchema(
|
||||||
|
schema: any,
|
||||||
|
basePath: string
|
||||||
|
): FieldPathNode[] {
|
||||||
|
const nodes: FieldPathNode[] = []
|
||||||
|
|
||||||
|
if (schema.type === 'object' && schema.properties) {
|
||||||
|
Object.entries(schema.properties).forEach(([key, propSchema]: [string, any]) => {
|
||||||
|
const path = `${basePath}.${key}`
|
||||||
|
const children = buildFieldTreeFromSchema(propSchema, path)
|
||||||
|
|
||||||
|
nodes.push({
|
||||||
|
key,
|
||||||
|
path,
|
||||||
|
type: propSchema.type || 'any',
|
||||||
|
description: propSchema.description,
|
||||||
|
example: propSchema.example,
|
||||||
|
children: children.length > 0 ? children : undefined,
|
||||||
|
isLeaf: children.length === 0,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
} else if (schema.type === 'array' && schema.items) {
|
||||||
|
// 数组类型,添加 [0] 示例
|
||||||
|
const path = `${basePath}[0]`
|
||||||
|
const children = buildFieldTreeFromSchema(schema.items, path)
|
||||||
|
|
||||||
|
if (children.length > 0) {
|
||||||
|
nodes.push({
|
||||||
|
key: '[0]',
|
||||||
|
path,
|
||||||
|
type: 'object',
|
||||||
|
description: '数组元素(示例)',
|
||||||
|
children,
|
||||||
|
isLeaf: false,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nodes
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成字段表达式
|
||||||
|
*/
|
||||||
|
function generateFieldExpression(fieldPath: string): string {
|
||||||
|
return `\${${fieldPath}}`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============== 工作流操作 ==============
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 加载工作流
|
||||||
|
*/
|
||||||
|
function loadWorkflow(workflow: WorkflowDefinition) {
|
||||||
|
nodes.value = workflow.nodes
|
||||||
|
edges.value = workflow.edges
|
||||||
|
workflowMeta.value = {
|
||||||
|
name: workflow.name,
|
||||||
|
description: workflow.description || '',
|
||||||
|
id: workflow.id || '',
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重置历史记录
|
||||||
|
history.value = [{
|
||||||
|
nodes: JSON.parse(JSON.stringify(nodes.value)),
|
||||||
|
edges: JSON.parse(JSON.stringify(edges.value)),
|
||||||
|
timestamp: Date.now(),
|
||||||
|
}]
|
||||||
|
historyIndex.value = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 导出工作流定义
|
||||||
|
*/
|
||||||
|
function exportWorkflow(): WorkflowDefinition {
|
||||||
|
return {
|
||||||
|
id: workflowMeta.value.id,
|
||||||
|
name: workflowMeta.value.name,
|
||||||
|
description: workflowMeta.value.description,
|
||||||
|
nodes: nodes.value,
|
||||||
|
edges: edges.value,
|
||||||
|
version: '1.0',
|
||||||
|
createdAt: new Date().toISOString(),
|
||||||
|
updatedAt: new Date().toISOString(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清空工作流
|
||||||
|
*/
|
||||||
|
function clearWorkflow() {
|
||||||
|
nodes.value = []
|
||||||
|
edges.value = []
|
||||||
|
selectedNodeId.value = null
|
||||||
|
selectedEdgeId.value = null
|
||||||
|
history.value = []
|
||||||
|
historyIndex.value = -1
|
||||||
|
workflowMeta.value = {
|
||||||
|
name: '未命名工作流',
|
||||||
|
description: '',
|
||||||
|
id: '',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============== 节点类型查询 ==============
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据类型获取节点元数据
|
||||||
|
*/
|
||||||
|
function getNodeType(type: string): NodeTypeMetadata | undefined {
|
||||||
|
return nodeTypes.value.find((t) => t.type === type)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建节点实例
|
||||||
|
*/
|
||||||
|
function createNodeInstance(
|
||||||
|
type: string,
|
||||||
|
position: { x: number; y: number }
|
||||||
|
): WorkflowNode | null {
|
||||||
|
const nodeType = getNodeType(type)
|
||||||
|
if (!nodeType) return null
|
||||||
|
|
||||||
|
const nodeId = `${type}_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: nodeId,
|
||||||
|
type: nodeType.type,
|
||||||
|
position,
|
||||||
|
data: {
|
||||||
|
label: nodeType.label,
|
||||||
|
type: nodeType.type,
|
||||||
|
description: nodeType.description,
|
||||||
|
inputs: JSON.parse(JSON.stringify(nodeType.inputs)),
|
||||||
|
outputs: JSON.parse(JSON.stringify(nodeType.outputs)),
|
||||||
|
config: {},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
// 状态
|
||||||
|
nodeTypes,
|
||||||
|
nodes,
|
||||||
|
edges,
|
||||||
|
selectedNodeId,
|
||||||
|
selectedEdgeId,
|
||||||
|
workflowMeta,
|
||||||
|
|
||||||
|
// 计算属性
|
||||||
|
selectedNode,
|
||||||
|
selectedEdge,
|
||||||
|
canUndo,
|
||||||
|
canRedo,
|
||||||
|
statistics,
|
||||||
|
|
||||||
|
// 节点操作
|
||||||
|
addNode,
|
||||||
|
removeNode,
|
||||||
|
updateNode,
|
||||||
|
updateNodeData,
|
||||||
|
|
||||||
|
// 边操作
|
||||||
|
addEdge,
|
||||||
|
removeEdge,
|
||||||
|
|
||||||
|
// 选择操作
|
||||||
|
selectNode,
|
||||||
|
selectEdge,
|
||||||
|
clearSelection,
|
||||||
|
|
||||||
|
// 历史记录
|
||||||
|
saveHistory,
|
||||||
|
undo,
|
||||||
|
redo,
|
||||||
|
|
||||||
|
// 字段映射
|
||||||
|
getUpstreamOutputs,
|
||||||
|
buildFieldTree,
|
||||||
|
generateFieldExpression,
|
||||||
|
|
||||||
|
// 工作流操作
|
||||||
|
loadWorkflow,
|
||||||
|
exportWorkflow,
|
||||||
|
clearWorkflow,
|
||||||
|
|
||||||
|
// 节点类型
|
||||||
|
getNodeType,
|
||||||
|
createNodeInstance,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
63
frontend/apps/web-antd/src/types/bpmn.d.ts
vendored
Normal file
63
frontend/apps/web-antd/src/types/bpmn.d.ts
vendored
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
/**
|
||||||
|
* BPMN.js 和 Camunda 相关的 TypeScript 类型声明
|
||||||
|
*/
|
||||||
|
|
||||||
|
declare module 'bpmn-js/lib/Modeler' {
|
||||||
|
import { ModuleDeclaration } from 'didi';
|
||||||
|
|
||||||
|
export interface ModelerOptions {
|
||||||
|
container?: HTMLElement | string;
|
||||||
|
width?: number | string;
|
||||||
|
height?: number | string;
|
||||||
|
moddleExtensions?: Record<string, any>;
|
||||||
|
modules?: ModuleDeclaration[];
|
||||||
|
additionalModules?: ModuleDeclaration[];
|
||||||
|
keyboard?: {
|
||||||
|
bindTo?: Document | HTMLElement;
|
||||||
|
};
|
||||||
|
propertiesPanel?: {
|
||||||
|
parent?: HTMLElement | string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class Modeler {
|
||||||
|
constructor(options?: ModelerOptions);
|
||||||
|
|
||||||
|
importXML(xml: string): Promise<{ warnings: Array<any> }>;
|
||||||
|
saveXML(options?: { format?: boolean; preamble?: boolean }): Promise<{ xml: string }>;
|
||||||
|
saveSVG(options?: Record<string, any>): Promise<{ svg: string }>;
|
||||||
|
|
||||||
|
get<T = any>(serviceName: string): T;
|
||||||
|
invoke<T = any>(fn: (...args: any[]) => T): T;
|
||||||
|
|
||||||
|
destroy(): void;
|
||||||
|
clear(): void;
|
||||||
|
|
||||||
|
attachTo(parentNode: HTMLElement): void;
|
||||||
|
detach(): void;
|
||||||
|
|
||||||
|
on(event: string, callback: (...args: any[]) => void, priority?: number): void;
|
||||||
|
off(event: string, callback?: (...args: any[]) => void): void;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module 'camunda-bpmn-moddle/resources/camunda.json' {
|
||||||
|
const camundaModdleDescriptor: any;
|
||||||
|
export default camundaModdleDescriptor;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module 'bpmn-js-properties-panel' {
|
||||||
|
export const BpmnPropertiesPanelModule: any;
|
||||||
|
export const BpmnPropertiesProviderModule: any;
|
||||||
|
export const CamundaPlatformPropertiesProviderModule: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module '@bpmn-io/properties-panel' {
|
||||||
|
export const PropertiesPanelModule: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module '@bpmn-io/properties-panel/assets/properties-panel.css' {
|
||||||
|
const content: string;
|
||||||
|
export default content;
|
||||||
|
}
|
||||||
|
|
||||||
365
frontend/apps/web-antd/src/types/workflow.ts
Normal file
365
frontend/apps/web-antd/src/types/workflow.ts
Normal file
@ -0,0 +1,365 @@
|
|||||||
|
/**
|
||||||
|
* 工作流类型定义
|
||||||
|
* 基于数据模型设计文档(JSON Schema,前后端统一)
|
||||||
|
* 参考:docs/04-数据模型设计.md
|
||||||
|
*/
|
||||||
|
|
||||||
|
// ============== 基础类型 ==============
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 字段类型(JSON Schema 标准类型)
|
||||||
|
*/
|
||||||
|
export type FieldType =
|
||||||
|
| 'string'
|
||||||
|
| 'number'
|
||||||
|
| 'boolean'
|
||||||
|
| 'object'
|
||||||
|
| 'array'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 表单字段类型(前端表单控件)
|
||||||
|
*/
|
||||||
|
export type FormFieldType =
|
||||||
|
| 'text' // 文本输入
|
||||||
|
| 'textarea' // 多行文本
|
||||||
|
| 'number' // 数字输入
|
||||||
|
| 'select' // 下拉选择
|
||||||
|
| 'code' // 代码编辑器
|
||||||
|
| 'key_value' // 键值对
|
||||||
|
| 'boolean' // 布尔开关
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 节点分类(与后端对齐)
|
||||||
|
*/
|
||||||
|
export type NodeCategory =
|
||||||
|
| 'api' // API 调用
|
||||||
|
| 'database' // 数据库操作
|
||||||
|
| 'logic' // 逻辑控制
|
||||||
|
| 'notification' // 通知
|
||||||
|
| 'transform' // 数据转换
|
||||||
|
| 'other' // 其他
|
||||||
|
|
||||||
|
// ============== 节点类型元数据(后端返回)==============
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 字段定义(NodeTypeMetadata.fields 中的元素)
|
||||||
|
* 对应后端 FieldDefinition
|
||||||
|
*/
|
||||||
|
export interface FieldDefinition {
|
||||||
|
/** 字段名称(用于 config 中的 key) */
|
||||||
|
name: string
|
||||||
|
/** 显示标签 */
|
||||||
|
label: string
|
||||||
|
/** 表单控件类型 */
|
||||||
|
type: FormFieldType
|
||||||
|
/** 是否必填 */
|
||||||
|
required?: boolean
|
||||||
|
/** 是否支持表达式(如 ${nodes.n1.output.body.email}) */
|
||||||
|
supportsExpression?: boolean
|
||||||
|
/** 是否支持字段映射(显示字段选择器) */
|
||||||
|
supportsFieldMapping?: boolean
|
||||||
|
/** 下拉选项(用于 select 类型) */
|
||||||
|
options?: string[]
|
||||||
|
/** 默认值 */
|
||||||
|
defaultValue?: any
|
||||||
|
/** 占位符 */
|
||||||
|
placeholder?: string
|
||||||
|
/** 代码编辑器语言(用于 code 类型) */
|
||||||
|
language?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 节点类型元数据(从后端 /api/node-types 获取)
|
||||||
|
* 对应后端 NodeTypeMetadata
|
||||||
|
*/
|
||||||
|
export interface NodeTypeMetadata {
|
||||||
|
/** 节点类型唯一标识(如 http_request) */
|
||||||
|
id: string
|
||||||
|
/** 驼峰命名(如 httpRequest) */
|
||||||
|
name: string
|
||||||
|
/** 显示名称 */
|
||||||
|
displayName: string
|
||||||
|
/** 节点分类 */
|
||||||
|
category: NodeCategory
|
||||||
|
/** 图标名称 */
|
||||||
|
icon?: string
|
||||||
|
/** 描述 */
|
||||||
|
description?: string
|
||||||
|
/** 字段定义列表(驱动动态表单) */
|
||||||
|
fields: FieldDefinition[]
|
||||||
|
/** 输出 Schema(JSON Schema 格式,用于字段映射) */
|
||||||
|
outputSchema: {
|
||||||
|
type: 'object'
|
||||||
|
properties: Record<string, any>
|
||||||
|
}
|
||||||
|
/** 后端实现类(可选) */
|
||||||
|
implementationClass?: string
|
||||||
|
/** 是否启用 */
|
||||||
|
enabled?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============== 工作流定义(对应后端 WorkflowDefinition)==============
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 位置信息
|
||||||
|
*/
|
||||||
|
export interface Position {
|
||||||
|
x: number
|
||||||
|
y: number
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 工作流节点(对应后端 WorkflowNode)
|
||||||
|
* 保存到数据库的标准格式
|
||||||
|
*/
|
||||||
|
export interface WorkflowNode {
|
||||||
|
/** 节点 ID(唯一) */
|
||||||
|
id: string
|
||||||
|
/** 节点类型(对应 NodeTypeMetadata.id,如 http_request) */
|
||||||
|
type: string
|
||||||
|
/** 节点显示名称 */
|
||||||
|
name: string
|
||||||
|
/** 节点位置(画布坐标) */
|
||||||
|
position: Position
|
||||||
|
/** 节点配置数据(字段值,可包含表达式 ${...}) */
|
||||||
|
config: Record<string, any>
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 工作流边(对应后端 WorkflowEdge)
|
||||||
|
*/
|
||||||
|
export interface WorkflowEdge {
|
||||||
|
/** 边 ID(可选,自动生成) */
|
||||||
|
id?: string
|
||||||
|
/** 源节点 ID */
|
||||||
|
source: string
|
||||||
|
/** 目标节点 ID */
|
||||||
|
target: string
|
||||||
|
/** 条件表达式(用于条件分支,必须是 ${...} 格式) */
|
||||||
|
condition?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 工作流定义(对应后端 WorkflowDefinition)
|
||||||
|
* 前后端统一的数据结构
|
||||||
|
*/
|
||||||
|
export interface WorkflowDefinition {
|
||||||
|
/** 工作流 ID */
|
||||||
|
id: string
|
||||||
|
/** 工作流名称 */
|
||||||
|
name: string
|
||||||
|
/** 工作流描述 */
|
||||||
|
description?: string
|
||||||
|
/** Schema 版本(固定为 "1.0") */
|
||||||
|
schemaVersion: '1.0'
|
||||||
|
/** 节点列表 */
|
||||||
|
nodes: WorkflowNode[]
|
||||||
|
/** 边列表 */
|
||||||
|
edges: WorkflowEdge[]
|
||||||
|
/** 工作流变量 */
|
||||||
|
variables?: Record<string, any>
|
||||||
|
/** 元数据 */
|
||||||
|
metadata?: Record<string, any>
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============== Vue Flow 扩展(仅前端使用)==============
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vue Flow 节点数据(前端画布渲染)
|
||||||
|
* 包含运行时状态和元数据信息
|
||||||
|
*/
|
||||||
|
export interface VueFlowNodeData {
|
||||||
|
/** 节点显示名称 */
|
||||||
|
name: string
|
||||||
|
/** 节点类型 */
|
||||||
|
type: string
|
||||||
|
/** 节点配置 */
|
||||||
|
config: Record<string, any>
|
||||||
|
/** 节点元数据(从 NodeTypeMetadata 获取) */
|
||||||
|
metadata?: NodeTypeMetadata
|
||||||
|
/** 执行状态(运行时) */
|
||||||
|
status?: 'pending' | 'running' | 'success' | 'error'
|
||||||
|
/** 错误信息 */
|
||||||
|
error?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vue Flow 节点(前端画布使用)
|
||||||
|
*/
|
||||||
|
export interface VueFlowNode {
|
||||||
|
id: string
|
||||||
|
type: string
|
||||||
|
position: Position
|
||||||
|
/** Vue Flow 节点数据 */
|
||||||
|
data: VueFlowNodeData
|
||||||
|
/** 是否选中 */
|
||||||
|
selected?: boolean
|
||||||
|
/** 是否可拖拽 */
|
||||||
|
draggable?: boolean
|
||||||
|
/** 是否可连接 */
|
||||||
|
connectable?: boolean
|
||||||
|
[key: string]: any
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vue Flow 边(前端画布使用)
|
||||||
|
*/
|
||||||
|
export interface VueFlowEdge {
|
||||||
|
id: string
|
||||||
|
source: string
|
||||||
|
target: string
|
||||||
|
/** 源节点输出句柄 ID */
|
||||||
|
sourceHandle?: string
|
||||||
|
/** 目标节点输入句柄 ID */
|
||||||
|
targetHandle?: string
|
||||||
|
/** 是否动画 */
|
||||||
|
animated?: boolean
|
||||||
|
/** 标签 */
|
||||||
|
label?: string
|
||||||
|
/** 样式 */
|
||||||
|
style?: Record<string, any>
|
||||||
|
/** 边类型 */
|
||||||
|
type?: string
|
||||||
|
/** 边数据 */
|
||||||
|
data?: {
|
||||||
|
condition?: string
|
||||||
|
[key: string]: any
|
||||||
|
}
|
||||||
|
[key: string]: any
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============== 字段映射相关 ==============
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JSON Schema 属性定义
|
||||||
|
*/
|
||||||
|
export interface JSONSchemaProperty {
|
||||||
|
type: FieldType | string
|
||||||
|
description?: string
|
||||||
|
properties?: Record<string, JSONSchemaProperty>
|
||||||
|
items?: JSONSchemaProperty
|
||||||
|
[key: string]: any
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 字段路径节点(用于字段树)
|
||||||
|
* 从 outputSchema 构建的树形结构,用于字段映射选择器
|
||||||
|
*/
|
||||||
|
export interface FieldPathNode {
|
||||||
|
/** 字段名称 */
|
||||||
|
name: string
|
||||||
|
/** 字段完整路径(如 output.body.email) */
|
||||||
|
path: string
|
||||||
|
/** 字段类型 */
|
||||||
|
type: FieldType | string
|
||||||
|
/** 字段描述 */
|
||||||
|
description?: string
|
||||||
|
/** 子字段 */
|
||||||
|
children?: FieldPathNode[]
|
||||||
|
/** 是否是叶子节点 */
|
||||||
|
isLeaf?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 上游节点输出(用于字段映射选择)
|
||||||
|
*/
|
||||||
|
export interface UpstreamNodeOutput {
|
||||||
|
/** 节点 ID */
|
||||||
|
nodeId: string
|
||||||
|
/** 节点名称 */
|
||||||
|
nodeName: string
|
||||||
|
/** 节点类型 */
|
||||||
|
nodeType: string
|
||||||
|
/** 输出 Schema */
|
||||||
|
outputSchema: NodeTypeMetadata['outputSchema']
|
||||||
|
/** 字段树(从 outputSchema 构建) */
|
||||||
|
fieldTree: FieldPathNode[]
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============== 执行结果(对应后端 NodeExecutionResult)==============
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 节点执行结果
|
||||||
|
*/
|
||||||
|
export interface NodeExecutionResult {
|
||||||
|
/** 执行状态 */
|
||||||
|
status: 'success' | 'failed' | 'running'
|
||||||
|
/** 输出数据 */
|
||||||
|
output: Record<string, any>
|
||||||
|
/** 错误信息 */
|
||||||
|
error: string | null
|
||||||
|
/** 开始时间(ISO 8601) */
|
||||||
|
startTime: string
|
||||||
|
/** 结束时间(ISO 8601) */
|
||||||
|
endTime: string
|
||||||
|
/** 执行时长(毫秒) */
|
||||||
|
durationMs: number
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============== API 响应类型 ==============
|
||||||
|
|
||||||
|
/**
|
||||||
|
* API 响应包装
|
||||||
|
*/
|
||||||
|
export interface ApiResponse<T = any> {
|
||||||
|
code: number
|
||||||
|
message: string
|
||||||
|
data: T
|
||||||
|
success: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 节点类型列表响应
|
||||||
|
*/
|
||||||
|
export interface NodeTypeListResponse {
|
||||||
|
nodeTypes: NodeTypeMetadata[]
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 工作流执行请求
|
||||||
|
*/
|
||||||
|
export interface ExecuteWorkflowRequest {
|
||||||
|
workflowId: string
|
||||||
|
input?: Record<string, any>
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 工作流执行结果
|
||||||
|
*/
|
||||||
|
export interface WorkflowExecutionResult {
|
||||||
|
executionId: string
|
||||||
|
status: 'running' | 'completed' | 'failed'
|
||||||
|
startTime: string
|
||||||
|
endTime?: string
|
||||||
|
result?: any
|
||||||
|
error?: string
|
||||||
|
nodeExecutions: Array<{
|
||||||
|
nodeId: string
|
||||||
|
status: 'pending' | 'running' | 'success' | 'error'
|
||||||
|
input?: any
|
||||||
|
output?: any
|
||||||
|
error?: string
|
||||||
|
startTime?: string
|
||||||
|
endTime?: string
|
||||||
|
}>
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============== 前端工具类型 ==============
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 历史记录项(前端撤销/重做)
|
||||||
|
*/
|
||||||
|
export interface HistoryItem {
|
||||||
|
nodes: WorkflowNode[]
|
||||||
|
edges: WorkflowEdge[]
|
||||||
|
timestamp: number
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 拖拽数据(从节点面板拖拽到画布)
|
||||||
|
*/
|
||||||
|
export interface DragNodeData {
|
||||||
|
nodeTypeId: string
|
||||||
|
metadata: NodeTypeMetadata
|
||||||
|
}
|
||||||
|
|
||||||
3
frontend/apps/web-antd/src/views/_core/README.md
Normal file
3
frontend/apps/web-antd/src/views/_core/README.md
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
# \_core
|
||||||
|
|
||||||
|
此目录包含应用程序正常运行所需的基本视图。这些视图是应用程序布局中使用的视图。
|
||||||
9
frontend/apps/web-antd/src/views/_core/about/index.vue
Normal file
9
frontend/apps/web-antd/src/views/_core/about/index.vue
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import { About } from '@vben/common-ui';
|
||||||
|
|
||||||
|
defineOptions({ name: 'About' });
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<About />
|
||||||
|
</template>
|
||||||
@ -0,0 +1,69 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import type { VbenFormSchema } from '@vben/common-ui';
|
||||||
|
import type { Recordable } from '@vben/types';
|
||||||
|
|
||||||
|
import { computed, ref } from 'vue';
|
||||||
|
|
||||||
|
import { AuthenticationCodeLogin, z } from '@vben/common-ui';
|
||||||
|
import { $t } from '@vben/locales';
|
||||||
|
|
||||||
|
defineOptions({ name: 'CodeLogin' });
|
||||||
|
|
||||||
|
const loading = ref(false);
|
||||||
|
const CODE_LENGTH = 6;
|
||||||
|
|
||||||
|
const formSchema = computed((): VbenFormSchema[] => {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
component: 'VbenInput',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: $t('authentication.mobile'),
|
||||||
|
},
|
||||||
|
fieldName: 'phoneNumber',
|
||||||
|
label: $t('authentication.mobile'),
|
||||||
|
rules: z
|
||||||
|
.string()
|
||||||
|
.min(1, { message: $t('authentication.mobileTip') })
|
||||||
|
.refine((v) => /^\d{11}$/.test(v), {
|
||||||
|
message: $t('authentication.mobileErrortip'),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
component: 'VbenPinInput',
|
||||||
|
componentProps: {
|
||||||
|
codeLength: CODE_LENGTH,
|
||||||
|
createText: (countdown: number) => {
|
||||||
|
const text =
|
||||||
|
countdown > 0
|
||||||
|
? $t('authentication.sendText', [countdown])
|
||||||
|
: $t('authentication.sendCode');
|
||||||
|
return text;
|
||||||
|
},
|
||||||
|
placeholder: $t('authentication.code'),
|
||||||
|
},
|
||||||
|
fieldName: 'code',
|
||||||
|
label: $t('authentication.code'),
|
||||||
|
rules: z.string().length(CODE_LENGTH, {
|
||||||
|
message: $t('authentication.codeTip', [CODE_LENGTH]),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
});
|
||||||
|
/**
|
||||||
|
* 异步处理登录操作
|
||||||
|
* Asynchronously handle the login process
|
||||||
|
* @param values 登录表单数据
|
||||||
|
*/
|
||||||
|
async function handleLogin(values: Recordable<any>) {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.log(values);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<AuthenticationCodeLogin
|
||||||
|
:form-schema="formSchema"
|
||||||
|
:loading="loading"
|
||||||
|
@submit="handleLogin"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
@ -0,0 +1,43 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import type { VbenFormSchema } from '@vben/common-ui';
|
||||||
|
import type { Recordable } from '@vben/types';
|
||||||
|
|
||||||
|
import { computed, ref } from 'vue';
|
||||||
|
|
||||||
|
import { AuthenticationForgetPassword, z } from '@vben/common-ui';
|
||||||
|
import { $t } from '@vben/locales';
|
||||||
|
|
||||||
|
defineOptions({ name: 'ForgetPassword' });
|
||||||
|
|
||||||
|
const loading = ref(false);
|
||||||
|
|
||||||
|
const formSchema = computed((): VbenFormSchema[] => {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
component: 'VbenInput',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: 'example@example.com',
|
||||||
|
},
|
||||||
|
fieldName: 'email',
|
||||||
|
label: $t('authentication.email'),
|
||||||
|
rules: z
|
||||||
|
.string()
|
||||||
|
.min(1, { message: $t('authentication.emailTip') })
|
||||||
|
.email($t('authentication.emailValidErrorTip')),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
});
|
||||||
|
|
||||||
|
function handleSubmit(value: Recordable<any>) {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.log('reset email:', value);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<AuthenticationForgetPassword
|
||||||
|
:form-schema="formSchema"
|
||||||
|
:loading="loading"
|
||||||
|
@submit="handleSubmit"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
@ -0,0 +1,98 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import type { VbenFormSchema } from '@vben/common-ui';
|
||||||
|
import type { BasicOption } from '@vben/types';
|
||||||
|
|
||||||
|
import { computed, markRaw } from 'vue';
|
||||||
|
|
||||||
|
import { AuthenticationLogin, SliderCaptcha, z } from '@vben/common-ui';
|
||||||
|
import { $t } from '@vben/locales';
|
||||||
|
|
||||||
|
import { useAuthStore } from '#/store';
|
||||||
|
|
||||||
|
defineOptions({ name: 'Login' });
|
||||||
|
|
||||||
|
const authStore = useAuthStore();
|
||||||
|
|
||||||
|
const MOCK_USER_OPTIONS: BasicOption[] = [
|
||||||
|
{
|
||||||
|
label: 'Super',
|
||||||
|
value: 'vben',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Admin',
|
||||||
|
value: 'admin',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'User',
|
||||||
|
value: 'jack',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const formSchema = computed((): VbenFormSchema[] => {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
component: 'VbenSelect',
|
||||||
|
componentProps: {
|
||||||
|
options: MOCK_USER_OPTIONS,
|
||||||
|
placeholder: $t('authentication.selectAccount'),
|
||||||
|
},
|
||||||
|
fieldName: 'selectAccount',
|
||||||
|
label: $t('authentication.selectAccount'),
|
||||||
|
rules: z
|
||||||
|
.string()
|
||||||
|
.min(1, { message: $t('authentication.selectAccount') })
|
||||||
|
.optional()
|
||||||
|
.default('vben'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
component: 'VbenInput',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: $t('authentication.usernameTip'),
|
||||||
|
},
|
||||||
|
dependencies: {
|
||||||
|
trigger(values, form) {
|
||||||
|
if (values.selectAccount) {
|
||||||
|
const findUser = MOCK_USER_OPTIONS.find(
|
||||||
|
(item) => item.value === values.selectAccount,
|
||||||
|
);
|
||||||
|
if (findUser) {
|
||||||
|
form.setValues({
|
||||||
|
password: '123456',
|
||||||
|
username: findUser.value,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
triggerFields: ['selectAccount'],
|
||||||
|
},
|
||||||
|
fieldName: 'username',
|
||||||
|
label: $t('authentication.username'),
|
||||||
|
rules: z.string().min(1, { message: $t('authentication.usernameTip') }),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
component: 'VbenInputPassword',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: $t('authentication.password'),
|
||||||
|
},
|
||||||
|
fieldName: 'password',
|
||||||
|
label: $t('authentication.password'),
|
||||||
|
rules: z.string().min(1, { message: $t('authentication.passwordTip') }),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
component: markRaw(SliderCaptcha),
|
||||||
|
fieldName: 'captcha',
|
||||||
|
rules: z.boolean().refine((value) => value, {
|
||||||
|
message: $t('authentication.verifyRequiredTip'),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<AuthenticationLogin
|
||||||
|
:form-schema="formSchema"
|
||||||
|
:loading="authStore.loginLoading"
|
||||||
|
@submit="authStore.authLogin"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
@ -0,0 +1,10 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import { AuthenticationQrCodeLogin } from '@vben/common-ui';
|
||||||
|
import { LOGIN_PATH } from '@vben/constants';
|
||||||
|
|
||||||
|
defineOptions({ name: 'QrCodeLogin' });
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<AuthenticationQrCodeLogin :login-path="LOGIN_PATH" />
|
||||||
|
</template>
|
||||||
@ -0,0 +1,96 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import type { VbenFormSchema } from '@vben/common-ui';
|
||||||
|
import type { Recordable } from '@vben/types';
|
||||||
|
|
||||||
|
import { computed, h, ref } from 'vue';
|
||||||
|
|
||||||
|
import { AuthenticationRegister, z } from '@vben/common-ui';
|
||||||
|
import { $t } from '@vben/locales';
|
||||||
|
|
||||||
|
defineOptions({ name: 'Register' });
|
||||||
|
|
||||||
|
const loading = ref(false);
|
||||||
|
|
||||||
|
const formSchema = computed((): VbenFormSchema[] => {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
component: 'VbenInput',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: $t('authentication.usernameTip'),
|
||||||
|
},
|
||||||
|
fieldName: 'username',
|
||||||
|
label: $t('authentication.username'),
|
||||||
|
rules: z.string().min(1, { message: $t('authentication.usernameTip') }),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
component: 'VbenInputPassword',
|
||||||
|
componentProps: {
|
||||||
|
passwordStrength: true,
|
||||||
|
placeholder: $t('authentication.password'),
|
||||||
|
},
|
||||||
|
fieldName: 'password',
|
||||||
|
label: $t('authentication.password'),
|
||||||
|
renderComponentContent() {
|
||||||
|
return {
|
||||||
|
strengthText: () => $t('authentication.passwordStrength'),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
rules: z.string().min(1, { message: $t('authentication.passwordTip') }),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
component: 'VbenInputPassword',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: $t('authentication.confirmPassword'),
|
||||||
|
},
|
||||||
|
dependencies: {
|
||||||
|
rules(values) {
|
||||||
|
const { password } = values;
|
||||||
|
return z
|
||||||
|
.string({ required_error: $t('authentication.passwordTip') })
|
||||||
|
.min(1, { message: $t('authentication.passwordTip') })
|
||||||
|
.refine((value) => value === password, {
|
||||||
|
message: $t('authentication.confirmPasswordTip'),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
triggerFields: ['password'],
|
||||||
|
},
|
||||||
|
fieldName: 'confirmPassword',
|
||||||
|
label: $t('authentication.confirmPassword'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
component: 'VbenCheckbox',
|
||||||
|
fieldName: 'agreePolicy',
|
||||||
|
renderComponentContent: () => ({
|
||||||
|
default: () =>
|
||||||
|
h('span', [
|
||||||
|
$t('authentication.agree'),
|
||||||
|
h(
|
||||||
|
'a',
|
||||||
|
{
|
||||||
|
class: 'vben-link ml-1 ',
|
||||||
|
href: '',
|
||||||
|
},
|
||||||
|
`${$t('authentication.privacyPolicy')} & ${$t('authentication.terms')}`,
|
||||||
|
),
|
||||||
|
]),
|
||||||
|
}),
|
||||||
|
rules: z.boolean().refine((value) => !!value, {
|
||||||
|
message: $t('authentication.agreeTip'),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
});
|
||||||
|
|
||||||
|
function handleSubmit(value: Recordable<any>) {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.log('register submit:', value);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<AuthenticationRegister
|
||||||
|
:form-schema="formSchema"
|
||||||
|
:loading="loading"
|
||||||
|
@submit="handleSubmit"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
@ -0,0 +1,7 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import { Fallback } from '@vben/common-ui';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Fallback status="coming-soon" />
|
||||||
|
</template>
|
||||||
@ -0,0 +1,9 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import { Fallback } from '@vben/common-ui';
|
||||||
|
|
||||||
|
defineOptions({ name: 'Fallback403Demo' });
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Fallback status="403" />
|
||||||
|
</template>
|
||||||
@ -0,0 +1,9 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import { Fallback } from '@vben/common-ui';
|
||||||
|
|
||||||
|
defineOptions({ name: 'Fallback500Demo' });
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Fallback status="500" />
|
||||||
|
</template>
|
||||||
@ -0,0 +1,9 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import { Fallback } from '@vben/common-ui';
|
||||||
|
|
||||||
|
defineOptions({ name: 'Fallback404Demo' });
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Fallback status="404" />
|
||||||
|
</template>
|
||||||
@ -0,0 +1,9 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import { Fallback } from '@vben/common-ui';
|
||||||
|
|
||||||
|
defineOptions({ name: 'FallbackOfflineDemo' });
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Fallback status="offline" />
|
||||||
|
</template>
|
||||||
@ -0,0 +1,98 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import type { EchartsUIType } from '@vben/plugins/echarts';
|
||||||
|
|
||||||
|
import { onMounted, ref } from 'vue';
|
||||||
|
|
||||||
|
import { EchartsUI, useEcharts } from '@vben/plugins/echarts';
|
||||||
|
|
||||||
|
const chartRef = ref<EchartsUIType>();
|
||||||
|
const { renderEcharts } = useEcharts(chartRef);
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
renderEcharts({
|
||||||
|
grid: {
|
||||||
|
bottom: 0,
|
||||||
|
containLabel: true,
|
||||||
|
left: '1%',
|
||||||
|
right: '1%',
|
||||||
|
top: '2 %',
|
||||||
|
},
|
||||||
|
series: [
|
||||||
|
{
|
||||||
|
areaStyle: {},
|
||||||
|
data: [
|
||||||
|
111, 2000, 6000, 16_000, 33_333, 55_555, 64_000, 33_333, 18_000,
|
||||||
|
36_000, 70_000, 42_444, 23_222, 13_000, 8000, 4000, 1200, 333, 222,
|
||||||
|
111,
|
||||||
|
],
|
||||||
|
itemStyle: {
|
||||||
|
color: '#5ab1ef',
|
||||||
|
},
|
||||||
|
smooth: true,
|
||||||
|
type: 'line',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
areaStyle: {},
|
||||||
|
data: [
|
||||||
|
33, 66, 88, 333, 3333, 6200, 20_000, 3000, 1200, 13_000, 22_000,
|
||||||
|
11_000, 2221, 1201, 390, 198, 60, 30, 22, 11,
|
||||||
|
],
|
||||||
|
itemStyle: {
|
||||||
|
color: '#019680',
|
||||||
|
},
|
||||||
|
smooth: true,
|
||||||
|
type: 'line',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
tooltip: {
|
||||||
|
axisPointer: {
|
||||||
|
lineStyle: {
|
||||||
|
color: '#019680',
|
||||||
|
width: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
trigger: 'axis',
|
||||||
|
},
|
||||||
|
// xAxis: {
|
||||||
|
// axisTick: {
|
||||||
|
// show: false,
|
||||||
|
// },
|
||||||
|
// boundaryGap: false,
|
||||||
|
// data: Array.from({ length: 18 }).map((_item, index) => `${index + 6}:00`),
|
||||||
|
// type: 'category',
|
||||||
|
// },
|
||||||
|
xAxis: {
|
||||||
|
axisTick: {
|
||||||
|
show: false,
|
||||||
|
},
|
||||||
|
boundaryGap: false,
|
||||||
|
data: Array.from({ length: 18 }).map((_item, index) => `${index + 6}:00`),
|
||||||
|
splitLine: {
|
||||||
|
lineStyle: {
|
||||||
|
type: 'solid',
|
||||||
|
width: 1,
|
||||||
|
},
|
||||||
|
show: true,
|
||||||
|
},
|
||||||
|
type: 'category',
|
||||||
|
},
|
||||||
|
yAxis: [
|
||||||
|
{
|
||||||
|
axisTick: {
|
||||||
|
show: false,
|
||||||
|
},
|
||||||
|
max: 80_000,
|
||||||
|
splitArea: {
|
||||||
|
show: true,
|
||||||
|
},
|
||||||
|
splitNumber: 4,
|
||||||
|
type: 'value',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<EchartsUI ref="chartRef" />
|
||||||
|
</template>
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user