8 天以前 8d3b1c06ed0ef0803c31085cb3e90aa6d3d622f0
refactor(workOrder): 重构工单管理页面为父子表格结构

- 移除工单编号搜索条件,保留生产订单号搜索
- 将原有PIMTable替换为el-table实现父子表格结构
- 添加生产订单主表和工单子表的嵌套展示功能
- 实现子表懒加载,通过getWorkOrderListByOrderId接口获取明细
- 添加返工返修工单的红色标记样式
- 优化报工按钮的禁用逻辑和状态判断
- 添加报废数量的红色标记显示
- 集成PaginationComp分页组件替代PIMTable内置分页
- 更新API接口调用方式适配新的数据结构
- 添加详细的生产工单模块前端联调文档
- 调整开发环境API基础地址配置
已添加1个文件
已修改3个文件
1010 ■■■■ 文件已修改
docs/生产工单模块前端联调文档.md 505 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/productionManagement/workOrder.js 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/productionManagement/workOrderManagement/index.vue 495 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
vite.config.js 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/Éú²ú¹¤µ¥Ä£¿éǰ¶ËÁªµ÷Îĵµ.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,505 @@
# ç”Ÿäº§å·¥å•模块前端联调文档
> æ–‡æ¡£ç”Ÿæˆæ—¥æœŸï¼š2026-06-11
> é€‚用分支:dev_天津_中兴实强
---
## å˜æ›´è®°å½•
| æ—¥æœŸ | å˜æ›´å†…容 | å½±å“èŒƒå›´ |
|------|----------|----------|
| 2026-06-11 | /page接口改为按订单分组聚合 | åˆ—表页展示逻辑调整 |
| 2026-06-11 | æ–°å¢ž/byOrder/{orderId}接口 | è®¢å•详情页工单列表 |
---
## ä¸€ã€æŽ¥å£åˆ—表
| æŽ¥å£åç§° | æŽ¥å£è·¯å¾„ | è¯·æ±‚方式 | è¯´æ˜Ž |
|----------|----------|----------|------|
| å·¥å•分页查询 | `/productionOperationTask/page` | GET | æŒ‰ç”Ÿäº§è®¢å•维度分组展示 |
| è®¢å•工单列表 | `/productionOperationTask/byOrder/{orderId}` | GET | æ ¹æ®è®¢å•ID查询工单明细 |
---
## äºŒã€æŽ¥å£è¯¦æƒ…
### 2.1 å·¥å•分页查询
#### åŸºæœ¬ä¿¡æ¯
```
GET /productionOperationTask/page
```
#### è¯·æ±‚参数(Query)
| å‚数名 | ç±»åž‹ | å¿…å¡« | è¯´æ˜Ž |
|--------|------|------|------|
| current | Number | å¦ | å½“前页码,默认1 |
| size | Number | å¦ | æ¯é¡µæ¡æ•°ï¼Œé»˜è®¤10 |
| npsNo | String | å¦ | ç”Ÿäº§è®¢å•号,模糊查询 |
| productionOrderId | Number | å¦ | ç”Ÿäº§è®¢å•ID,精确匹配 |
#### è¯·æ±‚示例
```javascript
// åŸºç¡€åˆ†é¡µæŸ¥è¯¢
axios.get('/productionOperationTask/page', {
  params: {
    current: 1,
    size: 10
  }
})
// æŒ‰è®¢å•号筛选
axios.get('/productionOperationTask/page', {
  params: {
    current: 1,
    size: 10,
    npsNo: 'SC202606'
  }
})
```
#### å“åº”结构
```json
{
  "code": 200,
  "msg": "操作成功",
  "data": {
    "records": [
      {
        "productionOrderId": 123,
        "npsNo": "SC20260611001",
        "endOrder": false,
        "productName": "电线电缆",
        "model": "RVV 3*2.5",
        "unit": "ç±³",
        "operationName": "绞线,绝缘,成缆",
        "productionTaskCount": 3,
        "planQuantity": 1000.00,
        "completeQuantity": 500.00,
        "goodQuantity": 500.00,
        "scrapQty": 10.00,
        "completionStatus": 50.00,
        "type": 1,
        "workOrderType": "正常",
        "workOrderNo": "GD20260611001,GD20260611002,GD20260611003",
        "planStartTime": "2026-06-10",
        "planEndTime": "2026-06-15",
        "actualStartTime": "2026-06-11",
        "actualEndTime": null,
        "status": 3
      }
    ],
    "total": 100,
    "current": 1,
    "size": 10
  }
}
```
#### å“åº”字段说明
| å­—段名 | ç±»åž‹ | è¯´æ˜Ž | å‰ç«¯å±•示建议 |
|--------|------|------|--------------|
| productionOrderId | Number | ç”Ÿäº§è®¢å•ID | ç”¨äºŽè·³è½¬è®¢å•详情 |
| npsNo | String | ç”Ÿäº§è®¢å•号 | åˆ—表主标题 |
| endOrder | Boolean | æ˜¯å¦ç»“束 | å·²ç»“束时显示"已结束"标签 |
| productName | String | äº§å“åç§° | åˆ—表副标题 |
| model | String | è§„格型号 | äº§å“è§„格展示 |
| unit | String | å•位 | æ•°é‡å•位 |
| operationName | String | å·¥åºåç§°ï¼ˆé€—号分隔) | å·¥åºåˆ—表展示 |
| productionTaskCount | Number | å·¥å•任务数量 | æ˜¾ç¤º"共X个工单" |
| planQuantity | Number | è®¡åˆ’数量(订单级) | è¿›åº¦æ¡æ€»æ•° |
| completeQuantity | Number | å®Œæˆæ•°é‡ï¼ˆè®¢å•级) | è¿›åº¦æ¡å·²å®Œæˆ |
| goodQuantity | Number | è‰¯å“æ•°é‡ | è‰¯å“ç»Ÿè®¡ |
| scrapQty | Number | æŠ¥åºŸæ•°é‡ | æŠ¥åºŸç»Ÿè®¡ï¼ˆçº¢è‰²æ ‡è®°ï¼‰ |
| completionStatus | Number | å®Œæˆè¿›åº¦ç™¾åˆ†æ¯” | è¿›åº¦æ¡æ•°å€¼ |
| type | Number | å·¥åºç±»åž‹ | 0=计时,1=计件 |
| workOrderType | String | å·¥å•类型 | "正常"或"返工返修" |
| workOrderNo | String | å·¥å•编号(逗号分隔) | ç‚¹å‡»å¯å±•开查看 |
| planStartTime | String | è®¡åˆ’开始时间 | æ ¼å¼ï¼šYYYY-MM-DD |
| planEndTime | String | è®¡åˆ’结束时间 | æ ¼å¼ï¼šYYYY-MM-DD |
| actualStartTime | String | å®žé™…开始时间 | æ ¼å¼ï¼šYYYY-MM-DD |
| actualEndTime | String | å®žé™…结束时间 | æ ¼å¼ï¼šYYYY-MM-DD |
| status | Number | çŠ¶æ€ | è§çŠ¶æ€æžšä¸¾è¡¨ |
---
### 2.2 è®¢å•工单列表
#### åŸºæœ¬ä¿¡æ¯
```
GET /productionOperationTask/byOrder/{orderId}
```
#### è¯·æ±‚参数(Path)
| å‚数名 | ç±»åž‹ | å¿…å¡« | è¯´æ˜Ž |
|--------|------|------|------|
| orderId | Number | æ˜¯ | ç”Ÿäº§è®¢å•ID |
#### è¯·æ±‚示例
```javascript
// æŸ¥è¯¢è®¢å•ID为123的所有工单
axios.get('/productionOperationTask/byOrder/123')
```
#### å“åº”结构
```json
{
  "code": 200,
  "msg": "操作成功",
  "data": [
    {
      "id": 1,
      "workOrderNo": "GD20260611001",
      "planStartTime": "2026-06-10",
      "planEndTime": "2026-06-12",
      "actualStartTime": "2026-06-11",
      "actualEndTime": null,
      "status": 3,
      "productionOrderId": 123,
      "planQuantity": 500.00,
      "completeQuantity": 300.00,
      "npsNo": "SC20260611001",
      "endOrder": false,
      "productName": "电线电缆",
      "model": "RVV 3*2.5",
      "unit": "ç±³",
      "operationName": "绞线",
      "type": 1,
      "workOrderType": "正常",
      "scrapQty": 5.00,
      "completionStatus": 60.00,
      "userNames": "张三,李四"
    },
    {
      "id": 2,
      "workOrderNo": "FG20260611001",
      "planStartTime": "2026-06-12",
      "planEndTime": "2026-06-15",
      "actualStartTime": "2026-06-13",
      "actualEndTime": null,
      "status": 3,
      "productionOrderId": 123,
      "planQuantity": 100.00,
      "completeQuantity": 50.00,
      "npsNo": "SC20260611001",
      "endOrder": false,
      "productName": "电线电缆",
      "model": "RVV 3*2.5",
      "unit": "ç±³",
      "operationName": "绝缘",
      "type": 0,
      "workOrderType": "返工返修",
      "scrapQty": 2.00,
      "completionStatus": 50.00,
      "userNames": "王五"
    }
  ]
}
```
#### å“åº”字段说明
| å­—段名 | ç±»åž‹ | è¯´æ˜Ž | å‰ç«¯å±•示建议 |
|--------|------|------|--------------|
| id | Number | å·¥å•ID | ç”¨äºŽæŠ¥å·¥ã€è¯¦æƒ…跳转 |
| workOrderNo | String | å·¥å•编号 | åˆ—表主标题 |
| planStartTime | String | è®¡åˆ’开始时间 | æ—¶é—´èŒƒå›´å±•示 |
| planEndTime | String | è®¡åˆ’结束时间 | æ—¶é—´èŒƒå›´å±•示 |
| actualStartTime | String | å®žé™…开始时间 | å®žé™…时间展示 |
| actualEndTime | String | å®žé™…结束时间 | å®žé™…时间展示 |
| status | Number | å·¥å•状态 | çŠ¶æ€æ ‡ç­¾ |
| productionOrderId | Number | ç”Ÿäº§è®¢å•ID | å…³è”订单 |
| planQuantity | Number | è®¡åˆ’数量(工单级) | è¿›åº¦æ¡æ€»æ•° |
| completeQuantity | Number | å®Œæˆæ•°é‡ï¼ˆå·¥å•级) | è¿›åº¦æ¡å·²å®Œæˆ |
| npsNo | String | ç”Ÿäº§è®¢å•号 | è®¢å•号展示 |
| endOrder | Boolean | æ˜¯å¦ç»“束 | ç»“束标签 |
| productName | String | äº§å“åç§° | äº§å“ä¿¡æ¯ |
| model | String | è§„格型号 | è§„格展示 |
| unit | String | å•位 | æ•°é‡å•位 |
| operationName | String | å·¥åºåç§° | å•个工序 |
| type | Number | å·¥åºç±»åž‹ | 0=计时,1=计件 |
| workOrderType | String | å·¥å•类型 | è¿”工返修用红色标记 |
| scrapQty | Number | æŠ¥åºŸæ•°é‡ | æŠ¥åºŸç»Ÿè®¡ |
| completionStatus | Number | å®Œæˆè¿›åº¦ç™¾åˆ†æ¯” | è¿›åº¦æ¡æ•°å€¼ |
| userNames | String | æŠ¥å·¥äººå‘˜ï¼ˆé€—号分隔) | äººå‘˜åˆ—表 |
---
## ä¸‰ã€æžšä¸¾å€¼æ˜ å°„
### 3.1 å·¥å•状态
| çŠ¶æ€å€¼ | æ–‡æ¡ˆ | æ ‡ç­¾é¢œè‰²å»ºè®® |
|--------|------|--------------|
| 1 | å¾…确认 | ç°è‰²/默认 |
| 2 | å¾…生产 | è“è‰²/info |
| 3 | ç”Ÿäº§ä¸­ | ç»¿è‰²/success |
| 4 | å·²ç”Ÿäº§ | æ·±ç»¿è‰²/success-dark |
#### å‰ç«¯çŠ¶æ€æ˜ å°„ç¤ºä¾‹
```javascript
const statusMap = {
  1: { text: '待确认', color: 'default' },
  2: { text: '待生产', color: 'info' },
  3: { text: '生产中', color: 'success' },
  4: { text: '已生产', color: 'success-dark' }
}
function getStatusTag(status) {
  return statusMap[status] || { text: '未知', color: 'default' }
}
```
### 3.2 å·¥åºç±»åž‹
| ç±»åž‹å€¼ | æ–‡æ¡ˆ | è¯´æ˜Ž |
|--------|------|------|
| 0 | è®¡æ—¶ | æŒ‰å·¥æ—¶è®¡ç®—工资 |
| 1 | è®¡ä»¶ | æŒ‰äº§é‡è®¡ç®—工资 |
```javascript
const typeMap = {
  0: '计时',
  1: '计件'
}
```
### 3.3 å·¥å•类型判断
| ç±»åž‹ | åˆ¤æ–­è§„则 | æ ‡ç­¾é¢œè‰²å»ºè®® |
|------|----------|--------------|
| æ­£å¸¸ | workOrderNo ä¸ä»¥ 'FG' å¼€å¤´ | é»˜è®¤è‰² |
| è¿”工返修 | workOrderNo ä»¥ 'FG' å¼€å¤´ | çº¢è‰²/error |
```javascript
function getWorkOrderType(workOrderNo) {
  return workOrderNo?.startsWith('FG') ? '返工返修' : '正常'
}
```
---
## å››ã€é¡µé¢è®¾è®¡å»ºè®®
### 4.1 å·¥å•列表页(使用 /page æŽ¥å£ï¼‰
#### è¡¨æ ¼åˆ—设计
| åˆ—名 | å­—段 | å®½åº¦ | è¯´æ˜Ž |
|------|------|------|------|
| ç”Ÿäº§è®¢å•号 | npsNo | 150px | å¯ç‚¹å‡»è·³è½¬è¯¦æƒ… |
| äº§å“åç§° | productName | 120px | - |
| è§„格型号 | model | 150px | - |
| å·¥åº | operationName | 200px | å¤šä¸ªå·¥åºé€—号分隔 |
| å·¥å•类型 | workOrderType | 100px | æ­£å¸¸/返工返修 |
| è®¡åˆ’数量 | planQuantity | 100px | è®¢å•级别 |
| å®Œæˆæ•°é‡ | completeQuantity | 100px | è®¢å•级别 |
| å®Œæˆè¿›åº¦ | completionStatus | 120px | è¿›åº¦æ¡+百分比 |
| æŠ¥åºŸæ•°é‡ | scrapQty | 80px | çº¢è‰²æ ‡è®° |
| å·¥å•æ•° | productionTaskCount | 80px | å…±X个工单 |
| çŠ¶æ€ | status | 80px | çŠ¶æ€æ ‡ç­¾ |
| æ“ä½œ | - | 150px | æŸ¥çœ‹è¯¦æƒ…、报工等 |
#### è¿›åº¦æ¡å±•示
```javascript
// ä½¿ç”¨è¿›åº¦æ¡ç»„ä»¶
<Progress
  percent={item.completionStatus}
  strokeColor="#52c41a"
  format={percent => `${percent}%`}
/>
```
### 4.2 è®¢å•详情页工单列表(使用 /byOrder/{orderId} æŽ¥å£ï¼‰
#### è¡¨æ ¼åˆ—设计
| åˆ—名 | å­—段 | å®½åº¦ | è¯´æ˜Ž |
|------|------|------|------|
| å·¥å•编号 | workOrderNo | 150px | ä¸»æ ‡è¯† |
| å·¥åºåç§° | operationName | 120px | å•个工序 |
| å·¥å•类型 | workOrderType | 100px | æ­£å¸¸/返工返修 |
| å·¥åºç±»åž‹ | type | 80px | è®¡æ—¶/计件 |
| è®¡åˆ’数量 | planQuantity | 100px | å·¥å•级别 |
| å®Œæˆæ•°é‡ | completeQuantity | 100px | å·¥å•级别 |
| å®Œæˆè¿›åº¦ | completionStatus | 120px | è¿›åº¦æ¡ |
| æŠ¥åºŸæ•°é‡ | scrapQty | 80px | çº¢è‰²æ ‡è®° |
| æŠ¥å·¥äººå‘˜ | userNames | 150px | å¤šäººé€—号分隔 |
| è®¡åˆ’æ—¶é—´ | planStartTime ~ planEndTime | 180px | æ—¶é—´èŒƒå›´ |
| å®žé™…æ—¶é—´ | actualStartTime ~ actualEndTime | 180px | æ—¶é—´èŒƒå›´ |
| çŠ¶æ€ | status | 80px | çŠ¶æ€æ ‡ç­¾ |
| æ“ä½œ | - | 150px | æŠ¥å·¥ã€æµè½¬å¡ç­‰ |
---
## äº”、两个接口数据差异对比
| å¯¹æ¯”项 | /page æŽ¥å£ | /byOrder/{orderId} æŽ¥å£ |
|--------|------------|-------------------------|
| æ•°æ®ç»´åº¦ | è®¢å•汇总 | å·¥å•明细 |
| planQuantity | è®¢å•计划数 | å·¥å•计划数 |
| completeQuantity | è®¢å•完成数 | å·¥å•完成数 |
| completionStatus | è®¢å•进度 | å·¥å•进度 |
| operationName | å¤šå·¥åºèšåˆ | å•个工序 |
| workOrderNo | å¤šå·¥å•聚合 | å•个工单 |
| userNames | æ—  | æœ‰æŠ¥å·¥äººå‘˜ |
| ç”¨é€” | åˆ—表汇总页 | è¯¦æƒ…明细页 |
---
## å…­ã€å‰ç«¯è°ƒç”¨æ³¨æ„äº‹é¡¹
### 6.1 åˆ†é¡µå‚æ•°
- `/page` æŽ¥å£æ”¯æŒåˆ†é¡µï¼Œéœ€ä¼  `current` å’Œ `size`
- `/byOrder/{orderId}` æŽ¥å£ä¸åˆ†é¡µï¼Œè¿”回全部工单列表
### 6.2 æ•°é‡å­—段精度
- æ‰€æœ‰æ•°é‡å­—段为 `BigDecimal` ç±»åž‹ï¼Œå‰ç«¯å±•示时保留2位小数
- å®Œæˆè¿›åº¦ `completionStatus` å·²ä¸ºç™¾åˆ†æ¯”数值(如50.00表示50%)
### 6.3 æ—¶é—´å­—段格式
- æ‰€æœ‰æ—¶é—´å­—段为 `LocalDate` æ ¼å¼ï¼š`YYYY-MM-DD`
- æ— æ—¶é—´å€¼æ—¶è¿”回 `null`,前端需做空值处理
### 6.4 å·¥å•类型判断
- æŽ¥å£å·²è¿”回 `workOrderType` å­—段,无需前端判断
- è¿”工返修工单建议使用红色标签突出显示
### 6.5 æŠ¥å·¥äººå‘˜å­—段
- `userNames` ä¸ºé€—号分隔字符串,可能为空
- å¤šäººæ—¶å»ºè®®ä½¿ç”¨æ ‡ç­¾æˆ–头像列表展示
---
## ä¸ƒã€å¸¸è§é—®é¢˜å¤„理
### 7.1 è¿›åº¦ä¸ºç©ºæˆ–除零
- `completionStatus` å·²åœ¨SQL中处理除零情况
- è‹¥è®¡åˆ’数量为0,进度显示为0
### 7.2 å·¥å•号为空
- `workOrderNo` ä¸ºå¿…填字段,不会为空
- è‹¥èšåˆå±•示时多个工单号用逗号分隔
### 7.3 æŠ¥åºŸæ•°é‡å±•示
- `scrapQty` ä¸ºç´¯è®¡æŠ¥åºŸæ•°é‡
- å»ºè®®ä½¿ç”¨çº¢è‰²å­—体或警告图标标识
---
## å…«ã€Mock数据示例
### 8.1 /page æŽ¥å£Mock
```json
{
  "code": 200,
  "data": {
    "records": [
      {
        "productionOrderId": 1,
        "npsNo": "SC20260611001",
        "endOrder": false,
        "productName": "电线电缆",
        "model": "RVV 3*2.5",
        "unit": "ç±³",
        "operationName": "绞线,绝缘,成缆",
        "productionTaskCount": 3,
        "planQuantity": 1000.00,
        "completeQuantity": 500.00,
        "goodQuantity": 500.00,
        "scrapQty": 10.00,
        "completionStatus": 50.00,
        "type": 1,
        "workOrderType": "正常",
        "workOrderNo": "GD20260611001,GD20260611002,GD20260611003",
        "planStartTime": "2026-06-10",
        "planEndTime": "2026-06-15",
        "actualStartTime": "2026-06-11",
        "actualEndTime": null,
        "status": 3
      }
    ],
    "total": 1,
    "current": 1,
    "size": 10
  }
}
```
### 8.2 /byOrder/{orderId} æŽ¥å£Mock
```json
{
  "code": 200,
  "data": [
    {
      "id": 1,
      "workOrderNo": "GD20260611001",
      "planStartTime": "2026-06-10",
      "planEndTime": "2026-06-12",
      "actualStartTime": "2026-06-11",
      "actualEndTime": null,
      "status": 3,
      "productionOrderId": 1,
      "planQuantity": 500.00,
      "completeQuantity": 300.00,
      "npsNo": "SC20260611001",
      "endOrder": false,
      "productName": "电线电缆",
      "model": "RVV 3*2.5",
      "unit": "ç±³",
      "operationName": "绞线",
      "type": 1,
      "workOrderType": "正常",
      "scrapQty": 5.00,
      "completionStatus": 60.00,
      "userNames": "张三,李四"
    },
    {
      "id": 2,
      "workOrderNo": "FG20260611001",
      "planStartTime": "2026-06-12",
      "planEndTime": "2026-06-15",
      "actualStartTime": "2026-06-13",
      "actualEndTime": null,
      "status": 3,
      "productionOrderId": 1,
      "planQuantity": 100.00,
      "completeQuantity": 50.00,
      "npsNo": "SC20260611001",
      "endOrder": false,
      "productName": "电线电缆",
      "model": "RVV 3*2.5",
      "unit": "ç±³",
      "operationName": "绝缘",
      "type": 0,
      "workOrderType": "返工返修",
      "scrapQty": 2.00,
      "completionStatus": 50.00,
      "userNames": "王五"
    }
  ]
}
```
src/api/productionManagement/workOrder.js
@@ -95,3 +95,11 @@
    params: query,
  });
}
// æ ¹æ®è®¢å•ID获取工单明细列表
export function getWorkOrderListByOrderId(orderId) {
  return request({
    url: `/productionOperationTask/byOrder/${orderId}`,
    method: "get",
  });
}
src/views/productionManagement/workOrderManagement/index.vue
@@ -3,15 +3,6 @@
    <div class="search_form mb20">
      <div class="search-row">
        <div class="search-item">
          <span class="search_title">工单编号:</span>
          <el-input v-model="searchForm.workOrderNo"
                    style="width: 240px"
                    placeholder="请输入"
                    @change="handleQuery"
                    clearable
                    prefix-icon="Search" />
        </div>
        <div class="search-item">
          <span class="search_title">生产订单号:</span>
          <el-input v-model="searchForm.npsNo"
                    style="width: 240px"
@@ -27,18 +18,199 @@
      </div>
    </div>
    <div class="table_list">
      <PIMTable rowKey="id"
                :column="tableColumn"
                :tableData="tableData"
                :page="page"
                :tableLoading="tableLoading"
                @pagination="pagination">
        <template #completionStatus="{ row }">
          <el-progress :percentage="toProgressPercentage(row?.completionStatus)"
                       :color="progressColor(toProgressPercentage(row?.completionStatus))"
                       :status="toProgressPercentage(row?.completionStatus) >= 100 ? 'success' : ''" />
        </template>
      </PIMTable>
      <el-table :data="tableData"
                border
                v-loading="tableLoading"
                :expand-row-keys="expandedRowKeys"
                :row-key="(row) => row.productionOrderId"
                @expand-change="expandChange"
                height="calc(100vh - 22em)">
        <!-- å±•开行列 -->
        <el-table-column type="expand"
                         width="60"
                         fixed="left">
          <template #default="props">
            <el-table :data="props.row.children || []"
                      border
                      :row-class-name="({ row }) => getChildRowClassName(row)"
                      v-loading="childLoading[props.row.productionOrderId]"
                      style="margin: 10px 0;">
              <el-table-column align="center"
                               label="序号"
                               type="index"
                               width="60" />
              <el-table-column label="工单编号"
                               prop="workOrderNo"
                               width="140">
                <template #default="scope">
                  <span :class="{ 'rework-text': scope.row.workOrderNo?.startsWith('FG') }">
                    {{ scope.row.workOrderNo }}
                  </span>
                </template>
              </el-table-column>
              <el-table-column label="工序名称"
                               prop="operationName"
                               width="100" />
              <el-table-column label="工序类型"
                               width="80"
                               align="center">
                <template #default="scope">
                  <span>{{ scope.row.type === 0 ? '计时' : '计件' }}</span>
                </template>
              </el-table-column>
              <el-table-column label="计划数量"
                               prop="planQuantity"
                               width="100" />
              <el-table-column label="完成数量"
                               prop="completeQuantity"
                               width="100" />
              <el-table-column label="完成进度"
                               width="140">
                <template #default="scope">
                  <el-progress :percentage="toProgressPercentage(scope.row?.completionStatus)"
                               :color="progressColor(toProgressPercentage(scope.row?.completionStatus))"
                               :status="toProgressPercentage(scope.row?.completionStatus) >= 100 ? 'success' : ''" />
                </template>
              </el-table-column>
              <el-table-column label="报废数量"
                               prop="scrapQty"
                               width="80">
                <template #default="scope">
                  <span :class="{ 'scrap-text': scope.row.scrapQty > 0 }">
                    {{ scope.row.scrapQty || 0 }}
                  </span>
                </template>
              </el-table-column>
              <el-table-column label="报工人员"
                               prop="userNames"
                               width="150"
                               show-overflow-tooltip />
              <el-table-column label="计划开始时间"
                               prop="planStartTime"
                               width="120" />
              <el-table-column label="计划结束时间"
                               prop="planEndTime"
                               width="120" />
              <el-table-column label="实际开始时间"
                               prop="actualStartTime"
                               width="120" />
              <el-table-column label="实际结束时间"
                               prop="actualEndTime"
                               width="120" />
              <el-table-column label="状态"
                               width="100"
                               align="center">
                <template #default="scope">
                  <el-tag :type="getStatusTagType(scope.row.status)">
                    {{ getStatusText(scope.row.status) }}
                  </el-tag>
                </template>
              </el-table-column>
              <el-table-column label="操作"
                               width="200"
                               align="center"
                               fixed="right">
                <template #default="scope">
                  <el-button link
                             type="primary"
                             @click="downloadAndPrintWorkOrder(scope.row)">
                    æµè½¬å¡
                  </el-button>
                  <el-button link
                             type="primary"
                             @click="openWorkOrderFiles(scope.row)">
                    é™„ä»¶
                  </el-button>
                  <el-button link
                             type="primary"
                             v-if="!scope.row.endOrder"
                             :disabled="isReportDisabled(scope.row)"
                             @click="showReportDialog(scope.row)">
                    æŠ¥å·¥
                  </el-button>
                </template>
              </el-table-column>
            </el-table>
          </template>
        </el-table-column>
        <!-- ä¸»è¡¨åˆ— -->
        <el-table-column align="center"
                         label="序号"
                         type="index"
                         width="60" />
        <el-table-column label="生产订单号"
                         prop="npsNo"
                         width="150" />
        <el-table-column label="产品名称"
                         prop="productName"
                         width="140" />
        <el-table-column label="规格"
                         prop="model"
                         width="150" />
        <el-table-column label="单位"
                         prop="unit"
                         width="80" />
        <el-table-column label="工序名称"
                         prop="operationName"
                         width="200"
                         show-overflow-tooltip />
        <el-table-column label="工单数量"
                         prop="productionTaskCount"
                         width="80">
          <template #default="scope">
            <span>共{{ scope.row.productionTaskCount || 0 }}个</span>
          </template>
        </el-table-column>
        <el-table-column label="计划数量"
                         prop="planQuantity"
                         width="100" />
        <el-table-column label="完成数量"
                         prop="completeQuantity"
                         width="100" />
        <el-table-column label="完成进度"
                         width="140">
          <template #default="scope">
            <el-progress :percentage="toProgressPercentage(scope.row?.completionStatus)"
                         :color="progressColor(toProgressPercentage(scope.row?.completionStatus))"
                         :status="toProgressPercentage(scope.row?.completionStatus) >= 100 ? 'success' : ''" />
          </template>
        </el-table-column>
        <el-table-column label="报废数量"
                         prop="scrapQty"
                         width="80">
          <template #default="scope">
            <span :class="{ 'scrap-text': scope.row.scrapQty > 0 }">
              {{ scope.row.scrapQty || 0 }}
            </span>
          </template>
        </el-table-column>
        <el-table-column label="状态"
                         width="100"
                         align="center">
          <template #default="scope">
            <el-tag :type="getStatusTagType(scope.row.status)">
              {{ getStatusText(scope.row.status) }}
            </el-tag>
          </template>
        </el-table-column>
        <el-table-column label="计划开始时间"
                         prop="planStartTime"
                         width="120" />
        <el-table-column label="计划结束时间"
                         prop="planEndTime"
                         width="120" />
        <el-table-column label="实际开始时间"
                         prop="actualStartTime"
                         width="120" />
        <el-table-column label="实际结束时间"
                         prop="actualEndTime"
                         width="120" />
      </el-table>
      <PaginationComp v-show="page.total > 0"
                  :total="page.total"
                  :page="page.current"
                  :limit="page.size"
                  @pagination="pagination" />
    </div>
    <!-- æµè½¬å¡å¼¹çª— -->
    <el-dialog v-model="transferCardVisible"
@@ -262,11 +434,12 @@
</template>
<script setup>
  import { onMounted, ref, nextTick } from "vue";
  import { onMounted, ref, nextTick, reactive, toRefs, getCurrentInstance } from "vue";
  import { ElMessageBox } from "element-plus";
  import dayjs from "dayjs";
  import {
    productWorkOrderPage,
    getWorkOrderListByOrderId,
    addProductMain,
    downProductWorkOrder,
  } from "@/api/productionManagement/workOrder.js";
@@ -275,7 +448,7 @@
  import { getUserProfile, userListNoPageByTenantId } from "@/api/system/user.js";
  import { getDicts } from "@/api/system/dict/data";
  import QRCode from "qrcode";
  import { getCurrentInstance, reactive, toRefs } from "vue";
  import PaginationComp from "@/components/PIMTable/Pagination.vue";
  import MaterialDialog from "./components/MaterialDialog.vue";
  const FileList = defineAsyncComponent(() =>
    import("@/components/Dialog/FileList.vue")
@@ -285,130 +458,31 @@
  const { proxy } = getCurrentInstance();
  const userStore = useUserStore();
  const tableColumn = ref([
    {
      label: "工单类型",
      prop: "workOrderType",
      width: "80",
    },
    {
      label: "工单编号",
      prop: "workOrderNo",
      width: "140",
    },
    {
      label: "生产订单号",
      prop: "npsNo",
      width: "140",
    },
    {
      label: "产品名称",
      prop: "productName",
      width: "140",
    },
    {
      label: "规格",
      prop: "model",
    },
    {
      label: "单位",
      prop: "unit",
    },
    {
      label: "工序名称",
      prop: "operationName",
      width: "100",
    },
    {
      label: "需求数量",
      prop: "planQuantity",
      width: "140",
    },
    {
      label: "完成数量",
      prop: "completeQuantity",
      width: "140",
    },
    {
      label: "完成进度",
      prop: "completionStatus",
      dataType: "slot",
      slot: "completionStatus",
      width: "140",
    },
    {
      label: "计划开始时间",
      prop: "planStartTime",
      width: "140",
    },
    {
      label: "计划结束时间",
      prop: "planEndTime",
      width: "140",
    },
    {
      label: "实际开始时间",
      prop: "actualStartTime",
      width: "140",
    },
    {
      label: "实际结束时间",
      prop: "actualEndTime",
      width: "140",
    },
    {
      label: "操作",
      width: "260",
      align: "center",
      dataType: "action",
      fixed: "right",
      operation: [
        {
          name: "流转卡",
          clickFun: row => {
            downloadAndPrintWorkOrder(row);
          },
        },
        {
          name: "附件",
          clickFun: row => {
            openWorkOrderFiles(row);
          },
        },
        // {
        //   name: "物料",
        //   clickFun: row => {
        //     openMaterialDialog(row);
        //   },
        // },
        {
          name: "报工",
          clickFun: row => {
            showReportDialog(row);
          },
          showHide: row => !row.endOrder,
          disabled: row => {
            if (row.planQuantity <= 0) return true;
            if (!row.userIds) return false;
            try {
              const userIds =
                typeof row.userIds === "string"
                  ? JSON.parse(row.userIds)
                  : row.userIds;
              if (Array.isArray(userIds)) {
                return !userIds.some(id => String(id) === String(userStore.id));
              }
              return true;
            } catch (e) {
              return true;
            }
          },
        },
      ],
    },
  ]);
  // çŠ¶æ€æžšä¸¾æ˜ å°„
  const statusMap = {
    1: '待确认',
    2: '待生产',
    3: '生产中',
    4: '已生产',
  };
  const getStatusText = (status) => statusMap[status] || '未知';
  const getStatusTagType = (status) => {
    switch (status) {
      case 1: return 'info';
      case 2: return 'warning';
      case 3: return 'primary';
      case 4: return 'success';
      default: return 'info';
    }
  };
  const tableData = ref([]);
  const tableLoading = ref(false);
  const expandedRowKeys = ref([]);
  const childLoading = ref({});
  const transferCardVisible = ref(false);
  const transferCardData = ref([]);
  const transferCardQrUrl = ref("");
@@ -434,7 +508,7 @@
    paramGroups: {},
  });
  const params = ref({});
  const params = ref([]);
  const dictOptions = ref({});
  const paramLoading = ref(false);
@@ -445,7 +519,6 @@
      return;
    }
    const num = Number(value);
    // æ•´æ•°ä¸”大于等于1
    if (isNaN(num) || !Number.isInteger(num) || num < 0) {
      callback(new Error("生产合格数量必须大于等于0"));
      return;
@@ -460,7 +533,6 @@
      return;
    }
    const num = Number(value);
    // æ•´æ•°ä¸”大于等于0
    if (isNaN(num) || !Number.isInteger(num) || num < 0) {
      callback(new Error("报废数量必须大于等于0"));
      return;
@@ -474,7 +546,7 @@
    scrapQty: [{ validator: validateScrapQty, trigger: "blur" }],
  };
  // å¤„理生产合格数量输入,限制必须大于等于0
  // å¤„理生产合格数量输入
  const handleQuantityInput = value => {
    if (value === "" || value === null || value === undefined) {
      reportForm.quantity = null;
@@ -484,15 +556,12 @@
    if (isNaN(num)) {
      return;
    }
    // å¦‚果小于1,清除
    if (num < 0) {
      reportForm.quantity = null;
      return;
    }
    // å¦‚果是小数取整数部分
    if (!Number.isInteger(num)) {
      const intValue = Math.floor(num);
      // å¦‚果取整后小于1,清除
      if (intValue < 0) {
        reportForm.quantity = null;
        return;
@@ -510,21 +579,17 @@
      return;
    }
    const num = Number(value);
    // å¦‚果是NaN,保持原值
    if (isNaN(num)) {
      return;
    }
    // å¦‚果是负数,清除输入
    if (num < 0) {
      reportForm.scrapQty = null;
      return;
    }
    // å¦‚果是小数,取整数部分
    if (!Number.isInteger(num)) {
      reportForm.scrapQty = Math.floor(num);
      return;
    }
    // æœ‰æ•ˆçš„非负整数(包括0)
    reportForm.scrapQty = num;
  };
@@ -539,11 +604,11 @@
  const data = reactive({
    searchForm: {
      workOrderNo: "",
      npsNo: "",
    },
  });
  const { searchForm } = toRefs(data);
  const toProgressPercentage = val => {
    const n = Number(val);
    if (!Number.isFinite(n)) return 0;
@@ -551,6 +616,7 @@
    if (n >= 100) return 100;
    return Math.round(n);
  };
  const progressColor = percentage => {
    const p = toProgressPercentage(percentage);
    if (p < 30) return "#f56c6c";
@@ -560,7 +626,6 @@
  };
  // æŸ¥è¯¢åˆ—表
  /** æœç´¢æŒ‰é’®æ“ä½œ */
  const handleQuery = () => {
    page.current = 1;
    getList();
@@ -574,11 +639,15 @@
  const getList = () => {
    tableLoading.value = true;
    expandedRowKeys.value = [];
    const params = { ...searchForm.value, ...page };
    productWorkOrderPage(params)
      .then(res => {
        tableLoading.value = false;
        tableData.value = res.data.records;
        tableData.value = res.data.records.map(item => ({
          ...item,
          children: [],
        }));
        page.total = res.data.total;
      })
      .catch(() => {
@@ -586,7 +655,67 @@
      });
  };
  // ä¸‹è½½å¹¶æ‰“印工单流转卡(文件流)
  // å±•开行懒加载
  const expandChange = (row, expandedRows) => {
    if (expandedRows.length > 0) {
      expandedRowKeys.value = [];
      const orderId = row.productionOrderId;
      // æ£€æŸ¥æ˜¯å¦å·²æœ‰æ•°æ®
      if (row.children && row.children.length > 0) {
        expandedRowKeys.value.push(orderId);
        return;
      }
      childLoading.value[orderId] = true;
      getWorkOrderListByOrderId(orderId)
        .then(res => {
          childLoading.value[orderId] = false;
          const index = tableData.value.findIndex(
            item => item.productionOrderId === orderId
          );
          if (index > -1) {
            tableData.value[index].children = res.data || [];
          }
          expandedRowKeys.value.push(orderId);
        })
        .catch(error => {
          childLoading.value[orderId] = false;
          console.error('加载工单明细失败:', error);
        });
    } else {
      expandedRowKeys.value = [];
    }
  };
  // å­è¡¨è¡Œæ ·å¼ï¼ˆè¿”工返修红色标记)
  const getChildRowClassName = (row) => {
    if (row.workOrderNo?.startsWith('FG')) {
      return 'row-rework';
    }
    return '';
  };
  // æŠ¥å·¥æŒ‰é’®ç¦ç”¨é€»è¾‘
  const isReportDisabled = (row) => {
    if (row.planQuantity <= 0) return true;
    if (!row.userIds) return false;
    try {
      const userIds =
        typeof row.userIds === "string"
          ? JSON.parse(row.userIds)
          : row.userIds;
      if (Array.isArray(userIds)) {
        return !userIds.some(id => String(id) === String(userStore.id));
      }
      return true;
    } catch (e) {
      return true;
    }
  };
  // ä¸‹è½½å¹¶æ‰“印工单流转卡
  const downloadAndPrintWorkOrder = async row => {
    if (!row || !row.id) {
      proxy.$modal.msgError("缺少工单ID,无法下载流转卡");
@@ -596,7 +725,6 @@
      ? `工单流转卡_${row.workOrderNo}.xlsx`
      : "工单流转卡.xlsx";
    try {
      // è°ƒç”¨æŽ¥å£ï¼Œä»¥ responseType: 'blob' èŽ·å–æ–‡ä»¶æµ
      const blob = await downProductWorkOrder(row.id);
      if (!blob) {
@@ -604,14 +732,12 @@
        return;
      }
      // åˆ›å»º Blob URL
      const fileBlob =
        blob instanceof Blob
          ? blob
          : new Blob([blob], { type: blob.type || "application/octet-stream" });
      const url = window.URL.createObjectURL(fileBlob);
      // åˆ›å»ºéšè— iframe,用于触发浏览器打印
      const iframe = document.createElement("iframe");
      iframe.style.position = "fixed";
      iframe.style.right = "0";
@@ -628,7 +754,6 @@
          iframe.contentWindow?.print();
        } catch (e) {
          console.error("自动调用打印失败", e);
          // é€€è€Œæ±‚其次,打开新窗口由用户手动打印
          window.open(url);
        }
      };
@@ -731,7 +856,6 @@
        return;
      }
      // éªŒè¯ç”Ÿäº§åˆæ ¼æ•°é‡
      if (
        reportForm.quantity === null ||
        reportForm.quantity === undefined ||
@@ -752,14 +876,6 @@
        return;
      }
      // if (quantity > reportForm.planQuantity) {
      //   ElMessageBox.alert("本次生产数量不能超过待生产数量", "提示", {
      //     confirmButtonText: "确定",
      //   });
      //   return;
      // }
      // éªŒè¯æŠ¥åºŸæ•°é‡
      const scrapQty = Number(reportForm.scrapQty);
      if (!isNaN(scrapQty) && scrapQty < 0) {
        ElMessageBox.alert("报废数量不能小于0", "提示", {
@@ -767,13 +883,6 @@
        });
        return;
      }
      // if (!isNaN(scrapQty) && scrapQty > quantity) {
      //   ElMessageBox.alert("报废数量不能大于本次生产数量", "提示", {
      //     confirmButtonText: "确定",
      //   });
      //   return;
      // }
      const productionOperationParamList = params.value.map(param => ({
        ...param,
@@ -863,7 +972,6 @@
  onMounted(() => {
    userStore.getInfo();
    getList();
    // èŽ·å–ç”¨æˆ·åˆ—è¡¨
    userListNoPageByTenantId().then(res => {
      if (res.code === 200) {
        userOptions.value = res.data;
@@ -955,6 +1063,31 @@
    font-size: 12px;
    min-width: 30px;
  }
  // è¿”工返修工单红色标记
  .row-rework {
    background-color: #fef0f0 !important;
    :deep(.el-table__cell) {
      background-color: #fef0f0 !important;
    }
  }
  .rework-text {
    color: #f56c6c;
    font-weight: bold;
  }
  // æŠ¥åºŸæ•°é‡çº¢è‰²æ ‡è®°
  .scrap-text {
    color: #f56c6c;
  }
  // å­è¡¨æ ¼æ ·å¼
  :deep(.el-table__expanded-cell) {
    padding: 10px 30px !important;
    background-color: #fafafa;
  }
</style>
<style  lang="scss">
vite.config.js
@@ -8,7 +8,7 @@
  const { VITE_APP_ENV } = env;
  const baseUrl =
      env.VITE_APP_ENV === "development"
          ? "http://1.15.17.182:9048"
          ? "http://localhost:7005"
          : env.VITE_BASE_API;
  const javaUrl =
      env.VITE_APP_ENV === "development"