Initial commit

This commit is contained in:
Zhongwei Li
2025-11-30 08:55:46 +08:00
commit b710247ba7
27 changed files with 6516 additions and 0 deletions

View 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 // 报工单位IDreportUnitId
},
executorIds: [1, 2, 3] // 执行人ID列表
}
```
---
## 🎨 样式配置
主题色配置在 `css/custom.css`
```css
:root {
--primary-color: #02b980; /* 主色 */
--primary-hover: #029968; /* 悬停色 */
--primary-light: #e6f7f1; /* 浅色背景 */
}
```
---
## ⚡ 技术栈
- **HTML5** + **Tailwind CSS** + **原生 JavaScript (ES6+)**
- **无需构建**,直接作为静态资源访问

View File

@@ -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 方法"
}

View File

@@ -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 方法"
}

View 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 方法"
}

View File

@@ -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 - 物料信息,包含 baseInfoid、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 方法"
}

View File

@@ -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 方法"
}

View File

@@ -0,0 +1,28 @@
# 接口文档
## 📋 概述
本文档详细说明了本项目涉及的所有接口调用情况。
## 🔄 接口调用流程
### 1. 页面初始化流程
```
页面加载 → url获取 code → 换取 access_token提供后续open接口调用
```
### 2. 选择工单
```
用户选择特定工单 → 选择后回显工单上的物料信息
```
### 3. 报工提交流程
```
用户填写报工数据 → 点击提交 → 构建批量报工请求参数 → 调用批量报工接口 → 清空表单
```
### 4. 查看报工记录(可选)
```
根据用户报工时使用的生产 taskId 或者 workOrderId → 构建报工记录的请求参数 → 调用报工记录的接口 → 渲染数据
```

View File

@@ -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 方法中的任务查询部分"
}

View File

@@ -0,0 +1,3 @@
{
"welcome_message": "右侧是当前<strong>生产任务执行(报工页面)</strong>的样式。<br><br>您可以:<ul><li>通过对话描述需要修改的部分</li><li>直接上传手绘稿告诉我您期望的样式</li></ul>我将通过AI魔法帮您心想事成。"
}

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

View 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">&nbsp;&nbsp;&nbsp;&nbsp;</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>

View 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');
}
}
};

View 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('开发模式:服务已暴露到全局');
}
});

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

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