Initial commit
This commit is contained in:
184
skills/web-build/templates/report-app/README.md
Normal file
184
skills/web-build/templates/report-app/README.md
Normal file
@@ -0,0 +1,184 @@
|
||||
# 报工页面模板
|
||||
|
||||
移动端报工 H5 页面,支持工单选择、物料信息展示和报工提交。
|
||||
|
||||
## 📁 文件结构
|
||||
|
||||
```
|
||||
report-app/
|
||||
├── index.html # 主页面
|
||||
├── css/custom.css # 自定义样式
|
||||
├── js/
|
||||
│ ├── main.js # 主应用逻辑(MainApp 类)
|
||||
│ └── common.js # 工具函数 + UI组件
|
||||
├── services/
|
||||
│ ├── core.js # API服务 + 认证服务
|
||||
│ └── business.js # 工单服务 + 报工服务
|
||||
├── api_doc/ # API 接口文档(精简版)
|
||||
│ ├── 工单列表_BLACKLAKE-1686655055663532.json
|
||||
│ ├── 生产任务列表_BLACKLAKE-1681109889053785.json
|
||||
│ ├── 报工物料列表_BLACKLAKE-1681369551143844.json
|
||||
│ ├── 批量报工_BLACKLAKE-1681109889053798.json
|
||||
│ └── 报工记录列表_BLACKLAKE-1681109889053794.json
|
||||
└── README.md
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔗 核心接口调用流程
|
||||
|
||||
```
|
||||
1. 工单列表 → 获取 workOrderId、物料信息
|
||||
↓
|
||||
2. 生产任务列表 → 获取 taskId、executorIds
|
||||
↓
|
||||
3. 报工物料列表 → 获取 progressReportKey、reportUnitId
|
||||
↓
|
||||
4. 批量报工 → 提交报工
|
||||
↓
|
||||
5. 报工记录列表 → 查询最新报工记录
|
||||
```
|
||||
|
||||
### 接口与代码位置对照
|
||||
|
||||
| 接口 | 文档 | 代码位置 | 方法 |
|
||||
|------|------|---------|------|
|
||||
| 工单列表 | `api_doc/工单列表_*.json` | `services/business.js:28` | `getWorkOrderList` |
|
||||
| 生产任务列表 | `api_doc/生产任务列表_*.json` | `services/business.js:303` | `getReportRequiredParams` |
|
||||
| 报工物料列表 | `api_doc/报工物料列表_*.json` | `services/business.js:324` | `getReportRequiredParams` |
|
||||
| 批量报工 | `api_doc/批量报工_*.json` | `services/business.js:499` | `submitReport` |
|
||||
| 报工记录列表 | `api_doc/报工记录列表_*.json` | `services/business.js:643` | `getReportRecordsByTask` |
|
||||
|
||||
---
|
||||
|
||||
## 🎯 关键代码位置
|
||||
|
||||
### index.html 关键区域
|
||||
|
||||
| 区域 | 元素ID | 行号 | 用途 |
|
||||
|------|--------|------|------|
|
||||
| 工单选择 | `#work-order-selector` | ~34 | 工单选择按钮 |
|
||||
| 工单下拉 | `#work-order-dropdown` | ~69 | 工单列表弹窗 |
|
||||
| 空状态 | `#empty-state` | ~111 | 未选工单时显示 |
|
||||
| 物料信息 | `#material-info` | ~122 | 显示物料名称、编号 |
|
||||
| 报工表单 | `#report-form` | ~167 | 数量输入等表单 |
|
||||
| 提交按钮 | `#submit-btn` | ~245 | 提交报工 |
|
||||
|
||||
### js/main.js 关键方法
|
||||
|
||||
| 方法 | 行号 | 用途 |
|
||||
|------|------|------|
|
||||
| `onWorkOrderSelected` | ~169 | 工单选择后的处理 |
|
||||
| `extractMaterialDataFromWorkOrder` | ~204 | 从工单提取物料信息 |
|
||||
| `switchToMaterialView` | ~252 | 显示物料信息区域 |
|
||||
| `handleSubmit` | ~323 | 提交报工处理 |
|
||||
| `handleSubmitSuccess` | ~380 | 报工成功后处理 |
|
||||
|
||||
### services/business.js 关键方法
|
||||
|
||||
| 方法 | 行号 | 用途 |
|
||||
|------|------|------|
|
||||
| `getWorkOrderList` | ~28 | 获取工单列表 |
|
||||
| `processWorkOrderListResponse` | ~85 | 处理工单响应 |
|
||||
| `getReportRequiredParams` | ~287 | 获取报工必填参数 |
|
||||
| `buildReportRequestParams` | ~406 | 构建报工请求 |
|
||||
| `submitReport` | ~499 | 提交报工 |
|
||||
| `getReportRecordsByTask` | ~643 | 查询报工记录 |
|
||||
|
||||
---
|
||||
|
||||
## 🛠️ AI 修改指南
|
||||
|
||||
### 常见修改场景
|
||||
|
||||
#### 场景 1:修改表单字段
|
||||
- **位置**: `index.html` 的 `#report-form` 区域 (~167行)
|
||||
- **模式**: 复制相邻的 `<div class="flex items-center justify-between">` 结构
|
||||
|
||||
#### 场景 2:修改物料信息显示
|
||||
- **位置**:
|
||||
- HTML: `index.html` 的 `#material-info` 区域 (~122行)
|
||||
- JS: `js/main.js` 的 `extractMaterialDataFromWorkOrder` 方法 (~204行)
|
||||
|
||||
#### 场景 3:修改报工提交逻辑
|
||||
- **位置**: `js/main.js` 的 `handleSubmit` 方法 (~323行)
|
||||
- **数据来源**: `this.state.selectedWorkOrder` 和 `this.state.materialData`
|
||||
|
||||
#### 场景 4:修改接口参数
|
||||
- **位置**: `services/business.js` 的对应方法
|
||||
- **必读**: `api_doc/` 下的接口文档
|
||||
|
||||
#### 场景 5:添加报工记录显示
|
||||
- **步骤**:
|
||||
1. 在 `index.html` 添加显示区域
|
||||
2. 在 `js/main.js` 的 `handleSubmitSuccess` 中调用 `reportService.getReportRecordsByTask`
|
||||
3. 渲染报工记录列表
|
||||
|
||||
### 修改原则
|
||||
|
||||
1. **接口字段必须与 api_doc 一致** - 禁止凭空创造字段
|
||||
2. **优先使用现有方法** - 不要重写已有逻辑
|
||||
3. **保持代码风格一致** - 参考相邻代码
|
||||
4. **批量修改** - 同一文件的修改一次完成
|
||||
|
||||
---
|
||||
|
||||
## 📝 关键数据结构
|
||||
|
||||
### 工单数据 (workOrder)
|
||||
|
||||
```javascript
|
||||
{
|
||||
workOrderId: 12345, // 工单ID(用于报工)
|
||||
workOrderCode: "WO-001", // 工单编号
|
||||
materialInfo: { // 物料信息
|
||||
baseInfo: {
|
||||
id: 100, // 物料ID
|
||||
name: "产品A", // 物料名称(加工物料名称)
|
||||
code: "M001" // 物料编码(物料编号)
|
||||
}
|
||||
},
|
||||
qualifiedHoldAmount: { // 合格数量
|
||||
amount: 100,
|
||||
amountDisplay: "100"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 报工必填参数 (requiredParams)
|
||||
|
||||
```javascript
|
||||
{
|
||||
taskId: 67890, // 生产任务ID(必填)
|
||||
progressReportMaterial: { // 报工物料(必填,不可修改结构)
|
||||
lineId: 111,
|
||||
materialId: 100,
|
||||
reportProcessId: 222
|
||||
},
|
||||
outputMaterialUnit: {
|
||||
id: 333 // 报工单位ID(reportUnitId)
|
||||
},
|
||||
executorIds: [1, 2, 3] // 执行人ID列表
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🎨 样式配置
|
||||
|
||||
主题色配置在 `css/custom.css`:
|
||||
|
||||
```css
|
||||
:root {
|
||||
--primary-color: #02b980; /* 主色 */
|
||||
--primary-hover: #029968; /* 悬停色 */
|
||||
--primary-light: #e6f7f1; /* 浅色背景 */
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ⚡ 技术栈
|
||||
|
||||
- **HTML5** + **Tailwind CSS** + **原生 JavaScript (ES6+)**
|
||||
- **无需构建**,直接作为静态资源访问
|
||||
@@ -0,0 +1,103 @@
|
||||
{
|
||||
"接口名称": "工单列表",
|
||||
"接口路径": "/openapi/domain/web/v1/route/med/open/v2/work_order/base/_list",
|
||||
"请求方法": "POST",
|
||||
"代码位置": "services/business.js line 59",
|
||||
|
||||
"请求参数": {
|
||||
"page": {
|
||||
"类型": "number",
|
||||
"必填": false,
|
||||
"默认": 1,
|
||||
"说明": "页码"
|
||||
},
|
||||
"size": {
|
||||
"类型": "number",
|
||||
"必填": false,
|
||||
"默认": 50,
|
||||
"说明": "每页数量"
|
||||
},
|
||||
"workOrderCode": {
|
||||
"类型": "string",
|
||||
"必填": false,
|
||||
"说明": "工单编号(模糊查询)"
|
||||
},
|
||||
"exactWorkOrderCode": {
|
||||
"类型": "string",
|
||||
"必填": false,
|
||||
"说明": "工单编号(精确查询)"
|
||||
},
|
||||
"workOrderStatusList": {
|
||||
"类型": "number[]",
|
||||
"必填": false,
|
||||
"说明": "工单业务状态列表"
|
||||
},
|
||||
"pauseFlag": {
|
||||
"类型": "number",
|
||||
"必填": false,
|
||||
"说明": "是否暂停 0未暂停 1已暂停"
|
||||
}
|
||||
},
|
||||
|
||||
"响应结构": {
|
||||
"code": "200 表示成功",
|
||||
"message": "返回信息",
|
||||
"data": {
|
||||
"list": "工单数组",
|
||||
"page": "当前页",
|
||||
"total": "总条数"
|
||||
}
|
||||
},
|
||||
|
||||
"关键响应字段": {
|
||||
"data.list[].workOrderId": "number - 工单ID(必填,用于查询任务和报工)",
|
||||
"data.list[].workOrderCode": "string - 工单编号(显示用)",
|
||||
"data.list[].materialInfo": "object - 完整物料信息对象,包含 baseInfo、attribute、conversions、unit 等",
|
||||
"data.list[].materialInfo.baseInfo.id": "number - 物料ID(主产出物料ID)",
|
||||
"data.list[].materialInfo.baseInfo.name": "string - 物料名称(显示:加工物料名称)",
|
||||
"data.list[].materialInfo.baseInfo.code": "string - 物料编码(显示:物料编号)",
|
||||
"data.list[].materialInfo.baseInfo.specification": "string - 规格",
|
||||
"data.list[].materialInfo.conversions[]": "array - 单位转换关系数组",
|
||||
"data.list[].materialInfo.conversions[].fromUnitId": "number - 基本单位ID",
|
||||
"data.list[].materialInfo.conversions[].fromUnitName": "string - 基本单位名称",
|
||||
"data.list[].materialInfo.conversions[].toUnitId": "number - 转换单位ID",
|
||||
"data.list[].materialInfo.conversions[].toUnitName": "string - 转换单位名称",
|
||||
"data.list[].materialInfo.unit": "object - 物料单位信息",
|
||||
"data.list[].materialInfo.unit.id": "number - 单位ID",
|
||||
"data.list[].materialInfo.unit.code": "string - 单位编码",
|
||||
"data.list[].materialInfo.unit.name": "string - 单位名称",
|
||||
"data.list[].qualifiedHoldAmount": "BaseAmountDisplay对象 - 合格报工数(显示用)",
|
||||
"data.list[].qualifiedHoldAmount.amount": "number - 合格数量",
|
||||
"data.list[].qualifiedHoldAmount.amountDisplay": "string - 合格数量显示",
|
||||
"data.list[].disqualifiedHoldAmount": "BaseAmountDisplay对象 - 不良报工数(显示用)",
|
||||
"data.list[].disqualifiedHoldAmount.amount": "number - 不良数量",
|
||||
"data.list[].totalHoldAmount": "BaseAmountDisplay对象 - 总报工数(显示用)",
|
||||
"data.list[].totalHoldAmount.amount": "number - 总数量",
|
||||
"data.list[].expectedAmount": "BaseAmountDisplay对象 - 预计生产数",
|
||||
"data.list[].plannedAmount": "BaseAmountDisplay对象 - 计划生产数",
|
||||
"data.list[].createdAt": "number - 创建时间戳",
|
||||
"data.list[].updatedAt": "number - 更新时间戳",
|
||||
"data.list[].plannedStartTime": "number - 计划开始时间戳",
|
||||
"data.list[].plannedEndTime": "number - 计划完工时间戳",
|
||||
"data.list[].pauseFlag": "number - 暂停状态 1:是 0:否",
|
||||
"data.list[].workOrderStatus": "object - 工单状态",
|
||||
"data.list[].workOrderStatus.code": "number - 状态码",
|
||||
"data.list[].workOrderStatus.message": "string - 状态描述",
|
||||
"data.list[].workOrderType": "object - 工单类型",
|
||||
"data.list[].processRoute": "object - 工艺路线信息",
|
||||
"data.list[].processRoute.id": "number - 工艺路线ID",
|
||||
"data.list[].processRoute.code": "string - 工艺路线编码",
|
||||
"data.list[].processRoute.name": "string - 工艺路线名称"
|
||||
},
|
||||
|
||||
"BaseAmountDisplay 结构": {
|
||||
"amount": "number - 数量",
|
||||
"amountDisplay": "string - 显示文本(无科学计数法)",
|
||||
"unitId": "number - 单位ID",
|
||||
"unitCode": "string - 单位编码",
|
||||
"unitName": "string - 单位名称"
|
||||
},
|
||||
|
||||
"使用示例": "参见 services/business.js:28-141 的 getWorkOrderList 方法和 processWorkOrderListResponse 方法"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
{
|
||||
"接口名称": "批量报工",
|
||||
"接口路径": "/openapi/domain/web/v1/route/mfg/open/v1/progress_report/_progress_report",
|
||||
"请求方法": "POST",
|
||||
"代码位置": "services/business.js line 516",
|
||||
|
||||
"请求参数": {
|
||||
"taskId": {
|
||||
"类型": "number",
|
||||
"必填": true,
|
||||
"说明": "生产任务ID(从生产任务列表获取)"
|
||||
},
|
||||
"progressReportMaterial": {
|
||||
"类型": "object",
|
||||
"必填": true,
|
||||
"说明": "报工物料对象(从报工物料列表获取)",
|
||||
"结构": {
|
||||
"lineId": "number - 物料行ID",
|
||||
"materialId": "number - 物料ID",
|
||||
"reportProcessId": "number - 报工工序ID"
|
||||
}
|
||||
},
|
||||
"qcStatus": {
|
||||
"类型": "number",
|
||||
"必填": false,
|
||||
"默认": 1,
|
||||
"说明": "质量状态(1:合格 2:让步合格 3:代检 4:不合格)"
|
||||
},
|
||||
"reportType": {
|
||||
"类型": "number",
|
||||
"必填": false,
|
||||
"默认": 2,
|
||||
"说明": "报工方式(1:扫码报工-合格 2:记录报工-合格 3:扫码报工-不合格 4:记录报工-不合格 5:打码报工-合格 6:打码报工-不合格)"
|
||||
},
|
||||
"progressReportItems": {
|
||||
"类型": "array",
|
||||
"必填": true,
|
||||
"说明": "报工详情数组",
|
||||
"结构": [{
|
||||
"executorIds": "number[] - 执行人ID列表",
|
||||
"progressReportMaterialItems": "array - 物料报工项",
|
||||
"progressReportMaterialItems[].reportAmount": "number - 报工数量(必填)",
|
||||
"progressReportMaterialItems[].reportUnitId": "number - 报工单位ID(必填,从 outputMaterialUnit.id 获取)",
|
||||
"progressReportMaterialItems[].remark": "string - 备注(可选)"
|
||||
}]
|
||||
},
|
||||
"storageLocationId": {
|
||||
"类型": "number",
|
||||
"必填": false,
|
||||
"说明": "仓位ID,当前预置为 1716848012872791"
|
||||
},
|
||||
"reportStartTime": {
|
||||
"类型": "number",
|
||||
"必填": false,
|
||||
"说明": "报工开始时间(毫秒时间戳)"
|
||||
},
|
||||
"reportEndTime": {
|
||||
"类型": "number",
|
||||
"必填": false,
|
||||
"说明": "报工结束时间(毫秒时间戳)"
|
||||
},
|
||||
"actualExecutorIds": {
|
||||
"类型": "number[]",
|
||||
"必填": false,
|
||||
"说明": "实际执行人ID列表"
|
||||
},
|
||||
"actualEquipmentIds": {
|
||||
"类型": "number[]",
|
||||
"必填": false,
|
||||
"说明": "设备ID列表"
|
||||
},
|
||||
"workHour": {
|
||||
"类型": "number",
|
||||
"必填": false,
|
||||
"说明": "工时"
|
||||
},
|
||||
"workHourUnit": {
|
||||
"类型": "number",
|
||||
"必填": false,
|
||||
"说明": "工时单位"
|
||||
},
|
||||
"qcDefectReasonIds": {
|
||||
"类型": "number[]",
|
||||
"必填": false,
|
||||
"说明": "不良原因ID列表(质量状态为不合格时)"
|
||||
}
|
||||
},
|
||||
|
||||
"响应结构": {
|
||||
"code": "200 表示成功",
|
||||
"message": "返回信息",
|
||||
"data": {
|
||||
"messageTraceId": "string - 消息追踪ID",
|
||||
"progressReportRecordIds": "number[] - 报工记录ID列表"
|
||||
}
|
||||
},
|
||||
|
||||
"关键说明": [
|
||||
"必须先调用 getReportRequiredParams 获取 taskId、progressReportMaterial、reportUnitId 等必填参数",
|
||||
"progressReportMaterial 对象的结构不可修改,必须从报工物料列表接口获取",
|
||||
"reportUnitId 必须从报工物料列表的 outputMaterialUnit.id 获取",
|
||||
"executorIds 必须从生产任务列表的 executorList[].id 获取"
|
||||
],
|
||||
|
||||
"完整调用流程示例": {
|
||||
"步骤1": "查询工单列表 -> 获取 workOrderId 和物料信息",
|
||||
"步骤2": "查询生产任务 -> 获取 taskId 和 executorIds",
|
||||
"步骤3": "查询报工物料 -> 获取 progressReportKey 和 reportUnitId",
|
||||
"步骤4": "构建报工参数 -> 调用批量报工接口",
|
||||
"步骤5": "查询报工记录 -> 显示最新报工信息"
|
||||
},
|
||||
|
||||
"使用示例": "参见 services/business.js:406-482 的 buildReportRequestParams 方法和 submitReport 方法"
|
||||
}
|
||||
|
||||
106
skills/web-build/templates/report-app/api_doc/批量报工_简化版.json
Normal file
106
skills/web-build/templates/report-app/api_doc/批量报工_简化版.json
Normal file
@@ -0,0 +1,106 @@
|
||||
{
|
||||
"接口名称": "批量报工",
|
||||
"接口路径": "/openapi/domain/web/v1/route/mfg/open/v1/progress_report/_progress_report",
|
||||
"请求方法": "POST",
|
||||
"代码位置": "services/business.js line 516",
|
||||
|
||||
"请求参数": {
|
||||
"taskId": {
|
||||
"类型": "number",
|
||||
"必填": true,
|
||||
"说明": "生产任务ID(从生产任务列表获取)"
|
||||
},
|
||||
"progressReportMaterial": {
|
||||
"类型": "object",
|
||||
"必填": true,
|
||||
"说明": "报工物料对象(从报工物料列表获取)",
|
||||
"结构": {
|
||||
"lineId": "number - 物料行ID",
|
||||
"materialId": "number - 物料ID",
|
||||
"reportProcessId": "number - 报工工序ID"
|
||||
}
|
||||
},
|
||||
"qcStatus": {
|
||||
"类型": "number",
|
||||
"必填": false,
|
||||
"默认": 1,
|
||||
"说明": "质量状态(1:合格 2:让步合格 3:代检 4:不合格)"
|
||||
},
|
||||
"reportType": {
|
||||
"类型": "number",
|
||||
"必填": false,
|
||||
"默认": 2,
|
||||
"说明": "报工方式(1:扫码报工-合格 2:记录报工-合格 3:扫码报工-不合格 4:记录报工-不合格 5:打码报工-合格 6:打码报工-不合格)"
|
||||
},
|
||||
"progressReportItems": {
|
||||
"类型": "array",
|
||||
"必填": true,
|
||||
"说明": "报工详情数组",
|
||||
"结构": [{
|
||||
"executorIds": "number[] - 执行人ID列表",
|
||||
"progressReportMaterialItems": "array - 物料报工项",
|
||||
"progressReportMaterialItems[].reportAmount": "number - 报工数量(必填)",
|
||||
"progressReportMaterialItems[].reportUnitId": "number - 报工单位ID(必填,从 outputMaterialUnit.id 获取)",
|
||||
"progressReportMaterialItems[].remark": "string - 备注(可选)"
|
||||
}]
|
||||
},
|
||||
"storageLocationId": {
|
||||
"类型": "number",
|
||||
"必填": false,
|
||||
"说明": "仓位ID,当前预置为 1716848012872791"
|
||||
},
|
||||
"reportStartTime": {
|
||||
"类型": "number",
|
||||
"必填": false,
|
||||
"说明": "报工开始时间(毫秒时间戳)"
|
||||
},
|
||||
"reportEndTime": {
|
||||
"类型": "number",
|
||||
"必填": false,
|
||||
"说明": "报工结束时间(毫秒时间戳)"
|
||||
},
|
||||
"actualExecutorIds": {
|
||||
"类型": "number[]",
|
||||
"必填": false,
|
||||
"说明": "实际执行人ID列表"
|
||||
},
|
||||
"actualEquipmentIds": {
|
||||
"类型": "number[]",
|
||||
"必填": false,
|
||||
"说明": "设备ID列表"
|
||||
},
|
||||
"workHour": {
|
||||
"类型": "number",
|
||||
"必填": false,
|
||||
"说明": "工时"
|
||||
},
|
||||
"workHourUnit": {
|
||||
"类型": "number",
|
||||
"必填": false,
|
||||
"说明": "工时单位"
|
||||
},
|
||||
"qcDefectReasonIds": {
|
||||
"类型": "number[]",
|
||||
"必填": false,
|
||||
"说明": "不良原因ID列表(质量状态为不合格时)"
|
||||
}
|
||||
},
|
||||
|
||||
"响应结构": {
|
||||
"code": "200 表示成功",
|
||||
"message": "返回信息",
|
||||
"data": {
|
||||
"messageTraceId": "string - 消息追踪ID",
|
||||
"progressReportRecordIds": "number[] - 报工记录ID列表"
|
||||
}
|
||||
},
|
||||
|
||||
"关键说明": [
|
||||
"必须先调用 getReportRequiredParams 获取 taskId、progressReportMaterial、reportUnitId 等必填参数",
|
||||
"progressReportMaterial 对象的结构不可修改,必须从报工物料列表接口获取",
|
||||
"reportUnitId 必须从报工物料列表的 outputMaterialUnit.id 获取"
|
||||
],
|
||||
|
||||
"使用示例": "参见 services/business.js:406-482 的 buildReportRequestParams 方法和 submitReport 方法"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
{
|
||||
"接口名称": "报工物料列表",
|
||||
"接口路径": "/openapi/domain/web/v1/route/mfg/open/v1/progress_report/_list_progress_report_materials",
|
||||
"请求方法": "POST",
|
||||
"代码位置": "services/business.js line 324",
|
||||
|
||||
"请求参数": {
|
||||
"taskId": {
|
||||
"类型": "number",
|
||||
"必填": true,
|
||||
"说明": "生产任务ID"
|
||||
}
|
||||
},
|
||||
|
||||
"响应结构": {
|
||||
"code": "200 表示成功",
|
||||
"message": "返回信息",
|
||||
"data": {
|
||||
"outputMaterials": "产出物料数组",
|
||||
"inputMaterials": "投入物料数组"
|
||||
}
|
||||
},
|
||||
|
||||
"关键响应字段 - outputMaterials[]": {
|
||||
"materialInfo": "object - 物料信息,包含 baseInfo(id、name、code 等)",
|
||||
"materialInfo.baseInfo.id": "number - 物料ID(主产出物料ID)",
|
||||
"materialInfo.baseInfo.name": "string - 物料名称(显示:加工物料名称)",
|
||||
"materialInfo.baseInfo.code": "string - 物料编码(显示:物料编号)",
|
||||
"materialInfo.baseInfo.specification": "string - 规格",
|
||||
"outputMaterialUnit": "object - 报工单位信息(必填,用于报工)",
|
||||
"outputMaterialUnit.id": "number - 单位ID(报工时必需:reportUnitId)",
|
||||
"outputMaterialUnit.code": "string - 单位编码",
|
||||
"outputMaterialUnit.name": "string - 单位名称(如:个、kg)",
|
||||
"mainFlag": "boolean - 是否为主产出物料(优先选择 mainFlag=true 的物料)",
|
||||
"reportType": "array<number> - 可报工方式列表",
|
||||
"warehousingFlag": "boolean - 是否入库",
|
||||
"autoWarehousingFlag": "boolean - 是否自动入库",
|
||||
"progressReportKey": "object - 报工关系信息(必填,用于构建报工参数)",
|
||||
"progressReportKey.lineId": "number - 物料行ID(报工时必需)",
|
||||
"progressReportKey.materialId": "number - 物料ID(报工时必需)",
|
||||
"progressReportKey.reportProcessId": "number - 报工工序ID(报工时必需)",
|
||||
"outputMaterialAmount": "object - 产出物料数量信息",
|
||||
"outputMaterialAmount.amount": "number - 数量",
|
||||
"outputMaterialAmount.amountDisplay": "string - 数量显示"
|
||||
},
|
||||
|
||||
"使用场景": "用于获取报工必填参数,特别是 progressReportKey 和 outputMaterialUnit",
|
||||
"使用示例": "参见 services/business.js:318-389 的 getReportRequiredParams 方法"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
{
|
||||
"接口名称": "报工记录列表",
|
||||
"接口路径": "/openapi/domain/web/v1/route/mfg/open/v1/progress_report/_list",
|
||||
"请求方法": "POST",
|
||||
"代码位置": "services/business.js line 718",
|
||||
|
||||
"请求参数": {
|
||||
"page": {
|
||||
"类型": "number",
|
||||
"必填": false,
|
||||
"默认": 1,
|
||||
"说明": "页码"
|
||||
},
|
||||
"size": {
|
||||
"类型": "number",
|
||||
"必填": false,
|
||||
"默认": 200,
|
||||
"说明": "每页数量"
|
||||
},
|
||||
"taskIds": {
|
||||
"类型": "number[]",
|
||||
"必填": false,
|
||||
"说明": "生产任务ID列表(与 workOrderIdList 至少提供一个)"
|
||||
},
|
||||
"workOrderIdList": {
|
||||
"类型": "number[]",
|
||||
"必填": false,
|
||||
"说明": "工单ID列表(与 taskIds 至少提供一个)"
|
||||
},
|
||||
"reportTimeFrom": {
|
||||
"类型": "number",
|
||||
"必填": false,
|
||||
"说明": "报工时间From(闭区间),毫秒时间戳"
|
||||
},
|
||||
"reportTimeTo": {
|
||||
"类型": "number",
|
||||
"必填": false,
|
||||
"说明": "报工时间To(开区间),毫秒时间戳"
|
||||
},
|
||||
"sorter": {
|
||||
"类型": "array",
|
||||
"必填": false,
|
||||
"默认": [{"field": "reportTime", "order": "desc"}],
|
||||
"说明": "排序条件列表",
|
||||
"结构": [{
|
||||
"field": "string - 排序字段(如 reportTime、taskCode)",
|
||||
"order": "string - 排序规律(asc 升序 / desc 降序,默认 asc)"
|
||||
}]
|
||||
},
|
||||
"processIdList": {
|
||||
"类型": "number[]",
|
||||
"必填": false,
|
||||
"说明": "工序ID列表"
|
||||
},
|
||||
"executorIdList": {
|
||||
"类型": "number[]",
|
||||
"必填": false,
|
||||
"说明": "可执行人ID列表"
|
||||
},
|
||||
"qcStatusList": {
|
||||
"类型": "number[]",
|
||||
"必填": false,
|
||||
"说明": "质量状态列表"
|
||||
}
|
||||
},
|
||||
|
||||
"响应结构": {
|
||||
"code": "200 表示成功",
|
||||
"message": "返回信息",
|
||||
"data": {
|
||||
"list": "报工记录数组",
|
||||
"page": "当前页",
|
||||
"total": "总条数"
|
||||
}
|
||||
},
|
||||
|
||||
"关键响应字段 - list[]": {
|
||||
"id": "number - 报工记录详情id",
|
||||
"lineId": "number - 物料行id",
|
||||
"reportRecordId": "number - 报工记录id",
|
||||
"taskId": "number - 生产任务Id",
|
||||
"taskCode": "string - 生产任务编号",
|
||||
"workOrderId": "number - 工单Id",
|
||||
"workOrderCode": "string - 工单编号(显示用)",
|
||||
"processId": "number - 工序Id",
|
||||
"processCode": "string - 工序编号",
|
||||
"processName": "string - 工序名称",
|
||||
"mainMaterialId": "number - 工单物料id",
|
||||
"mainMaterialCode": "string - 工单物料编号",
|
||||
"mainMaterialName": "string - 工单物料名称",
|
||||
"materialInfo": "object - 报工物料信息",
|
||||
"materialInfo.baseInfo.id": "number - 物料ID",
|
||||
"materialInfo.baseInfo.name": "string - 物料名称",
|
||||
"materialInfo.baseInfo.code": "string - 物料编码",
|
||||
"reportBaseAmount": "object - 报工数量对象",
|
||||
"reportBaseAmount.amount": "number - 报工数量(显示:报工数量 10个)",
|
||||
"reportBaseAmount.unit": "object - 单位信息",
|
||||
"reportBaseAmount.unit.name": "string - 单位名称",
|
||||
"reportBaseAmountDisplay": "object - 报工数量显示对象",
|
||||
"reportBaseAmountDisplay.amount": "number - 报工数量",
|
||||
"reportBaseAmountDisplay.amountDisplay": "string - 报工数量显示文本",
|
||||
"reportBaseAmountDisplay.unitCode": "string - 单位编码",
|
||||
"reportBaseAmountDisplay.unitId": "number - 单位ID",
|
||||
"reportBaseAmountDisplay.unitName": "string - 单位名称",
|
||||
"reportTime": "number - 报工时间,毫秒时间戳(显示:报工时间 2024-11-26 14:30)",
|
||||
"reporter": "object - 报工人员",
|
||||
"reporter.id": "number - 报工人ID",
|
||||
"reporter.name": "string - 报工人姓名",
|
||||
"reporter.username": "string - 报工人用户名",
|
||||
"producers": "array - 生产人员数组",
|
||||
"producers[].id": "number - 生产人ID",
|
||||
"producers[].name": "string - 生产人姓名",
|
||||
"qcStatus": "object - 质量状态",
|
||||
"qcStatus.code": "number - 质量状态码(1:合格 4:不合格)",
|
||||
"qcStatus.message": "string - 质量状态描述",
|
||||
"workHourUnit": "object - 工时单位",
|
||||
"startTime": "number - 报工开始时间,毫秒时间戳(可选)",
|
||||
"endTime": "number - 报工结束时间,毫秒时间戳(可选)",
|
||||
"workHour": "number - 工时(可选)",
|
||||
"remark": "string - 备注(可选)",
|
||||
"reportRecordCode": "string - 报工记录编号(可选)",
|
||||
"createdAt": "number - 创建时间戳",
|
||||
"updatedAt": "number - 更新时间戳"
|
||||
},
|
||||
|
||||
"使用示例": "参见 services/business.js:643-752 的 getReportRecordsByTask 方法"
|
||||
}
|
||||
|
||||
28
skills/web-build/templates/report-app/api_doc/接口说明.txt
Executable file
28
skills/web-build/templates/report-app/api_doc/接口说明.txt
Executable file
@@ -0,0 +1,28 @@
|
||||
# 接口文档
|
||||
|
||||
## 📋 概述
|
||||
|
||||
本文档详细说明了本项目涉及的所有接口调用情况。
|
||||
|
||||
## 🔄 接口调用流程
|
||||
|
||||
### 1. 页面初始化流程
|
||||
```
|
||||
页面加载 → url获取 code → 换取 access_token提供后续open接口调用
|
||||
```
|
||||
|
||||
### 2. 选择工单
|
||||
```
|
||||
用户选择特定工单 → 选择后回显工单上的物料信息
|
||||
```
|
||||
|
||||
### 3. 报工提交流程
|
||||
```
|
||||
用户填写报工数据 → 点击提交 → 构建批量报工请求参数 → 调用批量报工接口 → 清空表单
|
||||
```
|
||||
|
||||
### 4. 查看报工记录(可选)
|
||||
```
|
||||
根据用户报工时使用的生产 taskId 或者 workOrderId → 构建报工记录的请求参数 → 调用报工记录的接口 → 渲染数据
|
||||
```
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
{
|
||||
"接口名称": "生产任务列表",
|
||||
"接口路径": "/openapi/domain/web/v1/route/mfg/open/v1/produce_task/_list",
|
||||
"请求方法": "POST",
|
||||
"代码位置": "services/business.js line 303",
|
||||
|
||||
"请求参数": {
|
||||
"page": {
|
||||
"类型": "number",
|
||||
"必填": false,
|
||||
"默认": 1,
|
||||
"说明": "页码"
|
||||
},
|
||||
"size": {
|
||||
"类型": "number",
|
||||
"必填": false,
|
||||
"默认": 10,
|
||||
"说明": "每页数量"
|
||||
},
|
||||
"workOrderIdList": {
|
||||
"类型": "number[]",
|
||||
"必填": true,
|
||||
"说明": "工单ID列表"
|
||||
}
|
||||
},
|
||||
|
||||
"响应结构": {
|
||||
"code": "200 表示成功",
|
||||
"message": "返回信息",
|
||||
"data": {
|
||||
"list": "生产任务数组",
|
||||
"page": "当前页",
|
||||
"total": "总条数"
|
||||
}
|
||||
},
|
||||
|
||||
"关键响应字段": {
|
||||
"data.list[].taskId": "number - 生产任务ID(必填,用于报工)",
|
||||
"data.list[].taskCode": "string - 生产任务编号",
|
||||
"data.list[].workOrderId": "number - 工单ID",
|
||||
"data.list[].workOrderCode": "string - 工单编号",
|
||||
"data.list[].executorList": "array - 执行人列表(报工时需要)",
|
||||
"data.list[].executorList[].id": "number - 执行人ID",
|
||||
"data.list[].executorList[].code": "string - 执行人编号",
|
||||
"data.list[].executorList[].name": "string - 执行人姓名",
|
||||
"data.list[].executorList[].username": "string - 用户名",
|
||||
"data.list[].processId": "number - 工序ID",
|
||||
"data.list[].processCode": "string - 工序编号",
|
||||
"data.list[].processName": "string - 工序名称",
|
||||
"data.list[].taskStatus": "object - 任务状态",
|
||||
"data.list[].taskStatus.code": "number - 状态码",
|
||||
"data.list[].taskStatus.message": "string - 状态描述",
|
||||
"data.list[].materialInfo": "object - 主产出物料信息",
|
||||
"data.list[].materialInfo.baseInfo.id": "number - 物料ID",
|
||||
"data.list[].materialInfo.baseInfo.name": "string - 物料名称",
|
||||
"data.list[].materialInfo.baseInfo.code": "string - 物料编码"
|
||||
},
|
||||
|
||||
"使用场景": "用于获取工单下的生产任务信息,特别是获取 taskId 用于后续报工操作",
|
||||
"使用示例": "参见 services/business.js:287-317 的 getReportRequiredParams 方法中的任务查询部分"
|
||||
}
|
||||
3
skills/web-build/templates/report-app/config.json
Normal file
3
skills/web-build/templates/report-app/config.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"welcome_message": "右侧是当前<strong>生产任务执行(报工页面)</strong>的样式。<br><br>您可以:<ul><li>通过对话描述需要修改的部分</li><li>直接上传手绘稿告诉我您期望的样式</li></ul>我将通过AI魔法帮您心想事成。"
|
||||
}
|
||||
391
skills/web-build/templates/report-app/css/custom.css
Normal file
391
skills/web-build/templates/report-app/css/custom.css
Normal file
@@ -0,0 +1,391 @@
|
||||
/**
|
||||
* 报工页面自定义样式
|
||||
* 补充Tailwind CSS的样式定义
|
||||
* 主题色: #02B980
|
||||
*/
|
||||
|
||||
/* 全局样式 */
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* 主题色变量定义 */
|
||||
:root {
|
||||
--primary-color: #02B980;
|
||||
--primary-hover: #029968;
|
||||
--primary-light: #E6F7F1;
|
||||
--primary-dark: #016B4A;
|
||||
--primary-shadow: rgba(2, 185, 128, 0.1);
|
||||
}
|
||||
|
||||
/* 主题色相关的通用样式 */
|
||||
.btn-primary {
|
||||
background-color: var(--primary-color);
|
||||
color: white;
|
||||
transition: background-color 0.2s ease;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background-color: var(--primary-hover);
|
||||
}
|
||||
|
||||
.btn-primary:disabled {
|
||||
background-color: #9CA3AF;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.border-primary {
|
||||
border-color: var(--primary-color);
|
||||
}
|
||||
|
||||
.text-primary {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
.bg-primary {
|
||||
background-color: var(--primary-color);
|
||||
}
|
||||
|
||||
.bg-primary-light {
|
||||
background-color: var(--primary-light);
|
||||
}
|
||||
|
||||
.ring-primary {
|
||||
--tw-ring-color: var(--primary-color);
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||
}
|
||||
|
||||
/* 下拉菜单动画 */
|
||||
#work-order-dropdown {
|
||||
transition: opacity 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
#work-order-dropdown.show {
|
||||
display: flex !important;
|
||||
flex-direction: column;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
#work-order-dropdown .bg-white {
|
||||
transform: translateY(100%);
|
||||
transition: transform 0.3s ease-out;
|
||||
}
|
||||
|
||||
#work-order-dropdown.show .bg-white {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
/* 下拉箭头旋转动画 */
|
||||
#dropdown-arrow.rotate {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
|
||||
/* 工单列表项样式 */
|
||||
.work-order-item {
|
||||
padding: 12px 16px;
|
||||
border-bottom: 1px solid #f3f4f6;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s ease;
|
||||
}
|
||||
|
||||
.work-order-item:hover {
|
||||
background-color: #f9fafb;
|
||||
}
|
||||
|
||||
.work-order-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.work-order-item.selected {
|
||||
background-color: var(--primary-light);
|
||||
border-left: 4px solid var(--primary-color);
|
||||
}
|
||||
|
||||
/* 输入框焦点样式增强 */
|
||||
input:focus {
|
||||
outline: none;
|
||||
box-shadow: 0 0 0 3px rgba(2, 185, 128, 0.1);
|
||||
border-color: var(--primary-color) !important;
|
||||
}
|
||||
|
||||
/* 按钮点击效果 */
|
||||
button:active {
|
||||
transform: scale(0.98);
|
||||
}
|
||||
|
||||
/* 主题色按钮悬停效果 */
|
||||
button[style*="background-color: var(--primary-color)"]:hover {
|
||||
background-color: var(--primary-hover) !important;
|
||||
}
|
||||
|
||||
button[style*="border-color: var(--primary-color)"]:hover {
|
||||
background-color: var(--primary-light) !important;
|
||||
}
|
||||
|
||||
/* 复选框和单选框主题色 */
|
||||
input[type="checkbox"]:checked,
|
||||
input[type="radio"]:checked {
|
||||
background-color: var(--primary-color) !important;
|
||||
border-color: var(--primary-color) !important;
|
||||
}
|
||||
|
||||
input[type="checkbox"]:focus,
|
||||
input[type="radio"]:focus {
|
||||
box-shadow: 0 0 0 3px var(--primary-shadow) !important;
|
||||
border-color: var(--primary-color) !important;
|
||||
}
|
||||
|
||||
/* 自定义CSS类 */
|
||||
.hover-primary-bg:hover {
|
||||
background-color: var(--primary-hover) !important;
|
||||
}
|
||||
|
||||
.hover-primary-light-bg:hover {
|
||||
background-color: var(--primary-light) !important;
|
||||
}
|
||||
|
||||
.focus-primary-ring:focus {
|
||||
--tw-ring-color: var(--primary-color) !important;
|
||||
border-color: var(--primary-color) !important;
|
||||
}
|
||||
|
||||
.checkbox-primary:checked {
|
||||
background-color: var(--primary-color) !important;
|
||||
border-color: var(--primary-color) !important;
|
||||
}
|
||||
|
||||
.checkbox-primary:focus {
|
||||
box-shadow: 0 0 0 3px var(--primary-shadow) !important;
|
||||
border-color: var(--primary-color) !important;
|
||||
}
|
||||
|
||||
/* 状态标签动画 */
|
||||
.status-tag {
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
.status-tag:hover {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
/* 响应式调整 */
|
||||
@media (max-width: 640px) {
|
||||
/* 移动端下拉菜单全屏显示 */
|
||||
#work-order-dropdown .bg-white {
|
||||
min-height: 80vh;
|
||||
border-radius: 16px 16px 0 0;
|
||||
}
|
||||
|
||||
/* 移动端表单字段间距调整 */
|
||||
#report-form .space-y-4 > * + * {
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
|
||||
/* 移动端工单选择区域优化 */
|
||||
#work-order-selector {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
/* 移动端物料信息卡片优化 */
|
||||
#material-info .bg-white {
|
||||
padding: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
/* 移动端状态标签优化 */
|
||||
.status-tag {
|
||||
font-size: 0.75rem;
|
||||
padding: 0.25rem 0.75rem;
|
||||
}
|
||||
|
||||
/* 移动端输入框优化 */
|
||||
input[type="number"], input[type="text"] {
|
||||
font-size: 16px; /* 防止iOS缩放 */
|
||||
padding: 0.75rem;
|
||||
}
|
||||
|
||||
/* 移动端按钮优化 */
|
||||
#submit-btn {
|
||||
padding: 1rem;
|
||||
font-size: 1.125rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
/* 平板设备优化 */
|
||||
@media (min-width: 641px) and (max-width: 1024px) {
|
||||
/* 平板端下拉菜单适配 */
|
||||
#work-order-dropdown .bg-white {
|
||||
min-height: 60vh;
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
border-radius: 12px;
|
||||
margin-top: 10vh;
|
||||
}
|
||||
|
||||
/* 平板端表单布局优化 */
|
||||
#report-form .bg-white {
|
||||
max-width: 500px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
}
|
||||
|
||||
/* 大屏设备优化 */
|
||||
@media (min-width: 1025px) {
|
||||
/* 桌面端容器最大宽度 */
|
||||
#app {
|
||||
max-width: 480px;
|
||||
margin: 0 auto;
|
||||
box-shadow: 0 0 20px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
/* 桌面端下拉菜单 */
|
||||
#work-order-dropdown .bg-white {
|
||||
max-width: 480px;
|
||||
margin: 0 auto;
|
||||
border-radius: 12px;
|
||||
margin-top: 5vh;
|
||||
min-height: 50vh;
|
||||
}
|
||||
}
|
||||
|
||||
/* 横屏适配 */
|
||||
@media (orientation: landscape) and (max-height: 600px) {
|
||||
/* 横屏时减少垂直间距 */
|
||||
#header-nav {
|
||||
padding: 0.5rem 1rem;
|
||||
}
|
||||
|
||||
main {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
#work-order-dropdown .bg-white {
|
||||
min-height: 70vh;
|
||||
}
|
||||
|
||||
#empty-state {
|
||||
padding: 2rem 0;
|
||||
}
|
||||
|
||||
#empty-state .w-48 {
|
||||
width: 8rem;
|
||||
height: 8rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* 高分辨率屏幕优化 */
|
||||
@media (-webkit-min-device-pixel-ratio: 2), (min-resolution: 192dpi) {
|
||||
/* 高分辨率屏幕下的图标和边框优化 */
|
||||
.border {
|
||||
border-width: 0.5px;
|
||||
}
|
||||
|
||||
svg {
|
||||
shape-rendering: geometricPrecision;
|
||||
}
|
||||
}
|
||||
|
||||
/* 深色模式支持 */
|
||||
@media (prefers-color-scheme: dark) {
|
||||
/* 可以添加深色模式样式 */
|
||||
/* 目前保持浅色主题 */
|
||||
}
|
||||
|
||||
/* 减少动画偏好 */
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
* {
|
||||
animation-duration: 0.01ms !important;
|
||||
animation-iteration-count: 1 !important;
|
||||
transition-duration: 0.01ms !important;
|
||||
}
|
||||
|
||||
#work-order-dropdown,
|
||||
#work-order-dropdown .bg-white,
|
||||
#dropdown-arrow {
|
||||
transition: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* 触摸设备优化 */
|
||||
@media (pointer: coarse) {
|
||||
/* 增大触摸目标 */
|
||||
button, .cursor-pointer {
|
||||
min-height: 44px;
|
||||
min-width: 44px;
|
||||
}
|
||||
|
||||
/* 工单列表项触摸优化 */
|
||||
.work-order-item {
|
||||
padding: 1rem;
|
||||
min-height: 60px;
|
||||
}
|
||||
|
||||
/* 输入框触摸优化 */
|
||||
input, select, textarea {
|
||||
min-height: 44px;
|
||||
}
|
||||
}
|
||||
|
||||
/* 无障碍优化 */
|
||||
@media (prefers-contrast: high) {
|
||||
/* 高对比度模式 */
|
||||
.border-gray-300 {
|
||||
border-color: #000 !important;
|
||||
}
|
||||
|
||||
.text-gray-500 {
|
||||
color: #333 !important;
|
||||
}
|
||||
|
||||
.bg-gray-50 {
|
||||
background-color: #fff !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* 加载状态样式 */
|
||||
.loading {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.loading::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: -100%;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.4), transparent);
|
||||
animation: loading 1.5s infinite;
|
||||
}
|
||||
|
||||
@keyframes loading {
|
||||
0% {
|
||||
left: -100%;
|
||||
}
|
||||
100% {
|
||||
left: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
/* 滚动条样式 */
|
||||
#work-order-list::-webkit-scrollbar {
|
||||
width: 4px;
|
||||
}
|
||||
|
||||
#work-order-list::-webkit-scrollbar-track {
|
||||
background: #f1f5f9;
|
||||
}
|
||||
|
||||
#work-order-list::-webkit-scrollbar-thumb {
|
||||
background: #cbd5e1;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
#work-order-list::-webkit-scrollbar-thumb:hover {
|
||||
background: #94a3b8;
|
||||
}
|
||||
257
skills/web-build/templates/report-app/index.html
Normal file
257
skills/web-build/templates/report-app/index.html
Normal file
@@ -0,0 +1,257 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>报工 - 工单管理系统</title>
|
||||
<!-- Tailwind CSS CDN -->
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<!-- 自定义样式 -->
|
||||
<link rel="stylesheet" href="css/custom.css">
|
||||
</head>
|
||||
<body class="bg-gray-50 min-h-screen">
|
||||
<!-- 应用容器 -->
|
||||
<div id="app" class="min-h-screen flex flex-col">
|
||||
<!-- 顶部导航栏 -->
|
||||
<header id="header-nav" class="px-4 py-3 flex items-center justify-between" style="background-color: var(--primary-color)">
|
||||
<!-- 返回按钮 -->
|
||||
<button id="back-btn" class="p-2 rounded-lg transition-colors hover-primary-bg">
|
||||
<svg class="w-6 h-6 text-gray-800" fill="none" stroke="currentColor" viewBox="0 0 24 24" style="color: white;">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"></path>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<!-- 标题 -->
|
||||
<h1 class="text-lg font-semibold text-gray-800" style="color: white;">标准报工页面</h1>
|
||||
|
||||
<!-- 占位元素保持居中 -->
|
||||
<div class="w-10"></div>
|
||||
</header>
|
||||
|
||||
<!-- 主要内容区域 -->
|
||||
<main class="flex-1 px-4 py-6">
|
||||
<!-- 工单选择区域 -->
|
||||
<div id="work-order-selector" class="bg-white rounded-lg shadow-sm p-4 mb-6 cursor-pointer hover:shadow-md transition-shadow">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center space-x-3">
|
||||
<!-- 工单图标占位符 -->
|
||||
<div class="w-8 h-8 rounded-lg flex items-center justify-center" style="background-color: var(--primary-light)">
|
||||
<svg class="w-5 h-5" style="color: var(--primary-color)" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path d="M9 2a1 1 0 000 2h2a1 1 0 100-2H9z"></path>
|
||||
<path fill-rule="evenodd" d="M4 5a2 2 0 012-2v1a1 1 0 001 1h6a1 1 0 001-1V3a2 2 0 012 2v6.5a1.5 1.5 0 01-1.5 1.5h-7A1.5 1.5 0 014 11.5V5z" clip-rule="evenodd"></path>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<!-- 工单文字 -->
|
||||
<div>
|
||||
<span class="text-gray-800 font-medium">工单</span>
|
||||
<span id="selected-work-order-text" class="text-gray-500 ml-2">请选择</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center space-x-2">
|
||||
<!-- 扫码按钮 -->
|
||||
<button id="scan-btn" class="p-2 border-2 rounded-lg transition-colors border-primary hover-primary-light-bg">
|
||||
<svg class="w-5 h-5" style="color: var(--primary-color)" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v1m6 11h2m-6 0h-2v4m0-11v3m0 0h.01M12 12h4.01M16 20h4M4 12h4m12 0h2M4 4h5v5H4V4zm11 0h5v5h-5V4zM4 15h5v5H4v-5z"></path>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<!-- 下拉箭头 -->
|
||||
<svg id="dropdown-arrow" class="w-5 h-5 text-gray-400 transition-transform" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 工单下拉菜单 -->
|
||||
<div id="work-order-dropdown" class="fixed inset-0 bg-black bg-opacity-50 z-50 hidden">
|
||||
<div class="bg-white rounded-t-lg mt-auto min-h-[70vh] flex flex-col">
|
||||
<!-- 下拉菜单头部 -->
|
||||
<div class="flex items-center justify-between p-4 border-b">
|
||||
<h2 class="text-lg font-semibold text-gray-800">工单编号</h2>
|
||||
<button id="close-dropdown" class="p-2 hover:bg-gray-100 rounded-lg">
|
||||
<svg class="w-6 h-6 text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 搜索框 -->
|
||||
<div class="p-4 border-b">
|
||||
<div class="relative">
|
||||
<input id="search-input" type="text" placeholder="搜索工单编号"
|
||||
class="w-full px-4 py-2.5 border border-gray-300 rounded-lg focus:ring-2 focus:border-transparent focus-primary-ring">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 工单列表 -->
|
||||
<div id="work-order-list" class="flex-1 overflow-y-auto">
|
||||
<!-- 工单列表项将通过JavaScript动态生成 -->
|
||||
</div>
|
||||
|
||||
<!-- 底部操作区 -->
|
||||
<div class="p-4 border-t bg-gray-50 flex items-center justify-between">
|
||||
<div class="flex items-center space-x-4">
|
||||
<label class="flex items-center space-x-2 cursor-pointer">
|
||||
<input id="select-all" type="checkbox" class="rounded border-gray-300 checkbox-primary">
|
||||
<span class="text-sm text-gray-600">全选</span>
|
||||
</label>
|
||||
<span id="selected-count" class="text-sm text-gray-600">已选择0个</span>
|
||||
</div>
|
||||
<button id="confirm-selection" class="btn-primary text-white px-6 py-2 rounded-lg transition-colors disabled:bg-gray-300 disabled:cursor-not-allowed" disabled>
|
||||
确定
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 空状态区域 -->
|
||||
<div id="empty-state" class="text-center py-12">
|
||||
<!-- 插图占位符 -->
|
||||
<div class="w-48 h-48 mx-auto mb-6 bg-gray-100 rounded-lg flex items-center justify-center">
|
||||
<svg class="w-24 h-24 text-gray-300" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M3 4a1 1 0 011-1h12a1 1 0 011 1v2a1 1 0 01-1 1H4a1 1 0 01-1-1V4zm0 4a1 1 0 011-1h12a1 1 0 011 1v6a1 1 0 01-1 1H4a1 1 0 01-1-1V8zm8 2a1 1 0 100 2h2a1 1 0 100-2H11z" clip-rule="evenodd"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<p class="text-gray-500 text-lg">先选择工单</p>
|
||||
</div>
|
||||
|
||||
<!-- 物料信息区域 -->
|
||||
<div id="material-info" class="hidden">
|
||||
<!-- 工单信息卡片 -->
|
||||
<div class="bg-white rounded-lg shadow-sm p-4 mb-4">
|
||||
<div class="flex items-center justify-between mb-3" style="display: none;">
|
||||
<div class="flex items-center space-x-3">
|
||||
<div class="w-8 h-8 rounded-lg flex items-center justify-center" style="background-color: var(--primary-light)">
|
||||
<svg class="w-5 h-5" style="color: var(--primary-color)" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path d="M9 2a1 1 0 000 2h2a1 1 0 100-2H9z"></path>
|
||||
<path fill-rule="evenodd" d="M4 5a2 2 0 012-2v1a1 1 0 001 1h6a1 1 0 001-1V3a2 2 0 012 2v6.5a1.5 1.5 0 01-1.5 1.5h-7A1.5 1.5 0 014 11.5V5z" clip-rule="evenodd"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<span class="text-gray-800 font-medium">工单</span>
|
||||
<span id="selected-work-order-id" class="text-gray-800 font-semibold ml-2"></span>
|
||||
</div>
|
||||
</div>
|
||||
<button class="p-2 border-2 rounded-lg transition-colors border-primary hover-primary-light-bg">
|
||||
<svg class="w-5 h-5" style="color: var(--primary-color)" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v1m6 11h2m-6 0h-2v4m0-11v3m0 0h.01M12 12h4.01M16 20h4M4 12h4m12 0h2M4 4h5v5H4V4zm11 0h5v5h-5V4zM4 15h5v5H4v-5z"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 物料信息 -->
|
||||
<div class="space-y-2">
|
||||
<h3 id="material-name" class="text-lg font-semibold text-gray-800"></h3>
|
||||
<p id="material-code" class="text-gray-600"></p>
|
||||
|
||||
<!-- 状态标签 -->
|
||||
<div class="flex space-x-2 mt-3">
|
||||
<span class="px-3 py-1 rounded-full text-sm font-medium" style="background-color: var(--primary-light); color: var(--primary-dark)">
|
||||
合格 <span id="qualified-count">0</span>
|
||||
</span>
|
||||
<span class="px-3 py-1 bg-red-100 text-red-800 rounded-full text-sm font-medium">
|
||||
不良 <span id="defective-count">0</span>
|
||||
</span>
|
||||
<span class="px-3 py-1 bg-yellow-100 text-yellow-800 rounded-full text-sm font-medium">
|
||||
待产 <span id="pending-count">0</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 报工表单区域 -->
|
||||
<div id="report-form" class="hidden">
|
||||
<h2 class="text-lg font-semibold text-gray-800 mb-4">报工</h2>
|
||||
|
||||
<div class="bg-white rounded-lg shadow-sm p-4 space-y-4">
|
||||
<!-- 数量输入 -->
|
||||
<div class="flex items-center justify-between">
|
||||
<label class="text-gray-700 font-medium">
|
||||
数量 <span class="text-red-500">*</span>
|
||||
</label>
|
||||
<div class="flex items-center space-x-2">
|
||||
<input id="quantity" type="number" value="1" min="0"
|
||||
class="w-20 px-3 py-2 border border-gray-300 rounded-lg text-right focus:ring-2 focus:border-transparent focus-primary-ring">
|
||||
<span class="text-gray-500"> 个</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 辅助数量输入 -->
|
||||
<div class="flex items-center justify-between" style="display: none;">
|
||||
<label class="text-gray-700 font-medium">
|
||||
辅助数量 <span class="text-red-500">*</span>
|
||||
</label>
|
||||
<div class="flex items-center space-x-2">
|
||||
<input id="auxiliary-quantity" type="number" value="1" min="0"
|
||||
class="w-20 px-3 py-2 border border-gray-300 rounded-lg text-right focus:ring-2 focus:border-transparent focus-primary-ring">
|
||||
<span id="auxName" class="text-gray-500">公斤</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 工时输入 -->
|
||||
<div class="flex items-center justify-between">
|
||||
<label class="text-gray-700 font-medium">
|
||||
工时
|
||||
</label>
|
||||
<div class="flex items-center space-x-2">
|
||||
<input id="working-hours" type="number" value="1" min="0"
|
||||
class="w-20 px-3 py-2 border border-gray-300 rounded-lg text-right focus:ring-2 focus:border-transparent focus-primary-ring">
|
||||
<span class="text-gray-500">小时</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 报工开始时间 -->
|
||||
<div class="flex items-center justify-between">
|
||||
<label class="text-gray-700 font-medium">
|
||||
报工开始时间
|
||||
</label>
|
||||
<div class="flex items-center space-x-2">
|
||||
<input id="report-start-time" type="datetime-local" value="1" min="0"
|
||||
class="w-50 px-3 py-2 border border-gray-300 rounded-lg text-right focus:ring-2 focus:border-transparent focus-primary-ring">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 报工结束时间 -->
|
||||
<div class="flex items-center justify-between">
|
||||
<label class="text-gray-700 font-medium">
|
||||
报工结束时间
|
||||
</label>
|
||||
<div class="flex items-center space-x-2">
|
||||
<input id="report-end-time" type="datetime-local" value="1" min="0"
|
||||
class="w-50 px-3 py-2 border border-gray-300 rounded-lg text-right focus:ring-2 focus:border-transparent focus-primary-ring">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 备注输入 -->
|
||||
<div class="flex items-center justify-between">
|
||||
<label class="text-gray-700 font-medium">
|
||||
备注
|
||||
</label>
|
||||
<div class="flex items-center space-x-2">
|
||||
<input id="remark" type="text" value="" placeholder="请输入备注"
|
||||
class="w-60 px-3 py-2 border border-gray-300 rounded-lg text-right focus:ring-2 focus:border-transparent focus-primary-ring">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<!-- 提交按钮 -->
|
||||
<div id="submit-section" class="hidden p-4">
|
||||
<button id="submit-btn" class="w-full btn-primary text-white py-3 rounded-lg font-semibold transition-colors">
|
||||
提交
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- JavaScript文件 -->
|
||||
<script src="js/common.js"></script>
|
||||
<script src="services/core.js"></script>
|
||||
<script src="services/business.js"></script>
|
||||
<script src="js/main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
856
skills/web-build/templates/report-app/js/common.js
Normal file
856
skills/web-build/templates/report-app/js/common.js
Normal file
@@ -0,0 +1,856 @@
|
||||
/**
|
||||
* 通用代码
|
||||
* 包含工具函数和UI组件
|
||||
*/
|
||||
|
||||
// ==================== 工具函数 ====================
|
||||
|
||||
/**
|
||||
* 从URL查询参数中获取指定参数的值
|
||||
* @param {string} name - 参数名称
|
||||
* @returns {string|null} 参数值或null
|
||||
*/
|
||||
function getUrlParameter(name) {
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
return urlParams.get(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* 防抖函数
|
||||
* @param {Function} func - 要防抖的函数
|
||||
* @param {number} wait - 等待时间(毫秒)
|
||||
* @returns {Function} 防抖后的函数
|
||||
*/
|
||||
function debounce(func, wait) {
|
||||
let timeout;
|
||||
return function executedFunction(...args) {
|
||||
const later = () => {
|
||||
clearTimeout(timeout);
|
||||
func(...args);
|
||||
};
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(later, wait);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示Toast消息
|
||||
* @param {string} message - 消息内容
|
||||
* @param {string} type - 消息类型 ('success', 'error', 'warning', 'info')
|
||||
* @param {number} duration - 显示时长(毫秒),默认3000
|
||||
*/
|
||||
function showToast(message, type = 'info', duration = 3000) {
|
||||
// 创建toast容器(如果不存在)
|
||||
let toastContainer = document.getElementById('toast-container');
|
||||
if (!toastContainer) {
|
||||
toastContainer = document.createElement('div');
|
||||
toastContainer.id = 'toast-container';
|
||||
toastContainer.className = 'fixed top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 z-50 space-y-2';
|
||||
document.body.appendChild(toastContainer);
|
||||
}
|
||||
|
||||
// 创建toast元素
|
||||
const toast = document.createElement('div');
|
||||
const bgColor = {
|
||||
success: 'bg-green-500',
|
||||
error: 'bg-red-500',
|
||||
warning: 'bg-yellow-500',
|
||||
info: 'bg-blue-500'
|
||||
}[type] || 'bg-blue-500';
|
||||
|
||||
toast.className = `${bgColor} text-white px-4 py-2 rounded-lg shadow-lg transform transition-all duration-300 translate-x-full opacity-0`;
|
||||
toast.textContent = message;
|
||||
|
||||
// 添加到容器
|
||||
toastContainer.appendChild(toast);
|
||||
|
||||
// 显示动画
|
||||
setTimeout(() => {
|
||||
toast.classList.remove('translate-x-full', 'opacity-0');
|
||||
}, 100);
|
||||
|
||||
// 自动隐藏
|
||||
setTimeout(() => {
|
||||
toast.classList.add('translate-x-full', 'opacity-0');
|
||||
setTimeout(() => {
|
||||
if (toast.parentNode) {
|
||||
toast.parentNode.removeChild(toast);
|
||||
}
|
||||
}, 300);
|
||||
}, duration);
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证表单字段
|
||||
* @param {Object} data - 表单数据
|
||||
* @param {Object} rules - 验证规则
|
||||
* @returns {Object} 验证结果 {isValid: boolean, errors: Object}
|
||||
*/
|
||||
function validateForm(data, rules) {
|
||||
const errors = {};
|
||||
let isValid = true;
|
||||
|
||||
for (const field in rules) {
|
||||
const value = data[field];
|
||||
const rule = rules[field];
|
||||
|
||||
// 必填验证
|
||||
if (rule.required && (!value || value.toString().trim() === '')) {
|
||||
errors[field] = rule.message || `${field}是必填项`;
|
||||
isValid = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
// 数字验证
|
||||
if (rule.type === 'number' && value !== undefined && value !== '') {
|
||||
const num = Number(value);
|
||||
if (isNaN(num)) {
|
||||
errors[field] = `${field}必须是数字`;
|
||||
isValid = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (rule.min !== undefined && num < rule.min) {
|
||||
errors[field] = `${field}不能小于${rule.min}`;
|
||||
isValid = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (rule.max !== undefined && num > rule.max) {
|
||||
errors[field] = `${field}不能大于${rule.max}`;
|
||||
isValid = false;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// 字符串长度验证
|
||||
if (rule.type === 'string' && value) {
|
||||
if (rule.minLength && value.length < rule.minLength) {
|
||||
errors[field] = `${field}长度不能少于${rule.minLength}个字符`;
|
||||
isValid = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (rule.maxLength && value.length > rule.maxLength) {
|
||||
errors[field] = `${field}长度不能超过${rule.maxLength}个字符`;
|
||||
isValid = false;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { isValid, errors };
|
||||
}
|
||||
|
||||
/**
|
||||
* 深拷贝对象
|
||||
* @param {any} obj - 要拷贝的对象
|
||||
* @returns {any} 拷贝后的对象
|
||||
*/
|
||||
function deepClone(obj) {
|
||||
if (obj === null || typeof obj !== 'object') {
|
||||
return obj;
|
||||
}
|
||||
|
||||
if (obj instanceof Date) {
|
||||
return new Date(obj.getTime());
|
||||
}
|
||||
|
||||
if (obj instanceof Array) {
|
||||
return obj.map(item => deepClone(item));
|
||||
}
|
||||
|
||||
if (typeof obj === 'object') {
|
||||
const cloned = {};
|
||||
for (const key in obj) {
|
||||
if (obj.hasOwnProperty(key)) {
|
||||
cloned[key] = deepClone(obj[key]);
|
||||
}
|
||||
}
|
||||
return cloned;
|
||||
}
|
||||
|
||||
return obj;
|
||||
}
|
||||
|
||||
// ==================== UI组件 ====================
|
||||
|
||||
/**
|
||||
* 顶部导航栏组件
|
||||
*/
|
||||
class HeaderNav {
|
||||
constructor() {
|
||||
this.element = document.getElementById('header-nav');
|
||||
this.backBtn = document.getElementById('back-btn');
|
||||
this.init();
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化导航栏事件
|
||||
*/
|
||||
init() {
|
||||
this.backBtn.addEventListener('click', () => {
|
||||
// 返回上一页或关闭页面
|
||||
if (window.history.length > 1) {
|
||||
window.history.back();
|
||||
} else {
|
||||
window.close();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 工单选择器组件
|
||||
*/
|
||||
class WorkOrderSelector {
|
||||
constructor(onSelect) {
|
||||
this.element = document.getElementById('work-order-selector');
|
||||
this.selectedText = document.getElementById('selected-work-order-text');
|
||||
this.dropdownArrow = document.getElementById('dropdown-arrow');
|
||||
this.scanBtn = document.getElementById('scan-btn');
|
||||
this.onSelect = onSelect;
|
||||
this.selectedWorkOrder = null;
|
||||
this.init();
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化选择器事件
|
||||
*/
|
||||
init() {
|
||||
// 点击选择器展开下拉菜单
|
||||
this.element.addEventListener('click', (e) => {
|
||||
// 如果点击的是扫码按钮,不展开下拉菜单
|
||||
if (e.target.closest('#scan-btn')) {
|
||||
return;
|
||||
}
|
||||
this.onSelect();
|
||||
});
|
||||
|
||||
// 扫码按钮事件
|
||||
this.scanBtn.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
this.handleScan();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理扫码功能
|
||||
*/
|
||||
handleScan() {
|
||||
showToast('扫码功能开发中...', 'info');
|
||||
// TODO: 实现扫码功能
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新选中的工单显示
|
||||
* @param {Object} workOrder - 工单对象
|
||||
*/
|
||||
updateSelected(workOrder) {
|
||||
this.selectedWorkOrder = workOrder;
|
||||
if (workOrder) {
|
||||
this.selectedText.textContent = workOrder.id;
|
||||
this.selectedText.className = 'text-gray-800 ml-2 font-medium';
|
||||
} else {
|
||||
this.selectedText.textContent = '请选择';
|
||||
this.selectedText.className = 'text-gray-500 ml-2';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换下拉箭头状态
|
||||
* @param {boolean} expanded - 是否展开
|
||||
*/
|
||||
toggleArrow(expanded) {
|
||||
if (expanded) {
|
||||
this.dropdownArrow.classList.add('rotate');
|
||||
} else {
|
||||
this.dropdownArrow.classList.remove('rotate');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 工单下拉菜单组件
|
||||
*/
|
||||
class WorkOrderDropdown {
|
||||
constructor(onConfirm, onClose) {
|
||||
this.element = document.getElementById('work-order-dropdown');
|
||||
this.closeBtn = document.getElementById('close-dropdown');
|
||||
this.searchInput = document.getElementById('search-input');
|
||||
this.workOrderList = document.getElementById('work-order-list');
|
||||
this.selectAllCheckbox = document.getElementById('select-all');
|
||||
this.selectedCount = document.getElementById('selected-count');
|
||||
this.confirmBtn = document.getElementById('confirm-selection');
|
||||
|
||||
this.onConfirm = onConfirm;
|
||||
this.onClose = onClose;
|
||||
this.workOrders = [];
|
||||
this.filteredWorkOrders = [];
|
||||
this.selectedWorkOrders = new Set();
|
||||
|
||||
this.init();
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化下拉菜单事件
|
||||
*/
|
||||
init() {
|
||||
// 关闭按钮事件
|
||||
this.closeBtn.addEventListener('click', () => {
|
||||
this.hide();
|
||||
});
|
||||
|
||||
// 点击背景关闭
|
||||
this.element.addEventListener('click', (e) => {
|
||||
if (e.target === this.element) {
|
||||
this.hide();
|
||||
}
|
||||
});
|
||||
|
||||
// 搜索功能
|
||||
this.searchInput.addEventListener('input', debounce((e) => {
|
||||
this.filterWorkOrders(e.target.value);
|
||||
}, 300));
|
||||
|
||||
// 全选功能
|
||||
this.selectAllCheckbox.addEventListener('change', (e) => {
|
||||
this.toggleSelectAll(e.target.checked);
|
||||
});
|
||||
|
||||
// 确定按钮事件
|
||||
this.confirmBtn.addEventListener('click', () => {
|
||||
this.confirmSelection();
|
||||
});
|
||||
|
||||
// ESC键关闭
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Escape' && this.isVisible()) {
|
||||
this.hide();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示下拉菜单
|
||||
*/
|
||||
show() {
|
||||
this.element.classList.remove('hidden');
|
||||
this.element.classList.add('show');
|
||||
document.body.style.overflow = 'hidden';
|
||||
|
||||
// 聚焦搜索框
|
||||
setTimeout(() => {
|
||||
this.searchInput.focus();
|
||||
}, 300);
|
||||
}
|
||||
|
||||
/**
|
||||
* 隐藏下拉菜单
|
||||
*/
|
||||
hide() {
|
||||
this.element.classList.remove('show');
|
||||
document.body.style.overflow = '';
|
||||
|
||||
setTimeout(() => {
|
||||
this.element.classList.add('hidden');
|
||||
}, 300);
|
||||
|
||||
this.onClose();
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否可见
|
||||
* @returns {boolean}
|
||||
*/
|
||||
isVisible() {
|
||||
return !this.element.classList.contains('hidden');
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置工单数据
|
||||
* @param {Array} workOrders - 工单数组
|
||||
*/
|
||||
setWorkOrders(workOrders) {
|
||||
this.workOrders = workOrders;
|
||||
this.filteredWorkOrders = [...workOrders];
|
||||
this.renderWorkOrderList();
|
||||
}
|
||||
|
||||
/**
|
||||
* 过滤工单
|
||||
* @param {string} searchTerm - 搜索关键词
|
||||
*/
|
||||
filterWorkOrders(searchTerm) {
|
||||
const term = searchTerm.toLowerCase().trim();
|
||||
if (!term) {
|
||||
this.filteredWorkOrders = [...this.workOrders];
|
||||
} else {
|
||||
this.filteredWorkOrders = this.workOrders.filter(wo =>
|
||||
wo.id.toLowerCase().includes(term) ||
|
||||
(wo.name && wo.name.toLowerCase().includes(term))
|
||||
);
|
||||
}
|
||||
this.renderWorkOrderList();
|
||||
}
|
||||
|
||||
/**
|
||||
* 渲染工单列表
|
||||
*/
|
||||
renderWorkOrderList() {
|
||||
this.workOrderList.innerHTML = '';
|
||||
|
||||
if (this.filteredWorkOrders.length === 0) {
|
||||
const emptyDiv = document.createElement('div');
|
||||
emptyDiv.className = 'text-center py-8 text-gray-500';
|
||||
emptyDiv.textContent = '没有找到匹配的工单';
|
||||
this.workOrderList.appendChild(emptyDiv);
|
||||
return;
|
||||
}
|
||||
|
||||
this.filteredWorkOrders.forEach(workOrder => {
|
||||
const item = this.createWorkOrderItem(workOrder);
|
||||
this.workOrderList.appendChild(item);
|
||||
});
|
||||
|
||||
this.updateSelectAllState();
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建工单列表项
|
||||
* @param {Object} workOrder - 工单对象
|
||||
* @returns {HTMLElement}
|
||||
*/
|
||||
createWorkOrderItem(workOrder) {
|
||||
const item = document.createElement('div');
|
||||
item.className = 'work-order-item flex items-center space-x-3';
|
||||
item.dataset.workOrderId = workOrder.id;
|
||||
|
||||
const isSelected = this.selectedWorkOrders.has(workOrder.id);
|
||||
if (isSelected) {
|
||||
item.classList.add('selected');
|
||||
}
|
||||
|
||||
item.innerHTML = `
|
||||
<input type="radio" name="work-order" value="${workOrder.id}"
|
||||
class="checkbox-primary focus:ring-2" ${isSelected ? 'checked' : ''}>
|
||||
<span class="flex-1 text-gray-800">${workOrder.id}</span>
|
||||
`;
|
||||
|
||||
// 点击事件
|
||||
item.addEventListener('click', () => {
|
||||
this.selectWorkOrder(workOrder.id);
|
||||
});
|
||||
|
||||
return item;
|
||||
}
|
||||
|
||||
/**
|
||||
* 选择工单
|
||||
* @param {string} workOrderId - 工单ID
|
||||
*/
|
||||
selectWorkOrder(workOrderId) {
|
||||
// 单选模式:清除之前的选择
|
||||
this.selectedWorkOrders.clear();
|
||||
this.selectedWorkOrders.add(workOrderId);
|
||||
|
||||
// 更新UI
|
||||
this.workOrderList.querySelectorAll('.work-order-item').forEach(item => {
|
||||
const radio = item.querySelector('input[type="radio"]');
|
||||
const isSelected = item.dataset.workOrderId === workOrderId;
|
||||
|
||||
item.classList.toggle('selected', isSelected);
|
||||
radio.checked = isSelected;
|
||||
});
|
||||
|
||||
this.updateSelectedCount();
|
||||
this.updateConfirmButton();
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换全选状态
|
||||
* @param {boolean} selectAll - 是否全选
|
||||
*/
|
||||
toggleSelectAll(selectAll) {
|
||||
if (selectAll) {
|
||||
this.filteredWorkOrders.forEach(wo => {
|
||||
this.selectedWorkOrders.add(wo.id);
|
||||
});
|
||||
} else {
|
||||
this.selectedWorkOrders.clear();
|
||||
}
|
||||
|
||||
this.renderWorkOrderList();
|
||||
this.updateSelectedCount();
|
||||
this.updateConfirmButton();
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新全选状态
|
||||
*/
|
||||
updateSelectAllState() {
|
||||
const visibleCount = this.filteredWorkOrders.length;
|
||||
const selectedVisibleCount = this.filteredWorkOrders.filter(wo =>
|
||||
this.selectedWorkOrders.has(wo.id)
|
||||
).length;
|
||||
|
||||
this.selectAllCheckbox.checked = visibleCount > 0 && selectedVisibleCount === visibleCount;
|
||||
this.selectAllCheckbox.indeterminate = selectedVisibleCount > 0 && selectedVisibleCount < visibleCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新选中数量显示
|
||||
*/
|
||||
updateSelectedCount() {
|
||||
this.selectedCount.textContent = `已选择${this.selectedWorkOrders.size}个`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新确定按钮状态
|
||||
*/
|
||||
updateConfirmButton() {
|
||||
this.confirmBtn.disabled = this.selectedWorkOrders.size === 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 确认选择
|
||||
*/
|
||||
confirmSelection() {
|
||||
if (this.selectedWorkOrders.size === 0) {
|
||||
showToast('请选择工单', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
const selectedIds = Array.from(this.selectedWorkOrders);
|
||||
const selectedWorkOrder = this.workOrders.find(wo => wo.id === selectedIds[0]);
|
||||
|
||||
this.onConfirm(selectedWorkOrder);
|
||||
this.hide();
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置选择状态
|
||||
*/
|
||||
reset() {
|
||||
this.selectedWorkOrders.clear();
|
||||
this.searchInput.value = '';
|
||||
this.filteredWorkOrders = [...this.workOrders];
|
||||
this.renderWorkOrderList();
|
||||
this.updateSelectedCount();
|
||||
this.updateConfirmButton();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 物料信息组件
|
||||
*/
|
||||
class MaterialInfo {
|
||||
constructor() {
|
||||
this.element = document.getElementById('material-info');
|
||||
this.workOrderId = document.getElementById('selected-work-order-id');
|
||||
this.materialName = document.getElementById('material-name');
|
||||
this.materialCode = document.getElementById('material-code');
|
||||
this.qualifiedCount = document.getElementById('qualified-count');
|
||||
this.defectiveCount = document.getElementById('defective-count');
|
||||
this.pendingCount = document.getElementById('pending-count');
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示物料信息
|
||||
* @param {Object} data - 物料数据
|
||||
*/
|
||||
show(data) {
|
||||
this.workOrderId.textContent = data.workOrderId;
|
||||
this.materialName.textContent = data.materialName;
|
||||
this.materialCode.textContent = data.materialCode;
|
||||
this.qualifiedCount.textContent = data.qualifiedCount || 0;
|
||||
this.defectiveCount.textContent = data.defectiveCount || 0;
|
||||
this.pendingCount.textContent = data.pendingCount || 0;
|
||||
|
||||
this.element.classList.remove('hidden');
|
||||
}
|
||||
|
||||
/**
|
||||
* 隐藏物料信息
|
||||
*/
|
||||
hide() {
|
||||
this.element.classList.add('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 报工表单组件
|
||||
*/
|
||||
class ReportForm {
|
||||
constructor() {
|
||||
this.element = document.getElementById('report-form');
|
||||
this.quantityInput = document.getElementById('quantity');
|
||||
this.auxiliaryQuantityInput = document.getElementById('auxiliary-quantity');
|
||||
this.remarkInput = document.getElementById('remark');
|
||||
this.workingHoursInput = document.getElementById('working-hours');
|
||||
this.reportStartTimeInput = document.getElementById('report-start-time');
|
||||
this.reportEndTimeInput = document.getElementById('report-end-time');
|
||||
this.auxName = document.getElementById('auxName');
|
||||
|
||||
this.init();
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化表单事件
|
||||
*/
|
||||
init() {
|
||||
// 数字输入验证
|
||||
[this.quantityInput, this.auxiliaryQuantityInput].forEach(input => {
|
||||
if (input) {
|
||||
input.addEventListener('input', (e) => {
|
||||
this.validateNumberInput(e.target);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 表单字段变化时的验证
|
||||
this.getAllInputs().forEach(input => {
|
||||
if (input) {
|
||||
input.addEventListener('blur', () => {
|
||||
this.validateField(input);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证数字输入
|
||||
* @param {HTMLInputElement} input - 输入元素
|
||||
*/
|
||||
validateNumberInput(input) {
|
||||
if (!input) return;
|
||||
const value = parseFloat(input.value);
|
||||
if (isNaN(value) || value < 0) {
|
||||
input.classList.add('border-red-500');
|
||||
this.showFieldError(input, '请输入有效的数字');
|
||||
} else {
|
||||
input.classList.remove('border-red-500');
|
||||
this.hideFieldError(input);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证单个字段
|
||||
* @param {HTMLInputElement} input - 输入元素
|
||||
*/
|
||||
validateField(input) {
|
||||
if (!input) return false;
|
||||
const value = input.value.trim();
|
||||
const isRequired = input.previousElementSibling?.querySelector('.text-red-500');
|
||||
|
||||
if (isRequired && !value) {
|
||||
input.classList.add('border-red-500');
|
||||
this.showFieldError(input, '此字段为必填项');
|
||||
return false;
|
||||
} else {
|
||||
input.classList.remove('border-red-500');
|
||||
this.hideFieldError(input);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示字段错误
|
||||
* @param {HTMLInputElement} input - 输入元素
|
||||
* @param {string} message - 错误消息
|
||||
*/
|
||||
showFieldError(input, message) {
|
||||
let errorElement = input.parentNode.querySelector('.field-error');
|
||||
if (!errorElement) {
|
||||
errorElement = document.createElement('div');
|
||||
// errorElement.className = 'field-error text-red-500 text-sm mt-1';
|
||||
errorElement.className = 'field-error text-red-500 text-sm mt-1 w-full';
|
||||
input.parentNode.appendChild(errorElement);
|
||||
}
|
||||
errorElement.textContent = message;
|
||||
}
|
||||
|
||||
/**
|
||||
* 隐藏字段错误
|
||||
* @param {HTMLInputElement} input - 输入元素
|
||||
*/
|
||||
hideFieldError(input) {
|
||||
const errorElement = input.parentNode.querySelector('.field-error');
|
||||
if (errorElement) {
|
||||
errorElement.remove();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有输入元素
|
||||
* @returns {HTMLInputElement[]}
|
||||
*/
|
||||
getAllInputs() {
|
||||
return [
|
||||
this.quantityInput,
|
||||
this.auxiliaryQuantityInput,
|
||||
this.remarkInput,
|
||||
this.workingHoursInput,
|
||||
this.reportStartTimeInput,
|
||||
this.reportEndTimeInput,
|
||||
].filter(input => input !== null && input !== undefined);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取表单数据
|
||||
* @returns {Object}
|
||||
*/
|
||||
getData() {
|
||||
return {
|
||||
quantity: parseFloat(this.quantityInput?.value) || 0,
|
||||
auxiliaryQuantity: parseFloat(this.auxiliaryQuantityInput?.value) || 0,
|
||||
remark: this.remarkInput?.value?.trim() || '',
|
||||
workingHours: parseFloat(this.workingHoursInput?.value) || 0,
|
||||
reportStartTime: this.reportStartTimeInput?.value?.trim() || '',
|
||||
reportEndTimeInput: this.reportEndTimeInput?.value?.trim() || '',
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置表单数据
|
||||
* @param {Object} data - 表单数据
|
||||
*/
|
||||
setData(data) {
|
||||
if (this.quantityInput) {
|
||||
this.quantityInput.value = data.quantity || '';
|
||||
}
|
||||
if (this.auxiliaryQuantityInput) {
|
||||
this.auxiliaryQuantityInput.value = data.auxiliaryQuantity || '';
|
||||
}
|
||||
}
|
||||
|
||||
setAuxName(data) {
|
||||
if (this.auxName) {
|
||||
this.auxName.innerText = data.auxName || '公斤';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证整个表单
|
||||
* @returns {Object} 验证结果
|
||||
*/
|
||||
validate() {
|
||||
const data = this.getData();
|
||||
const rules = {
|
||||
quantity: { required: true, type: 'number', min: 0 },
|
||||
auxiliaryQuantity: { required: true, type: 'number', min: 0 }
|
||||
};
|
||||
|
||||
return validateForm(data, rules);
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示表单
|
||||
*/
|
||||
show() {
|
||||
this.element.classList.remove('hidden');
|
||||
}
|
||||
|
||||
/**
|
||||
* 隐藏表单
|
||||
*/
|
||||
hide() {
|
||||
this.element.classList.add('hidden');
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置表单
|
||||
*/
|
||||
reset() {
|
||||
this.getAllInputs().forEach(input => {
|
||||
input.value = '';
|
||||
input.classList.remove('border-red-500');
|
||||
this.hideFieldError(input);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const GlobalLoading = {
|
||||
element: null,
|
||||
|
||||
init() {
|
||||
if (this.element) return;
|
||||
|
||||
this.element = document.createElement('div');
|
||||
this.element.id = 'global-loading';
|
||||
this.element.innerHTML = `
|
||||
<div class="loading-backdrop"></div>
|
||||
<div class="loading-spinner">
|
||||
<div class="spinner"></div>
|
||||
<div class="loading-text">加载中...</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// 添加样式
|
||||
const style = document.createElement('style');
|
||||
style.textContent = `
|
||||
#global-loading {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
z-index: 9999;
|
||||
display: none;
|
||||
}
|
||||
.loading-backdrop {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
.loading-spinner {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
text-align: center;
|
||||
}
|
||||
.spinner {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border: 4px solid #f3f3f3;
|
||||
border-top: 4px solid #007bff;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
margin: 0 auto 10px;
|
||||
}
|
||||
.loading-text {
|
||||
color: white;
|
||||
font-size: 14px;
|
||||
}
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
body.loading-active {
|
||||
overflow: hidden;
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(style);
|
||||
document.body.appendChild(this.element);
|
||||
},
|
||||
|
||||
show(text = '加载中...') {
|
||||
this.init();
|
||||
const textElement = this.element.querySelector('.loading-text');
|
||||
if (textElement) {
|
||||
textElement.textContent = text;
|
||||
}
|
||||
this.element.style.display = 'block';
|
||||
document.body.classList.add('loading-active');
|
||||
},
|
||||
|
||||
hide() {
|
||||
if (this.element) {
|
||||
this.element.style.display = 'none';
|
||||
document.body.classList.remove('loading-active');
|
||||
}
|
||||
}
|
||||
};
|
||||
457
skills/web-build/templates/report-app/js/main.js
Normal file
457
skills/web-build/templates/report-app/js/main.js
Normal file
@@ -0,0 +1,457 @@
|
||||
/**
|
||||
* 主应用逻辑
|
||||
* 管理页面状态、组件交互和业务流程
|
||||
*/
|
||||
|
||||
class MainApp {
|
||||
constructor() {
|
||||
// 应用状态
|
||||
this.state = {
|
||||
currentView: 'empty', // empty, material, form
|
||||
selectedWorkOrder: null,
|
||||
materialData: null,
|
||||
formData: null,
|
||||
isLoading: false
|
||||
};
|
||||
|
||||
// 组件实例
|
||||
this.components = {};
|
||||
|
||||
// 初始化应用
|
||||
this.init();
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化应用
|
||||
*/
|
||||
async init() {
|
||||
try {
|
||||
console.log('初始化报工应用...');
|
||||
|
||||
// 等待认证服务初始化完成
|
||||
if (authService && !authService.isAuthenticated()) {
|
||||
await this.waitForAuth();
|
||||
}
|
||||
|
||||
// 初始化组件
|
||||
this.initComponents();
|
||||
|
||||
// 绑定事件
|
||||
this.bindEvents();
|
||||
|
||||
// 预加载数据
|
||||
this.preloadData();
|
||||
|
||||
console.log('报工应用初始化完成');
|
||||
|
||||
} catch (error) {
|
||||
console.error('应用初始化失败:', error);
|
||||
showToast('应用初始化失败,请刷新页面重试', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 等待认证完成
|
||||
*/
|
||||
async waitForAuth() {
|
||||
let retries = 0;
|
||||
const maxRetries = 10;
|
||||
|
||||
while (!authService.isAuthenticated() && retries < maxRetries) {
|
||||
await new Promise(resolve => setTimeout(resolve, 500));
|
||||
retries++;
|
||||
}
|
||||
|
||||
if (!authService.isAuthenticated()) {
|
||||
console.warn('认证超时,继续使用离线模式');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化组件
|
||||
*/
|
||||
initComponents() {
|
||||
// 初始化顶部导航栏
|
||||
this.components.headerNav = new HeaderNav();
|
||||
|
||||
// 初始化工单选择器
|
||||
this.components.workOrderSelector = new WorkOrderSelector(() => {
|
||||
this.showWorkOrderDropdown();
|
||||
});
|
||||
|
||||
// 初始化工单下拉菜单
|
||||
this.components.workOrderDropdown = new WorkOrderDropdown(
|
||||
(workOrder) => this.onWorkOrderSelected(workOrder),
|
||||
() => this.onWorkOrderDropdownClosed()
|
||||
);
|
||||
|
||||
// 初始化物料信息组件
|
||||
this.components.materialInfo = new MaterialInfo();
|
||||
|
||||
// 初始化报工表单组件
|
||||
this.components.reportForm = new ReportForm();
|
||||
|
||||
console.log('组件初始化完成');
|
||||
}
|
||||
|
||||
/**
|
||||
* 绑定事件
|
||||
*/
|
||||
bindEvents() {
|
||||
// 提交按钮事件
|
||||
const submitBtn = document.getElementById('submit-btn');
|
||||
if (submitBtn) {
|
||||
submitBtn.addEventListener('click', () => {
|
||||
this.handleSubmit();
|
||||
});
|
||||
}
|
||||
|
||||
// 网络状态变化事件
|
||||
window.addEventListener('online', () => {
|
||||
console.log('网络已连接');
|
||||
showToast('网络已连接', 'success');
|
||||
});
|
||||
|
||||
window.addEventListener('offline', () => {
|
||||
console.log('网络已断开');
|
||||
showToast('网络已断开,将使用离线模式', 'warning');
|
||||
});
|
||||
|
||||
console.log('事件绑定完成');
|
||||
}
|
||||
|
||||
/**
|
||||
* 预加载数据
|
||||
*/
|
||||
async preloadData() {
|
||||
try {
|
||||
// 预加载工单数据
|
||||
if (workOrderService) {
|
||||
await workOrderService.preloadWorkOrders();
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('预加载数据失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示工单下拉菜单
|
||||
*/
|
||||
async showWorkOrderDropdown() {
|
||||
try {
|
||||
this.setLoading(true);
|
||||
|
||||
// 获取工单列表
|
||||
const workOrderData = await workOrderService.getWorkOrderList();
|
||||
|
||||
// 设置工单数据到下拉菜单
|
||||
this.components.workOrderDropdown.setWorkOrders(workOrderData.workOrders);
|
||||
|
||||
// 显示下拉菜单
|
||||
this.components.workOrderDropdown.show();
|
||||
|
||||
// 更新选择器箭头状态
|
||||
this.components.workOrderSelector.toggleArrow(true);
|
||||
|
||||
} catch (error) {
|
||||
console.error('加载工单列表失败:', error);
|
||||
showToast('加载工单列表失败', 'error');
|
||||
} finally {
|
||||
this.setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 工单选择事件处理
|
||||
* @param {Object} workOrder - 选中的工单
|
||||
*/
|
||||
async onWorkOrderSelected(workOrder) {
|
||||
try {
|
||||
console.log('选择工单:', workOrder);
|
||||
|
||||
this.setLoading(true);
|
||||
|
||||
// 更新状态
|
||||
this.state.selectedWorkOrder = workOrder;
|
||||
|
||||
// 更新工单选择器显示
|
||||
this.components.workOrderSelector.updateSelected(workOrder);
|
||||
|
||||
// 从工单数据中提取物料信息(工单数据中已包含materialInfo)
|
||||
const materialData = this.extractMaterialDataFromWorkOrder(workOrder);
|
||||
this.state.materialData = materialData;
|
||||
|
||||
// 切换到物料和表单视图
|
||||
this.switchToMaterialView();
|
||||
|
||||
// 初始化表单数据
|
||||
this.initializeFormData();
|
||||
|
||||
} catch (error) {
|
||||
console.error('处理工单选择失败:', error);
|
||||
showToast('处理工单选择失败', 'error');
|
||||
} finally {
|
||||
this.setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从工单数据中提取物料信息
|
||||
* @param {Object} workOrder - 工单对象
|
||||
* @returns {Object} 物料数据
|
||||
*/
|
||||
extractMaterialDataFromWorkOrder(workOrder) {
|
||||
// 从materialInfo中提取详细信息
|
||||
const materialInfo = workOrder.materialInfo || {};
|
||||
const materialBaseInfo = materialInfo.baseInfo || {};
|
||||
|
||||
// 提取单位转换关系
|
||||
const conversions = materialInfo.conversions || [];
|
||||
let unit = '个';
|
||||
let auxiliaryUnit = '公斤';
|
||||
|
||||
if (conversions.length > 0) {
|
||||
const firstConversion = conversions[0];
|
||||
unit = firstConversion.fromUnitName || '个';
|
||||
auxiliaryUnit = firstConversion.toUnitName || '公斤';
|
||||
}
|
||||
|
||||
return {
|
||||
workOrderId: workOrder.workOrderId || workOrder.id,
|
||||
workOrderCode: workOrder.workOrderCode || workOrder.id,
|
||||
materialId: workOrder.materialId || materialBaseInfo.id,
|
||||
materialName: workOrder.materialName || materialBaseInfo.name || '',
|
||||
materialCode: workOrder.materialCode || materialBaseInfo.code || '',
|
||||
materialSpec: materialBaseInfo.specification || '',
|
||||
unit: unit,
|
||||
auxiliaryUnit: auxiliaryUnit,
|
||||
conversions: conversions,
|
||||
// 数量信息
|
||||
qualifiedCount: workOrder.qualifiedQuantity || 0,
|
||||
qualifiedCountDisplay: workOrder.qualifiedQuantityDisplay || '0',
|
||||
defectiveCount: workOrder.disqualifiedQuantity || 0,
|
||||
defectiveCountDisplay: workOrder.disqualifiedQuantityDisplay || '0',
|
||||
pendingCount: Math.max(0, (workOrder.totalQuantity || 0) - (workOrder.qualifiedQuantity || 0) - (workOrder.disqualifiedQuantity || 0)),
|
||||
totalCount: workOrder.totalQuantity,
|
||||
unitId: materialInfo?.unit?.id
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 工单下拉菜单关闭事件处理
|
||||
*/
|
||||
onWorkOrderDropdownClosed() {
|
||||
// 更新选择器箭头状态
|
||||
this.components.workOrderSelector.toggleArrow(false);
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换到物料视图
|
||||
*/
|
||||
switchToMaterialView() {
|
||||
// 隐藏空状态
|
||||
const emptyState = document.getElementById('empty-state');
|
||||
if (emptyState) {
|
||||
emptyState.classList.add('hidden');
|
||||
}
|
||||
|
||||
// 显示物料信息
|
||||
this.components.materialInfo.show(this.state.materialData);
|
||||
|
||||
// 显示报工表单
|
||||
this.components.reportForm.show();
|
||||
|
||||
// 显示提交按钮
|
||||
const submitSection = document.getElementById('submit-section');
|
||||
if (submitSection) {
|
||||
submitSection.classList.remove('hidden');
|
||||
}
|
||||
|
||||
// 更新状态
|
||||
this.state.currentView = 'material';
|
||||
|
||||
this.components.reportForm.setAuxName({ auxName: this.state.materialData.unit });
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换到空状态视图
|
||||
*/
|
||||
switchToEmptyView() {
|
||||
// 显示空状态
|
||||
const emptyState = document.getElementById('empty-state');
|
||||
if (emptyState) {
|
||||
emptyState.classList.remove('hidden');
|
||||
}
|
||||
|
||||
// 隐藏物料信息
|
||||
this.components.materialInfo.hide();
|
||||
|
||||
// 隐藏报工表单
|
||||
this.components.reportForm.hide();
|
||||
|
||||
// 隐藏提交按钮
|
||||
const submitSection = document.getElementById('submit-section');
|
||||
if (submitSection) {
|
||||
submitSection.classList.add('hidden');
|
||||
}
|
||||
|
||||
// 重置状态
|
||||
this.state.currentView = 'empty';
|
||||
this.state.selectedWorkOrder = null;
|
||||
this.state.materialData = null;
|
||||
this.state.formData = null;
|
||||
|
||||
// 重置工单选择器
|
||||
this.components.workOrderSelector.updateSelected(null);
|
||||
|
||||
console.log('切换到空状态视图');
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化表单数据
|
||||
*/
|
||||
initializeFormData() {
|
||||
// 直接从 HTML 读取值,不设置默认值
|
||||
const formData = this.components.reportForm.getData();
|
||||
this.state.formData = formData;
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理提交事件
|
||||
*/
|
||||
async handleSubmit() {
|
||||
try {
|
||||
// 验证表单
|
||||
const validation = this.components.reportForm.validate();
|
||||
if (!validation.isValid) {
|
||||
const errorMessage = Object.values(validation.errors)[0];
|
||||
showToast(errorMessage, 'error');
|
||||
return;
|
||||
}
|
||||
// 获取表单数据
|
||||
const formData = this.components.reportForm.getData();
|
||||
console.log('表单数据:', formData);
|
||||
// 构建报工数据
|
||||
const reportData = {
|
||||
workOrderId: this.state.selectedWorkOrder.workOrderId, // 使用数字类型的工单ID
|
||||
workOrderCode: this.state.selectedWorkOrder.workOrderCode, // 同时传递工单编号作为备用
|
||||
materialId: this.state.materialData.materialId,
|
||||
quantity: formData.quantity,
|
||||
auxiliaryQuantity: formData.auxiliaryQuantity,
|
||||
remark: formData.remark,
|
||||
workHour: formData.workingHours,
|
||||
workHourUnit: 2,
|
||||
auxUnitId1: this.state.materialData.unitId,
|
||||
reportStartTime: formData.reportStartTime,
|
||||
reportEndTime: formData.reportEndTimeInput
|
||||
};
|
||||
|
||||
console.log('提交报工数据:', reportData);
|
||||
|
||||
// this.setLoading(true);
|
||||
GlobalLoading.show('提交中...');
|
||||
|
||||
// 提交报工
|
||||
const result = await reportService.submitReport(reportData);
|
||||
|
||||
console.log('报工提交结果:', result);
|
||||
|
||||
if (result.success) {
|
||||
// 提交成功,重置表单
|
||||
this.handleSubmitSuccess(result);
|
||||
} else {
|
||||
throw new Error(result.message || '提交失败');
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('提交报工失败:', error);
|
||||
showToast(error.message || '提交失败', 'error');
|
||||
} finally {
|
||||
// this.setLoading(false);
|
||||
GlobalLoading.hide();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理提交成功
|
||||
* @param {Object} result - 提交结果
|
||||
*/
|
||||
handleSubmitSuccess(result) {
|
||||
// 显示成功消息
|
||||
showToast('报工提交成功', 'success');
|
||||
|
||||
// 重置表单
|
||||
this.components.reportForm.reset();
|
||||
|
||||
// 重新初始化表单数据
|
||||
this.initializeFormData();
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置加载状态
|
||||
* @param {boolean} loading - 是否加载中
|
||||
*/
|
||||
setLoading(loading) {
|
||||
this.state.isLoading = loading;
|
||||
|
||||
// 更新提交按钮状态
|
||||
const submitBtn = document.getElementById('submit-btn');
|
||||
if (submitBtn) {
|
||||
submitBtn.disabled = loading;
|
||||
submitBtn.textContent = loading ? '提交中...' : '提交';
|
||||
}
|
||||
|
||||
// 可以添加全局加载指示器
|
||||
if (loading) {
|
||||
document.body.style.cursor = 'wait';
|
||||
} else {
|
||||
document.body.style.cursor = '';
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 重置应用状态
|
||||
*/
|
||||
reset() {
|
||||
this.switchToEmptyView();
|
||||
this.components.workOrderDropdown.reset();
|
||||
console.log('应用状态已重置');
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取应用状态
|
||||
* @returns {Object}
|
||||
*/
|
||||
getState() {
|
||||
return deepClone(this.state);
|
||||
}
|
||||
|
||||
/**
|
||||
* 销毁应用
|
||||
*/
|
||||
destroy() {
|
||||
// 清理事件监听器
|
||||
// 清理组件
|
||||
// 清理定时器等
|
||||
console.log('应用已销毁');
|
||||
}
|
||||
}
|
||||
|
||||
// 页面加载完成后初始化应用
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
console.log('DOM加载完成,初始化应用...');
|
||||
|
||||
// 创建全局应用实例
|
||||
window.MainApp = new MainApp();
|
||||
|
||||
// 开发模式下暴露服务到全局
|
||||
if (window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1') {
|
||||
window.authService = authService;
|
||||
window.apiService = apiService;
|
||||
window.workOrderService = workOrderService;
|
||||
window.reportService = reportService;
|
||||
console.log('开发模式:服务已暴露到全局');
|
||||
}
|
||||
});
|
||||
758
skills/web-build/templates/report-app/services/business.js
Normal file
758
skills/web-build/templates/report-app/services/business.js
Normal file
@@ -0,0 +1,758 @@
|
||||
/**
|
||||
* 业务服务
|
||||
* 包含工单服务和报工服务
|
||||
*/
|
||||
|
||||
// ==================== 工单服务 ====================
|
||||
|
||||
/**
|
||||
* 工单服务
|
||||
* 处理工单相关的API请求
|
||||
*/
|
||||
class WorkOrderService {
|
||||
constructor() {
|
||||
this.api = apiService;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取工单列表
|
||||
* @param {Object} params - 查询参数
|
||||
* @param {string} params.search - 搜索关键词(工单编号模糊查询)
|
||||
* @param {string} params.exactWorkOrderCode - 工单编号精确查询
|
||||
* @param {number} params.page - 页码
|
||||
* @param {number} params.pageSize - 每页数量
|
||||
* @param {Array<number>} params.workOrderStatusList - 工单业务状态列表
|
||||
* @param {number} params.pauseFlag - 是否暂停 0未暂停 1已暂停
|
||||
* @returns {Promise<Object>}
|
||||
*/
|
||||
async getWorkOrderList(params = {}) {
|
||||
try {
|
||||
console.log('从服务器获取工单列表', params);
|
||||
|
||||
// 构建请求体,根据API文档格式
|
||||
const requestBody = {
|
||||
page: params.page || 1,
|
||||
size: params.pageSize || 50
|
||||
};
|
||||
|
||||
// 如果有搜索关键词,使用模糊查询
|
||||
if (params.search) {
|
||||
requestBody.workOrderCode = params.search.trim();
|
||||
}
|
||||
|
||||
// 如果有精确查询
|
||||
if (params.exactWorkOrderCode) {
|
||||
requestBody.exactWorkOrderCode = params.exactWorkOrderCode.trim();
|
||||
}
|
||||
|
||||
// 工单状态筛选
|
||||
if (params.workOrderStatusList && Array.isArray(params.workOrderStatusList)) {
|
||||
requestBody.workOrderStatusList = params.workOrderStatusList;
|
||||
}
|
||||
|
||||
// 暂停状态筛选
|
||||
if (params.pauseFlag !== undefined) {
|
||||
requestBody.pauseFlag = params.pauseFlag;
|
||||
}
|
||||
|
||||
// 调用API接口(POST方法)
|
||||
const response = await this.api.post('/openapi/domain/web/v1/route/med/open/v2/work_order/base/_list', requestBody);
|
||||
|
||||
// 处理响应数据
|
||||
const result = this.processWorkOrderListResponse(response);
|
||||
|
||||
return result;
|
||||
|
||||
} catch (error) {
|
||||
console.error('获取工单列表失败:', error);
|
||||
|
||||
// 如果是网络错误,返回模拟数据
|
||||
if (error.name === 'TypeError' || error.message.includes('Failed to fetch')) {
|
||||
console.log('网络错误,返回模拟工单数据');
|
||||
return this.getMockWorkOrderList(params);
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 处理工单列表响应数据
|
||||
* @param {Object} response - API响应
|
||||
* @returns {Object}
|
||||
*/
|
||||
processWorkOrderListResponse(response) {
|
||||
// 根据API文档,响应格式:{ code, message, data: { list, page, total } }
|
||||
if (response.code === 200 || response.code === 0) {
|
||||
const data = response.data;
|
||||
|
||||
if (!data || !data.list) {
|
||||
throw new Error('响应数据格式错误:缺少data或list字段');
|
||||
}
|
||||
|
||||
return {
|
||||
workOrders: data.list.map(item => {
|
||||
// 提取物料信息
|
||||
const materialInfo = item.materialInfo || {};
|
||||
const materialBaseInfo = materialInfo.baseInfo || {};
|
||||
|
||||
// 提取数量信息(BaseAmountDisplay对象)
|
||||
const qualifiedAmount = item.qualifiedHoldAmount || {};
|
||||
const disqualifiedAmount = item.disqualifiedHoldAmount || {};
|
||||
const totalAmount = item.totalHoldAmount || {};
|
||||
|
||||
return {
|
||||
id: item.workOrderCode || item.workOrderId?.toString() || '',
|
||||
workOrderId: item.workOrderId,
|
||||
workOrderCode: item.workOrderCode,
|
||||
name: materialBaseInfo.name || materialInfo.name || item.workOrderCode,
|
||||
status: item.workOrderStatus?.code || 'active',
|
||||
// 物料信息(保留完整结构)
|
||||
materialInfo: item.materialInfo, // 保留完整的materialInfo对象
|
||||
materialId: materialBaseInfo.id || materialInfo.id,
|
||||
materialName: materialBaseInfo.name || materialInfo.name,
|
||||
materialCode: materialBaseInfo.code || materialInfo.code,
|
||||
// 数量信息
|
||||
qualifiedQuantity: qualifiedAmount.amount || 0,
|
||||
qualifiedQuantityDisplay: qualifiedAmount.amountDisplay || '0',
|
||||
disqualifiedQuantity: disqualifiedAmount.amount || 0,
|
||||
disqualifiedQuantityDisplay: disqualifiedAmount.amountDisplay || '0',
|
||||
totalQuantity: totalAmount.amount || 0,
|
||||
totalQuantityDisplay: totalAmount.amountDisplay || '0',
|
||||
// 时间信息
|
||||
createdAt: item.createdAt,
|
||||
updatedAt: item.updatedAt,
|
||||
plannedStartTime: item.plannedStartTime,
|
||||
plannedEndTime: item.plannedEndTime,
|
||||
// 其他信息
|
||||
pauseFlag: item.pauseFlag,
|
||||
workOrderStatus: item.workOrderStatus,
|
||||
workOrderType: item.workOrderType
|
||||
};
|
||||
}),
|
||||
total: data.total || 0,
|
||||
page: data.page || 1,
|
||||
pageSize: data.size || 50
|
||||
};
|
||||
} else {
|
||||
throw new Error(response.message || `获取工单列表失败 (code: ${response.code})`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 获取模拟工单列表(用于离线模式或测试)
|
||||
* @param {Object} params - 查询参数
|
||||
* @returns {Object}
|
||||
*/
|
||||
getMockWorkOrderList(params = {}) {
|
||||
const mockData = [
|
||||
{
|
||||
id: 'cyy-SOP',
|
||||
name: '测试工单-SOP',
|
||||
status: 'active',
|
||||
materialId: 'M001',
|
||||
materialName: '物料1-工序a-10',
|
||||
materialCode: 'wuliao1-gongxua-10'
|
||||
},
|
||||
{
|
||||
id: 'cyy-chengliangrenwu',
|
||||
name: '测试工单-成量任务',
|
||||
status: 'active',
|
||||
materialId: 'M002',
|
||||
materialName: '物料2-工序b-20',
|
||||
materialCode: 'wuliao2-gongxub-20'
|
||||
},
|
||||
{
|
||||
id: 'gd2510300131-',
|
||||
name: '工单2510300131',
|
||||
status: 'active',
|
||||
materialId: 'M003',
|
||||
materialName: '物料3-工序c-30',
|
||||
materialCode: 'wuliao3-gongxuc-30'
|
||||
},
|
||||
{
|
||||
id: 'gd2510270130-',
|
||||
name: '工单2510270130',
|
||||
status: 'active',
|
||||
materialId: 'M004',
|
||||
materialName: '物料4-工序d-40',
|
||||
materialCode: 'wuliao4-gongxud-40'
|
||||
},
|
||||
{
|
||||
id: 'gd2510210129-',
|
||||
name: '工单2510210129',
|
||||
status: 'active',
|
||||
materialId: 'M005',
|
||||
materialName: '物料5-工序e-50',
|
||||
materialCode: 'wuliao5-gongxue-50'
|
||||
},
|
||||
{
|
||||
id: 'gd2509020128-',
|
||||
name: '工单2509020128',
|
||||
status: 'active',
|
||||
materialId: 'M006',
|
||||
materialName: '物料6-工序f-60',
|
||||
materialCode: 'wuliao6-gongxuf-60'
|
||||
},
|
||||
{
|
||||
id: 'gd2509010126-',
|
||||
name: '工单2509010126',
|
||||
status: 'active',
|
||||
materialId: 'M007',
|
||||
materialName: '物料7-工序g-70',
|
||||
materialCode: 'wuliao7-gongxug-70'
|
||||
},
|
||||
{
|
||||
id: 'gd2509010125-',
|
||||
name: '工单2509010125',
|
||||
status: 'active',
|
||||
materialId: 'M008',
|
||||
materialName: '物料8-工序h-80',
|
||||
materialCode: 'wuliao8-gongxuh-80'
|
||||
},
|
||||
{
|
||||
id: 'gd2508260124-',
|
||||
name: '工单2508260124',
|
||||
status: 'active',
|
||||
materialId: 'M009',
|
||||
materialName: '物料9-工序i-90',
|
||||
materialCode: 'wuliao9-gongxui-90'
|
||||
},
|
||||
{
|
||||
id: 'gd2507210123-',
|
||||
name: '工单2507210123',
|
||||
status: 'active',
|
||||
materialId: 'M010',
|
||||
materialName: '物料10-工序j-100',
|
||||
materialCode: 'wuliao10-gongxuj-100'
|
||||
}
|
||||
];
|
||||
|
||||
// 应用搜索过滤
|
||||
let filteredData = mockData;
|
||||
if (params.search) {
|
||||
const searchTerm = params.search.toLowerCase();
|
||||
filteredData = mockData.filter(item =>
|
||||
item.id.toLowerCase().includes(searchTerm) ||
|
||||
item.name.toLowerCase().includes(searchTerm)
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
workOrders: filteredData,
|
||||
total: filteredData.length,
|
||||
page: params.page || 1,
|
||||
pageSize: params.pageSize || 50
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 预加载工单数据
|
||||
*/
|
||||
async preloadWorkOrders() {
|
||||
try {
|
||||
console.log('预加载工单数据...');
|
||||
await this.getWorkOrderList({ pageSize: 100 });
|
||||
} catch (error) {
|
||||
console.error('预加载工单数据失败:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 创建全局工单服务实例
|
||||
const workOrderService = new WorkOrderService();
|
||||
|
||||
// ==================== 报工服务 ====================
|
||||
|
||||
/**
|
||||
* 报工服务
|
||||
* 处理报工相关的API请求
|
||||
*/
|
||||
class ReportService {
|
||||
constructor() {
|
||||
this.api = apiService;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取报工必填参数
|
||||
* 从接口获取报工所需的必填参数(如taskId、progressReportMaterial等)
|
||||
* @param {Object} params - 查询参数
|
||||
* @param {string|number} params.workOrderId - 工单ID
|
||||
* @param {string|number} params.materialId - 物料ID
|
||||
* @returns {Promise<Object>} 返回必填参数对象
|
||||
*/
|
||||
async getReportRequiredParams(params) {
|
||||
try {
|
||||
console.log('获取报工必填参数:', params);
|
||||
|
||||
const { workOrderId, materialId } = params;
|
||||
|
||||
if (!workOrderId ) {
|
||||
throw new Error('工单ID获取失败');
|
||||
}
|
||||
|
||||
const taskListRequest = {
|
||||
page: 1,
|
||||
size: 10,
|
||||
workOrderIdList: [workOrderId]
|
||||
};
|
||||
|
||||
const taskListResponse = await this.api.post('/openapi/domain/web/v1/route/mfg/open/v1/produce_task/_list', taskListRequest);
|
||||
|
||||
if (taskListResponse.code !== 200 || !taskListResponse.data || !taskListResponse.data.list || taskListResponse.data.list.length === 0) {
|
||||
throw new Error('未找到对应的生产任务');
|
||||
}
|
||||
|
||||
// 取第一个任务(通常一个工单对应一个主任务)
|
||||
const task = taskListResponse.data.list[0];
|
||||
const taskId = task.taskId;
|
||||
const executorIds = (task.executorList || []).map(item => item.id); // 只取执行人id数组
|
||||
|
||||
if (!taskId) {
|
||||
throw new Error('生产任务中未找到任务ID');
|
||||
}
|
||||
|
||||
// 2. 通过生产任务ID获取报工物料列表
|
||||
console.log('获取报工物料列表...');
|
||||
const materialListRequest = {
|
||||
taskId: taskId
|
||||
};
|
||||
|
||||
const materialListResponse = await this.api.post('/openapi/domain/web/v1/route/mfg/open/v1/progress_report/_list_progress_report_materials', materialListRequest);
|
||||
|
||||
if (materialListResponse.code !== 200 || !materialListResponse.data || !materialListResponse.data.outputMaterials || materialListResponse.data.outputMaterials.length === 0) {
|
||||
throw new Error('未找到报工物料信息');
|
||||
}
|
||||
|
||||
// 获取报工物料列表
|
||||
const outputMaterials = materialListResponse.data.outputMaterials;
|
||||
|
||||
// 优先选择主产出物料(mainFlag为true),否则选择第一个
|
||||
let selectedMaterial = outputMaterials.find(m => m.mainFlag === true) || outputMaterials[0];
|
||||
|
||||
if (!selectedMaterial) {
|
||||
throw new Error('未找到可报工的物料');
|
||||
}
|
||||
|
||||
// 从报工物料中提取报工关系信息
|
||||
const progressReportKey = selectedMaterial.progressReportKey;
|
||||
|
||||
if (!progressReportKey) {
|
||||
throw new Error('报工物料中未找到报工关系信息');
|
||||
}
|
||||
|
||||
// 3. 构建progressReportMaterial对象
|
||||
const progressReportMaterial = {
|
||||
lineId: progressReportKey.lineId,
|
||||
materialId: progressReportKey.materialId || selectedMaterial.materialInfo?.baseInfo?.id || materialId,
|
||||
reportProcessId: progressReportKey.reportProcessId
|
||||
};
|
||||
|
||||
// 验证必填字段
|
||||
if (!progressReportMaterial.lineId) {
|
||||
throw new Error('无法获取物料行ID');
|
||||
}
|
||||
if (!progressReportMaterial.materialId) {
|
||||
throw new Error('无法获取物料ID');
|
||||
}
|
||||
if (!progressReportMaterial.reportProcessId) {
|
||||
throw new Error('无法获取报工工序ID');
|
||||
}
|
||||
|
||||
// 4. 返回必填参数
|
||||
const requiredParams = {
|
||||
taskId: taskId,
|
||||
progressReportMaterial: progressReportMaterial,
|
||||
// 额外信息,可能用于构建报工参数
|
||||
materialInfo: selectedMaterial.materialInfo,
|
||||
outputMaterialUnit: selectedMaterial.outputMaterialUnit, // 报工单位信息
|
||||
lineId: progressReportMaterial.lineId,
|
||||
reportProcessId: progressReportMaterial.reportProcessId,
|
||||
executorIds: executorIds, // 执行人列表
|
||||
// 报工物料的其他信息
|
||||
reportType: selectedMaterial.reportType || [], // 可报工方式
|
||||
mainFlag: selectedMaterial.mainFlag, // 是否为主产出
|
||||
warehousingFlag: selectedMaterial.warehousingFlag, // 是否入库
|
||||
autoWarehousingFlag: selectedMaterial.autoWarehousingFlag // 是否自动入库
|
||||
};
|
||||
|
||||
console.log('成功获取报工必填参数:', requiredParams);
|
||||
return requiredParams;
|
||||
|
||||
} catch (error) {
|
||||
console.error('获取报工必填参数失败:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建报工请求参数
|
||||
* 将必填参数和表单数据合并,构建完整的报工请求参数
|
||||
* @param {Object} requiredParams - 从接口获取的必填参数
|
||||
* @param {Object} formData - 表单数据
|
||||
* @param {Object} formData.workOrderId - 工单ID
|
||||
* @param {Object} formData.materialId - 物料ID
|
||||
* @param {number} formData.quantity - 报工数量
|
||||
* @param {number} formData.auxiliaryQuantity - 辅助数量
|
||||
* @param {number} formData.qcStatus - 质量状态(1:合格 2:让步合格 3:代检 4:不合格)
|
||||
* @param {number} formData.reportType - 报工方式(1:扫码报工-合格 2:记录报工-合格 3:扫码报工-不合格 4:记录报工-不合格 5:打码报工-合格 6:打码报工-不合格)
|
||||
* @param {Array<number>} formData.executorIds - 执行人ID列表
|
||||
* @returns {Object} 完整的报工请求参数
|
||||
* @note storageLocationId 已预置为 1716848012872791
|
||||
*/
|
||||
buildReportRequestParams(requiredParams, formData) {
|
||||
// 从必填参数中获取基础信息
|
||||
const {
|
||||
taskId, // 生产任务ID(必填)
|
||||
progressReportMaterial, // 报工物料对象(必填)
|
||||
reportProcessId, // 报工工序ID
|
||||
lineId, // 物料行ID
|
||||
outputMaterialUnit, // 报工单位信息
|
||||
executorIds // 执行人ID列表
|
||||
} = requiredParams;
|
||||
|
||||
// 获取报工单位ID(从outputMaterialUnit中获取)
|
||||
const reportUnitId = outputMaterialUnit?.id;
|
||||
if (!reportUnitId) {
|
||||
throw new Error('无法获取报工单位ID');
|
||||
}
|
||||
|
||||
// 构建报工请求参数
|
||||
const requestParams = {
|
||||
// 必填字段
|
||||
taskId: taskId,
|
||||
progressReportMaterial: progressReportMaterial || {
|
||||
materialId: formData.materialId,
|
||||
lineId: lineId,
|
||||
reportProcessId: reportProcessId
|
||||
},
|
||||
qcStatus: formData.qcStatus || 1, // 默认合格
|
||||
reportType: formData.reportType || 2, // 默认记录报工-合格
|
||||
|
||||
// 报工详情(必填)
|
||||
progressReportItems: [{
|
||||
executorIds: executorIds ,
|
||||
progressReportMaterialItems: [{
|
||||
reportAmount: formData.quantity,
|
||||
reportUnitId: reportUnitId, // 必填:报工单位ID
|
||||
remark: formData.remark || undefined,
|
||||
// auxAmount1: formData.auxiliaryQuantity || undefined,
|
||||
// auxUnitId1: formData.auxUnitId1 || undefined
|
||||
}]
|
||||
}],
|
||||
|
||||
// 可选字段
|
||||
storageLocationId: 1716848012872791,// 预置仓位 Id,不可修改
|
||||
reportStartTime: formData.reportStartTime ? new Date(formData.reportStartTime).getTime() : undefined,
|
||||
reportEndTime: formData.reportEndTime ? new Date(formData.reportEndTime).getTime() : undefined,
|
||||
actualExecutorIds: executorIds || [],
|
||||
actualEquipmentIds: formData.equipmentIds || [],
|
||||
workHour: formData.workHour,
|
||||
workHourUnit: formData.workHourUnit ,
|
||||
qcDefectReasonIds: formData.qcDefectReasonIds || [] // 不良原因(质量状态为不合格时)
|
||||
};
|
||||
|
||||
// 移除undefined字段
|
||||
Object.keys(requestParams).forEach(key => {
|
||||
if (requestParams[key] === undefined) {
|
||||
delete requestParams[key];
|
||||
}
|
||||
});
|
||||
|
||||
// 递归移除嵌套对象中的undefined字段
|
||||
const cleanUndefined = (obj) => {
|
||||
if (Array.isArray(obj)) {
|
||||
return obj.map(item => cleanUndefined(item));
|
||||
} else if (obj && typeof obj === 'object') {
|
||||
const cleaned = {};
|
||||
Object.keys(obj).forEach(key => {
|
||||
if (obj[key] !== undefined) {
|
||||
cleaned[key] = cleanUndefined(obj[key]);
|
||||
}
|
||||
});
|
||||
return cleaned;
|
||||
}
|
||||
return obj;
|
||||
};
|
||||
|
||||
return cleanUndefined(requestParams);
|
||||
}
|
||||
|
||||
/**
|
||||
* 提交报工
|
||||
* @param {Object} reportData - 报工数据
|
||||
* @param {string|number} reportData.workOrderId - 工单ID
|
||||
* @param {string|number} reportData.workOrderCode - 工单编号
|
||||
* @param {string|number} reportData.materialId - 物料ID
|
||||
* @param {number} reportData.quantity - 数量
|
||||
* @param {number} reportData.auxiliaryQuantity - 辅助数量
|
||||
* @param {number} reportData.qcStatus - 质量状态(1:合格 2:让步合格 3:代检 4:不合格)
|
||||
* @param {number} reportData.reportType - 报工方式(1-6)
|
||||
* @param {Array<number>} reportData.executorIds - 执行人ID列表
|
||||
* @param {number} reportData.reportUnitId - 报工单位ID
|
||||
* @param {number} reportData.auxUnitId1 - 辅助单位1 ID
|
||||
* @returns {Promise<Object>}
|
||||
*/
|
||||
async submitReport(reportData) {
|
||||
try {
|
||||
// 验证报工数据
|
||||
this.validateReportData(reportData);
|
||||
|
||||
// 1. 获取报工必填参数
|
||||
const requiredParams = await this.getReportRequiredParams({
|
||||
workOrderId: reportData.workOrderId,
|
||||
materialId: reportData.materialId
|
||||
});
|
||||
|
||||
// 2. 构建完整的报工请求参数
|
||||
const requestParams = this.buildReportRequestParams(requiredParams, reportData);
|
||||
|
||||
console.log('提交报工请求参数:', requestParams);
|
||||
|
||||
// 3. 调用批量报工接口
|
||||
const response = await this.api.post('/openapi/domain/web/v1/route/mfg/open/v1/progress_report/_progress_report', requestParams);
|
||||
|
||||
// 4. 处理响应
|
||||
const result = this.processReportResponse(response);
|
||||
|
||||
console.log('报工提交成功:', result);
|
||||
|
||||
return result;
|
||||
|
||||
} catch (error) {
|
||||
console.error('报工提交失败:', error);
|
||||
showToast(error.message || '报工提交失败', 'error');
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 验证报工数据
|
||||
* @param {Object} data - 报工数据
|
||||
*/
|
||||
validateReportData(data) {
|
||||
const errors = [];
|
||||
|
||||
if (!data.workOrderId) {
|
||||
errors.push('工单ID不能为空');
|
||||
}
|
||||
|
||||
if (!data.quantity || data.quantity <= 0) {
|
||||
errors.push('数量必须大于0');
|
||||
}
|
||||
|
||||
if (!data.auxiliaryQuantity || data.auxiliaryQuantity <= 0) {
|
||||
errors.push('辅助数量必须大于0');
|
||||
}
|
||||
|
||||
if (errors.length > 0) {
|
||||
throw new Error(errors.join(', '));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理报工响应
|
||||
* 根据批量报工接口文档,响应格式:{ code, message, data: { messageTraceId, progressReportRecordIds } }
|
||||
* @param {Object} response - API响应
|
||||
* @returns {Object}
|
||||
*/
|
||||
processReportResponse(response) {
|
||||
// 根据批量报工接口文档,响应格式:{ code, message, data: { messageTraceId, progressReportRecordIds } }
|
||||
if (response.code === 200 || response.code === 0) {
|
||||
const data = response.data || {};
|
||||
|
||||
return {
|
||||
success: true,
|
||||
messageTraceId: data.messageTraceId,
|
||||
progressReportRecordIds: data.progressReportRecordIds || [],
|
||||
message: response.message || '报工提交成功',
|
||||
reportTime: new Date().toISOString()
|
||||
};
|
||||
} else {
|
||||
throw new Error(response.message || `报工提交失败 (code: ${response.code})`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据生产任务查询报工记录列表
|
||||
* @param {Object} params - 查询参数
|
||||
* @param {number|Array<number>} params.taskId - 生产任务ID或ID列表(可选)
|
||||
* @param {number|Array<number>} params.workOrderId - 工单ID或ID列表(可选)
|
||||
* @param {number} params.reportTimeFrom - 报工时间From(闭区间),时间戳(可选)
|
||||
* @param {number} params.reportTimeTo - 报工时间To(开区间),时间戳(可选)
|
||||
* @param {number} params.page - 请求页,默认1(可选)
|
||||
* @param {number} params.size - 每页大小,默认200(可选)
|
||||
* @param {Array<number>} params.processIdList - 工序ID列表(可选)
|
||||
* @param {Array<number>} params.executorIdList - 可执行人ID列表(可选)
|
||||
* @param {Array<number>} params.qcStatusList - 质量状态列表(可选)
|
||||
* @param {Array<Object>} params.sorter - 排序条件列表(可选),列表顺序表示排序顺序
|
||||
* @param {string} params.sorter[].field - 排序字段,如 "reportTime", "taskCode" 等
|
||||
* @param {string} params.sorter[].order - 排序规律,默认 "asc","asc" 升序 "desc" 降序
|
||||
* @returns {Promise<Object>} 返回报工记录列表(分页响应)
|
||||
* @returns {boolean} returns.success - 是否成功
|
||||
* @returns {Array<Object>} returns.records - 报工记录数组,每个记录包含以下字段:
|
||||
* @returns {number} returns.records[].id - 报工记录详情id(必填)
|
||||
* @returns {number} returns.records[].lineId - 物料行id(必填)
|
||||
* @returns {number} returns.records[].reportRecordId - 报工记录id(必填)
|
||||
* @returns {number} returns.records[].taskId - 生产任务Id(必填)
|
||||
* @returns {string} returns.records[].taskCode - 生产任务编号(必填)
|
||||
* @returns {number} returns.records[].workOrderId - 工单Id(必填)
|
||||
* @returns {string} returns.records[].workOrderCode - 工单编号(必填)
|
||||
* @returns {number} returns.records[].processId - 工序Id(必填)
|
||||
* @returns {string} returns.records[].processCode - 工序编号(必填)
|
||||
* @returns {string} returns.records[].processName - 工序名称(必填)
|
||||
* @returns {number} returns.records[].mainMaterialId - 工单物料id(必填)
|
||||
* @returns {string} returns.records[].mainMaterialCode - 工单物料编号(必填)
|
||||
* @returns {string} returns.records[].mainMaterialName - 工单物料名称(必填)
|
||||
* @returns {Object} returns.records[].materialInfo - 报工物料信息(必填),包含 baseInfo(物料基本信息)、attribute(物料属性信息)等
|
||||
* @returns {Object} returns.records[].reportBaseAmount - 报工数量1对应数量对象(必填),包含 amount(数量)和 unit(单位信息)
|
||||
* @returns {Object} returns.records[].reportBaseAmountDisplay - 报工数量1对应数量对象-无科学技术法(必填),包含 amount、amountDisplay、unitCode、unitId、unitName
|
||||
* @returns {number} returns.records[].reportTime - 报工时间,时间戳(必填)
|
||||
* @returns {Object} returns.records[].reporter - 报工人员(必填),包含 id、code、name、username、avatarUrl
|
||||
* @returns {Array<Object>} returns.records[].producers - 生产人员(必填),数组元素包含 id、code、name、username、avatarUrl
|
||||
* @returns {Object} returns.records[].qcStatus - 质量状态(必填),包含 code(枚举code)和 message(枚举信息)
|
||||
* @returns {Object} returns.records[].workHourUnit - 工时单位(必填),包含 code 和 message
|
||||
* @returns {number} returns.records[].startTime - 报工开始时间,时间戳(可选)
|
||||
* @returns {number} returns.records[].endTime - 报工结束时间,时间戳(可选)
|
||||
* @returns {number} returns.records[].workHour - 工时(可选)
|
||||
* @returns {string} returns.records[].batchNo - 批次号(可选)
|
||||
* @returns {number} returns.records[].batchNoId - 批次号Id(可选)
|
||||
* @returns {string} returns.records[].flowCardCode - 流转卡编号(可选)
|
||||
* @returns {string} returns.records[].qrCode - 标示码(可选)
|
||||
* @returns {string} returns.records[].reportRecordCode - 报工记录编号(可选)
|
||||
* @returns {string} returns.records[].remark - 备注(可选)
|
||||
* @returns {number} returns.records[].autoWarehousingFlag - 是否自动入库标识 0-否 1-是(可选)
|
||||
* @returns {Array<Object>} returns.records[].equipments - 设备列表(可选),数组元素包含 id、code、name
|
||||
* @returns {Array<Object>} returns.records[].executors - 计划执行人(可选),数组元素包含 id、code、name、username、avatarUrl
|
||||
* @returns {Array<Object>} returns.records[].produceDepartments - 生产部门列表(可选),数组元素包含 id、code、name
|
||||
* @returns {Array<Object>} returns.records[].customFields - 自定义字段(可选),数组元素包含 fieldCode、fieldName、fieldValue 等
|
||||
* @returns {Array<Object>} returns.records[].qcDefectReasons - 不良原因(可选),数组元素包含 id、name
|
||||
* @returns {Array<string>} returns.records[].salesOrderCode - 订单编号(可选)
|
||||
* @returns {Array<number>} returns.records[].pictureIds - 图片ids(可选)
|
||||
* @returns {Object} returns.records[].reportAuxBaseAmount1 - 报工数量2对应数量对象(可选),包含 amount 和 unit
|
||||
* @returns {number} returns.records[].updatedAt - 更新时间,时间戳(可选)
|
||||
* @returns {number} returns.total - 总条数
|
||||
* @returns {number} returns.page - 当前页
|
||||
* @returns {number} returns.size - 每页大小
|
||||
* @returns {string} returns.message - 返回消息
|
||||
*/
|
||||
async getReportRecordsByTask(params) {
|
||||
try {
|
||||
const {
|
||||
taskId,
|
||||
workOrderId,
|
||||
reportTimeFrom,
|
||||
reportTimeTo,
|
||||
page = 1,
|
||||
size = 200,
|
||||
processIdList,
|
||||
executorIdList,
|
||||
qcStatusList,
|
||||
sorter
|
||||
} = params;
|
||||
|
||||
// 构建请求参数
|
||||
const requestParams = {
|
||||
page: parseInt(page),
|
||||
size: parseInt(size)
|
||||
};
|
||||
|
||||
// 添加排序参数(如果未提供,默认按报工时间降序)
|
||||
if (sorter && Array.isArray(sorter) && sorter.length > 0) {
|
||||
// 使用用户提供的排序条件
|
||||
requestParams.sorter = sorter.map(item => ({
|
||||
field: item.field,
|
||||
order: item.order
|
||||
}));
|
||||
} else {
|
||||
// 默认按报工时间降序排序
|
||||
requestParams.sorter = [{
|
||||
field: 'reportTime',
|
||||
order: 'desc'
|
||||
}];
|
||||
}
|
||||
|
||||
// 添加任务ID列表(支持单个或数组)
|
||||
if (taskId) {
|
||||
requestParams.taskIds = Array.isArray(taskId) ? taskId.map(id => parseInt(id)) : [parseInt(taskId)];
|
||||
}
|
||||
|
||||
// 添加工单ID列表(支持单个或数组)
|
||||
if (workOrderId) {
|
||||
requestParams.workOrderIdList = Array.isArray(workOrderId)
|
||||
? workOrderId.map(id => parseInt(id))
|
||||
: [parseInt(workOrderId)];
|
||||
}
|
||||
|
||||
// 添加报工时间范围
|
||||
if (reportTimeFrom) {
|
||||
requestParams.reportTimeFrom = parseInt(reportTimeFrom);
|
||||
}
|
||||
if (reportTimeTo) {
|
||||
requestParams.reportTimeTo = parseInt(reportTimeTo);
|
||||
}
|
||||
|
||||
// 添加其他可选参数
|
||||
if (processIdList && processIdList.length > 0) {
|
||||
requestParams.processIdList = processIdList.map(id => parseInt(id));
|
||||
}
|
||||
if (executorIdList && executorIdList.length > 0) {
|
||||
requestParams.executorIdList = executorIdList.map(id => parseInt(id));
|
||||
}
|
||||
if (qcStatusList && qcStatusList.length > 0) {
|
||||
requestParams.qcStatusList = qcStatusList.map(status => parseInt(status));
|
||||
}
|
||||
|
||||
// 验证至少有一个查询条件
|
||||
if (!requestParams.taskIds && !requestParams.workOrderIdList) {
|
||||
throw new Error('必须提供 taskId 或 workOrderId 中的至少一个');
|
||||
}
|
||||
|
||||
console.log('查询报工记录,参数:', requestParams);
|
||||
|
||||
// 调用报工记录列表接口
|
||||
const response = await this.api.post('/openapi/domain/web/v1/route/mfg/open/v1/progress_report/_list', requestParams);
|
||||
|
||||
// 处理响应
|
||||
if (response.code !== 200 && response.code !== 0) {
|
||||
throw new Error(response.message || `查询报工记录失败 (code: ${response.code})`);
|
||||
}
|
||||
|
||||
// 响应格式:{ code, message, data: { list, page, total } }
|
||||
// data.list 是报工记录数组,每个记录包含以下主要字段:
|
||||
// - 必填字段:id, lineId, reportRecordId, taskId, taskCode, workOrderId, workOrderCode,
|
||||
// processId, processCode, processName, mainMaterialId, mainMaterialCode, mainMaterialName,
|
||||
// materialInfo, reportBaseAmount, reportBaseAmountDisplay, reportTime, reporter, producers, qcStatus, workHourUnit
|
||||
// - 可选字段:startTime, endTime, workHour, batchNo, flowCardCode, qrCode, remark,
|
||||
// equipments, executors, customFields, qcDefectReasons 等
|
||||
const data = response.data || {};
|
||||
const reportRecords = data.list || [];
|
||||
const total = data.total || 0;
|
||||
const currentPage = data.page || page;
|
||||
|
||||
console.log(`查询到 ${reportRecords.length} 条报工记录,共 ${total} 条`);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
records: reportRecords, // 报工记录数组,详细字段说明见函数 JSDoc 注释
|
||||
total: total,
|
||||
page: currentPage,
|
||||
size: size,
|
||||
message: response.message || '查询成功'
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
console.error('查询报工记录失败:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// 创建全局报工服务实例
|
||||
const reportService = new ReportService();
|
||||
|
||||
781
skills/web-build/templates/report-app/services/core.js
Normal file
781
skills/web-build/templates/report-app/services/core.js
Normal file
@@ -0,0 +1,781 @@
|
||||
/**
|
||||
* 核心服务
|
||||
* 包含API服务和认证服务
|
||||
*/
|
||||
|
||||
// ==================== API服务 ====================
|
||||
|
||||
/**
|
||||
* API服务基础类
|
||||
* 封装HTTP请求,自动处理认证和错误
|
||||
*/
|
||||
class ApiService {
|
||||
constructor() {
|
||||
this.baseURL = this.getBaseURL();
|
||||
this.defaultHeaders = {
|
||||
'Content-Type': 'application/json'
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取API基础URL
|
||||
* @returns {string}
|
||||
*/
|
||||
getBaseURL() {
|
||||
// 根据环境自动判断API地址
|
||||
const hostname = window.location.hostname;
|
||||
|
||||
if (hostname === 'localhost' || hostname === '127.0.0.1') {
|
||||
// 开发环境,使用黑湖API服务器
|
||||
return 'https://v3-feature.blacklake.cn/api';
|
||||
} else {
|
||||
// 生产环境,使用黑湖API服务器
|
||||
return 'https://v3-feature.blacklake.cn/api';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建完整的请求URL
|
||||
* @param {string} endpoint - API端点
|
||||
* @returns {string}
|
||||
*/
|
||||
buildURL(endpoint) {
|
||||
// 如果endpoint已经是完整URL,直接返回
|
||||
if (endpoint.startsWith('http://') || endpoint.startsWith('https://')) {
|
||||
return endpoint;
|
||||
}
|
||||
|
||||
// 确保endpoint以/开头
|
||||
if (!endpoint.startsWith('/')) {
|
||||
endpoint = '/' + endpoint;
|
||||
}
|
||||
|
||||
return this.baseURL + endpoint;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取请求头
|
||||
* @param {Object} customHeaders - 自定义请求头
|
||||
* @returns {Object}
|
||||
*/
|
||||
getHeaders(customHeaders = {}) {
|
||||
const headers = { ...this.defaultHeaders, ...customHeaders };
|
||||
|
||||
// 添加认证头
|
||||
if (authService && authService.isAuthenticated()) {
|
||||
Object.assign(headers, authService.getAuthHeaders());
|
||||
}
|
||||
|
||||
return headers;
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理响应
|
||||
* @param {Response} response - fetch响应对象
|
||||
* @returns {Promise<any>}
|
||||
*/
|
||||
async handleResponse(response) {
|
||||
// 解析响应数据
|
||||
const contentType = response.headers.get('content-type');
|
||||
let data;
|
||||
|
||||
if (contentType && contentType.includes('application/json')) {
|
||||
data = await response.json();
|
||||
} else {
|
||||
data = await response.text();
|
||||
}
|
||||
|
||||
// 处理HTTP状态码错误
|
||||
if (!response.ok) {
|
||||
let errorMessage = `请求失败: ${response.status}`;
|
||||
|
||||
if (typeof data === 'object' && data.message) {
|
||||
errorMessage = data.message;
|
||||
} else if (typeof data === 'string') {
|
||||
errorMessage = data;
|
||||
}
|
||||
|
||||
// 处理认证错误
|
||||
if (response.status === 401 || response.status === 403) {
|
||||
if (authService) {
|
||||
const canRetry = await authService.handleAuthError(response);
|
||||
if (canRetry) {
|
||||
throw new Error('AUTH_RETRY'); // 特殊错误,表示可以重试
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
|
||||
// 检查业务错误码(即使HTTP状态码是200,业务也可能返回错误)
|
||||
if (typeof data === 'object' && data.code !== undefined) {
|
||||
// 业务错误码:非200且非0表示错误
|
||||
if (data.code !== 200 && data.code !== 0) {
|
||||
// 处理认证相关的业务错误
|
||||
if (data.subCode && (
|
||||
data.subCode.includes('TOKEN') ||
|
||||
data.subCode.includes('AUTH') ||
|
||||
data.subCode.includes('APP_KEY') ||
|
||||
data.subCode.includes('NOT_EXIST')
|
||||
)) {
|
||||
console.warn('业务认证错误:', data.subCode, data.message);
|
||||
if (authService) {
|
||||
const canRetry = await authService.handleAuthError(response);
|
||||
if (canRetry) {
|
||||
throw new Error('AUTH_RETRY');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error(data.message || `业务错误 (code: ${data.code}, subCode: ${data.subCode})`);
|
||||
}
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送HTTP请求
|
||||
* @param {string} endpoint - API端点
|
||||
* @param {Object} options - 请求选项
|
||||
* @returns {Promise<any>}
|
||||
*/
|
||||
async request(endpoint, options = {}) {
|
||||
const url = this.buildURL(endpoint);
|
||||
const config = {
|
||||
method: 'GET',
|
||||
headers: this.getHeaders(options.headers),
|
||||
...options
|
||||
};
|
||||
|
||||
// 如果有body且不是FormData,转换为JSON字符串
|
||||
if (config.body && !(config.body instanceof FormData)) {
|
||||
if (typeof config.body === 'object') {
|
||||
config.body = JSON.stringify(config.body);
|
||||
}
|
||||
}
|
||||
|
||||
let retryCount = 0;
|
||||
const maxRetries = 2;
|
||||
|
||||
while (retryCount <= maxRetries) {
|
||||
try {
|
||||
console.log(`API请求: ${config.method} ${url}`, config.body ? JSON.parse(config.body) : '');
|
||||
|
||||
const response = await fetch(url, config);
|
||||
const result = await this.handleResponse(response);
|
||||
|
||||
console.log(`API响应: ${config.method} ${url}`, result);
|
||||
return result;
|
||||
|
||||
} catch (error) {
|
||||
console.error(`API错误: ${config.method} ${url}`, error);
|
||||
|
||||
// 如果是认证错误且可以重试
|
||||
if (error.message === 'AUTH_RETRY' && retryCount < maxRetries) {
|
||||
retryCount++;
|
||||
// 更新请求头中的认证信息
|
||||
config.headers = this.getHeaders(options.headers);
|
||||
continue;
|
||||
}
|
||||
|
||||
// 网络错误重试
|
||||
if (error.name === 'TypeError' && error.message.includes('Failed to fetch') && retryCount < maxRetries) {
|
||||
retryCount++;
|
||||
await this.delay(1000 * retryCount); // 延迟重试
|
||||
continue;
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GET请求
|
||||
* @param {string} endpoint - API端点
|
||||
* @param {Object} params - 查询参数
|
||||
* @param {Object} options - 请求选项
|
||||
* @returns {Promise<any>}
|
||||
*/
|
||||
async get(endpoint, params = {}, options = {}) {
|
||||
// 构建查询字符串
|
||||
const queryString = new URLSearchParams(params).toString();
|
||||
const url = queryString ? `${endpoint}?${queryString}` : endpoint;
|
||||
|
||||
return this.request(url, {
|
||||
method: 'GET',
|
||||
...options
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* POST请求
|
||||
* @param {string} endpoint - API端点
|
||||
* @param {any} data - 请求数据
|
||||
* @param {Object} options - 请求选项
|
||||
* @returns {Promise<any>}
|
||||
*/
|
||||
async post(endpoint, data = null, options = {}) {
|
||||
return this.request(endpoint, {
|
||||
method: 'POST',
|
||||
body: data,
|
||||
...options
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* PUT请求
|
||||
* @param {string} endpoint - API端点
|
||||
* @param {any} data - 请求数据
|
||||
* @param {Object} options - 请求选项
|
||||
* @returns {Promise<any>}
|
||||
*/
|
||||
async put(endpoint, data = null, options = {}) {
|
||||
return this.request(endpoint, {
|
||||
method: 'PUT',
|
||||
body: data,
|
||||
...options
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* DELETE请求
|
||||
* @param {string} endpoint - API端点
|
||||
* @param {Object} options - 请求选项
|
||||
* @returns {Promise<any>}
|
||||
*/
|
||||
async delete(endpoint, options = {}) {
|
||||
return this.request(endpoint, {
|
||||
method: 'DELETE',
|
||||
...options
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传文件
|
||||
* @param {string} endpoint - API端点
|
||||
* @param {File|FileList} files - 文件对象
|
||||
* @param {Object} additionalData - 额外数据
|
||||
* @param {Function} onProgress - 进度回调
|
||||
* @returns {Promise<any>}
|
||||
*/
|
||||
async upload(endpoint, files, additionalData = {}, onProgress = null) {
|
||||
const formData = new FormData();
|
||||
|
||||
// 添加文件
|
||||
if (files instanceof FileList) {
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
formData.append('files', files[i]);
|
||||
}
|
||||
} else if (files instanceof File) {
|
||||
formData.append('file', files);
|
||||
}
|
||||
|
||||
// 添加额外数据
|
||||
for (const key in additionalData) {
|
||||
formData.append(key, additionalData[key]);
|
||||
}
|
||||
|
||||
const options = {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
headers: {} // 不设置Content-Type,让浏览器自动设置
|
||||
};
|
||||
|
||||
// 如果需要进度回调,使用XMLHttpRequest
|
||||
if (onProgress) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const xhr = new XMLHttpRequest();
|
||||
|
||||
xhr.upload.addEventListener('progress', (e) => {
|
||||
if (e.lengthComputable) {
|
||||
const percentComplete = (e.loaded / e.total) * 100;
|
||||
onProgress(percentComplete);
|
||||
}
|
||||
});
|
||||
|
||||
xhr.addEventListener('load', () => {
|
||||
if (xhr.status >= 200 && xhr.status < 300) {
|
||||
try {
|
||||
const result = JSON.parse(xhr.responseText);
|
||||
resolve(result);
|
||||
} catch (e) {
|
||||
resolve(xhr.responseText);
|
||||
}
|
||||
} else {
|
||||
reject(new Error(`上传失败: ${xhr.status}`));
|
||||
}
|
||||
});
|
||||
|
||||
xhr.addEventListener('error', () => {
|
||||
reject(new Error('上传失败'));
|
||||
});
|
||||
|
||||
xhr.open('POST', this.buildURL(endpoint));
|
||||
|
||||
// 添加认证头
|
||||
const headers = this.getHeaders();
|
||||
for (const key in headers) {
|
||||
if (key !== 'Content-Type') { // 不设置Content-Type
|
||||
xhr.setRequestHeader(key, headers[key]);
|
||||
}
|
||||
}
|
||||
|
||||
xhr.send(formData);
|
||||
});
|
||||
}
|
||||
|
||||
return this.request(endpoint, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* 延迟函数
|
||||
* @param {number} ms - 延迟毫秒数
|
||||
* @returns {Promise}
|
||||
*/
|
||||
delay(ms) {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量请求
|
||||
* @param {Array} requests - 请求数组,每个元素包含 {method, endpoint, data}
|
||||
* @returns {Promise<Array>}
|
||||
*/
|
||||
async batch(requests) {
|
||||
const promises = requests.map(req => {
|
||||
const { method = 'GET', endpoint, data, options = {} } = req;
|
||||
|
||||
switch (method.toUpperCase()) {
|
||||
case 'GET':
|
||||
return this.get(endpoint, data, options);
|
||||
case 'POST':
|
||||
return this.post(endpoint, data, options);
|
||||
case 'PUT':
|
||||
return this.put(endpoint, data, options);
|
||||
case 'DELETE':
|
||||
return this.delete(endpoint, options);
|
||||
default:
|
||||
return Promise.reject(new Error(`不支持的请求方法: ${method}`));
|
||||
}
|
||||
});
|
||||
|
||||
return Promise.allSettled(promises);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置基础URL
|
||||
* @param {string} baseURL - 新的基础URL
|
||||
*/
|
||||
setBaseURL(baseURL) {
|
||||
this.baseURL = baseURL;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置默认请求头
|
||||
* @param {Object} headers - 请求头对象
|
||||
*/
|
||||
setDefaultHeaders(headers) {
|
||||
this.defaultHeaders = { ...this.defaultHeaders, ...headers };
|
||||
}
|
||||
}
|
||||
|
||||
// 创建全局API服务实例
|
||||
const apiService = new ApiService();
|
||||
|
||||
// ==================== 认证服务 ====================
|
||||
|
||||
/**
|
||||
* 默认配置
|
||||
* 修改登录账号时,需要同时更新 appKey
|
||||
*/
|
||||
const AUTH_CONFIG = {
|
||||
// 登录信息
|
||||
login: {
|
||||
type: 1,
|
||||
username: "cyy",
|
||||
code: "67768820",
|
||||
password: "794db7135639d5b59cd7e53b325f36a8f24fb906e95e403c49e89b99046654fae36a940c1e8496b75b9e69d4f79022c9123aa0d8cd665e2ea9cf584242a702664e77fdd2399c452bd03a174a3edbb41b86a406851b4da8b11b8faa7044925e3e9bffd4fd5afb14c70f592a2114ce5f45cf567e2e1f0d8688ef345ca28c2687c5"
|
||||
},
|
||||
// AppKey(用于获取user-access token)
|
||||
appKey: "cli_1764100796489835"
|
||||
};
|
||||
|
||||
/**
|
||||
* 认证服务
|
||||
* 处理token获取、存储和管理
|
||||
*/
|
||||
class AuthService {
|
||||
constructor() {
|
||||
this.token = null; // 最终用于业务接口的user-access token
|
||||
this.tokenKey = 'work_report_user_access_token';
|
||||
this.init();
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化认证服务
|
||||
*/
|
||||
async init() {
|
||||
try {
|
||||
// 1. 先查看URL里是否存在code
|
||||
const code = getUrlParameter('code');
|
||||
|
||||
if (code) {
|
||||
// 如果存在code,直接使用code获取user-access token
|
||||
console.log('从URL参数获取到code:', code);
|
||||
await this.getUserAccessTokenByCode(code);
|
||||
} else {
|
||||
// 如果不存在code,走登录流程
|
||||
console.log('URL参数中没有code,走登录流程...');
|
||||
|
||||
// 2.1 先登录获取token
|
||||
console.log('开始登录...');
|
||||
const loginResult = await this.loginAndGetToken();
|
||||
|
||||
if (!loginResult || !loginResult.token) {
|
||||
throw new Error('登录失败,未获取到token');
|
||||
}
|
||||
|
||||
console.log('登录成功,获取到登录token');
|
||||
|
||||
// 2.2 使用登录token获取code
|
||||
console.log('使用登录token获取code...');
|
||||
const codeFromApi = await this.getCodeByLoginToken(loginResult.token);
|
||||
|
||||
if (!codeFromApi) {
|
||||
throw new Error('获取code失败');
|
||||
}
|
||||
|
||||
console.log('成功获取code:', codeFromApi);
|
||||
|
||||
// 2.3 使用code获取user-access token
|
||||
await this.getUserAccessTokenByCode(codeFromApi);
|
||||
}
|
||||
|
||||
console.log('认证初始化完成,user-access token已获取');
|
||||
|
||||
} catch (error) {
|
||||
console.error('认证初始化失败:', error);
|
||||
showToast('认证初始化失败,请刷新页面重试', 'error');
|
||||
throw error; // 抛出错误,让调用方知道初始化失败
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用code获取user-access token
|
||||
* @param {string} code - code参数
|
||||
*/
|
||||
async getUserAccessTokenByCode(code) {
|
||||
try {
|
||||
if (!code) {
|
||||
throw new Error('code参数不能为空');
|
||||
}
|
||||
|
||||
console.log('使用code获取user-access token...');
|
||||
|
||||
const requestBody = {
|
||||
code: code,
|
||||
appKey: AUTH_CONFIG.appKey
|
||||
};
|
||||
|
||||
const response = await fetch('https://v3-feature.blacklake.cn/api/openapi/domain/api/v1/access_token/_get_user_token_for_customized', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(requestBody)
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`获取user-access token失败: ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
console.log('获取user-access token响应:', data);
|
||||
|
||||
// 根据实际响应格式提取token
|
||||
if (data.code === 200 || data.code === 0) {
|
||||
const tokenData = data.data || data.result || data;
|
||||
const userAccessToken = tokenData.token ||
|
||||
tokenData.userAccessToken ||
|
||||
tokenData.accessToken ||
|
||||
data.token ||
|
||||
data.userAccessToken ||
|
||||
data.accessToken;
|
||||
|
||||
if (userAccessToken) {
|
||||
this.token = userAccessToken;
|
||||
this.saveToken(userAccessToken);
|
||||
console.log('成功获取user-access token');
|
||||
} else {
|
||||
throw new Error('响应中没有找到user-access token');
|
||||
}
|
||||
} else {
|
||||
throw new Error(data.message || `获取user-access token失败 (code: ${data.code})`);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('获取user-access token失败:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用登录token获取code
|
||||
* @param {string} loginToken - 登录获取的token
|
||||
* @returns {Promise<string>} 返回code
|
||||
*/
|
||||
async getCodeByLoginToken(loginToken) {
|
||||
try {
|
||||
if (!loginToken) {
|
||||
throw new Error('登录token不能为空');
|
||||
}
|
||||
|
||||
console.log('使用登录token获取code...');
|
||||
|
||||
const response = await fetch('https://v3-feature.blacklake.cn/api/openapiadmin/domain/web/v1/access_token/_code', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-AUTH': loginToken,
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`获取code失败: ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
console.log('获取code响应:', data);
|
||||
|
||||
// 根据实际响应格式提取code
|
||||
if (data.code === 200 || data.code === 0) {
|
||||
const codeData = data.data || data.result || data;
|
||||
const code = codeData.code ||
|
||||
codeData.accessToken ||
|
||||
data.code ||
|
||||
data.accessToken;
|
||||
|
||||
if (code) {
|
||||
console.log('成功获取code');
|
||||
return code;
|
||||
} else {
|
||||
throw new Error('响应中没有找到code');
|
||||
}
|
||||
} else {
|
||||
throw new Error(data.message || `获取code失败 (code: ${data.code})`);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('获取code失败:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 登录并获取token
|
||||
*/
|
||||
async loginAndGetToken() {
|
||||
try {
|
||||
const loginData = {
|
||||
type: AUTH_CONFIG.login.type,
|
||||
username: AUTH_CONFIG.login.username,
|
||||
code: AUTH_CONFIG.login.code,
|
||||
password: AUTH_CONFIG.login.password
|
||||
};
|
||||
|
||||
console.log('尝试登录获取token...');
|
||||
const response = await fetch('https://v3-feature.blacklake.cn/api/user/domain/web/v1/login', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(loginData)
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`登录失败: ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
console.log('登录响应完整数据:', JSON.stringify(data, null, 2));
|
||||
console.log('登录响应状态码:', response.status);
|
||||
console.log('登录响应headers:', Object.fromEntries(response.headers.entries()));
|
||||
|
||||
// 检查响应是否成功(支持多种响应格式)
|
||||
const isSuccess = data.success === true ||
|
||||
data.code === 0 ||
|
||||
data.code === 200 ||
|
||||
(data.code === undefined && response.ok);
|
||||
|
||||
console.log('登录响应是否成功:', isSuccess, 'code:', data.code, 'success:', data.success);
|
||||
|
||||
if (isSuccess) {
|
||||
// 尝试从不同位置提取token(这是登录token,用于后续获取code)
|
||||
const tokenData = data.data || data.result || data;
|
||||
const loginToken = tokenData?.token ||
|
||||
tokenData?.access_token ||
|
||||
tokenData?.accessToken ||
|
||||
data.token ||
|
||||
data.access_token ||
|
||||
data.accessToken;
|
||||
|
||||
if (loginToken) {
|
||||
console.log('登录成功,获取到登录token');
|
||||
return { token: loginToken };
|
||||
} else {
|
||||
const errorMsg = '登录响应中没有找到token';
|
||||
console.error(errorMsg, data);
|
||||
throw new Error(errorMsg);
|
||||
}
|
||||
} else {
|
||||
// 登录失败,抛出错误
|
||||
const errorMsg = data.message || data.msg || `登录失败 (code: ${data.code})`;
|
||||
console.error('登录失败:', errorMsg, data);
|
||||
throw new Error(errorMsg);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('登录接口调用失败:', error);
|
||||
throw error; // 重新抛出错误
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 获取当前token
|
||||
* @returns {string|null}
|
||||
*/
|
||||
getToken() {
|
||||
return this.token;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否已认证
|
||||
* @returns {boolean}
|
||||
*/
|
||||
isAuthenticated() {
|
||||
return !!this.token;
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存token到本地存储
|
||||
* @param {string} token - 要保存的token
|
||||
*/
|
||||
saveToken(token) {
|
||||
try {
|
||||
const tokenData = {
|
||||
token: token,
|
||||
timestamp: Date.now(),
|
||||
expiresAt: Date.now() + (24 * 60 * 60 * 1000) // 24小时后过期
|
||||
};
|
||||
localStorage.setItem(this.tokenKey, JSON.stringify(tokenData));
|
||||
} catch (error) {
|
||||
console.error('保存token失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从本地存储获取token
|
||||
* @returns {string|null}
|
||||
*/
|
||||
getStoredToken() {
|
||||
try {
|
||||
const stored = localStorage.getItem(this.tokenKey);
|
||||
if (!stored) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const tokenData = JSON.parse(stored);
|
||||
|
||||
// 检查是否过期
|
||||
if (tokenData.expiresAt && Date.now() > tokenData.expiresAt) {
|
||||
console.log('存储的token已过期');
|
||||
this.clearToken();
|
||||
return null;
|
||||
}
|
||||
|
||||
return tokenData.token;
|
||||
|
||||
} catch (error) {
|
||||
console.error('获取存储token失败:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除token
|
||||
*/
|
||||
clearToken() {
|
||||
this.token = null;
|
||||
try {
|
||||
localStorage.removeItem(this.tokenKey);
|
||||
} catch (error) {
|
||||
console.error('清除token失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 刷新token(重新执行完整的认证流程)
|
||||
*/
|
||||
async refreshToken() {
|
||||
console.log('刷新token,重新执行认证流程...');
|
||||
this.clearToken();
|
||||
await this.init();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取认证头部
|
||||
* 根据接口文档,所有业务接口都需要使用 X-AUTH 头部,值为 user-access token
|
||||
* @returns {Object}
|
||||
*/
|
||||
getAuthHeaders() {
|
||||
const headers = {};
|
||||
if (this.token) {
|
||||
// 使用最终的user-access token
|
||||
headers['X-AUTH'] = this.token;
|
||||
}
|
||||
return headers;
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理认证错误
|
||||
* @param {Response} response - HTTP响应对象
|
||||
*/
|
||||
async handleAuthError(response) {
|
||||
if (response.status === 401 || response.status === 403) {
|
||||
console.log('认证失败,尝试刷新token');
|
||||
showToast('认证已过期,正在重新获取...', 'info');
|
||||
|
||||
try {
|
||||
await this.refreshToken();
|
||||
return true; // 表示可以重试请求
|
||||
} catch (error) {
|
||||
console.error('刷新token失败:', error);
|
||||
showToast('认证失败,请刷新页面', 'error');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 登出
|
||||
*/
|
||||
logout() {
|
||||
this.clearToken();
|
||||
showToast('已退出登录', 'info');
|
||||
// 可以重定向到登录页面或刷新页面
|
||||
setTimeout(() => {
|
||||
window.location.reload();
|
||||
}, 1000);
|
||||
}
|
||||
}
|
||||
|
||||
// 创建全局认证服务实例
|
||||
const authService = new AuthService();
|
||||
|
||||
Reference in New Issue
Block a user