Commit 415edb68 by 李宁

1

1 parent 8d4d8b90
...@@ -5,7 +5,7 @@ ...@@ -5,7 +5,7 @@
| Method | URL | Description | Request Parameters (Key) | | Method | URL | Description | Request Parameters (Key) |
| :--- | :--- | :--- | :--- | | :--- | :--- | :--- | :--- |
| POST | `/zhijian/opt/unCheckByOpt` | 淮安工维质检-无法质检提交 | `[applyId, uncheckReason]` | | POST | `/zhijian/opt/unCheckByOpt` | 淮安工维质检-无法质检提交 | `[applyId, checkStatus, failReason]` |
| POST | `/zhijian/opt/setRtcType` | RTC类型切换 - 声网/LiveKit | `[type]` | | POST | `/zhijian/opt/setRtcType` | RTC类型切换 - 声网/LiveKit | `[type]` |
| POST | `/zhijian/opt/queryVideoList` | 查询视频列表 | `[callId, applyId]` | | POST | `/zhijian/opt/queryVideoList` | 查询视频列表 | `[callId, applyId]` |
| POST | `/zhijian/opt/getSNStatisticsSummary` | 获取串号统计汇总数据 | `[startDate, endDate, province, areaType]` | | POST | `/zhijian/opt/getSNStatisticsSummary` | 获取串号统计汇总数据 | `[startDate, endDate, province, areaType]` |
...@@ -31,7 +31,7 @@ ...@@ -31,7 +31,7 @@
| POST | `/zhijian/applyInfoDetail/qualityCheckList` | 质检工单列表查询(分页) | `[pageNum, pageSize, accNbr, applyId, orderId, campaignId, areaType, checkStatus, startTime, endTime, noShowNumType, manualInputNumType, cheatNumType, isCheat, sortField, sortOrder]` | | POST | `/zhijian/applyInfoDetail/qualityCheckList` | 质检工单列表查询(分页) | `[pageNum, pageSize, accNbr, applyId, orderId, campaignId, areaType, checkStatus, startTime, endTime, noShowNumType, manualInputNumType, cheatNumType, isCheat, sortField, sortOrder]` |
| POST | `/zhijian/applyInfoDetail/markCheat` | 标记/取消标记作弊 | `[applyId, isCheat]` | | POST | `/zhijian/applyInfoDetail/markCheat` | 标记/取消标记作弊 | `[applyId, isCheat]` |
| POST | `/zhijian/applyInfoDetail/getProcessDetailList` | 查询流程明细列表(无法拍摄/手动输入/环境异常) | `[applyId]` | | POST | `/zhijian/applyInfoDetail/getProcessDetailList` | 查询流程明细列表(无法拍摄/手动输入/环境异常) | `[applyId]` |
| POST | `/zhijian/applyInfoDetail/getDevicesByApplyId` | 根据applyId查询工单设备列表 | `[applyId]` | | POST | `/zhijian/applyInfoDetail/getDevicesByApplyId` | 根据applyId查询工单设备列表 | `[applyId,markType(1-标记作弊 2-取消标记作弊),cause(异常原因),causeOther(备注)]` |
| POST | `/zhijian/applyInfoDetail/getCheatMarkDetail` | 查看作弊标记详情 | `[applyId]` | | POST | `/zhijian/applyInfoDetail/getCheatMarkDetail` | 查看作弊标记详情 | `[applyId]` |
| POST | `/zhijian/applyInfoDetail/exportQualityCheckList` | 导出质检工单列表到Excel | - | | POST | `/zhijian/applyInfoDetail/exportQualityCheckList` | 导出质检工单列表到Excel | - |
| POST | `/zhijian/applyInfoDetail/exportLLMResultList` | 导出LLM结果列表到Excel | - | | POST | `/zhijian/applyInfoDetail/exportLLMResultList` | 导出LLM结果列表到Excel | - |
......
# OrderList.vue 接口集成完成总结
## 完成时间
2026-01-23
## 修改内容
### 1. API 配置更新 (`src/api/index.ts`)
#### 修正的接口路径
- ✅ 质检工单列表查询: `/zhijian/applyInfoDetail/qualityCheckList`
- ✅ 查询流程明细列表: `/zhijian/applyInfoDetail/getProcessDetailList`
#### 新增的接口
- ✅ 标记/取消作弊: `markCheat()`
- ✅ 查看作弊标记详情: `getCheatMarkDetail()`
- ✅ 根据applyId查询工单设备列表: `getDevicesByApplyId()`
- ✅ 导出质检工单列表: `exportQualityCheckList()`
- ✅ 无法质检提交: `unCheckByOpt()`
#### 类型定义
- ✅ 为所有 API 方法添加了 `Promise<any>` 返回类型
- ✅ 导出接口返回类型为 `Promise<Blob>`
### 2. 类型定义更新 (`src/types/order.ts`)
新增类型:
-`ApiResponse<T>`: API 响应通用类型
-`PageResponse<T>`: 分页响应类型
### 3. 请求拦截器优化 (`src/utils/request.ts`)
- ✅ 添加了 blob 类型响应的特殊处理
- ✅ 对文件下载请求直接返回 data,不检查 code
### 4. OrderList.vue 功能集成
#### 列表查询 (`fetchData`)
✅ 完整的参数映射:
- `businessAccount``accNbr` (业务账号)
- `applyId``applyId`
- `orderId``orderId`
- `workerId``campaignId` (师傅工号)
- `city``areaType` (所属地市)
- `status``checkStatus` (质检状态)
- `dateRange``startTime`, `endTime` (质检时间)
- `noPhotoCountMin``noShowNumType` (无法拍摄)
- `manualInputCountMin``manualInputNumType` (手动输入)
- `envAbnormalCountMin``cheatNumType` (环境异常)
- `isAbnormal``isCheat` (是否异常: abnormal=1, normal=0)
#### 导出功能 (`handleExport`)
✅ 实现:
- 使用相同的查询参数
- 处理 Blob 响应
- 自动下载 Excel 文件
- 文件名包含时间戳
#### 无法质检 (`submitCannotQc`)
✅ 实现:
- 调用 `unCheckByOpt` 接口
- 组合原因类型和具体原因
- 提交成功后刷新列表
#### 标记作弊 (`submitMarkCheating`)
✅ 实现:
- 调用 `markCheat` 接口,传入 `isCheat: 1`
- 提交成功后刷新列表
- 完整的错误处理
#### 取消作弊 (`openCancelCheating`)
✅ 实现:
- 二次确认弹窗
- 调用 `markCheat` 接口,传入 `isCheat: 0`
- 取消成功后刷新列表
- 区分用户取消和接口错误
#### 查看作弊详情 (`openCheatingInfo`)
✅ 实现:
- 调用 `getCheatMarkDetail` 接口
- 动态更新作弊信息(原因、备注、时间)
- 接口失败时使用现有数据
#### 流程明细弹窗 (`openDetailsDialog`)
✅ 实现:
- 调用 `getProcessDetailList` 接口
- 支持三种类型:无法拍摄、手动输入、环境异常
- 动态映射后端数据字段
- 完整的错误处理
## 技术要点
### 1. TypeScript 类型安全
- 使用类型断言 `as ApiResponse<PageResponse<Order>>`
- 为所有 API 方法添加返回类型
- 导入并使用自定义类型
### 2. 错误处理
- 所有 API 调用都包含 try-catch
- 区分不同类型的错误(用户取消 vs 接口错误)
- 友好的错误提示
### 3. 用户体验
- 操作成功后自动刷新列表
- Loading 状态管理
- 二次确认重要操作
- 详细的成功/失败提示
### 4. 代码质量
- 移除未使用的导入
- 完善的注释
- 统一的代码风格
## 待测试项
1. ✅ 列表查询 - 各种筛选条件组合
2. ✅ 分页功能
3. ✅ 导出 Excel
4. ✅ 无法质检提交
5. ✅ 标记作弊
6. ✅ 取消作弊
7. ✅ 查看作弊详情
8. ✅ 流程明细弹窗
## 注意事项
1. **后端接口字段映射**: 某些字段映射是根据 API 文档推测的,实际使用时可能需要根据后端返回的数据结构调整:
- 师傅工号映射为 `campaignId`,可能需要确认
- 流程明细的字段名(`processName`, `createTime` 等)需要根据实际返回调整
2. **响应数据结构**: 假设后端返回格式为:
```json
{
"code": 200,
"message": "success",
"data": {
"list": [...],
"total": 100
}
}
```
3. **Blob 下载**: 导出功能需要后端设置正确的响应头
## 下一步建议
1. 在浏览器中测试所有功能
2. 根据实际后端返回调整字段映射
3. 完善错误提示信息
4. 添加更多的数据验证
5. 考虑添加请求缓存或防抖
# OrderList.vue 接口响应适配更新
## 更新时间
2026-01-23 16:30
## 更新原因
根据实际的 `getQualityCheckList` 接口返回结构调整代码
## 实际接口返回结构
```json
{
"msg": "操作成功",
"code": 200,
"data": {
"records": [
{
"applyId": "AP2008001564517023744",
"campaignId": "123456",
"accNbr": "13112345678",
"areaName": "北京丰台",
"checkStatus": 1,
"checkStatusDesc": "未开始",
"failReason": null,
"startTime": null,
"endTime": null,
"noShowNum": 0,
"manualInputNum": 0,
"cheatNum": 0,
"isCheat": 0
}
],
"total": 1071,
"size": 10,
"current": 1,
"pages": 108
},
"timestamp": 1769154819516
}
```
## 主要修改点
### 1. 数据列表字段
- ❌ 之前: `data.list`
- ✅ 现在: `data.records`
### 2. 字段映射关系
| 后端字段 | 前端字段 | 说明 |
|---------|---------|------|
| applyId | applyId, id | Apply ID,同时作为唯一标识 |
| campaignId | workerId | 师傅工号 |
| accNbr | businessAccount | 业务账号 |
| areaName | city | 所属地市 |
| checkStatus | - | 质检状态(数字) |
| checkStatusDesc | status | 质检状态(文字描述) |
| failReason | cannotQcReason | 无法质检原因 |
| startTime | startTime | 开始时间 |
| endTime | endTime | 结束时间 |
| noShowNum | noPhotoCount | 无法拍摄次数 |
| manualInputNum | manualInputCount | 手动输入次数 |
| cheatNum | envAbnormalCount | 环境异常次数 |
| isCheat | isCheating | 是否作弊(0/1 → false/true) |
### 3. 状态转换
添加了 `getStatusText` 辅助函数,用于将数字状态转换为文字:
```typescript
const getStatusText = (status: number): string => {
const statusMap: Record<number, string> = {
0: '未开始',
1: '未开始',
2: '进行中',
3: '已完成',
4: '无法质检'
}
return statusMap[status] || '未知'
}
```
优先使用后端返回的 `checkStatusDesc`,如果没有则使用 `getStatusText` 转换。
### 4. 数据映射逻辑
```typescript
tableData.value = (data.records || []).map((item: any) => ({
id: item.applyId,
applyId: item.applyId,
workerId: item.campaignId,
businessAccount: item.accNbr,
orderIds: [], // 工单ID列表需要单独查询
city: item.areaName,
status: item.checkStatusDesc || getStatusText(item.checkStatus),
cannotQcReason: item.failReason || '',
startTime: item.startTime || '',
endTime: item.endTime || '',
noPhotoCount: item.noShowNum || 0,
manualInputCount: item.manualInputNum || 0,
envAbnormalCount: item.cheatNum || 0,
isCheating: item.isCheat === 1,
cheatingReason: '',
cheatingRemark: '',
cheatingTime: ''
}))
```
## 注意事项
### 1. 工单ID列表
`orderIds` 字段在当前接口中没有返回,设置为空数组。如果需要显示工单ID列表,可能需要:
- 调用其他接口获取
- 或者后端在 records 中增加该字段
### 2. 作弊相关信息
列表接口中的作弊信息(`cheatingReason`, `cheatingRemark`, `cheatingTime`)需要在点击"查看作弊详情"时通过 `getCheatMarkDetail` 接口获取。
### 3. 分页信息
后端返回了完整的分页信息:
- `total`: 总记录数
- `size`: 每页大小
- `current`: 当前页
- `pages`: 总页数
目前只使用了 `total`,其他字段可以根据需要使用。
## 测试建议
1. ✅ 测试列表数据是否正确显示
2. ✅ 测试各个字段的映射是否正确
3. ✅ 测试状态显示是否正确
4. ✅ 测试作弊标记显示(isCheat = 1 时显示"是")
5. ✅ 测试分页功能
6. ✅ 测试筛选条件
## 下一步
如果其他接口(如 `getProcessDetailList`, `getCheatMarkDetail` 等)的返回结构与预期不同,也需要进行类似的适配调整。
...@@ -74,3 +74,470 @@ ...@@ -74,3 +74,470 @@
登录接口的传参key值错误了,账号是adminNum,修改下 登录接口的传参key值错误了,账号是adminNum,修改下
"无法质检"弹框逻辑修改
点击“提交”,接口地址:/zhijian/opt/unCheckByOpt;需要传递的参数有(applyId, checkStatus, failReason)
其中checkStatus(个人原因:2,用户原因:3,其他:4),failReason:具体原因
标记作弊弹框修改
异常原因列表:疑似不在用户家质检,一直点击无法拍摄跳过,识别屏幕/识别白纸,其他
请求接口为: '/zhijian/applyInfoDetail/getDevicesByApplyId';传参:applyId,markType(1-标记作弊 2-取消标记作弊),cause(异常原因),causeOther(备注)
取消作弊
接口:'/zhijian/applyInfoDetail/getDevicesByApplyId';参数applyId,markType(1-标记作弊 2-取消标记作弊)
疑似作弊弹框修改
疑似作弊原因(取值字段是cause),备注(取值字段是causeOhter)
质检工单详情页面修改
1、改成接口查询获取详情信息 ,接口‘/zhijian/applyInfoDetail/getProcessByApplyId’
2、接口返回结构如下,根据返回修改页面上信息的显示(基础信息:从列表页面orderList获取,质检详情:processList字段,质检视频:videoList)
{
"msg": "操作成功",
"code": 200,
"data": {
"processList": [
{
"id": 0,
"applyId": "",
"callId": "",
"process": "",
"picUrl": "",
"pubPicUrl": "",
"videoUrl": "",
"pubVideoUrl": "",
"videoResult": "",
"content": "",
"result": "",
"recognizeType": 0,
"useTime": 0,
"tryNum": 0,
"findCheat": 0,
"cheatReason": "",
"areaType": 0,
"createTime": "",
"optTime": "",
"updateTime": ""
}
],
"videoList": [
{
"id": 0,
"applyId": "",
"callId": "",
"recordId": "",
"uid": "",
"recordFile": "",
"recordStatus": 0,
"peeravatarId": 0,
"videoDuration": 0,
"rtcType": 0,
"createTime": "",
"endTime": "",
"updateTime": ""
}
]
},
"timestamp": 1609459200000
}
质检详情表格中添加一列“识别方式”,取值recognizeType,1:自动识别,2:手输
质检工单详情页面修改
1、接口返回结构发生了变化,目前返回结构如下
{
"msg": "操作成功",
"code": 200,
"data": {
"applyId": "AP1999131958729719808",
"accNbr": "13701370001",
"campaignId": "123456",
"orderId": "3356158",
"areaType": "3208",
"areaName": "淮安市",
"addressName": "北京市丰台区丰科中心A座1502",
"orderCode": "104,110,103",
"finish": "0",
"checkStatus": "1",
"startTime": 1765465228000,
"endTime": null,
"videoDuration": null,
"noShowNum": null,
"manualInputNum": null,
"cheatNum": null,
"terminalClassList": [
"主光猫",
"子光猫",
"机顶盒"
],
"serviceNames": [
"FTTR-装机",
"FTTR-装机",
"互联网电视订购-装机"
],
"processList": [
{
"id": 24777,
"applyId": "AP1999131958729719808",
"callId": "lk-sj06jdh3kodos07rurk8k4tym9ojhxj1",
"process": "accEvn",
"picUrl": "http://172.29.250.160:9800/huaian/2025/12/11/screenshot_20251211_230416_1_52896350.jpg",
"pubPicUrl": "https://zjjt.eos-wuxi-1.cmecloud.cn/2025/12/11/2304161_5456_78382558.jpg",
"videoUrl": null,
"pubVideoUrl": null,
"videoResult": null,
"content": null,
"result": "anXian",
"recognizeType": 1,
"useTime": 3324,
"tryNum": 1,
"findCheat": 0,
"cheatReason": null,
"areaType": 3208,
"createTime": "2025-12-11 23:04:13",
"optTime": "2025-12-11 23:04:16",
"updateTime": "2025-12-11 23:04:16"
}
],
"videoList": [
{
"id": 3533,
"applyId": "AP1999131958729719808",
"callId": "lk-sj06jdh3kodos07rurk8k4tym9ojhxj1",
"recordId": null,
"uid": "user001",
"recordFile": null,
"recordStatus": 0,
"peeravatarId": 23,
"videoDuration": null,
"rtcType": 2,
"createTime": "2025-12-11 23:00:28",
"endTime": "2025-12-11 23:04:50",
"updateTime": "2025-12-11 23:04:50"
}
]
},
"timestamp": 1769420643182
}
2、基础信息改为从接口返回中获取(装机地址:addressName,设备类型:terminalClassList,工单类型:serviceNames,工单id:orderCode,总耗时:videoDuration)
质检视频列表,只展示名字即可,点击视频名字时查询接口/zhijian/applyInfoDetail/view获取视频地址播放,接口传参url(取值视频列表里的recordFile字段,如果recordFile不存在,则提示暂不支持播放)
下载全部视频改为接口调用,接口地址‘/zhijian/applyInfoDetail/downVideoByApplyId’,传参applyId即可
下载质检截图改为接口调用,接口地址‘/zhijian/applyInfoDetail/downImageByApplyId’,传参applyId即可
数据统计-质检工单数据页面修改
1、页面进入默认查询昨天的数据
2、地市选择数组修改:添加区域code的对应,查询接口传参时传区域code,code和地市对应关系如下(3201:南京市,3202:无锡市,3203:徐州市,3204:常州市,3205:苏州市,3206:南通市,3207:连云港市,3208:淮安市,3209:盐城市,3210:扬州市,3211:镇江市,3212:泰州市,3213:宿迁市)
3、页面数据内容改为接口请求,接口地址“/zhijian/opt/getAllStatisticsByCity”,接口返回结构如下
{
"msg": "操作成功",
"code": 200,
"data": {
"totalOrders": 0,
"finishOrders": 0,
"resultCount": 0,
"unfinishOrders": 0,
"finishResult": 0,
"unfinishResult": 0,
"complaintTotal": 0,
"complaintFinish": 0,
"complaintUnfinish": 0,
"noShowCount": 0,
"averageDuration": 0,
"totalDuration": 0,
"moreThan5Min": 0,
"moreThan4Min": 0,
"moreThan3Min": 0,
"lessThan3Min": 0,
"areaTypeList": [
{
"date": "",
"areaName": "",
"orderCount": 0,
"finishOrders": 0,
"unfinishOrders": 0,
"resultCount": 0,
"finishResult": 0,
"unfinishResult": 0,
"complaintTotal": 0,
"complaintFinish": 0,
"complaintUnfinish": 0,
"noShowCount": 0,
"averageDuration": 0,
"totalDuration": 0,
"moreThan5Min": 0,
"moreThan4Min": 0,
"moreThan3Min": 0,
"lessThan3Min": 0
}
]
},
"timestamp": 1609459200000
}
4、顶部的数据展示以及参数对应关系如下(工单总数:totalOrders,已完成工单数:finishOrders,质检工单总数:resultCount,已完成质检工单数:finishResult,投诉工单总数:complaintTotal,已完成投诉工单数:complaintFinish,无法质检数:noShowCount,平均耗时:averageDuration)
5、地市工单统计取值areaTypeList数组,数据展示以及参数对应关系如下(地区名称:areaName,工单总数:totalOrders,已完成工单数:finishOrders,未完成工单数:unfinishOrders,质检工单总数:resultCount,已完成质检工单数:finishResult,未完成质检工单数:unfinishResult,投诉工单总数:complaintTotal,已完成投诉工单数:complaintFinish,未完成投诉工单数:complaintUnfinish,无法质检数:noShowCount)
6、耗时统计取值areaTypeList数组,数据展示以及参数对应关系如下(地区名称:areaName,<3分钟:lessThan3Min,3-4分钟:moreThan3Min,4-5分钟:moreThan4Min,>=5分钟:moreThan5Min,总耗时:totalDuration,平均耗时:averageDuration)
7、质检环节统计数据从新接口“/zhijian/opt/getProcessTotalStatistics”中获取,接口返回结构如下:
{
"msg": "操作成功",
"code": 200,
"data": [
{
"date": "",
"areaName": "",
"processCode": "",
"processName": "",
"averageDuration": 0,
"averageInspectionTimes": 0,
"totalCount": 0,
"noShowCount": 0,
"totalTryNum": 0,
"tryNum1": 0,
"tryNum2": 0,
"tryNum3": 0,
"tryNum4": 0,
"tryNum5": 0,
"tryNumGte6": 0,
"totalDurationSeconds": 0,
"recognizeType1": 0,
"recognizeType2": 0
}
],
"timestamp": 1609459200000
}
数据展示以及参数对应关系如下(环节名称:processName,平均识别数:averageInspectionTimes,识别总数:totalCount,平均耗时:averageDuration)
顶部的总数据展示有缺失,添加(已完成工单数:finishOrders,已完成质检工单数:finishResult,已完成投诉工单数:complaintFinish,平均耗时:averageDuration)
导出表格功能修改
1、改为调用接口导出数据,接口为“/zhijian/opt/exportAllStatisticsByCity”,传参和数据查询接口一样
数据统计-设备识别数据页面修改
1、表格中6次和6次以上和成一列,再加一列“总识别次数”
数据统计-设备识别数据页面修改
1、页面内数据改为接口获取,接口地址“/zhijian/opt/getProcessRecognizeStatistics”,传参:startDate(格式2010-01-01),endDate(格式2010-01-01),接口返回结构如下:
{
"msg": "操作成功",
"code": 200,
"data": [
{
"date": "",
"areaName": "",
"processCode": "",
"processName": "",
"averageDuration": 0,
"averageInspectionTimes": 0,
"totalCount": 0,
"noShowCount": 0,
"totalTryNum": 0,
"tryNum1": 0,
"tryNum2": 0,
"tryNum3": 0,
"tryNum4": 0,
"tryNum5": 0,
"tryNumGte6": 0,
"totalDurationSeconds": 0,
"recognizeType1": 0,
"recognizeType2": 0
}
],
"timestamp": 1609459200000
}
2、数据对应关系如下(设备名称:processName,识别1次:tryNum1,识别2次:tryNum2,识别3次:tryNum3,识别4次:tryNum4,识别5次:tryNum5,大于等于6次:tryNumGte6,总识别次数:totalCount)
数据统计-设备识别数据页面-查看视频弹框修改
1、去掉地区的筛选框,增加applyId的输入框筛选
2、点击“查询”,调用接口/zhijian/opt/getVideoList,接口传参(startDate,endDate,applyId,page,pageSize),接口返回结构如下:
{
"msg": "操作成功",
"code": 200,
"data": {
"records": [
{
"id": 0,
"applyId": "",
"callId": "",
"recordId": "",
"uid": "",
"recordFile": "",
"recordStatus": 0,
"peeravatarId": 0,
"createTime": "",
"updateTime": "",
"areaType": 0,
"accNbr": "",
"campaignId": "",
"spendTime": "",
"processStep": "",
"num": 0,
"videoUtlList": []
}
],
"total": 0,
"size": 0,
"current": 0,
"orders": [
{
"column": "",
"asc": true
}
],
"optimizeCountSql": {},
"searchCount": {},
"optimizeJoinOfCountSql": true,
"maxLimit": 0,
"countId": "",
"pages": 0
},
"timestamp": 1609459200000
}
3、结果表格添加分页功能,表格列表数据取值records,视频ID改为applyId,视频链接取取值videoUtlList(可能多个,要求每个链接都支持点击打开弹框播放查看)
数据统计-设备识别数据页面-导出图片弹框修改
1、选择设备的选择框数据修改为(工服,工牌,宽带账号,普通光猫,主光猫,子光猫,机顶盒,电视画面,电视软终端,路由器,室内摄像头,室外摄像头,云电脑终端,云电脑显示器,POE交换机,POE面板,故障投诉光猫灯,故障投诉主光猫灯,环境)
2、点击“确定导出”,请求接口/zhijian/applyInfoDetail/exportLLMResultList,接口传参(startTime,endTime,选择设备:deviceTypes(数组类型,支持多选))
3、去掉“导出数量”
数据统计-串号数据统计页面修改
1、去掉“2次通过率”的显示
数据统计-串号数据统计页面修改
1、数据获取,改为接口查询,接口地址为“/zhijian/opt/getSNStatisticsSummary”,接口查询传参(startDate,endDate),接口返回结构如下:
{
"msg": "操作成功",
"code": 200,
"data": {
"totalSubmitCount": 0,
"autoRecognizeRate": 0,
"passRate1": 0,
"passRateWithin3": 0,
"avgDuration": 0,
"avgRecognizeTimes": 0,
"datesList": [
{
"date": "",
"processName": "",
"processCode": "",
"totalCount": 0,
"totalTryNum": 0,
"totalDurationSeconds": 0,
"passNum1": 0,
"passRate1": 0,
"passNum2": 0,
"passRate2": 0,
"passNum3": 0,
"passRate3": 0,
"passRateWithin3": 0,
"autoRecognizeCount": 0,
"autoRecognizeRate": 0,
"avgRecognizeTimes": 0,
"avgDuration": 0
}
],
"processList": [
{
"date": "",
"processName": "",
"processCode": "",
"totalCount": 0,
"totalTryNum": 0,
"totalDurationSeconds": 0,
"passNum1": 0,
"passRate1": 0,
"passNum2": 0,
"passRate2": 0,
"passNum3": 0,
"passRate3": 0,
"passRateWithin3": 0,
"autoRecognizeCount": 0,
"autoRecognizeRate": 0,
"avgRecognizeTimes": 0,
"avgDuration": 0
}
]
},
"timestamp": 1609459200000
}
2、卡片数据对应关系(提交总次数:totalSubmitCount,自动识别占比:autoRecognizeRate,1次通过率:passRate1,3次内通过率:passRateWithin3,平均耗时:avgDuration,平均尝试改为平均识别:avgRecognizeTimes)
3、“整体趋势”取值datesList数组,数据对应关系(日期:date,1次通过率:passRate1,3次通过率:passRate3,自动识别率:autoRecognizeRate)
4、设备详情取值processList数组,数据对应关系(设备名称:processName,1次通过率:passRate1,3次通过率:passRate3,自动识别占比:autoRecognizeRate,提交总次数:totalCount,3次内通过率:passRateWithin3,平均识别次数:avgRecognizeTimes,平均耗时:avgDuration)
设备识别数据页面
1、“导出图片”的接口调用,有时候不会返回流,会返回一个json对象,但因为接口设置了responseType为blob,导致错误提示没弹出,还是当成了blob处理,优化下
串号数据统计页面修改
1、设备详情下的表格中,除了“提交总次数”和”平均识别次数“,其他列表表头中的升降排序都不好使,优化下
无法拍摄数据页面修改
1、去掉4次及以上、不能拍总次数、占比三个数据的展示
数据统计-无法拍摄数据页面修改
1、页面进入默认查询昨天的数据
2、地市选择数组修改:添加区域code的对应,查询接口传参时传区域code,code和地市对应关系如下(3201:南京市,3202:无锡市,3203:徐州市,3204:常州市,3205:苏州市,3206:南通市,3207:连云港市,3208:淮安市,3209:盐城市,3210:扬州市,3211:镇江市,3212:泰州市,3213:宿迁市)
3、地市不能拍次数统计:数据内容改为接口请求,接口地址“/zhijian/opt/getNoShowCityStatistics”,接口返回结构如下
{
"msg": "操作成功",
"code": 200,
"data": [
{
"areaName": "",
"areaType": "",
"count1": 0,
"percentage1": 0,
"count2": 0,
"percentage2": 0,
"count3": 0,
"percentage3": 0,
"count4Plus": 0,
"totalCount": 0,
"finishCount": 0
}
],
"timestamp": 1609459200000
}
数据对应关系如下(地市:areaName,1次不能拍:count1,1次不不能拍占比:percentage1,2次不能拍:count2,2次不能拍占比:percentage2,3次不能拍:count3,3次不能拍占比:percentage3)
4、设备不能拍次数统计:数据内容改为接口请求,接口地址“/zhijian/opt/getNoShowProcessStatistics”,接口返回结构如下:
{
"msg": "操作成功",
"code": 200,
"data": [
{
"date": "",
"areaName": "",
"processCode": "",
"processName": "",
"averageDuration": 0,
"averageInspectionTimes": 0,
"totalCount": 0,
"noShowCount": 0,
"totalTryNum": 0,
"tryNum1": 0,
"tryNum2": 0,
"tryNum3": 0,
"tryNum4": 0,
"tryNum5": 0,
"tryNumGte6": 0,
"totalDurationSeconds": 0,
"recognizeType1": 0,
"recognizeType2": 0
}
],
"timestamp": 1609459200000
}
数据对应关系如下(设备名称:processName,不能拍总次数:noShowCount)
5、两个接口的传参是一样的(startDate,endDate,areaType)
无法拍摄数据页面修改
1、地市不能拍次数统计导出:传参(startTime,endTime,areaType),接口地址:/zhijian/opt/exportNoShowGoupByAreaType,get请求
2、设备不能拍次数统计导出:传参(startTime,endTime),接口地址:/zhijian/opt/exportNoShowProcessStatistics,get请求
问题记录
1、质检工单数据页面:导出表格,报错500(传参加上areaType可以成功)
2、
\ No newline at end of file \ No newline at end of file
import request from '../utils/request' import request from '../utils/request'
export const loginApi = { export const loginApi = {
login(data: any) { login(data: any): Promise<any> {
return request({ return request({
url: '/zhijian/opt/login', url: '/zhijian/opt/login',
method: 'post', method: 'post',
data data
}) })
}, },
logout() { logout(): Promise<any> {
return request({ return request({
url: '/zhijian/opt/loginOut', url: '/zhijian/opt/loginOut',
method: 'get' method: 'get'
...@@ -17,27 +17,120 @@ export const loginApi = { ...@@ -17,27 +17,120 @@ export const loginApi = {
} }
export const qualityApi = { export const qualityApi = {
// Quality Check List // Quality Check List - 质检工单列表查询
getQualityCheckList(data: any) { getQualityCheckList(data: any): Promise<any> {
return request({ return request({
url: '/zhijian/opt/qualityCheckList', url: '/zhijian/applyInfoDetail/qualityCheckList',
method: 'post', method: 'post',
data data
}) })
}, },
// Get Process Detail // Get Process Detail - 查询流程明细列表
getProcessDetailList(data: any) { getProcessDetailList(data: any): Promise<any> {
return request({ return request({
url: '/zhijian/opt/getProcessDetailList', url: '/zhijian/applyInfoDetail/getProcessDetailList',
method: 'post', method: 'post',
data data
}) })
},
// Mark Cheat - 标记/取消作弊
markCheat(data: { applyId: string; markType: number; cause?: string; causeOther?: string }): Promise<any> {
return request({
url: '/zhijian/applyInfoDetail/markCheat',
method: 'post',
data
})
},
// Get Cheat Mark Detail - 查看作弊标记详情
getCheatMarkDetail(data: { applyId: string }): Promise<any> {
return request({
url: '/zhijian/applyInfoDetail/getCheatMarkDetail',
method: 'post',
data
})
},
// Get Devices By ApplyId - 根据applyId查询工单设备列表
getDevicesByApplyId(data: { applyId: string }): Promise<any> {
return request({
url: '/zhijian/applyInfoDetail/getDevicesByApplyId',
method: 'post',
data
})
},
// Export Quality Check List - 导出质检工单列表
exportQualityCheckList(data: any): Promise<Blob> {
return request({
url: '/zhijian/applyInfoDetail/exportQualityCheckList',
method: 'post',
data,
responseType: 'blob'
})
},
// Cannot Check - 无法质检提交
unCheckByOpt(data: { applyId: string; checkStatus: number; failReason: string }): Promise<any> {
return request({
url: '/zhijian/opt/unCheckByOpt',
method: 'post',
data
})
},
// Get Process By ApplyId - 获取工单详情
getProcessByApplyId(data: { applyId: string }): Promise<any> {
return request({
url: '/zhijian/applyInfoDetail/getProcessByApplyId',
method: 'get',
params: data
})
},
// Get Video URL - 获取视频播放地址
getVideoUrl(data: { url: string }): Promise<any> {
return request({
url: '/zhijian/applyInfoDetail/view',
method: 'get',
params: data
})
},
// Download All Videos - 下载全部视频
downloadAllVideos(data: { applyId: string }): Promise<any> {
return request({
url: '/zhijian/applyInfoDetail/downVideoByApplyId',
method: 'get',
params: data,
responseType: 'blob'
})
},
// Download Screenshots - 下载质检截图
downloadScreenshots(data: { applyId: string }): Promise<any> {
return request({
url: '/zhijian/applyInfoDetail/downImageByApplyId',
method: 'get',
params: data,
responseType: 'blob'
})
},
// Export LLM Result List - 导出LLM结果列表
exportLLMResultList(data: any): Promise<any> {
return request({
url: '/zhijian/applyInfoDetail/exportLLMResultList',
method: 'post',
data,
responseType: 'blob'
})
},
// Export Process Recognize Statistics - 导出设备识别统计明细
exportProcessRecognizeStatistics(data: any): Promise<any> {
return request({
url: '/zhijian/opt/exportProcessRecognizeStatistics',
method: 'get',
params: data,
responseType: 'blob'
})
} }
} }
export const statsApi = { export const statsApi = {
// Overall stats // Overall stats
getAllStatisticsByCity(data: any) { getAllStatisticsByCity(data: any): Promise<any> {
return request({ return request({
url: '/zhijian/opt/getAllStatisticsByCity', url: '/zhijian/opt/getAllStatisticsByCity',
method: 'post', method: 'post',
...@@ -45,15 +138,32 @@ export const statsApi = { ...@@ -45,15 +138,32 @@ export const statsApi = {
}) })
}, },
// SN stats summary // SN stats summary
getSNStatisticsSummary(data: any) { getSNStatisticsSummary(data: any): Promise<any> {
return request({ return request({
url: '/zhijian/opt/getSNStatisticsSummary', url: '/zhijian/opt/getSNStatisticsSummary',
method: 'post', method: 'post',
data data
}) })
}, },
// Process Total Statistics - 质检环节统计
getProcessTotalStatistics(data: any): Promise<any> {
return request({
url: '/zhijian/opt/getProcessTotalStatistics',
method: 'post',
data
})
},
// Export All Statistics By City - 导出统计数据
exportAllStatisticsByCity(data: any): Promise<any> {
return request({
url: '/zhijian/opt/exportAllStatisticsByCity',
method: 'get',
params: data,
responseType: 'blob'
})
},
// Device recognize stats // Device recognize stats
getProcessRecognizeStatistics(data: any) { getProcessRecognizeStatistics(data: any): Promise<any> {
return request({ return request({
url: '/zhijian/opt/getProcessRecognizeStatistics', url: '/zhijian/opt/getProcessRecognizeStatistics',
method: 'post', method: 'post',
...@@ -61,11 +171,45 @@ export const statsApi = { ...@@ -61,11 +171,45 @@ export const statsApi = {
}) })
}, },
// No Photo stats // No Photo stats
getNoShowCityStatistics(data: any) { getNoShowCityStatistics(data: any): Promise<any> {
return request({ return request({
url: '/zhijian/opt/getNoShowCityStatistics', url: '/zhijian/opt/getNoShowCityStatistics',
method: 'post', method: 'post',
data data
}) })
},
getNoShowProcessStatistics(data: any): Promise<any> {
return request({
url: '/zhijian/opt/getNoShowProcessStatistics',
method: 'post',
data
})
},
// Export No Show Group By Area Type - 导出地市不能拍次数统计
exportNoShowGoupByAreaType(data: any): Promise<any> {
return request({
url: '/zhijian/opt/exportNoShowCityStatistics',
method: 'get',
params: data,
responseType: 'blob'
})
},
// Export No Show Process Statistics - 导出设备不能拍次数统计
exportNoShowProcessStatistics(data: any): Promise<any> {
return request({
url: '/zhijian/opt/exportNoShowProcessStatistics',
method: 'get',
params: data,
responseType: 'blob'
})
},
// Get Video List - 获取视频列表
getVideoList(data: any): Promise<any> {
return request({
url: '/zhijian/opt/getVideoList',
method: 'post',
data
})
} }
} }
// 工单设备信息
export interface OrderDevice {
orderCode: string; // 工单号
deviceNumber: string; // 设备串号
}
export interface Order { export interface Order {
id: string; id: string;
applyId: string; applyId: string;
workerId: string; workerId: string;
businessAccount: string; businessAccount: string;
orderIds: string[]; // List of Order IDs for the popup orderIds: OrderDevice[]; // 工单设备列表
city: string; city: string;
status: '未开始' | '进行中' | '已完成' | '无法质检'; status: '未开始' | '进行中' | '已完成' | '无法质检';
cannotQcReason?: string; cannotQcReason?: string;
...@@ -18,13 +24,14 @@ export interface Order { ...@@ -18,13 +24,14 @@ export interface Order {
cheatingTime?: string; cheatingTime?: string;
} }
export interface OrderQuery { export interface OrderQuery {
businessAccount?: string; businessAccount?: string;
applyId?: string; applyId?: string;
orderId?: string; orderId?: string;
workerId?: string; workerId?: string;
city?: string; city?: string;
status?: string; status?: number | string; // 1-未开始 2-进行中 3-已完成 4-无法质检,空字符串表示全部
dateRange?: [string, string]; dateRange?: [string, string];
noPhotoCountMin?: number; noPhotoCountMin?: number;
manualInputCountMin?: number; manualInputCountMin?: number;
...@@ -66,3 +73,18 @@ export interface OrderDetail extends Order { ...@@ -66,3 +73,18 @@ export interface OrderDetail extends Order {
steps: QcStep[]; steps: QcStep[];
videos: QcVideo[]; videos: QcVideo[];
} }
// API Response Types
export interface ApiResponse<T = any> {
code: number;
message?: string;
data?: T;
}
export interface PageResponse<T> {
list: T[];
total: number;
pageNum?: number;
pageSize?: number;
}
...@@ -3,7 +3,7 @@ import { ElMessage } from 'element-plus' ...@@ -3,7 +3,7 @@ import { ElMessage } from 'element-plus'
const service = axios.create({ const service = axios.create({
baseURL: import.meta.env.VITE_API_URL || '', // Use proxy baseURL: import.meta.env.VITE_API_URL || '', // Use proxy
timeout: 5000 timeout: 10000
}) })
service.interceptors.request.use( service.interceptors.request.use(
...@@ -22,18 +22,84 @@ service.interceptors.request.use( ...@@ -22,18 +22,84 @@ service.interceptors.request.use(
) )
service.interceptors.response.use( service.interceptors.response.use(
(response) => { async (response) => {
// 如果是 blob 类型(文件下载)
if (response.config.responseType === 'blob') {
// 检查是否是 JSON 错误响应
if (response.data.type === 'application/json') {
try {
const text = await response.data.text()
const errorData = JSON.parse(text)
// 处理 401 登录失效
if (errorData.code === 401) {
ElMessage.error(errorData.msg || errorData.message || '登录已失效,请重新登录')
// 清除本地存储的 token
localStorage.removeItem('hzMgrtoken')
// 延迟跳转到登录页
setTimeout(() => {
window.location.href = '/login'
}, 1500)
return Promise.reject(new Error(errorData.msg || errorData.message || '登录已失效'))
}
// 其他错误
ElMessage.error(errorData.msg || errorData.message || '请求失败')
return Promise.reject(new Error(errorData.msg || errorData.message || '请求失败'))
} catch (e) {
// 解析失败,返回原始 blob
return response.data
}
}
// 正常的文件流,直接返回
return response.data
}
const res = response.data const res = response.data
// 处理 401 登录失效
if (res.code === 401) {
ElMessage.error(res.msg || res.message || '登录已失效,请重新登录')
// 清除本地存储的 token
localStorage.removeItem('hzMgrtoken')
// 延迟跳转到登录页,让用户看到提示信息
setTimeout(() => {
window.location.href = '/login'
}, 1500)
return Promise.reject(new Error(res.msg || res.message || '登录已失效'))
}
// Adjust success code check based on actual API response structure // Adjust success code check based on actual API response structure
// Typically 200 or 0 indicates success // Typically 200 or 0 indicates success
if (res.code !== 200 && res.code !== 0) { if (res.code !== 200 && res.code !== 0) {
ElMessage.error(res.message || 'Error') ElMessage.error(res.msg || res.message || '请求失败')
return Promise.reject(new Error(res.message || 'Error')) return Promise.reject(new Error(res.msg || res.message || '请求失败'))
} }
return res return res
}, },
(error) => { (error) => {
ElMessage.error(error.message) // 处理 HTTP 401 状态码
if (error.response && error.response.status === 401) {
ElMessage.error('登录已失效,请重新登录')
// 清除本地存储的 token
localStorage.removeItem('hzMgrtoken')
// 延迟跳转到登录页
setTimeout(() => {
window.location.href = '/login'
}, 1500)
} else {
ElMessage.error(error.message || '请求失败')
}
return Promise.reject(error) return Promise.reject(error)
} }
) )
......
...@@ -2,8 +2,8 @@ ...@@ -2,8 +2,8 @@
import { ref, onMounted } from 'vue' import { ref, onMounted } from 'vue'
import { useRoute, useRouter } from 'vue-router' import { useRoute, useRouter } from 'vue-router'
import { ArrowLeft, VideoPlay, Download, Picture } from '@element-plus/icons-vue' import { ArrowLeft, VideoPlay, Download, Picture } from '@element-plus/icons-vue'
import type { OrderDetail } from '../types/order' import type { OrderDetail, ApiResponse } from '../types/order'
import { getMockOrderDetail } from '../mock/orderData' import { qualityApi } from '../api'
import { ElMessage } from 'element-plus' import { ElMessage } from 'element-plus'
const route = useRoute() const route = useRoute()
...@@ -19,20 +19,121 @@ const envImagesVisible = ref(false) ...@@ -19,20 +19,121 @@ const envImagesVisible = ref(false)
const envImageList = ref<string[]>([]) const envImageList = ref<string[]>([])
// Fetch Data // Fetch Data
const fetchDetail = () => { const fetchDetail = async () => {
loading.value = true loading.value = true
// Mock API call
setTimeout(() => {
const id = route.params.id as string
orderDetail.value = getMockOrderDetail(id)
// Prepare images for preview try {
if (orderDetail.value?.steps) { // 从路由参数获取 applyId
previewImageList.value = orderDetail.value.steps.map(s => s.imageUrl) const applyId = route.params.id as string
// 调用接口获取详情
const response = await qualityApi.getProcessByApplyId({
applyId
}) as ApiResponse<any>
if (response.code === 200 || response.code === 0) {
const data = response.data || {}
const processList = data.processList || []
const videoList = data.videoList || []
// 格式化时间戳
const formatTimestamp = (timestamp: number | null) => {
if (!timestamp) return ''
const date = new Date(timestamp)
return date.toLocaleString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
hour12: false
}).replace(/\//g, '-')
}
// 获取质检状态描述
const getStatusDesc = (checkStatus: string) => {
const statusMap: any = {
'1': '未开始',
'2': '进行中',
'3': '已完成',
'4': '无法质检'
}
return statusMap[checkStatus] || '未知'
}
// 合并基础信息和详情数据
orderDetail.value = {
// 基础信息(从接口 data 获取)
id: data.applyId || applyId,
applyId: data.applyId || applyId,
workerId: data.campaignId || '',
businessAccount: data.accNbr || '',
orderIds: data.orderCode || '', // 直接使用 orderCode 字符串
city: data.areaName || '',
status: getStatusDesc(data.checkStatus),
cannotQcReason: data.failReason || '',
startTime: formatTimestamp(data.startTime),
endTime: formatTimestamp(data.endTime),
completeTime: formatTimestamp(data.endTime),
noPhotoCount: data.noShowNum || 0,
manualInputCount: data.manualInputNum || 0,
envAbnormalCount: data.cheatNum || 0,
isCheating: data.isCheat === 1 || false,
// 新增字段(从接口 data 获取)
installAddress: data.addressName || '',
orderType: data.serviceNames ? data.serviceNames.join(', ') : '',
deviceType: data.terminalClassList ? data.terminalClassList.join(', ') : '',
totalDuration: data.videoDuration ? formatDuration(data.videoDuration) : '',
// 质检步骤(从 processList 获取)
steps: processList.map((item: any, index: number) => ({
id: item.id || index,
name: item.process || `步骤${index + 1}`,
duration: formatDuration(item.useTime),
result: item.result || '通过',
imageUrl: item.pubPicUrl || item.picUrl || '',
isAbnormal: item.findCheat || 0,
recognizeType: item.recognizeType || 0
})),
// 视频列表(从 videoList 获取)
videos: videoList.map((item: any, index: number) => ({
id: item.id || index,
name: `视频${index + 1} - ${item.callId || ''}`,
videoUrl: item.recordFile || '',
thumbnailUrl: '', // 如果有缩略图字段可以映射
duration: formatDuration(item.videoDuration || 0)
}))
} }
// 准备图片预览列表
previewImageList.value = orderDetail.value.steps
.map(s => s.imageUrl)
.filter(url => url)
}
} catch (error) {
console.error('获取工单详情失败:', error)
// 错误提示已在 request.ts 中统一处理
} finally {
loading.value = false loading.value = false
}, 500) }
}
// 辅助函数:格式化时长
const formatDuration = (seconds: number): string => {
if (!seconds) return '0秒'
const hours = Math.floor(seconds / 3600)
const minutes = Math.floor((seconds % 3600) / 60)
const secs = seconds % 60
const parts = []
if (hours > 0) parts.push(`${hours}小时`)
if (minutes > 0) parts.push(`${minutes}分`)
if (secs > 0 || parts.length === 0) parts.push(`${secs}秒`)
return parts.join('')
} }
// Handlers // Handlers
...@@ -41,13 +142,12 @@ const handleBack = () => { ...@@ -41,13 +142,12 @@ const handleBack = () => {
} }
const showEnvImages = () => { const showEnvImages = () => {
// Show abnormal images (Mock: just pick images from steps that have abnormal flag, or all if none) // Show abnormal images
if (orderDetail.value?.steps) { if (orderDetail.value?.steps) {
// Filter steps with abnormal or just take first 3 for demo
const abnormalSteps = orderDetail.value.steps.filter(s => s.isAbnormal > 0) const abnormalSteps = orderDetail.value.steps.filter(s => s.isAbnormal > 0)
const sources = abnormalSteps.length > 0 ? abnormalSteps : orderDetail.value.steps.slice(0, 3) const sources = abnormalSteps.length > 0 ? abnormalSteps : orderDetail.value.steps.slice(0, 3)
envImageList.value = sources.map(s => s.imageUrl) envImageList.value = sources.map(s => s.imageUrl).filter(url => url)
if (envImageList.value.length > 0) { if (envImageList.value.length > 0) {
envImagesVisible.value = true envImagesVisible.value = true
...@@ -57,9 +157,29 @@ const showEnvImages = () => { ...@@ -57,9 +157,29 @@ const showEnvImages = () => {
} }
} }
const playVideo = (url: string) => { const playVideo = async (recordFile: string) => {
currentVideoUrl.value = url // 检查 recordFile 是否存在
if (!recordFile) {
ElMessage.warning('暂不支持播放')
return
}
try {
// 调用接口获取视频播放地址
const response = await qualityApi.getVideoUrl({
url: recordFile
}) as ApiResponse<any>
if (response.code === 200 || response.code === 0) {
// 假设接口返回的视频地址在 data 字段中
const videoUrl = response.data || recordFile
currentVideoUrl.value = videoUrl
videoDialogVisible.value = true videoDialogVisible.value = true
}
} catch (error) {
console.error('获取视频地址失败:', error)
// 错误提示已在 request.ts 中统一处理
}
} }
const downloadVideo = (url: string) => { const downloadVideo = (url: string) => {
...@@ -72,12 +192,48 @@ const downloadVideo = (url: string) => { ...@@ -72,12 +192,48 @@ const downloadVideo = (url: string) => {
document.body.removeChild(link) document.body.removeChild(link)
} }
const downloadAllVideos = () => { const downloadAllVideos = async () => {
ElMessage.success('已开始批量下载所有视频') try {
const applyId = route.params.id as string
const blob = await qualityApi.downloadAllVideos({ applyId })
// 创建下载链接
const url = window.URL.createObjectURL(blob)
const link = document.createElement('a')
link.href = url
link.download = `视频_${applyId}.zip`
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
window.URL.revokeObjectURL(url)
ElMessage.success('下载成功')
} catch (error) {
console.error('下载视频失败:', error)
// 错误提示已在 request.ts 中统一处理
}
} }
const downloadScreenshots = () => { const downloadScreenshots = async () => {
ElMessage.success('已开始下载质检截图') try {
const applyId = route.params.id as string
const blob = await qualityApi.downloadScreenshots({ applyId })
// 创建下载链接
const url = window.URL.createObjectURL(blob)
const link = document.createElement('a')
link.href = url
link.download = `截图_${applyId}.zip`
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
window.URL.revokeObjectURL(url)
ElMessage.success('下载成功')
} catch (error) {
console.error('下载截图失败:', error)
// 错误提示已在 request.ts 中统一处理
}
} }
onMounted(() => { onMounted(() => {
...@@ -104,15 +260,13 @@ onMounted(() => { ...@@ -104,15 +260,13 @@ onMounted(() => {
<el-descriptions-item label="业务账号">{{ orderDetail.businessAccount }}</el-descriptions-item> <el-descriptions-item label="业务账号">{{ orderDetail.businessAccount }}</el-descriptions-item>
<el-descriptions-item label="所属地市">{{ orderDetail.city }}</el-descriptions-item> <el-descriptions-item label="所属地市">{{ orderDetail.city }}</el-descriptions-item>
<el-descriptions-item label="装机地址">{{ orderDetail.installAddress }}</el-descriptions-item> <el-descriptions-item label="装机地址">{{ orderDetail.installAddress || '-' }}</el-descriptions-item>
<el-descriptions-item label="师傅工号">{{ orderDetail.workerId }}</el-descriptions-item> <el-descriptions-item label="师傅工号">{{ orderDetail.workerId }}</el-descriptions-item>
<el-descriptions-item label="工单类型">{{ orderDetail.orderType }}</el-descriptions-item> <el-descriptions-item label="工单类型">{{ orderDetail.orderType || '-' }}</el-descriptions-item>
<el-descriptions-item label="设备类型">{{ orderDetail.deviceType }}</el-descriptions-item> <el-descriptions-item label="设备类型">{{ orderDetail.deviceType || '-' }}</el-descriptions-item>
<el-descriptions-item label="工单ID"> <el-descriptions-item label="工单ID">
<template v-for="id in orderDetail.orderIds" :key="id"> {{ orderDetail.orderIds || '-' }}
<div class="text-xs">{{ id }}</div>
</template>
</el-descriptions-item> </el-descriptions-item>
<el-descriptions-item label="质检状态"> <el-descriptions-item label="质检状态">
<el-tag :type="orderDetail.status === '已完成' ? 'success' : ''">{{ orderDetail.status }}</el-tag> <el-tag :type="orderDetail.status === '已完成' ? 'success' : ''">{{ orderDetail.status }}</el-tag>
...@@ -122,7 +276,7 @@ onMounted(() => { ...@@ -122,7 +276,7 @@ onMounted(() => {
<el-descriptions-item label="开始时间">{{ orderDetail.startTime }}</el-descriptions-item> <el-descriptions-item label="开始时间">{{ orderDetail.startTime }}</el-descriptions-item>
<el-descriptions-item label="完成时间">{{ orderDetail.completeTime || '-' }}</el-descriptions-item> <el-descriptions-item label="完成时间">{{ orderDetail.completeTime || '-' }}</el-descriptions-item>
<el-descriptions-item label="只能计数"> <el-descriptions-item label="不能拍/手动">
<div class="flex gap-4"> <div class="flex gap-4">
<span>不能拍: <span class="font-bold text-red-500">{{ orderDetail.noPhotoCount }}</span></span> <span>不能拍: <span class="font-bold text-red-500">{{ orderDetail.noPhotoCount }}</span></span>
<span>手动: <span class="font-bold text-orange-500">{{ orderDetail.manualInputCount }}</span></span> <span>手动: <span class="font-bold text-orange-500">{{ orderDetail.manualInputCount }}</span></span>
...@@ -154,6 +308,7 @@ onMounted(() => { ...@@ -154,6 +308,7 @@ onMounted(() => {
<el-table-column label="查看图片"> <el-table-column label="查看图片">
<template #default="{ row, $index }"> <template #default="{ row, $index }">
<el-image <el-image
v-if="row.imageUrl"
style="width: 50px; height: 50px" style="width: 50px; height: 50px"
:src="row.imageUrl" :src="row.imageUrl"
:zoom-rate="1.2" :zoom-rate="1.2"
...@@ -165,11 +320,17 @@ onMounted(() => { ...@@ -165,11 +320,17 @@ onMounted(() => {
preview-teleported preview-teleported
hide-on-click-modal hide-on-click-modal
/> />
<span v-else class="text-gray-400">无图片</span>
</template>
</el-table-column>
<el-table-column prop="recognizeType" label="识别方式">
<template #default="{ row }">
<span>{{ row.recognizeType === 1 ? '自动识别' : row.recognizeType === 2 ? '手输' : '-' }}</span>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column prop="isAbnormal" label="是否异常"> <el-table-column prop="isAbnormal" label="是否异常">
<template #default="{ row }"> <template #default="{ row }">
<span :class="{'text-red-500 font-bold': row.isAbnormal > 0}">{{ row.isAbnormal }}</span> <span :class="{'text-red-500 font-bold': row.isAbnormal === 1}">{{ row.isAbnormal === 1 ? '是' : '否' }}</span>
</template> </template>
</el-table-column> </el-table-column>
</el-table> </el-table>
...@@ -187,22 +348,35 @@ onMounted(() => { ...@@ -187,22 +348,35 @@ onMounted(() => {
</div> </div>
</template> </template>
<div class="grid grid-cols-1 md:grid-cols-3 lg:grid-cols-4 gap-4">
<div v-for="video in orderDetail.videos" :key="video.id" class="border rounded-lg overflow-hidden relative group"> <div v-if="orderDetail.videos.length > 0" class="space-y-2">
<!-- Video Thumbnail --> <div
<div class="aspect-video bg-black relative flex items-center justify-center cursor-pointer" @click="playVideo(video.videoUrl)"> v-for="(video, index) in orderDetail.videos"
<img :src="video.thumbnailUrl" class="w-full h-full object-cover opacity-80 hover:opacity-100 transition" /> :key="video.id"
<el-icon class="absolute text-white text-5xl opacity-80 group-hover:opacity-100 group-hover:scale-110 transition"><VideoPlay /></el-icon> class="flex items-center justify-between p-3 border rounded hover:bg-gray-50 transition"
</div> >
<!-- Video Footer --> <div class="flex items-center gap-3 flex-1">
<div class="p-2 bg-gray-50 flex justify-between items-center"> <el-icon class="text-blue-500"><VideoPlay /></el-icon>
<span class="text-sm truncate w-2/3" :title="video.name">{{ video.name }}</span> <span
<el-button link type="primary" :icon="Download" @click="downloadVideo(video.videoUrl)">下载</el-button> class="text-blue-600 hover:text-blue-800 cursor-pointer hover:underline"
@click="playVideo(video.videoUrl)"
>
视频{{ index + 1 }}{{ video.name ? ' - ' + video.name : '' }}
</span>
</div> </div>
<el-button
v-if="video.videoUrl"
link
type="primary"
:icon="Download"
@click="downloadVideo(video.videoUrl)"
>
下载
</el-button>
</div> </div>
</div> </div>
<div v-if="orderDetail.videos.length === 0" class="text-center text-gray-400 py-8"> <div v-else class="text-center text-gray-400 py-8">
暂无视频 暂无视频
</div> </div>
</el-card> </el-card>
......
<script setup lang="ts"> <script setup lang="ts">
import { ref, reactive } from 'vue' import { ref, reactive } from 'vue'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { Search, Download, View, Edit, Warning } from '@element-plus/icons-vue' import { Search, Download } from '@element-plus/icons-vue'
import type { Order, OrderQuery } from '../types/order' import type { Order, OrderQuery, DetailRecord, ApiResponse } from '../types/order'
import { generateMockOrders } from '../mock/orderData' import { qualityApi } from '../api'
import { ElMessage, ElMessageBox } from 'element-plus' import { ElMessage, ElMessageBox } from 'element-plus'
// --- State --- // --- State ---
...@@ -29,32 +29,201 @@ const queryForm = reactive<OrderQuery>({ ...@@ -29,32 +29,201 @@ const queryForm = reactive<OrderQuery>({
pageSize: 10 pageSize: 10
}) })
const total = ref(100) // Mock total const total = ref(0)
// --- Methods --- // --- Methods ---
const fetchData = () => { const fetchData = async () => {
loading.value = true loading.value = true
setTimeout(() => { try {
tableData.value = generateMockOrders(queryForm.pageSize) // 构建接口参数,将前端筛选条件映射到后端接口参数
const params: any = {
pageNum: queryForm.page,
pageSize: queryForm.pageSize
}
// 业务账号 -> accNbr
if (queryForm.businessAccount) {
params.accNbr = queryForm.businessAccount
}
// Apply_id
if (queryForm.applyId) {
params.applyId = queryForm.applyId
}
// 工单ID -> orderId
if (queryForm.orderId) {
params.orderId = queryForm.orderId
}
// 师傅工号 -> campaignId (根据实际业务逻辑,这里假设师傅工号对应 campaignId)
if (queryForm.workerId) {
params.campaignId = queryForm.workerId
}
// 所属地市 -> areaType
if (queryForm.city) {
params.areaType = queryForm.city
}
// 质检状态 -> checkStatus
if (queryForm.status) {
params.checkStatus = queryForm.status
}
// 质检时间范围 -> startTime, endTime
if (queryForm.dateRange && queryForm.dateRange.length === 2) {
params.startTime = queryForm.dateRange[0]
params.endTime = queryForm.dateRange[1]
}
// 无法拍摄次数 -> noShowNumType
if (queryForm.noPhotoCountMin !== undefined && queryForm.noPhotoCountMin > 0) {
params.noShowNumType = queryForm.noPhotoCountMin
}
// 手动输入次数 -> manualInputNumType
if (queryForm.manualInputCountMin !== undefined && queryForm.manualInputCountMin > 0) {
params.manualInputNumType = queryForm.manualInputCountMin
}
// 环境异常次数 -> cheatNumType
if (queryForm.envAbnormalCountMin !== undefined && queryForm.envAbnormalCountMin > 0) {
params.cheatNumType = queryForm.envAbnormalCountMin
}
// 是否异常 -> isCheat
if (queryForm.isAbnormal === 'abnormal') {
params.isCheat = 1
} else if (queryForm.isAbnormal === 'normal') {
params.isCheat = 0
}
const response = await qualityApi.getQualityCheckList(params) as ApiResponse<any>
if (response.code === 200 || response.code === 0) {
const data = response.data || { records: [], total: 0 }
// 将后端数据映射到前端 Order 类型
tableData.value = (data.records || []).map((item: any) => ({
id: item.applyId, // 使用 applyId 作为唯一 id
applyId: item.applyId,
workerId: item.campaignId,
businessAccount: item.accNbr,
orderIds: [], // 工单ID列表需要单独查询或从其他字段获取
city: item.areaName,
status: item.checkStatusDesc || getStatusText(item.checkStatus),
cannotQcReason: item.failReason || '',
startTime: item.startTime || '',
endTime: item.endTime || '',
noPhotoCount: item.noShowNum || 0,
manualInputCount: item.manualInputNum || 0,
envAbnormalCount: item.cheatNum || 0,
isCheating: item.isCheat === 1,
cheatingReason: '',
cheatingRemark: '',
cheatingTime: ''
}))
total.value = data.total || 0
}
} catch (error) {
console.error('获取质检工单列表失败:', error)
// 错误提示已在 request.ts 中统一处理
} finally {
loading.value = false loading.value = false
}, 500) }
}
// 辅助函数:将数字状态转换为文字
const getStatusText = (status: number): string => {
const statusMap: Record<number, string> = {
0: '未开始',
1: '未开始',
2: '进行中',
3: '已完成',
4: '无法质检'
}
return statusMap[status] || '未知'
} }
const handleSearch = () => { const handleSearch = () => {
queryForm.page = 1 queryForm.page = 1
fetchData() fetchData()
} }
const handleReset = () => { const handleReset = () => {
// Reset logic // Reset all form fields
queryForm.businessAccount = '' queryForm.businessAccount = ''
queryForm.applyId = '' queryForm.applyId = ''
// ... others queryForm.orderId = ''
queryForm.workerId = ''
queryForm.city = ''
queryForm.status = ''
queryForm.dateRange = undefined
queryForm.noPhotoCountMin = undefined
queryForm.manualInputCountMin = undefined
queryForm.envAbnormalCountMin = undefined
queryForm.isAbnormal = ''
queryForm.page = 1
handleSearch() handleSearch()
} }
const handleExport = () => { const handleExport = async () => {
ElMessage.success('正在导出Excel...') try {
loading.value = true
// 使用相同的查询参数进行导出
const params: any = {
pageNum: queryForm.page,
pageSize: queryForm.pageSize
}
if (queryForm.businessAccount) params.accNbr = queryForm.businessAccount
if (queryForm.applyId) params.applyId = queryForm.applyId
if (queryForm.orderId) params.orderId = queryForm.orderId
if (queryForm.workerId) params.campaignId = queryForm.workerId
if (queryForm.city) params.areaType = queryForm.city
if (queryForm.status) params.checkStatus = queryForm.status
if (queryForm.dateRange && queryForm.dateRange.length === 2) {
params.startTime = queryForm.dateRange[0]
params.endTime = queryForm.dateRange[1]
}
if (queryForm.noPhotoCountMin !== undefined && queryForm.noPhotoCountMin > 0) {
params.noShowNumType = queryForm.noPhotoCountMin
}
if (queryForm.manualInputCountMin !== undefined && queryForm.manualInputCountMin > 0) {
params.manualInputNumType = queryForm.manualInputCountMin
}
if (queryForm.envAbnormalCountMin !== undefined && queryForm.envAbnormalCountMin > 0) {
params.cheatNumType = queryForm.envAbnormalCountMin
}
if (queryForm.isAbnormal === 'abnormal') {
params.isCheat = 1
} else if (queryForm.isAbnormal === 'normal') {
params.isCheat = 0
}
const response = await qualityApi.exportQualityCheckList(params)
// 处理文件下载
const blob = new Blob([response], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' })
const url = window.URL.createObjectURL(blob)
const link = document.createElement('a')
link.href = url
link.download = `质检工单列表_${new Date().getTime()}.xlsx`
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
window.URL.revokeObjectURL(url)
ElMessage.success('导出成功')
} catch (error) {
console.error('导出失败:', error)
// 错误提示已在 request.ts 中统一处理
} finally {
loading.value = false
}
} }
// Table Handlers // Table Handlers
...@@ -91,40 +260,100 @@ const markCheatingForm = reactive({ ...@@ -91,40 +260,100 @@ const markCheatingForm = reactive({
// --- Actions --- // --- Actions ---
// 1. Order IDs // 1. Order IDs
const openIdsDialog = (row: Order) => { const openIdsDialog = async (row: Order) => {
currentOrder.value = row currentOrder.value = row
try {
// 调用接口获取工单设备列表
const response = await qualityApi.getDevicesByApplyId({
applyId: row.applyId
}) as ApiResponse<any>
if (response.code === 200 || response.code === 0) {
// 更新当前订单的工单设备列表
const devices = response.data || []
// 将设备列表映射为包含工单号和设备串号的对象数组
currentOrder.value.orderIds = devices.map((device: any) => ({
orderCode: device.orderCode || '',
deviceNumber: device.deviceNumber || ''
}))
}
} catch (error) {
console.error('获取工单设备列表失败:', error)
// 错误提示已在 request.ts 中统一处理
}
dialogVisible.orderIds = true dialogVisible.orderIds = true
} }
// 2. Numeric Details Popup // 2. Numeric Details Popup
const openDetailsDialog = (row: Order, type: 'noPhoto' | 'manual' | 'env') => { const openDetailsDialog = async (row: Order, type: 'noPhoto' | 'manual' | 'env') => {
currentOrder.value = row currentOrder.value = row
detailsType.value = type detailsType.value = type
// 设置标题
if (type === 'noPhoto') { if (type === 'noPhoto') {
detailsTitle.value = '无法拍摄明细' detailsTitle.value = '无法拍摄明细'
// Mock details
detailsData.value = [
{ id: 1, process: '光猫识别', reason: '光线太暗', time: '2023-10-01 10:05:00' },
{ id: 2, process: '机顶盒识别', reason: '设备遮挡', time: '2023-10-01 10:10:00' }
]
} else if (type === 'manual') { } else if (type === 'manual') {
detailsTitle.value = '手动输入明细' detailsTitle.value = '手动输入明细'
detailsData.value = [
{ id: 1, process: 'SN码输入', time: '2023-10-01 10:15:00' }
]
} else { } else {
detailsTitle.value = '环境异常明细' detailsTitle.value = '环境异常明细'
detailsData.value = [
{ id: 1, process: '背景检测', reason: '疑似非用户家', time: '2023-10-01 10:00:00' }
]
} }
try {
// 调用接口获取流程明细列表
const response = await qualityApi.getProcessDetailList({
applyId: row.applyId
}) as ApiResponse<any[]>
if (response.code === 200 || response.code === 0) {
const data = response.data || []
// 根据类型过滤数据
// 这里假设后端返回的数据包含 type 字段来区分不同类型
// 实际字段名需要根据后端接口文档调整
detailsData.value = data.map((item: any, index: number) => ({
id: index + 1,
process: item.processName || item.process || '',
reason: item.reason || '',
time: item.createTime || item.time || ''
}))
} else {
detailsData.value = []
}
} catch (error) {
console.error('获取流程明细失败:', error)
detailsData.value = []
// 错误提示已在 request.ts 中统一处理
}
dialogVisible.details = true dialogVisible.details = true
} }
// 3. Cheating Info Popup // 3. Cheating Info Popup
const openCheatingInfo = (row: Order) => { const openCheatingInfo = async (row: Order) => {
currentOrder.value = row currentOrder.value = row
try {
// 调用接口获取作弊标记详情
const response = await qualityApi.getCheatMarkDetail({
applyId: row.applyId
}) as ApiResponse<any>
if (response.code === 200 || response.code === 0) {
// 更新当前订单的作弊信息
if (response.data && currentOrder.value) {
currentOrder.value.cheatingReason = response.data.cause || row.cheatingReason
currentOrder.value.cheatingRemark = response.data.causeOther || row.cheatingRemark
currentOrder.value.cheatingTime = response.data.createTime || row.cheatingTime
}
}
} catch (error) {
console.error('获取作弊详情失败:', error)
// 即使获取失败也显示弹窗,使用现有数据
}
dialogVisible.cheatingInfo = true dialogVisible.cheatingInfo = true
} }
...@@ -135,14 +364,42 @@ const openCannotQcDialog = (row: Order) => { ...@@ -135,14 +364,42 @@ const openCannotQcDialog = (row: Order) => {
cannotQcForm.reason = '' cannotQcForm.reason = ''
dialogVisible.cannotQc = true dialogVisible.cannotQc = true
} }
const submitCannotQc = () => { const submitCannotQc = async () => {
if (!cannotQcForm.type || !cannotQcForm.reason) { if (!cannotQcForm.type || !cannotQcForm.reason) {
ElMessage.warning('请填写完整信息') ElMessage.warning('请填写完整信息')
return return
} }
if (!currentOrder.value) {
ElMessage.warning('未选择工单')
return
}
try {
// 将原因类型映射为对应的 checkStatus
// 个人原因: 2, 用户原因: 3, 其他: 4
let checkStatus = 4 // 默认为"其他"
if (cannotQcForm.type === '个人原因') {
checkStatus = 2
} else if (cannotQcForm.type === '用户原因') {
checkStatus = 3
}
await qualityApi.unCheckByOpt({
applyId: currentOrder.value.applyId,
checkStatus,
failReason: cannotQcForm.reason
})
ElMessage.success('提交成功') ElMessage.success('提交成功')
dialogVisible.cannotQc = false dialogVisible.cannotQc = false
// Refresh data logic here
// 刷新列表
fetchData()
} catch (error) {
console.error('无法质检提交失败:', error)
// 错误提示已在 request.ts 中统一处理
}
} }
// 5. Mark Cheating Action // 5. Mark Cheating Action
...@@ -152,19 +409,41 @@ const openMarkCheating = (row: Order) => { ...@@ -152,19 +409,41 @@ const openMarkCheating = (row: Order) => {
markCheatingForm.remark = '' markCheatingForm.remark = ''
dialogVisible.markCheating = true dialogVisible.markCheating = true
} }
const submitMarkCheating = () => { const submitMarkCheating = async () => {
if (!markCheatingForm.reason) { if (!markCheatingForm.reason) {
ElMessage.warning('请选择异常原因') ElMessage.warning('请选择异常原因')
return return
} }
if (!currentOrder.value) {
ElMessage.warning('未选择工单')
return
}
try {
// 调用标记作弊接口, markType=1 表示标记作弊
await qualityApi.markCheat({
applyId: currentOrder.value.applyId,
markType: 1,
cause: markCheatingForm.reason,
causeOther: markCheatingForm.remark || undefined
})
ElMessage.success('标记成功') ElMessage.success('标记成功')
dialogVisible.markCheating = false dialogVisible.markCheating = false
if (currentOrder.value) currentOrder.value.isCheating = true
// 刷新列表
fetchData()
} catch (error) {
console.error('标记作弊失败:', error)
// 错误提示已在 request.ts 中统一处理
}
} }
// 6. Cancel Cheating Action // 6. Cancel Cheating Action
const openCancelCheating = (row: Order) => { const openCancelCheating = async (row: Order) => {
ElMessageBox.confirm( try {
await ElMessageBox.confirm(
'确认要取消该工单的作弊标记吗?', '确认要取消该工单的作弊标记吗?',
'取消作弊', '取消作弊',
{ {
...@@ -172,10 +451,25 @@ const openCancelCheating = (row: Order) => { ...@@ -172,10 +451,25 @@ const openCancelCheating = (row: Order) => {
cancelButtonText: '取消', cancelButtonText: '取消',
type: 'warning', type: 'warning',
} }
).then(() => { )
ElMessage.success('取消成功')
row.isCheating = false // 调用取消作弊接口, markType=2 表示取消作弊标记
await qualityApi.markCheat({
applyId: row.applyId,
markType: 2
}) })
ElMessage.success('取消成功')
// 刷新列表
fetchData()
} catch (error: any) {
// 用户取消操作时不显示错误
if (error !== 'cancel') {
console.error('取消作弊失败:', error)
// 错误提示已在 request.ts 中统一处理
}
}
} }
// Initial Fetch // Initial Fetch
...@@ -207,10 +501,10 @@ fetchData() ...@@ -207,10 +501,10 @@ fetchData()
<el-form-item label="质检状态"> <el-form-item label="质检状态">
<el-select v-model="queryForm.status" placeholder="请选择" clearable class="!w-48"> <el-select v-model="queryForm.status" placeholder="请选择" clearable class="!w-48">
<el-option label="全部" value="" /> <el-option label="全部" value="" />
<el-option label="未开始" value="未开始" /> <el-option label="未开始" :value="1" />
<el-option label="进行中" value="进行中" /> <el-option label="进行中" :value="2" />
<el-option label="已完成" value="已完成" /> <el-option label="已完成" :value="3" />
<el-option label="无法质检" value="无法质检" /> <el-option label="无法质检" :value="4" />
</el-select> </el-select>
</el-form-item> </el-form-item>
<el-form-item label="质检时间"> <el-form-item label="质检时间">
...@@ -251,23 +545,23 @@ fetchData() ...@@ -251,23 +545,23 @@ fetchData()
<!-- Table Area --> <!-- Table Area -->
<el-card shadow="never"> <el-card shadow="never">
<el-table :data="tableData" v-loading="loading" border style="width: 100%"> <el-table :data="tableData" v-loading="loading" border style="width: 100%">
<el-table-column prop="applyId" label="Apply_id" min-width="120" /> <el-table-column prop="applyId" label="Apply_id" min-width="120" show-overflow-tooltip />
<el-table-column prop="workerId" label="师傅工号" width="100" /> <el-table-column prop="workerId" label="师傅工号" width="100" show-overflow-tooltip />
<el-table-column prop="businessAccount" label="业务账号" min-width="120" /> <el-table-column prop="businessAccount" label="业务账号" min-width="120" show-overflow-tooltip />
<el-table-column label="工单ID" width="100"> <el-table-column label="工单ID" width="100">
<template #default="{ row }"> <template #default="{ row }">
<el-button link type="primary" @click="openIdsDialog(row)">查看</el-button> <el-button link type="primary" @click="openIdsDialog(row)">查看</el-button>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column prop="city" label="所属地市" width="100" /> <el-table-column prop="city" label="所属地市" width="100" show-overflow-tooltip />
<el-table-column prop="status" label="质检状态" width="100"> <el-table-column prop="status" label="质检状态" width="100">
<template #default="{ row }"> <template #default="{ row }">
<el-tag :type="row.status === '已完成' ? 'success' : row.status === '无法质检' ? 'info' : ''">{{ row.status }}</el-tag> <el-tag :type="row.status === '已完成' ? 'success' : row.status === '无法质检' ? 'info' : ''">{{ row.status }}</el-tag>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column prop="cannotQcReason" label="无法质检原因" min-width="120" show-overflow-tooltip /> <el-table-column prop="cannotQcReason" label="无法质检原因" min-width="120" show-overflow-tooltip />
<el-table-column prop="startTime" label="开始时间" width="160" /> <el-table-column prop="startTime" label="开始时间" width="160" show-overflow-tooltip />
<el-table-column prop="endTime" label="结束时间" width="160" /> <el-table-column prop="endTime" label="结束时间" width="160" show-overflow-tooltip />
<!-- Numeric Columns with Sort --> <!-- Numeric Columns with Sort -->
<el-table-column prop="noPhotoCount" label="无法拍摄" sortable width="110"> <el-table-column prop="noPhotoCount" label="无法拍摄" sortable width="110">
...@@ -310,8 +604,8 @@ fetchData() ...@@ -310,8 +604,8 @@ fetchData()
<el-table-column label="操作" width="220" fixed="right"> <el-table-column label="操作" width="220" fixed="right">
<template #default="{ row }"> <template #default="{ row }">
<el-button link type="primary" @click="router.push(`/order-detail/${row.id}`)">详情</el-button> <el-button link type="primary" @click="router.push({ path: `/order-detail/${row.applyId}`, query: { data: JSON.stringify(row) } })">详情</el-button>
<el-button link type="warning" @click="openCannotQcDialog(row)">无法质检</el-button> <el-button link type="warning" v-if="row.status !== '已完成' && row.status !== '无法质检'" @click="openCannotQcDialog(row)">无法质检</el-button>
<el-button link type="danger" v-if="!row.isCheating" @click="openMarkCheating(row)">标记作弊</el-button> <el-button link type="danger" v-if="!row.isCheating" @click="openMarkCheating(row)">标记作弊</el-button>
<el-button link type="info" v-else @click="openCancelCheating(row)">取消作弊</el-button> <el-button link type="info" v-else @click="openCancelCheating(row)">取消作弊</el-button>
</template> </template>
...@@ -331,12 +625,14 @@ fetchData() ...@@ -331,12 +625,14 @@ fetchData()
</el-card> </el-card>
<!-- Dialog: Order IDs --> <!-- Dialog: Order IDs -->
<el-dialog v-model="dialogVisible.orderIds" title="工单ID列表" width="400px"> <el-dialog v-model="dialogVisible.orderIds" title="工单设备列表" width="600px">
<div v-if="currentOrder"> <div v-if="currentOrder">
<p class="mb-2"><strong>Apply_id:</strong> {{ currentOrder.applyId }}</p> <p class="mb-2"><strong>Apply_id:</strong> {{ currentOrder.applyId }}</p>
<p class="mb-4"><strong>业务账号:</strong> {{ currentOrder.businessAccount }}</p> <p class="mb-4"><strong>业务账号:</strong> {{ currentOrder.businessAccount }}</p>
<el-table :data="currentOrder.orderIds.map(id => ({ id }))" border stripe max-height="300"> <el-table :data="currentOrder.orderIds" border stripe max-height="300">
<el-table-column prop="id" label="工单ID" /> <el-table-column type="index" label="序号" width="60" align="center" />
<el-table-column prop="orderCode" label="工单号" show-overflow-tooltip />
<el-table-column prop="deviceNumber" label="设备串号" show-overflow-tooltip />
</el-table> </el-table>
</div> </div>
</el-dialog> </el-dialog>
...@@ -354,9 +650,9 @@ fetchData() ...@@ -354,9 +650,9 @@ fetchData()
<!-- Dialog: Cheating Info --> <!-- Dialog: Cheating Info -->
<el-dialog v-model="dialogVisible.cheatingInfo" title="疑似作弊详情" width="400px"> <el-dialog v-model="dialogVisible.cheatingInfo" title="疑似作弊详情" width="400px">
<div v-if="currentOrder"> <div v-if="currentOrder">
<p class="mb-2"><strong>疑似作弊原因:</strong> {{ currentOrder.cheatingReason }}</p> <p class="mb-2"><strong>疑似作弊原因:</strong> {{ currentOrder.cheatingReason || '无' }}</p>
<p class="mb-2"><strong>备注:</strong> {{ currentOrder.cheatingRemark || '无' }}</p> <p class="mb-2"><strong>备注:</strong> {{ currentOrder.cheatingRemark || '无' }}</p>
<p class="mb-2"><strong>标记时间:</strong> {{ currentOrder.cheatingTime }}</p> <p class="mb-2"><strong>标记时间:</strong> {{ currentOrder.cheatingTime || '无' }}</p>
</div> </div>
</el-dialog> </el-dialog>
...@@ -389,8 +685,9 @@ fetchData() ...@@ -389,8 +685,9 @@ fetchData()
<el-form label-width="80px"> <el-form label-width="80px">
<el-form-item label="异常原因" required> <el-form-item label="异常原因" required>
<el-select v-model="markCheatingForm.reason" class="w-full"> <el-select v-model="markCheatingForm.reason" class="w-full">
<el-option value="在家质检" label="在家质检" /> <el-option value="疑似不在用户家质检" label="疑似不在用户家质检" />
<el-option value="一直点击无法质检" label="一直点击无法质检" /> <el-option value="一直点击无法拍摄跳过" label="一直点击无法拍摄跳过" />
<el-option value="识别屏幕/识别白纸" label="识别屏幕/识别白纸" />
<el-option value="其他" label="其他" /> <el-option value="其他" label="其他" />
</el-select> </el-select>
</el-form-item> </el-form-item>
...@@ -411,4 +708,28 @@ fetchData() ...@@ -411,4 +708,28 @@ fetchData()
margin-right: 16px; margin-right: 16px;
margin-bottom: 16px; margin-bottom: 16px;
} }
/* 表格单元格不换行,超出显示省略号 */
:deep(.el-table) {
.el-table__cell {
/* 防止单元格内容换行 */
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
/* 固定表格行高 */
.el-table__row {
height: 50px;
}
/* 单元格内容垂直居中 */
.cell {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
line-height: 50px;
}
}
</style> </style>
<script setup lang="ts"> <script setup lang="ts">
import { ref, reactive } from 'vue' import { ref, reactive, onMounted } from 'vue'
import { Search, Download, Picture, VideoPlay } from '@element-plus/icons-vue' import { Search, Download, Picture, VideoPlay } from '@element-plus/icons-vue'
import { ElMessage } from 'element-plus'
import { statsApi, qualityApi } from '../../api'
import type { ApiResponse } from '../../types/order'
// 获取昨天的日期范围
const getYesterdayRange = () => {
const yesterday = new Date()
yesterday.setDate(yesterday.getDate() - 1)
yesterday.setHours(0, 0, 0, 0)
const start = new Date(yesterday)
const end = new Date(yesterday)
end.setHours(23, 59, 59, 999)
return [start, end]
}
const queryForm = reactive({ const queryForm = reactive({
dateRange: [], dateRange: getYesterdayRange(),
page: 1, page: 1,
pageSize: 10 pageSize: 10
}) })
const deviceStats = ref([ const loading = ref(false)
{ name: '工服', t1: 120, t1p: 40, t2: 80, t2p: 25, t3: 50, t3p: 15, t4: 20, t4p: 6, t5: 10, t5p: 3, t6: 5, t6p: 1.5, t6plus: 15, t6plus_p: 4.5 }, const deviceStats = ref<any[]>([])
{ name: '光猫', t1: 100, t1p: 33, t2: 90, t2p: 30, t3: 60, t3p: 20, t4: 30, t4p: 10, t5: 10, t5p: 3, t6: 5, t6p: 1.5, t6plus: 5, t6plus_p: 1.5 },
{ name: '机顶盒', t1: 150, t1p: 50, t2: 60, t2p: 20, t3: 40, t3p: 13, t4: 20, t4p: 6, t5: 10, t5p: 3, t6: 5, t6p: 1.5, t6plus: 15, t6plus_p: 5 }, // 格式化日期为 YYYY-MM-DD
]) const formatDate = (date: Date): string => {
const year = date.getFullYear()
const month = String(date.getMonth() + 1).padStart(2, '0')
const day = String(date.getDate()).padStart(2, '0')
return `${year}-${month}-${day}`
}
// 获取设备识别统计数据
const fetchDeviceStats = async () => {
loading.value = true
try {
const params: any = {}
// 处理日期范围
if (queryForm.dateRange && queryForm.dateRange.length === 2) {
const start = queryForm.dateRange[0]
const end = queryForm.dateRange[1]
if (start && end) {
params.startDate = formatDate(new Date(start))
params.endDate = formatDate(new Date(end))
}
}
const response = await statsApi.getProcessRecognizeStatistics(params) as ApiResponse<any>
if (response.code === 200 || response.code === 0) {
deviceStats.value = response.data || []
}
} catch (error) {
console.error('获取设备识别统计失败:', error)
} finally {
loading.value = false
}
}
// 查询按钮
const handleQuery = () => {
fetchDeviceStats()
}
// Dialogs // Dialogs
const exportImgDialog = ref(false) const exportImgDialog = ref(false)
const videoDialog = ref(false) const videoDialog = ref(false)
// 导出图片表单
const exportImgForm = reactive({
dateRange: getYesterdayRange(),
deviceTypes: [] as string[]
})
// 设备类型列表
const deviceTypeList = [
'工服', '工牌', '宽带账号', '普通光猫', '主光猫', '子光猫',
'机顶盒', '电视画面', '电视软终端', '路由器', '室内摄像头',
'室外摄像头', '云电脑终端', '云电脑显示器', 'POE交换机',
'POE面板', '故障投诉光猫灯', '故障投诉主光猫灯', '环境'
]
// 格式化日期时间为 yyyy-MM-dd HH:mm:ss
const formatDateTime = (date: Date): string => {
const year = date.getFullYear()
const month = String(date.getMonth() + 1).padStart(2, '0')
const day = String(date.getDate()).padStart(2, '0')
const hours = String(date.getHours()).padStart(2, '0')
const minutes = String(date.getMinutes()).padStart(2, '0')
const seconds = String(date.getSeconds()).padStart(2, '0')
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`
}
// 导出明细
const handleExportDetail = async () => {
try {
const params: any = {}
// 处理日期范围
if (queryForm.dateRange && queryForm.dateRange.length === 2) {
const start = queryForm.dateRange[0]
const end = queryForm.dateRange[1]
if (start && end) {
params.startDate = formatDate(new Date(start))
params.endDate = formatDate(new Date(end))
}
}
// 调用导出接口 (request.ts 会自动处理 JSON 错误和 401)
const blob = await qualityApi.exportProcessRecognizeStatistics(params)
// 创建下载链接
const url = window.URL.createObjectURL(blob)
const link = document.createElement('a')
link.href = url
// 生成文件名
const dateStr = formatDate(new Date())
link.download = `设备识别统计明细_${dateStr}.xlsx`
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
window.URL.revokeObjectURL(url)
ElMessage.success('导出成功')
} catch (error) {
// request.ts 已经显示了错误消息,这里只需要记录日志
console.error('导出明细失败:', error)
}
}
// 导出图片
const handleExportImages = async () => {
try {
const params: any = {
deviceTypes: exportImgForm.deviceTypes
}
// 处理日期范围
if (exportImgForm.dateRange && exportImgForm.dateRange.length === 2) {
const start = exportImgForm.dateRange[0]
const end = exportImgForm.dateRange[1]
if (start && end) {
params.startTime = formatDateTime(new Date(start))
// 设置结束时间为当天的 23:59:59
const endDate = new Date(end)
endDate.setHours(23, 59, 59, 999)
params.endTime = formatDateTime(endDate)
}
}
// 调用导出接口 (request.ts 会自动处理 JSON 错误和 401)
const blob = await qualityApi.exportLLMResultList(params)
// 创建下载链接
const url = window.URL.createObjectURL(blob)
const link = document.createElement('a')
link.href = url
// 生成文件名
const dateStr = formatDate(new Date())
link.download = `设备识别数据_${dateStr}.xlsx`
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
window.URL.revokeObjectURL(url)
ElMessage.success('导出成功')
exportImgDialog.value = false
} catch (error) {
// request.ts 已经显示了错误消息,这里只需要记录日志
console.error('导出图片失败:', error)
}
}
// 视频查询表单
const videoQueryForm = reactive({
dateRange: getYesterdayRange(),
applyId: '',
page: 1,
pageSize: 10
})
// 视频列表数据
const videoList = ref<any[]>([])
const videoTotal = ref(0)
const videoLoading = ref(false)
// 当前播放的视频URL
const currentVideoUrl = ref('')
const videoPlayerDialog = ref(false)
// 获取视频列表
const fetchVideoList = async () => {
videoLoading.value = true
try {
const params: any = {
page: videoQueryForm.page,
pageSize: videoQueryForm.pageSize
}
// 处理日期范围
if (videoQueryForm.dateRange && videoQueryForm.dateRange.length === 2) {
const start = videoQueryForm.dateRange[0]
const end = videoQueryForm.dateRange[1]
if (start && end) {
params.startDate = formatDateTime(new Date(start))
// 设置结束时间为当天的 23:59:59
const endDate = new Date(end)
endDate.setHours(23, 59, 59, 999)
params.endDate = formatDateTime(endDate)
}
}
// applyId
if (videoQueryForm.applyId) {
params.applyId = videoQueryForm.applyId
}
const response = await statsApi.getVideoList(params) as ApiResponse<any>
if (response.code === 200 || response.code === 0) {
const data = response.data || {}
videoList.value = data.records || []
videoTotal.value = data.total || 0
}
} catch (error) {
console.error('获取视频列表失败:', error)
} finally {
videoLoading.value = false
}
}
// 查询视频
const handleVideoQuery = () => {
videoQueryForm.page = 1
fetchVideoList()
}
// 分页改变
const handleVideoPageChange = (page: number) => {
videoQueryForm.page = page
fetchVideoList()
}
const handleVideoSizeChange = (size: number) => {
videoQueryForm.pageSize = size
videoQueryForm.page = 1
fetchVideoList()
}
// 播放视频
const playVideo = (url: string) => {
currentVideoUrl.value = url
videoPlayerDialog.value = true
}
onMounted(() => {
// 默认查询昨天的数据
fetchDeviceStats()
})
</script> </script>
<template> <template>
<div class="device-stats-page"> <div class="device-stats-page" v-loading="loading">
<!-- Filter --> <!-- Filter -->
<el-card shadow="never" class="mb-4"> <el-card shadow="never" class="mb-4">
<el-form :inline="true" :model="queryForm"> <el-form :inline="true" :model="queryForm">
...@@ -35,8 +289,8 @@ const videoDialog = ref(false) ...@@ -35,8 +289,8 @@ const videoDialog = ref(false)
/> />
</el-form-item> </el-form-item>
<el-form-item> <el-form-item>
<el-button type="primary" :icon="Search">查询</el-button> <el-button type="primary" :icon="Search" @click="handleQuery">查询</el-button>
<el-button type="success" :icon="Download">导出明细</el-button> <el-button type="success" :icon="Download" @click="handleExportDetail">导出明细</el-button>
<el-button :icon="Picture" @click="exportImgDialog = true">导出图片</el-button> <el-button :icon="Picture" @click="exportImgDialog = true">导出图片</el-button>
<el-button :icon="VideoPlay" @click="videoDialog = true">查看视频</el-button> <el-button :icon="VideoPlay" @click="videoDialog = true">查看视频</el-button>
...@@ -47,40 +301,40 @@ const videoDialog = ref(false) ...@@ -47,40 +301,40 @@ const videoDialog = ref(false)
<!-- Table --> <!-- Table -->
<el-card shadow="never"> <el-card shadow="never">
<el-table :data="deviceStats" border stripe> <el-table :data="deviceStats" border stripe>
<el-table-column prop="name" label="设备名称" fixed width="120" /> <el-table-column prop="processName" label="设备名称" fixed width="150" />
<el-table-column prop="t1" label="识别1次" align="center" sortable> <el-table-column prop="tryNum1" label="识别1次" align="center" sortable>
<template #default="{ row }"> <template #default="{ row }">
<div>{{ row.t1 }}</div><div class="text-xs text-gray-400">{{ row.t1p }}%</div> <div>{{ row.tryNum1 || 0 }}</div>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column prop="t2" label="识别2次" align="center" sortable> <el-table-column prop="tryNum2" label="识别2次" align="center" sortable>
<template #default="{ row }"> <template #default="{ row }">
<div>{{ row.t2 }}</div><div class="text-xs text-gray-400">{{ row.t2p }}%</div> <div>{{ row.tryNum2 || 0 }}</div>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column prop="t3" label="识别3次" align="center" sortable> <el-table-column prop="tryNum3" label="识别3次" align="center" sortable>
<template #default="{ row }"> <template #default="{ row }">
<div>{{ row.t3 }}</div><div class="text-xs text-gray-400">{{ row.t3p }}%</div> <div>{{ row.tryNum3 || 0 }}</div>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column prop="t4" label="识别4次" align="center" sortable> <el-table-column prop="tryNum4" label="识别4次" align="center" sortable>
<template #default="{ row }"> <template #default="{ row }">
<div>{{ row.t4 }}</div><div class="text-xs text-gray-400">{{ row.t4p }}%</div> <div>{{ row.tryNum4 || 0 }}</div>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column prop="t5" label="识别5次" align="center" sortable> <el-table-column prop="tryNum5" label="识别5次" align="center" sortable>
<template #default="{ row }"> <template #default="{ row }">
<div>{{ row.t5 }}</div><div class="text-xs text-gray-400">{{ row.t5p }}%</div> <div>{{ row.tryNum5 || 0 }}</div>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column prop="t6" label="识别6次" align="center" sortable> <el-table-column prop="tryNumGte6" label="≥6次" align="center" sortable>
<template #default="{ row }"> <template #default="{ row }">
<div>{{ row.t6 }}</div><div class="text-xs text-gray-400">{{ row.t6p }}%</div> <div>{{ row.tryNumGte6 || 0 }}</div>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column prop="t6plus" label=">6次" align="center" sortable> <el-table-column prop="totalCount" label="总识别次数" align="center" sortable>
<template #default="{ row }"> <template #default="{ row }">
<div>{{ row.t6plus }}</div><div class="text-xs text-gray-400">{{ row.t6plus_p }}%</div> <div class="font-bold text-blue-600">{{ row.totalCount || 0 }}</div>
</template> </template>
</el-table-column> </el-table-column>
</el-table> </el-table>
...@@ -88,37 +342,104 @@ const videoDialog = ref(false) ...@@ -88,37 +342,104 @@ const videoDialog = ref(false)
<!-- Export Image Dialog --> <!-- Export Image Dialog -->
<el-dialog v-model="exportImgDialog" title="导出图片配置" width="500px"> <el-dialog v-model="exportImgDialog" title="导出图片配置" width="500px">
<el-form label-width="100px"> <el-form label-width="100px" :model="exportImgForm">
<el-form-item label="日期范围"> <el-form-item label="日期范围">
<el-date-picker type="datetimerange" /> <el-date-picker
v-model="exportImgForm.dateRange"
type="datetimerange"
range-separator="至"
start-placeholder="开始时间"
end-placeholder="结束时间"
style="width: 100%"
/>
</el-form-item> </el-form-item>
<el-form-item label="选择设备"> <el-form-item label="选择设备">
<el-select placeholder="请选择"> <el-select
<el-option label="工服" value="工服" /> v-model="exportImgForm.deviceTypes"
<el-option label="光猫" value="光猫" /> placeholder="请选择设备"
multiple
collapse-tags
collapse-tags-tooltip
style="width: 100%"
>
<el-option
v-for="device in deviceTypeList"
:key="device"
:label="device"
:value="device"
/>
</el-select> </el-select>
</el-form-item> </el-form-item>
<el-form-item label="导出数量">
<el-input-number :min="1" />
</el-form-item>
</el-form> </el-form>
<template #footer> <template #footer>
<el-button @click="exportImgDialog = false">取消</el-button> <el-button @click="exportImgDialog = false">取消</el-button>
<el-button type="primary">确定导出</el-button> <el-button type="primary" @click="handleExportImages">确定导出</el-button>
</template> </template>
</el-dialog> </el-dialog>
<!-- Video List Dialog Placeholder --> <!-- Video List Dialog -->
<el-dialog v-model="videoDialog" title="查看视频" width="800px"> <el-dialog v-model="videoDialog" title="查看视频" width="1000px" @open="fetchVideoList">
<el-form :inline="true"> <el-form :inline="true" :model="videoQueryForm">
<el-form-item label="日期"><el-date-picker type="datetimerange" /></el-form-item> <el-form-item label="日期">
<el-form-item label="地区"><el-select placeholder="请选择"><el-option label="南京" value="南京"/></el-select></el-form-item> <el-date-picker
<el-form-item><el-button type="primary">查询</el-button></el-form-item> v-model="videoQueryForm.dateRange"
type="datetimerange"
range-separator="至"
start-placeholder="开始时间"
end-placeholder="结束时间"
/>
</el-form-item>
<el-form-item label="Apply ID">
<el-input v-model="videoQueryForm.applyId" placeholder="请输入Apply ID" clearable style="width: 200px" />
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleVideoQuery">查询</el-button>
</el-form-item>
</el-form> </el-form>
<el-table :data="[]" border height="400">
<el-table-column label="视频ID" /> <el-table :data="videoList" border height="400" v-loading="videoLoading">
<el-table-column label="链接" /> <el-table-column prop="applyId" label="Apply ID" width="200" />
<el-table-column label="视频链接" min-width="300">
<template #default="{ row }">
<div v-if="row.videoUtlList && row.videoUtlList.length > 0" class="flex flex-wrap gap-2">
<el-button
v-for="(url, index) in row.videoUtlList"
:key="index"
link
type="primary"
size="small"
@click="playVideo(url)"
>
视频{{ index + 1 }}
</el-button>
</div>
<span v-else class="text-gray-400">暂无视频</span>
</template>
</el-table-column>
</el-table> </el-table>
<div class="mt-4 flex justify-end">
<el-pagination
v-model:current-page="videoQueryForm.page"
v-model:page-size="videoQueryForm.pageSize"
:page-sizes="[10, 20, 50, 100]"
:total="videoTotal"
layout="total, sizes, prev, pager, next, jumper"
@size-change="handleVideoSizeChange"
@current-change="handleVideoPageChange"
/>
</div>
</el-dialog>
<!-- Video Player Dialog -->
<el-dialog v-model="videoPlayerDialog" title="视频播放" width="800px">
<video v-if="currentVideoUrl" :src="currentVideoUrl" controls style="width: 100%; max-height: 500px;">
您的浏览器不支持视频播放
</video>
<div v-else class="text-center text-gray-400 py-8">
暂无视频
</div>
</el-dialog> </el-dialog>
</div> </div>
</template> </template>
...@@ -2,52 +2,256 @@ ...@@ -2,52 +2,256 @@
import { ref, reactive, onMounted } from 'vue' import { ref, reactive, onMounted } from 'vue'
import { Search, Download } from '@element-plus/icons-vue' import { Search, Download } from '@element-plus/icons-vue'
import * as echarts from 'echarts' import * as echarts from 'echarts'
import { statsApi } from '../../api'
import type { ApiResponse } from '../../types/order'
// 获取昨天的日期范围
const getYesterdayRange = () => {
const yesterday = new Date()
yesterday.setDate(yesterday.getDate() - 1)
yesterday.setHours(0, 0, 0, 0)
const start = new Date(yesterday)
const end = new Date(yesterday)
end.setHours(23, 59, 59, 999)
return [start, end]
}
// 格式化日期时间为 yyyy-MM-dd HH:mm:ss
const formatDateTime = (date: Date): string => {
const year = date.getFullYear()
const month = String(date.getMonth() + 1).padStart(2, '0')
const day = String(date.getDate()).padStart(2, '0')
const hours = String(date.getHours()).padStart(2, '0')
const minutes = String(date.getMinutes()).padStart(2, '0')
const seconds = String(date.getSeconds()).padStart(2, '0')
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`
}
const queryForm = reactive({ const queryForm = reactive({
dateRange: [], dateRange: getYesterdayRange() as any[],
city: '' city: ''
}) })
const cities = ['南京市', '无锡市', '徐州市', '常州市', '苏州市', '南通市', '连云港市', '淮安市', '盐城市', '扬州市', '镇江市', '泰州市', '宿迁市'] const loading = ref(false)
const cityTableData = ref([ const cities = [
{ city: '南京市', count1: 100, count1p: '50%', count2: 50, count2p: '25%', count3: 30, count3p: '15%', count4: 20, count4p: '10%', total: 200, rate: '2.5%' }, { code: '3201', name: '南京市' },
{ city: '苏州市', count1: 90, count1p: '45%', count2: 60, count2p: '30%', count3: 30, count3p: '15%', count4: 20, count4p: '10%', total: 200, rate: '2.2%' }, { code: '3202', name: '无锡市' },
]) { code: '3203', name: '徐州市' },
{ code: '3204', name: '常州市' },
{ code: '3205', name: '苏州市' },
{ code: '3206', name: '南通市' },
{ code: '3207', name: '连云港市' },
{ code: '3208', name: '淮安市' },
{ code: '3209', name: '盐城市' },
{ code: '3210', name: '扬州市' },
{ code: '3211', name: '镇江市' },
{ code: '3212', name: '泰州市' },
{ code: '3213', name: '宿迁市' }
]
const deviceTableData = ref([ const cityTableData = ref<any[]>([])
{ device: '光猫', count1: 50, count1p: '50%', count2: 25, count2p: '25%', count3: 15, count3p: '15%', count4: 10, count4p: '10%', total: 100, rate: '1.2%' }, const deviceTableData = ref<any[]>([])
])
const chartRef = ref<HTMLElement>() const chartRef = ref<HTMLElement>()
let chart: echarts.ECharts | null = null
// 获取地市统计数据
const fetchCityStats = async () => {
loading.value = true
try {
const params: any = {}
// 处理日期范围
if (queryForm.dateRange && queryForm.dateRange.length === 2) {
const start = queryForm.dateRange[0]
const end = queryForm.dateRange[1]
if (start && end) {
params.startDate = formatDateTime(new Date(start))
// 设置结束时间为当天的 23:59:59
const endDate = new Date(end)
endDate.setHours(23, 59, 59, 999)
params.endDate = formatDateTime(endDate)
}
}
// 处理地市选择
if (queryForm.city) {
params.areaType = queryForm.city
}
const response = await statsApi.getNoShowCityStatistics(params) as ApiResponse<any>
if (response.code === 200 || response.code === 0) {
cityTableData.value = response.data || []
updateChart()
}
} catch (error) {
console.error('获取地市统计数据失败:', error)
} finally {
loading.value = false
}
}
// 获取设备统计数据
const fetchDeviceStats = async () => {
try {
const params: any = {}
// 处理日期范围
if (queryForm.dateRange && queryForm.dateRange.length === 2) {
const start = queryForm.dateRange[0]
const end = queryForm.dateRange[1]
if (start && end) {
params.startDate = formatDateTime(new Date(start))
// 设置结束时间为当天的 23:59:59
const endDate = new Date(end)
endDate.setHours(23, 59, 59, 999)
params.endDate = formatDateTime(endDate)
}
}
// 处理地市选择
if (queryForm.city) {
params.areaType = queryForm.city
}
const response = await statsApi.getNoShowProcessStatistics(params) as ApiResponse<any>
if (response.code === 200 || response.code === 0) {
deviceTableData.value = response.data || []
}
} catch (error) {
console.error('获取设备统计数据失败:', error)
}
}
// 更新图表
const updateChart = () => {
if (chartRef.value && cityTableData.value.length > 0) {
if (!chart) {
chart = echarts.init(chartRef.value)
}
const cityNames = cityTableData.value.map(item => item.areaName)
const count1Data = cityTableData.value.map(item => item.percentage1 || 0)
const count2Data = cityTableData.value.map(item => item.percentage2 || 0)
const count3Data = cityTableData.value.map(item => item.percentage3 || 0)
const initChart = () => {
if (chartRef.value) {
const chart = echarts.init(chartRef.value)
chart.setOption({ chart.setOption({
title: { text: '地市不能拍次数占比统计' }, title: { text: '地市不能拍次数占比统计' },
tooltip: { trigger: 'axis', axisPointer: { type: 'shadow' } }, tooltip: { trigger: 'axis', axisPointer: { type: 'shadow' } },
legend: { data: ['1次', '2次', '3次', '4次及以上'], bottom: 0 }, legend: { data: ['1次', '2次', '3次'], bottom: 0 },
grid: { left: '3%', right: '4%', bottom: '15%', containLabel: true }, grid: { left: '3%', right: '4%', bottom: '15%', containLabel: true },
xAxis: { type: 'category', data: ['南京', '无锡', '徐州', '常州', '苏州', '南通', '连云港', '淮安', '盐城', '扬州', '镇江', '泰州', '宿迁'], axisLabel: { interval: 0, rotate: 30 } }, xAxis: {
type: 'category',
data: cityNames,
axisLabel: { interval: 0, rotate: 30 }
},
yAxis: { type: 'value', name: '占比%' }, yAxis: { type: 'value', name: '占比%' },
series: [ series: [
{ name: '1次', type: 'bar', label: { show: true, position: 'top' }, data: [50, 45, 60, 55, 40, 50, 60, 50, 55, 45, 60, 50, 40] }, { name: '1次', type: 'bar', data: count1Data },
{ name: '2次', type: 'bar', label: { show: true, position: 'top' }, data: [25, 30, 20, 25, 30, 25, 20, 25, 20, 30, 20, 25, 30] }, { name: '2次', type: 'bar', data: count2Data },
{ name: '3次', type: 'bar', label: { show: true, position: 'top' }, data: [15, 15, 10, 10, 20, 15, 10, 15, 15, 15, 10, 15, 20] }, { name: '3次', type: 'bar', data: count3Data }
{ name: '4次及以上', type: 'bar', label: { show: true, position: 'top' }, data: [10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10] }
] ]
}) })
} }
} }
// 查询
const handleSearch = () => {
fetchCityStats()
fetchDeviceStats()
}
// 导出地市统计
const handleExportCity = async () => {
try {
const params: any = {}
// 处理日期范围
if (queryForm.dateRange && queryForm.dateRange.length === 2) {
const start = queryForm.dateRange[0]
const end = queryForm.dateRange[1]
if (start && end) {
params.startDate = formatDateTime(new Date(start))
// 设置结束时间为当天的 23:59:59
const endDate = new Date(end)
endDate.setHours(23, 59, 59, 999)
params.endDate = formatDateTime(endDate)
}
}
// 处理地市选择
if (queryForm.city) {
params.areaType = queryForm.city
}
const blob = await statsApi.exportNoShowGoupByAreaType(params)
const url = window.URL.createObjectURL(blob)
const link = document.createElement('a')
link.href = url
link.download = `地市不能拍次数统计_${new Date().toISOString().split('T')[0]}.xlsx`
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
window.URL.revokeObjectURL(url)
} catch (error) {
console.error('导出地市统计失败:', error)
}
}
// 导出设备统计
const handleExportDevice = async () => {
try {
const params: any = {}
// 处理日期范围
if (queryForm.dateRange && queryForm.dateRange.length === 2) {
const start = queryForm.dateRange[0]
const end = queryForm.dateRange[1]
if (start && end) {
params.startDate = formatDateTime(new Date(start))
// 设置结束时间为当天的 23:59:59
const endDate = new Date(end)
endDate.setHours(23, 59, 59, 999)
params.endDate = formatDateTime(endDate)
}
}
const blob = await statsApi.exportNoShowProcessStatistics(params)
const url = window.URL.createObjectURL(blob)
const link = document.createElement('a')
link.href = url
link.download = `设备不能拍次数统计_${new Date().toISOString().split('T')[0]}.xlsx`
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
window.URL.revokeObjectURL(url)
} catch (error) {
console.error('导出设备统计失败:', error)
}
}
onMounted(() => { onMounted(() => {
initChart() fetchCityStats()
fetchDeviceStats()
}) })
</script> </script>
<template> <template>
<div class="nophoto-stats-page"> <div class="nophoto-stats-page" v-loading="loading">
<!-- Filter --> <!-- Filter -->
<el-card shadow="never" class="mb-4"> <el-card shadow="never" class="mb-4">
<el-form :inline="true" :model="queryForm"> <el-form :inline="true" :model="queryForm">
...@@ -62,11 +266,11 @@ onMounted(() => { ...@@ -62,11 +266,11 @@ onMounted(() => {
</el-form-item> </el-form-item>
<el-form-item label="地区"> <el-form-item label="地区">
<el-select v-model="queryForm.city" placeholder="请选择" class="!w-40" clearable> <el-select v-model="queryForm.city" placeholder="请选择" class="!w-40" clearable>
<el-option v-for="c in cities" :key="c" :label="c" :value="c" /> <el-option v-for="c in cities" :key="c.code" :label="c.name" :value="c.code" />
</el-select> </el-select>
</el-form-item> </el-form-item>
<el-form-item> <el-form-item>
<el-button type="primary" :icon="Search">查询</el-button> <el-button type="primary" :icon="Search" @click="handleSearch">查询</el-button>
</el-form-item> </el-form-item>
</el-form> </el-form>
</el-card> </el-card>
...@@ -82,25 +286,20 @@ onMounted(() => { ...@@ -82,25 +286,20 @@ onMounted(() => {
<template #header> <template #header>
<div class="flex justify-between"> <div class="flex justify-between">
<span class="font-bold">地市不能拍次数统计</span> <span class="font-bold">地市不能拍次数统计</span>
<el-button link type="primary" :icon="Download">导出</el-button> <el-button link type="primary" :icon="Download" @click="handleExportCity">导出</el-button>
</div> </div>
</template> </template>
<el-table :data="cityTableData" border stripe> <el-table :data="cityTableData" border stripe>
<el-table-column prop="city" label="地市" /> <el-table-column prop="areaName" label="地市" />
<el-table-column label="1次不能拍"> <el-table-column label="1次不能拍">
<template #default="{row}">{{row.count1}} ({{row.count1p}})</template> <template #default="{row}">{{row.count1}} ({{row.percentage1}}%)</template>
</el-table-column> </el-table-column>
<el-table-column label="2次不能拍"> <el-table-column label="2次不能拍">
<template #default="{row}">{{row.count2}} ({{row.count2p}})</template> <template #default="{row}">{{row.count2}} ({{row.percentage2}}%)</template>
</el-table-column> </el-table-column>
<el-table-column label="3次不能拍"> <el-table-column label="3次不能拍">
<template #default="{row}">{{row.count3}} ({{row.count3p}})</template> <template #default="{row}">{{row.count3}} ({{row.percentage3}}%)</template>
</el-table-column> </el-table-column>
<el-table-column label="4次及以上">
<template #default="{row}">{{row.count4}} ({{row.count4p}})</template>
</el-table-column>
<el-table-column prop="total" label="不能拍总次数" sortable />
<el-table-column prop="rate" label="占比(本市)" sortable />
</el-table> </el-table>
</el-card> </el-card>
...@@ -108,25 +307,12 @@ onMounted(() => { ...@@ -108,25 +307,12 @@ onMounted(() => {
<template #header> <template #header>
<div class="flex justify-between"> <div class="flex justify-between">
<span class="font-bold">设备不能拍次数统计</span> <span class="font-bold">设备不能拍次数统计</span>
<el-button link type="primary" :icon="Download">导出</el-button> <el-button link type="primary" :icon="Download" @click="handleExportDevice">导出</el-button>
</div> </div>
</template> </template>
<el-table :data="deviceTableData" border stripe> <el-table :data="deviceTableData" border stripe>
<el-table-column prop="device" label="设备名称" /> <el-table-column prop="processName" label="设备名称" />
<el-table-column label="1次不能拍"> <el-table-column prop="noShowCount" label="不能拍总次数" sortable />
<template #default="{row}">{{row.count1}} ({{row.count1p}})</template>
</el-table-column>
<el-table-column label="2次不能拍">
<template #default="{row}">{{row.count2}} ({{row.count2p}})</template>
</el-table-column>
<el-table-column label="3次不能拍">
<template #default="{row}">{{row.count3}} ({{row.count3p}})</template>
</el-table-column>
<el-table-column label="4次及以上">
<template #default="{row}">{{row.count4}} ({{row.count4p}})</template>
</el-table-column>
<el-table-column prop="total" label="不能拍总次数" sortable />
<el-table-column prop="rate" label="占比(设备)" sortable />
</el-table> </el-table>
</el-card> </el-card>
</div> </div>
......
<script setup lang="ts"> <script setup lang="ts">
import { ref, onMounted, nextTick, watch } from 'vue' import { ref, onMounted, nextTick, watch } from 'vue'
import { Search, Download, DataLine } from '@element-plus/icons-vue' import { Search, Download } from '@element-plus/icons-vue'
import * as echarts from 'echarts' import * as echarts from 'echarts'
import { statsApi } from '../../api'
import type { ApiResponse } from '../../types/order'
const loading = ref(false) const loading = ref(false)
// 地市列表(带code)
const cityList = [
{ code: '3201', name: '南京市' },
{ code: '3202', name: '无锡市' },
{ code: '3203', name: '徐州市' },
{ code: '3204', name: '常州市' },
{ code: '3205', name: '苏州市' },
{ code: '3206', name: '南通市' },
{ code: '3207', name: '连云港市' },
{ code: '3208', name: '淮安市' },
{ code: '3209', name: '盐城市' },
{ code: '3210', name: '扬州市' },
{ code: '3211', name: '镇江市' },
{ code: '3212', name: '泰州市' },
{ code: '3213', name: '宿迁市' }
]
// 获取昨天的日期范围
const getYesterdayRange = () => {
const yesterday = new Date()
yesterday.setDate(yesterday.getDate() - 1)
yesterday.setHours(0, 0, 0, 0)
const start = new Date(yesterday)
const end = new Date(yesterday)
end.setHours(23, 59, 59, 999)
return [start, end]
}
const queryForm = ref({ const queryForm = ref({
dateRange: [] as string[], dateRange: getYesterdayRange(),
city: '' city: ''
}) })
// Statistics Data // Statistics Data
const stats = ref({ const stats = ref({
total: 12580, totalOrders: 0,
completed: 11200, finishOrders: 0,
completedRate: 89.03, resultCount: 0,
qcTotal: 12580, finishResult: 0,
qcCompleted: 11000, complaintTotal: 0,
qcCompletedRate: 87.44, complaintFinish: 0,
complaintTotal: 50, noShowCount: 0,
complaintCompleted: 45, averageDuration: 0
complaintRate: 90,
cannotQc: 200,
cannotQcRate: 1.59
}) })
// --- Constants --- // 地市统计数据
const cityList = ['南京市', '无锡市', '徐州市', '常州市', '苏州市', '南通市', '连云港市', '淮安市', '盐城市', '扬州市', '镇江市', '泰州市', '宿迁市'] const areaTypeList = ref<any[]>([])
const processList = [ // 质检环节统计数据
'工服', '工牌', '账号', '环境', const processStats = ref<any[]>([])
'普通光猫', '普通光猫串号', '主光猫', '主光猫串号',
'子光猫', '子光猫串号', '机顶盒', '机顶盒串号',
'路由器', '路由器串号', '电视画面', '电视软终端串号',
'云电脑终端', '云电脑终端串号', '云电脑', '室内摄像头',
'室内摄像头串号', '室外摄像头', '故障投诉光猫灯', '故障投诉主光猫灯'
]
// --- Refs --- // --- Refs ---
const cityStatsTab = ref('order') // 'order', 'qc', 'complaint', 'cannotQc' const cityStatsTab = ref('order') // 'order', 'qc', 'complaint', 'cannotQc'
const timeStatsTab = ref('dist') // 'dist', 'avg' const timeStatsTab = ref('dist') // 'dist', 'avg'
const processStatsTab = ref('avgCount') // 'avgCount', 'totalCount', 'avgTime' const processStatsTab = ref('avgCount') // 'avgCount', 'totalCount', 'avgTime'
const processLimit = ref(10) // 10, 25, 1000(all) const processLimit = ref(10) // 10, 15, 1000(all)
// Toggle Table Visibility // Toggle Table Visibility
const showCityTable = ref(false) const showCityTable = ref(false)
...@@ -58,49 +82,191 @@ const processAvgCountChartRef = ref<HTMLElement>() ...@@ -58,49 +82,191 @@ const processAvgCountChartRef = ref<HTMLElement>()
const processTotalCountChartRef = ref<HTMLElement>() const processTotalCountChartRef = ref<HTMLElement>()
const processAvgTimeChartRef = ref<HTMLElement>() const processAvgTimeChartRef = ref<HTMLElement>()
// --- Data --- // 格式化日期为 YYYY-MM-DD
const cityTableData = ref(cityList.map(city => ({ const formatDate = (date: Date): string => {
date: '10-01', city, const year = date.getFullYear()
total: 300, totalDone: 250, totalUndone: 50, const month = String(date.getMonth() + 1).padStart(2, '0')
qcTotal: 280, qcDone: 240, qcUndone: 40, const day = String(date.getDate()).padStart(2, '0')
complaintTotal: 10, complaintDone: 8, complaintUndone: 2, return `${year}-${month}-${day}`
cannotQc: 5 }
})))
// 格式化日期时间为 yyyy-MM-dd HH:mm:ss
const timeTableData = ref(cityList.map(city => { const formatDateTime = (date: Date): string => {
const gt5 = Math.floor(Math.random() * 20) const year = date.getFullYear()
const bt45 = Math.floor(Math.random() * 30) const month = String(date.getMonth() + 1).padStart(2, '0')
const bt34 = Math.floor(Math.random() * 50) const day = String(date.getDate()).padStart(2, '0')
const lt3 = Math.floor(Math.random() * 100) const hours = String(date.getHours()).padStart(2, '0')
return { const minutes = String(date.getMinutes()).padStart(2, '0')
date: '10-01', city, const seconds = String(date.getSeconds()).padStart(2, '0')
gt5, bt45, bt34, lt3, return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`
total: gt5 + bt45 + bt34 + lt3, }
avgTime: Math.floor(Math.random() * 100 + 100)
// 获取统计数据
const fetchStats = async () => {
loading.value = true
try {
const params: any = {}
// 处理日期范围
if (queryForm.value.dateRange && queryForm.value.dateRange.length === 2) {
const start = queryForm.value.dateRange[0]
const end = queryForm.value.dateRange[1]
if (start && end) {
params.startDate = formatDateTime(new Date(start))
// 设置结束时间为当天的 23:59:59
const endDate = new Date(end)
endDate.setHours(23, 59, 59, 999)
params.endDate = formatDateTime(endDate)
}
}
// 处理地市选择(传code)
if (queryForm.value.city) {
params.areaType = queryForm.value.city
} }
}))
const processTableData = ref(processList.map((name, i) => ({ // 调用接口
date: '10-01', name, const response = await statsApi.getAllStatisticsByCity(params) as ApiResponse<any>
totalCount: 1000 + i * 100,
avgTime: (5 + Math.random() * 5).toFixed(2), if (response.code === 200 || response.code === 0) {
avgCount: (1 + Math.random()).toFixed(2) const data = response.data || {}
})))
// 更新顶部统计数据
stats.value = {
totalOrders: data.totalOrders || 0,
finishOrders: data.finishOrders || 0,
resultCount: data.resultCount || 0,
finishResult: data.finishResult || 0,
complaintTotal: data.complaintTotal || 0,
complaintFinish: data.complaintFinish || 0,
noShowCount: data.noShowCount || 0,
averageDuration: data.averageDuration || 0
}
// 更新地市统计数据
areaTypeList.value = data.areaTypeList || []
// 重新渲染图表
nextTick(() => {
renderAllCharts()
})
}
} catch (error) {
console.error('获取统计数据失败:', error)
} finally {
loading.value = false
}
}
// 获取质检环节统计数据
const fetchProcessStats = async () => {
try {
const params: any = {}
// 处理日期范围
if (queryForm.value.dateRange && queryForm.value.dateRange.length === 2) {
const start = queryForm.value.dateRange[0]
const end = queryForm.value.dateRange[1]
if (start && end) {
params.startDate = formatDateTime(new Date(start))
// 设置结束时间为当天的 23:59:59
const endDate = new Date(end)
endDate.setHours(23, 59, 59, 999)
params.endDate = formatDateTime(endDate)
}
}
// 处理地市选择
if (queryForm.value.city) {
params.areaType = queryForm.value.city
}
const response = await statsApi.getProcessTotalStatistics(params) as ApiResponse<any>
if (response.code === 200 || response.code === 0) {
processStats.value = response.data || []
// 重新渲染质检环节图表
nextTick(() => {
handleProcessTabChange(processStatsTab.value)
})
}
} catch (error) {
console.error('获取质检环节统计失败:', error)
}
}
// 查询按钮
const handleQuery = () => {
fetchStats()
fetchProcessStats()
}
// 导出表格
const handleExport = async () => {
try {
const params: any = {}
// 处理日期范围
if (queryForm.value.dateRange && queryForm.value.dateRange.length === 2) {
const start = queryForm.value.dateRange[0]
const end = queryForm.value.dateRange[1]
if (start && end) {
params.startDate = formatDate(new Date(start))
params.endDate = formatDate(new Date(end))
}
}
// 处理地市选择
if (queryForm.value.city) {
params.areaType = queryForm.value.city
}
// 调用导出接口
const blob = await statsApi.exportAllStatisticsByCity(params)
// 创建下载链接
const url = window.URL.createObjectURL(blob)
const link = document.createElement('a')
link.href = url
// 生成文件名
const dateStr = params.startDate || formatDate(new Date())
link.download = `质检工单统计_${dateStr}.xlsx`
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
window.URL.revokeObjectURL(url)
} catch (error) {
console.error('导出失败:', error)
}
}
// --- Chart Renderers --- // --- Chart Renderers ---
const renderOrderChart = () => { const renderOrderChart = () => {
if (!orderChartRef.value) return if (!orderChartRef.value) return
const chart = echarts.getInstanceByDom(orderChartRef.value) || echarts.init(orderChartRef.value) const chart = echarts.getInstanceByDom(orderChartRef.value) || echarts.init(orderChartRef.value)
const cities = areaTypeList.value.map(item => item.areaName)
const totalData = areaTypeList.value.map(item => item.orderCount || 0)
const finishData = areaTypeList.value.map(item => item.finishOrders || 0)
const unfinishData = areaTypeList.value.map(item => item.unfinishOrders || 0)
chart.setOption({ chart.setOption({
tooltip: { trigger: 'axis' }, tooltip: { trigger: 'axis' },
legend: { data: ['工单总数', '完成工单数', '未完成工单数'], bottom: 0 }, legend: { data: ['工单总数', '完成工单数', '未完成工单数'], bottom: 0 },
xAxis: { type: 'category', data: cityList, axisLabel: { interval: 0, rotate: 30 } }, xAxis: { type: 'category', data: cities, axisLabel: { interval: 0, rotate: 30 } },
yAxis: { type: 'value' }, yAxis: { type: 'value' },
grid: { left: '3%', right: '4%', bottom: '10%', top: '3%', containLabel: true }, grid: { left: '3%', right: '4%', bottom: '10%', top: '3%', containLabel: true },
series: [ series: [
{ name: '工单总数', type: 'bar', data: cityList.map(() => 300 + Math.random() * 50) }, { name: '工单总数', type: 'bar', data: totalData },
{ name: '完成工单数', type: 'bar', data: cityList.map(() => 250 + Math.random() * 40) }, { name: '完成工单数', type: 'bar', data: finishData },
{ name: '未完成工单数', type: 'bar', data: cityList.map(() => 50 + Math.random() * 10) } { name: '未完成工单数', type: 'bar', data: unfinishData }
] ]
}) })
chart.resize() chart.resize()
...@@ -109,16 +275,22 @@ const renderOrderChart = () => { ...@@ -109,16 +275,22 @@ const renderOrderChart = () => {
const renderQcChart = () => { const renderQcChart = () => {
if (!qcChartRef.value) return if (!qcChartRef.value) return
const chart = echarts.getInstanceByDom(qcChartRef.value) || echarts.init(qcChartRef.value) const chart = echarts.getInstanceByDom(qcChartRef.value) || echarts.init(qcChartRef.value)
const cities = areaTypeList.value.map(item => item.areaName)
const totalData = areaTypeList.value.map(item => item.resultCount || 0)
const finishData = areaTypeList.value.map(item => item.finishResult || 0)
const unfinishData = areaTypeList.value.map(item => item.unfinishResult || 0)
chart.setOption({ chart.setOption({
tooltip: { trigger: 'axis' }, tooltip: { trigger: 'axis' },
legend: { data: ['质检工单总数', '完成质检工单数', '未完成质检工单数'], bottom: 0 }, legend: { data: ['质检工单总数', '完成质检工单数', '未完成质检工单数'], bottom: 0 },
xAxis: { type: 'category', data: cityList, axisLabel: { interval: 0, rotate: 30 } }, xAxis: { type: 'category', data: cities, axisLabel: { interval: 0, rotate: 30 } },
yAxis: { type: 'value' }, yAxis: { type: 'value' },
grid: { left: '3%', right: '4%', bottom: '10%', top: '3%', containLabel: true }, grid: { left: '3%', right: '4%', bottom: '10%', top: '3%', containLabel: true },
series: [ series: [
{ name: '质检工单总数', type: 'bar', data: cityList.map(() => 280 + Math.random() * 40), itemStyle: { color: '#67C23A' } }, { name: '质检工单总数', type: 'bar', data: totalData, itemStyle: { color: '#67C23A' } },
{ name: '完成质检工单数', type: 'bar', data: cityList.map(() => 240 + Math.random() * 30), itemStyle: { color: '#95d475' } }, { name: '完成质检工单数', type: 'bar', data: finishData, itemStyle: { color: '#95d475' } },
{ name: '未完成质检工单数', type: 'bar', data: cityList.map(() => 40 + Math.random() * 10), itemStyle: { color: '#b3e19d' } } { name: '未完成质检工单数', type: 'bar', data: unfinishData, itemStyle: { color: '#b3e19d' } }
] ]
}) })
chart.resize() chart.resize()
...@@ -127,16 +299,22 @@ const renderQcChart = () => { ...@@ -127,16 +299,22 @@ const renderQcChart = () => {
const renderComplaintChart = () => { const renderComplaintChart = () => {
if (!complaintChartRef.value) return if (!complaintChartRef.value) return
const chart = echarts.getInstanceByDom(complaintChartRef.value) || echarts.init(complaintChartRef.value) const chart = echarts.getInstanceByDom(complaintChartRef.value) || echarts.init(complaintChartRef.value)
const cities = areaTypeList.value.map(item => item.areaName)
const totalData = areaTypeList.value.map(item => item.complaintTotal || 0)
const finishData = areaTypeList.value.map(item => item.complaintFinish || 0)
const unfinishData = areaTypeList.value.map(item => item.complaintUnfinish || 0)
chart.setOption({ chart.setOption({
tooltip: { trigger: 'axis' }, tooltip: { trigger: 'axis' },
legend: { data: ['投诉工单总数', '完成投诉工单数', '未完成投诉工单数'], bottom: 0 }, legend: { data: ['投诉工单总数', '完成投诉工单数', '未完成投诉工单数'], bottom: 0 },
xAxis: { type: 'category', data: cityList, axisLabel: { interval: 0, rotate: 30 } }, xAxis: { type: 'category', data: cities, axisLabel: { interval: 0, rotate: 30 } },
yAxis: { type: 'value' }, yAxis: { type: 'value' },
grid: { left: '3%', right: '4%', bottom: '10%', top: '3%', containLabel: true }, grid: { left: '3%', right: '4%', bottom: '10%', top: '3%', containLabel: true },
series: [ series: [
{ name: '投诉工单总数', type: 'bar', data: cityList.map(() => 10 + Math.random() * 5), itemStyle: { color: '#E6A23C' } }, { name: '投诉工单总数', type: 'bar', data: totalData, itemStyle: { color: '#E6A23C' } },
{ name: '完成投诉工单数', type: 'bar', data: cityList.map(() => 8 + Math.random() * 4), itemStyle: { color: '#f3d19e' } }, { name: '完成投诉工单数', type: 'bar', data: finishData, itemStyle: { color: '#f3d19e' } },
{ name: '未完成投诉工单数', type: 'bar', data: cityList.map(() => 2 + Math.random() * 2), itemStyle: { color: '#faecd8' } } { name: '未完成投诉工单数', type: 'bar', data: unfinishData, itemStyle: { color: '#faecd8' } }
] ]
}) })
chart.resize() chart.resize()
...@@ -145,13 +323,17 @@ const renderComplaintChart = () => { ...@@ -145,13 +323,17 @@ const renderComplaintChart = () => {
const renderCannotQcChart = () => { const renderCannotQcChart = () => {
if (!cannotQcChartRef.value) return if (!cannotQcChartRef.value) return
const chart = echarts.getInstanceByDom(cannotQcChartRef.value) || echarts.init(cannotQcChartRef.value) const chart = echarts.getInstanceByDom(cannotQcChartRef.value) || echarts.init(cannotQcChartRef.value)
const cities = areaTypeList.value.map(item => item.areaName)
const data = areaTypeList.value.map(item => item.noShowCount || 0)
chart.setOption({ chart.setOption({
tooltip: { trigger: 'axis' }, tooltip: { trigger: 'axis' },
xAxis: { type: 'category', data: cityList, axisLabel: { interval: 0, rotate: 30 } }, xAxis: { type: 'category', data: cities, axisLabel: { interval: 0, rotate: 30 } },
yAxis: { type: 'value' }, yAxis: { type: 'value' },
grid: { left: '3%', right: '4%', bottom: '3%', top: '3%', containLabel: true }, grid: { left: '3%', right: '4%', bottom: '3%', top: '3%', containLabel: true },
series: [ series: [
{ name: '无法质检数', type: 'bar', data: cityList.map(() => 5 + Math.random() * 5), itemStyle: { color: '#909399' } } { name: '无法质检数', type: 'bar', data: data, itemStyle: { color: '#909399' } }
] ]
}) })
chart.resize() chart.resize()
...@@ -160,17 +342,24 @@ const renderCannotQcChart = () => { ...@@ -160,17 +342,24 @@ const renderCannotQcChart = () => {
const renderTimeDistChart = () => { const renderTimeDistChart = () => {
if (!timeDistChartRef.value) return if (!timeDistChartRef.value) return
const chart = echarts.getInstanceByDom(timeDistChartRef.value) || echarts.init(timeDistChartRef.value) const chart = echarts.getInstanceByDom(timeDistChartRef.value) || echarts.init(timeDistChartRef.value)
const cities = areaTypeList.value.map(item => item.areaName)
const gt5 = areaTypeList.value.map(item => item.moreThan5Min || 0)
const bt45 = areaTypeList.value.map(item => item.moreThan4Min || 0)
const bt34 = areaTypeList.value.map(item => item.moreThan3Min || 0)
const lt3 = areaTypeList.value.map(item => item.lessThan3Min || 0)
chart.setOption({ chart.setOption({
tooltip: { trigger: 'axis', axisPointer: { type: 'shadow' } }, tooltip: { trigger: 'axis', axisPointer: { type: 'shadow' } },
legend: { data: ['>5分钟', '4-5分钟', '3-4分钟', '<3分钟'], bottom: 0 }, legend: { data: ['>=5分钟', '4-5分钟', '3-4分钟', '<3分钟'], bottom: 0 },
xAxis: { type: 'value' }, xAxis: { type: 'value' },
yAxis: { type: 'category', data: cityList }, yAxis: { type: 'category', data: cities },
grid: { left: '3%', right: '4%', bottom: '10%', top: '3%', containLabel: true }, grid: { left: '3%', right: '4%', bottom: '10%', top: '3%', containLabel: true },
series: [ series: [
{ name: '>5分钟', type: 'bar', stack: 'total', data: cityList.map(() => Math.random() * 20) }, { name: '>=5分钟', type: 'bar', stack: 'total', data: gt5 },
{ name: '4-5分钟', type: 'bar', stack: 'total', data: cityList.map(() => Math.random() * 30) }, { name: '4-5分钟', type: 'bar', stack: 'total', data: bt45 },
{ name: '3-4分钟', type: 'bar', stack: 'total', data: cityList.map(() => Math.random() * 40) }, { name: '3-4分钟', type: 'bar', stack: 'total', data: bt34 },
{ name: '<3分钟', type: 'bar', stack: 'total', data: cityList.map(() => Math.random() * 50) } { name: '<3分钟', type: 'bar', stack: 'total', data: lt3 }
] ]
}) })
chart.resize() chart.resize()
...@@ -179,15 +368,19 @@ const renderTimeDistChart = () => { ...@@ -179,15 +368,19 @@ const renderTimeDistChart = () => {
const renderAvgTimeChart = () => { const renderAvgTimeChart = () => {
if (!avgTimeChartRef.value) return if (!avgTimeChartRef.value) return
const chart = echarts.getInstanceByDom(avgTimeChartRef.value) || echarts.init(avgTimeChartRef.value) const chart = echarts.getInstanceByDom(avgTimeChartRef.value) || echarts.init(avgTimeChartRef.value)
const cities = areaTypeList.value.map(item => item.areaName)
const avgTimes = areaTypeList.value.map(item => item.averageDuration || 0)
chart.setOption({ chart.setOption({
tooltip: { trigger: 'axis' }, tooltip: { trigger: 'axis' },
xAxis: { type: 'category', data: cityList, axisLabel: { interval: 0, rotate: 30 } }, xAxis: { type: 'category', data: cities, axisLabel: { interval: 0, rotate: 30 } },
yAxis: { type: 'value', name: '秒' }, yAxis: { type: 'value', name: '秒' },
grid: { left: '3%', right: '4%', bottom: '3%', top: '10%', containLabel: true }, grid: { left: '3%', right: '4%', bottom: '3%', top: '10%', containLabel: true },
series: [ series: [
{ {
name: '平均耗时', type: 'line', name: '平均耗时', type: 'line',
data: cityList.map(() => 150 + Math.random() * 50), data: avgTimes,
markLine: { markLine: {
data: [{ type: 'average', name: '全省平均' }] data: [{ type: 'average', name: '全省平均' }]
} }
...@@ -202,16 +395,18 @@ const renderProcessAvgCountChart = () => { ...@@ -202,16 +395,18 @@ const renderProcessAvgCountChart = () => {
const chart = echarts.getInstanceByDom(processAvgCountChartRef.value) || echarts.init(processAvgCountChartRef.value) const chart = echarts.getInstanceByDom(processAvgCountChartRef.value) || echarts.init(processAvgCountChartRef.value)
const limit = processLimit.value const limit = processLimit.value
const currentData = processList.slice(0, limit) const currentData = processStats.value.slice(0, limit)
const names = currentData.map(item => item.processName)
const avgCounts = currentData.map(item => item.averageInspectionTimes || 0)
chart.setOption({ chart.setOption({
tooltip: { trigger: 'axis' }, tooltip: { trigger: 'axis' },
legend: { data: ['平均识别次数'], bottom: 0 }, legend: { data: ['平均识别次数'], bottom: 0 },
xAxis: { type: 'category', data: currentData, axisLabel: { interval: 0, rotate: 30 } }, xAxis: { type: 'category', data: names, axisLabel: { interval: 0, rotate: 30 } },
yAxis: { type: 'value' }, yAxis: { type: 'value' },
grid: { left: '3%', right: '4%', bottom: '15%', top: '10%', containLabel: true }, grid: { left: '3%', right: '4%', bottom: '15%', top: '10%', containLabel: true },
series: [ series: [
{ name: '平均识别次数', type: 'bar', data: currentData.map(() => 1 + Math.random()), itemStyle: { color: '#409EFF' } } { name: '平均识别次数', type: 'bar', data: avgCounts, itemStyle: { color: '#409EFF' } }
] ]
}, true) }, true)
chart.resize() chart.resize()
...@@ -222,16 +417,18 @@ const renderProcessTotalCountChart = () => { ...@@ -222,16 +417,18 @@ const renderProcessTotalCountChart = () => {
const chart = echarts.getInstanceByDom(processTotalCountChartRef.value) || echarts.init(processTotalCountChartRef.value) const chart = echarts.getInstanceByDom(processTotalCountChartRef.value) || echarts.init(processTotalCountChartRef.value)
const limit = processLimit.value const limit = processLimit.value
const currentData = processList.slice(0, limit) const currentData = processStats.value.slice(0, limit)
const names = currentData.map(item => item.processName)
const totalCounts = currentData.map(item => item.totalCount || 0)
chart.setOption({ chart.setOption({
tooltip: { trigger: 'axis' }, tooltip: { trigger: 'axis' },
legend: { data: ['识别总次数'], bottom: 0 }, legend: { data: ['识别总次数'], bottom: 0 },
xAxis: { type: 'category', data: currentData, axisLabel: { interval: 0, rotate: 30 } }, xAxis: { type: 'category', data: names, axisLabel: { interval: 0, rotate: 30 } },
yAxis: { type: 'value' }, yAxis: { type: 'value' },
grid: { left: '3%', right: '4%', bottom: '15%', top: '10%', containLabel: true }, grid: { left: '3%', right: '4%', bottom: '15%', top: '10%', containLabel: true },
series: [ series: [
{ name: '识别总次数', type: 'bar', data: currentData.map(() => 1000 + Math.random() * 500), itemStyle: { color: '#67C23A' } } { name: '识别总次数', type: 'bar', data: totalCounts, itemStyle: { color: '#67C23A' } }
] ]
}, true) }, true)
chart.resize() chart.resize()
...@@ -242,16 +439,18 @@ const renderProcessAvgTimeChart = () => { ...@@ -242,16 +439,18 @@ const renderProcessAvgTimeChart = () => {
const chart = echarts.getInstanceByDom(processAvgTimeChartRef.value) || echarts.init(processAvgTimeChartRef.value) const chart = echarts.getInstanceByDom(processAvgTimeChartRef.value) || echarts.init(processAvgTimeChartRef.value)
const limit = processLimit.value const limit = processLimit.value
const currentData = processList.slice(0, limit) const currentData = processStats.value.slice(0, limit)
const names = currentData.map(item => item.processName)
const avgTimes = currentData.map(item => item.averageDuration || 0)
chart.setOption({ chart.setOption({
tooltip: { trigger: 'axis' }, tooltip: { trigger: 'axis' },
legend: { data: ['平均耗时(秒)'], bottom: 0 }, legend: { data: ['平均耗时(秒)'], bottom: 0 },
xAxis: { type: 'category', data: currentData, axisLabel: { interval: 0, rotate: 30 } }, xAxis: { type: 'category', data: names, axisLabel: { interval: 0, rotate: 30 } },
yAxis: { type: 'value' }, yAxis: { type: 'value' },
grid: { left: '3%', right: '4%', bottom: '15%', top: '10%', containLabel: true }, grid: { left: '3%', right: '4%', bottom: '15%', top: '10%', containLabel: true },
series: [ series: [
{ name: '平均耗时(秒)', type: 'bar', data: currentData.map(() => 5 + Math.random() * 5), itemStyle: { color: '#E6A23C' } } { name: '平均耗时(秒)', type: 'bar', data: avgTimes, itemStyle: { color: '#E6A23C' } }
] ]
}, true) }, true)
chart.resize() chart.resize()
...@@ -287,20 +486,20 @@ watch(processLimit, () => { ...@@ -287,20 +486,20 @@ watch(processLimit, () => {
handleProcessTabChange(processStatsTab.value) handleProcessTabChange(processStatsTab.value)
}) })
const initCharts = () => { const renderAllCharts = () => {
// Init default active tabs
renderOrderChart() renderOrderChart()
renderTimeDistChart() renderTimeDistChart()
renderProcessAvgCountChart() renderProcessAvgCountChart()
} }
onMounted(() => { onMounted(() => {
initCharts() // 默认查询昨天的数据
handleQuery()
}) })
</script> </script>
<template> <template>
<div class="stats-quality-page"> <div class="stats-quality-page" v-loading="loading">
<!-- Filter --> <!-- Filter -->
<el-card shadow="never" class="mb-4"> <el-card shadow="never" class="mb-4">
<el-form :inline="true" :model="queryForm"> <el-form :inline="true" :model="queryForm">
...@@ -315,38 +514,52 @@ onMounted(() => { ...@@ -315,38 +514,52 @@ onMounted(() => {
</el-form-item> </el-form-item>
<el-form-item label="地区"> <el-form-item label="地区">
<el-select v-model="queryForm.city" placeholder="请选择" class="!w-48" clearable> <el-select v-model="queryForm.city" placeholder="请选择" class="!w-48" clearable>
<el-option v-for="c in cityList" :key="c" :label="c" :value="c" /> <el-option v-for="c in cityList" :key="c.code" :label="c.name" :value="c.code" />
</el-select> </el-select>
</el-form-item> </el-form-item>
<el-form-item> <el-form-item>
<el-button type="primary" :icon="Search">查询</el-button> <el-button type="primary" :icon="Search" @click="handleQuery">查询</el-button>
<el-button type="success" :icon="Download">导出表格</el-button> <el-button type="success" :icon="Download" @click="handleExport">导出表格</el-button>
</el-form-item> </el-form-item>
</el-form> </el-form>
</el-card> </el-card>
<!-- Cards --> <!-- Cards -->
<div class="grid grid-cols-4 gap-4 mb-4"> <div class="grid grid-cols-2 gap-4 mb-4">
<!-- ... keep cards ... -->
<el-card shadow="never" class="bg-blue-50"> <el-card shadow="never" class="bg-blue-50">
<div class="text-gray-500 mb-2">工单总数</div> <div class="text-gray-500 mb-2">工单总数</div>
<div class="text-2xl font-bold">{{ stats.total }}</div> <div class="text-3xl font-bold mb-3">{{ stats.totalOrders }}</div>
<div class="text-sm text-gray-500 mt-2">已完成: {{ stats.completed }} ({{ stats.completedRate }}%)</div> <div class="flex justify-between text-sm">
<span class="text-gray-600">已完成工单数</span>
<span class="font-bold text-blue-600">{{ stats.finishOrders }}</span>
</div>
</el-card> </el-card>
<el-card shadow="never" class="bg-green-50"> <el-card shadow="never" class="bg-green-50">
<div class="text-gray-500 mb-2">质检工单总数</div> <div class="text-gray-500 mb-2">质检工单总数</div>
<div class="text-2xl font-bold text-green-600">{{ stats.qcTotal }}</div> <div class="text-3xl font-bold text-green-600 mb-3">{{ stats.resultCount }}</div>
<div class="text-sm text-gray-500 mt-2">已完成: {{ stats.qcCompleted }} ({{ stats.qcCompletedRate }}%)</div> <div class="flex justify-between text-sm">
<span class="text-gray-600">已完成质检工单数</span>
<span class="font-bold text-green-600">{{ stats.finishResult }}</span>
</div>
</el-card> </el-card>
<el-card shadow="never" class="bg-orange-50"> <el-card shadow="never" class="bg-orange-50">
<div class="text-gray-500 mb-2">投诉工单总数</div> <div class="text-gray-500 mb-2">投诉工单总数</div>
<div class="text-2xl font-bold text-orange-600">{{ stats.complaintTotal }}</div> <div class="text-3xl font-bold text-orange-600 mb-3">{{ stats.complaintTotal }}</div>
<div class="text-sm text-gray-500 mt-2">已完成: {{ stats.complaintCompleted }} ({{ stats.complaintRate }}%)</div> <div class="flex justify-between text-sm">
<span class="text-gray-600">已完成投诉工单数</span>
<span class="font-bold text-orange-600">{{ stats.complaintFinish }}</span>
</div>
</el-card> </el-card>
<el-card shadow="never" class="bg-gray-50">
<el-card shadow="never" class="bg-purple-50">
<div class="text-gray-500 mb-2">无法质检数</div> <div class="text-gray-500 mb-2">无法质检数</div>
<div class="text-2xl font-bold text-gray-600">{{ stats.cannotQc }}</div> <div class="text-3xl font-bold text-purple-600 mb-3">{{ stats.noShowCount }}</div>
<div class="text-sm text-gray-500 mt-2">占比: {{ stats.cannotQcRate }}%</div> <div class="flex justify-between text-sm">
<span class="text-gray-600">平均耗时</span>
<span class="font-bold text-purple-600">{{ stats.averageDuration }}</span>
</div>
</el-card> </el-card>
</div> </div>
...@@ -362,25 +575,24 @@ onMounted(() => { ...@@ -362,25 +575,24 @@ onMounted(() => {
</template> </template>
<!-- Table --> <!-- Table -->
<el-table v-show="showCityTable" :data="cityTableData" border stripe height="250" class="mb-4"> <el-table v-show="showCityTable" :data="areaTypeList" border stripe height="250" class="mb-4">
<el-table-column prop="date" label="日期" width="120" /> <el-table-column prop="areaName" label="地区" width="100" />
<el-table-column prop="city" label="地区" width="100" />
<el-table-column label="工单总数" align="center"> <el-table-column label="工单总数" align="center">
<el-table-column prop="total" label="总数" sortable /> <el-table-column prop="orderCount" label="总数" sortable />
<el-table-column prop="totalDone" label="已完成" /> <el-table-column prop="finishOrders" label="已完成" />
<el-table-column prop="totalUndone" label="未完成" /> <el-table-column prop="unfinishOrders" label="未完成" />
</el-table-column> </el-table-column>
<el-table-column label="质检工单数" align="center"> <el-table-column label="质检工单数" align="center">
<el-table-column prop="qcTotal" label="总数" sortable /> <el-table-column prop="resultCount" label="总数" sortable />
<el-table-column prop="qcDone" label="已完成" /> <el-table-column prop="finishResult" label="已完成" />
<el-table-column prop="qcUndone" label="未完成" /> <el-table-column prop="unfinishResult" label="未完成" />
</el-table-column> </el-table-column>
<el-table-column label="投诉工单数" align="center"> <el-table-column label="投诉工单数" align="center">
<el-table-column prop="complaintTotal" label="总数" sortable /> <el-table-column prop="complaintTotal" label="总数" sortable />
<el-table-column prop="complaintDone" label="已完成" /> <el-table-column prop="complaintFinish" label="已完成" />
<el-table-column prop="complaintUndone" label="未完成" /> <el-table-column prop="complaintUnfinish" label="未完成" />
</el-table-column> </el-table-column>
<el-table-column prop="cannotQc" label="无法质检数" sortable /> <el-table-column prop="noShowCount" label="无法质检数" sortable />
</el-table> </el-table>
<el-tabs v-model="cityStatsTab" class="mb-4" @tab-change="handleTabChange"> <el-tabs v-model="cityStatsTab" class="mb-4" @tab-change="handleTabChange">
...@@ -410,15 +622,14 @@ onMounted(() => { ...@@ -410,15 +622,14 @@ onMounted(() => {
</div> </div>
</template> </template>
<el-table v-show="showTimeTable" :data="timeTableData" border stripe height="250" class="mb-4"> <el-table v-show="showTimeTable" :data="areaTypeList" border stripe height="250" class="mb-4">
<el-table-column prop="date" label="日期" width="120" /> <el-table-column prop="areaName" label="地市" width="100" />
<el-table-column prop="city" label="地市" width="100" /> <el-table-column prop="lessThan3Min" label="<3分钟" sortable />
<el-table-column prop="gt5" label=">5分钟" sortable /> <el-table-column prop="moreThan3Min" label="3-4分钟" sortable />
<el-table-column prop="bt45" label="4≤x<5分钟" sortable /> <el-table-column prop="moreThan4Min" label="4-5分钟" sortable />
<el-table-column prop="bt34" label="3≤x<4分钟" sortable /> <el-table-column prop="moreThan5Min" label=">=5分钟" sortable />
<el-table-column prop="lt3" label="<3分钟" sortable /> <el-table-column prop="totalDuration" label="总耗时(s)" sortable />
<el-table-column prop="total" label="总计" sortable /> <el-table-column prop="averageDuration" label="平均耗时(s)" sortable />
<el-table-column prop="avgTime" label="平均耗时(s)" sortable />
</el-table> </el-table>
<el-tabs v-model="timeStatsTab" class="mb-4" @tab-change="handleTimeTabChange"> <el-tabs v-model="timeStatsTab" class="mb-4" @tab-change="handleTimeTabChange">
...@@ -431,7 +642,7 @@ onMounted(() => { ...@@ -431,7 +642,7 @@ onMounted(() => {
</el-tabs> </el-tabs>
</el-card> </el-card>
<!-- 3. Process Stats Section (New) --> <!-- 3. Process Stats Section -->
<el-card shadow="never" class="mb-4"> <el-card shadow="never" class="mb-4">
<template #header> <template #header>
<div class="flex justify-between items-center"> <div class="flex justify-between items-center">
...@@ -443,12 +654,11 @@ onMounted(() => { ...@@ -443,12 +654,11 @@ onMounted(() => {
</template> </template>
<!-- Table --> <!-- Table -->
<el-table v-show="showProcessTable" :data="processTableData" border stripe height="250" class="mb-4"> <el-table v-show="showProcessTable" :data="processStats" border stripe height="250" class="mb-4">
<el-table-column prop="date" label="日期" width="120" /> <el-table-column prop="processName" label="环节名称" />
<el-table-column prop="name" label="环节名称" /> <el-table-column prop="averageInspectionTimes" label="平均识别数" sortable />
<el-table-column prop="totalCount" label="识别总次数" sortable /> <el-table-column prop="totalCount" label="识别总数" sortable />
<el-table-column prop="avgTime" label="平均耗时(s)" sortable /> <el-table-column prop="averageDuration" label="平均耗时(s)" sortable />
<el-table-column prop="avgCount" label="平均识别次数" sortable />
</el-table> </el-table>
<div class="relative"> <div class="relative">
......
...@@ -3,40 +3,54 @@ import { ref, reactive, onMounted, computed, nextTick, watch } from 'vue' ...@@ -3,40 +3,54 @@ import { ref, reactive, onMounted, computed, nextTick, watch } from 'vue'
import { Search, Download } from '@element-plus/icons-vue' import { Search, Download } from '@element-plus/icons-vue'
import * as echarts from 'echarts' import * as echarts from 'echarts'
import dayjs from 'dayjs' import dayjs from 'dayjs'
import { statsApi } from '../../api'
import type { ApiResponse } from '../../types/order'
// 获取昨天的日期范围
const getYesterdayRange = () => {
const yesterday = new Date()
yesterday.setDate(yesterday.getDate() - 1)
yesterday.setHours(0, 0, 0, 0)
const start = new Date(yesterday)
const end = new Date(yesterday)
end.setHours(23, 59, 59, 999)
return [start, end]
}
const queryForm = reactive({ const queryForm = reactive({
dateRange: [] as string[], dateRange: getYesterdayRange() as any[],
page: 1, page: 1,
pageSize: 10 pageSize: 10
}) })
// Detailed Serial Device List const loading = ref(false)
const deviceList = [
'普通光猫串号', '机顶盒串号', '路由器串号', '从光猫串号', // 格式化日期时间为 yyyy-MM-dd HH:mm:ss
'主光猫串号', '室内安防串号', '软终端串号', '云电脑终端串号', 'POE交换机串号' const formatDateTime = (date: Date): string => {
] const year = date.getFullYear()
const month = String(date.getMonth() + 1).padStart(2, '0')
const day = String(date.getDate()).padStart(2, '0')
const hours = String(date.getHours()).padStart(2, '0')
const minutes = String(date.getMinutes()).padStart(2, '0')
const seconds = String(date.getSeconds()).padStart(2, '0')
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`
}
// Summary data
const summary = ref({ const summary = ref({
total: 5000, totalSubmitCount: 0,
autoRate: 85.5, autoRecognizeRate: 0,
pass1: 70.2, passRate1: 0,
pass2: 15.3, passRateWithin3: 0,
pass3: 10.1, avgDuration: 0,
avgTime: 12.5, avgRecognizeTimes: 0
avgAttempts: 1.4
}) })
const tableData = ref(deviceList.map(name => ({ // Data lists
name, const datesList = ref<any[]>([])
total: Math.floor(Math.random() * 1000 + 500), const processList = ref<any[]>([])
pass1: (Math.random() * 20 + 60).toFixed(1) + '%',
pass2: (Math.random() * 10 + 10).toFixed(1) + '%',
pass3: (Math.random() * 5 + 5).toFixed(1) + '%',
pass3in: '95%',
autoRate: (Math.random() * 15 + 75).toFixed(1) + '%',
avgCount: (Math.random() * 0.5 + 1).toFixed(1),
avgTime: (Math.random() * 5 + 8).toFixed(1) + 's'
})))
// Computed property to check if date range > 1 day // Computed property to check if date range > 1 day
const isMultiDay = computed(() => { const isMultiDay = computed(() => {
...@@ -50,9 +64,7 @@ const isMultiDay = computed(() => { ...@@ -50,9 +64,7 @@ const isMultiDay = computed(() => {
const trendTab = ref('overall') // 'overall', 'details' const trendTab = ref('overall') // 'overall', 'details'
const passRateChartRef = ref<HTMLElement>() const passRateChartRef = ref<HTMLElement>()
const autoRateChartRef = ref<HTMLElement>()
const trendChartRef = ref<HTMLElement>() const trendChartRef = ref<HTMLElement>()
const deviceTrendChartRef = ref<HTMLElement>()
// New Trend Charts Refs // New Trend Charts Refs
const overallPassRateTrendRef = ref<HTMLElement>() const overallPassRateTrendRef = ref<HTMLElement>()
...@@ -68,6 +80,57 @@ let overallAutoRateChart: echarts.ECharts | null = null ...@@ -68,6 +80,57 @@ let overallAutoRateChart: echarts.ECharts | null = null
let devicePassRateChart: echarts.ECharts | null = null let devicePassRateChart: echarts.ECharts | null = null
let deviceAutoRateChart: echarts.ECharts | null = null let deviceAutoRateChart: echarts.ECharts | null = null
// 获取统计数据
const fetchStats = async () => {
loading.value = true
try {
const params: any = {}
// 处理日期范围
if (queryForm.dateRange && queryForm.dateRange.length === 2) {
const start = queryForm.dateRange[0]
const end = queryForm.dateRange[1]
if (start && end) {
params.startDate = formatDateTime(new Date(start))
// 设置结束时间为当天的 23:59:59
const endDate = new Date(end)
endDate.setHours(23, 59, 59, 999)
params.endDate = formatDateTime(endDate)
}
}
const response = await statsApi.getSNStatisticsSummary(params) as ApiResponse<any>
if (response.code === 200 || response.code === 0) {
const data = response.data || {}
// 更新顶部统计数据
summary.value = {
totalSubmitCount: data.totalSubmitCount || 0,
autoRecognizeRate: data.autoRecognizeRate || 0,
passRate1: data.passRate1 || 0,
passRateWithin3: data.passRateWithin3 || 0,
avgDuration: data.avgDuration || 0,
avgRecognizeTimes: data.avgRecognizeTimes || 0
}
// 更新列表数据
datesList.value = data.datesList || []
processList.value = data.processList || []
// 重新渲染图表
nextTick(() => {
updateView()
})
}
} catch (error) {
console.error('获取串号统计数据失败:', error)
} finally {
loading.value = false
}
}
/* -------------------------------------------------------------------------- */ /* -------------------------------------------------------------------------- */
/* SINGLE DAY CHARTS */ /* SINGLE DAY CHARTS */
...@@ -75,36 +138,44 @@ let deviceAutoRateChart: echarts.ECharts | null = null ...@@ -75,36 +138,44 @@ let deviceAutoRateChart: echarts.ECharts | null = null
const initSingleDayCharts = () => { const initSingleDayCharts = () => {
// 1. Device Pass Rate Bar Chart // 1. Device Pass Rate Bar Chart
if (passRateChartRef.value) { if (passRateChartRef.value && processList.value.length > 0) {
passRateChart = echarts.getInstanceByDom(passRateChartRef.value) || echarts.init(passRateChartRef.value) passRateChart = echarts.getInstanceByDom(passRateChartRef.value) || echarts.init(passRateChartRef.value)
const deviceNames = processList.value.map(item => item.processName)
const pass1Data = processList.value.map(item => item.passRate1 || 0)
const pass3Data = processList.value.map(item => item.passRate3 || 0)
passRateChart.setOption({ passRateChart.setOption({
title: { text: '设备识别通过率' }, title: { text: '设备识别通过率' },
tooltip: { trigger: 'axis' }, tooltip: { trigger: 'axis' },
legend: { data: ['1次通过', '2次通过', '3次通过'], bottom: 0 }, legend: { data: ['1次通过', '3次通过'], bottom: 0 },
grid: { left: '3%', right: '4%', bottom: '15%', containLabel: true }, grid: { left: '3%', right: '4%', bottom: '15%', containLabel: true },
xAxis: { type: 'category', data: deviceList, axisLabel: { interval: 0, rotate: 30 } }, xAxis: { type: 'category', data: deviceNames, axisLabel: { interval: 0, rotate: 30 } },
yAxis: { type: 'value', name: '%' }, yAxis: { type: 'value', name: '%' },
series: [ series: [
{ name: '1次通过', type: 'bar', data: deviceList.map(() => Math.random() * 20 + 60) }, { name: '1次通过', type: 'bar', data: pass1Data },
{ name: '2次通过', type: 'bar', data: deviceList.map(() => Math.random() * 10 + 10) }, { name: '3次通过', type: 'bar', data: pass3Data }
{ name: '3次通过', type: 'bar', data: deviceList.map(() => Math.random() * 5 + 5) }
] ]
}) })
passRateChart.resize() passRateChart.resize()
} }
// 2. Device Auto Identify Rate Chart (Single Day) // 2. Device Auto Identify Rate Chart (Single Day)
if (trendChartRef.value) { if (trendChartRef.value && processList.value.length > 0) {
trendChart = echarts.getInstanceByDom(trendChartRef.value) || echarts.init(trendChartRef.value) trendChart = echarts.getInstanceByDom(trendChartRef.value) || echarts.init(trendChartRef.value)
const deviceNames = processList.value.map(item => item.processName)
const autoRateData = processList.value.map(item => item.autoRecognizeRate || 0)
trendChart.setOption({ trendChart.setOption({
title: { text: '设备自动识别通过率' }, title: { text: '设备自动识别通过率' },
tooltip: { trigger: 'axis' }, tooltip: { trigger: 'axis' },
legend: { data: ['自动识别率'], bottom: 0 }, legend: { data: ['自动识别率'], bottom: 0 },
xAxis: { type: 'category', data: deviceList, axisLabel: { interval: 0, rotate: 30 } }, xAxis: { type: 'category', data: deviceNames, axisLabel: { interval: 0, rotate: 30 } },
yAxis: { type: 'value', name: '%' }, yAxis: { type: 'value', name: '%' },
grid: { left: '3%', right: '4%', bottom: '15%', containLabel: true }, grid: { left: '3%', right: '4%', bottom: '15%', containLabel: true },
series: [ series: [
{ name: '自动识别率', type: 'bar', data: deviceList.map(() => Math.random() * 10 + 80), itemStyle: { color: '#67C23A' } } { name: '自动识别率', type: 'bar', data: autoRateData, itemStyle: { color: '#67C23A' } }
] ]
}) })
trendChart.resize() trendChart.resize()
...@@ -117,73 +188,69 @@ const initSingleDayCharts = () => { ...@@ -117,73 +188,69 @@ const initSingleDayCharts = () => {
/* -------------------------------------------------------------------------- */ /* -------------------------------------------------------------------------- */
const initMultiDayCharts = () => { const initMultiDayCharts = () => {
// Generate dates based on selection, or mock if not selected
let dates = []
if (queryForm.dateRange && queryForm.dateRange.length === 2) {
let current = dayjs(queryForm.dateRange[0])
const end = dayjs(queryForm.dateRange[1])
while(current.isBefore(end) || current.isSame(end, 'day')) {
dates.push(current.format('YYYY-MM-DD'))
current = current.add(1, 'day')
}
// Limit to avoid too many points for mock
if(dates.length > 20) dates = dates.slice(0, 20)
} else {
// Default 7 days
for(let i=0; i<7; i++) dates.push(dayjs().subtract(6-i, 'day').format('YYYY-MM-DD'))
}
if (trendTab.value === 'overall') { if (trendTab.value === 'overall') {
// 1. Overall Pass Rate Trend: Stacked or Multi-Line for 1st, 2nd, 3rd pass rates // 1. Overall Pass Rate Trend
if (overallPassRateTrendRef.value) { if (overallPassRateTrendRef.value && datesList.value.length > 0) {
overallPassRateChart = echarts.getInstanceByDom(overallPassRateTrendRef.value) || echarts.init(overallPassRateTrendRef.value) overallPassRateChart = echarts.getInstanceByDom(overallPassRateTrendRef.value) || echarts.init(overallPassRateTrendRef.value)
const dates = datesList.value.map(item => item.date)
const pass1Data = datesList.value.map(item => item.passRate1 || 0)
const pass3Data = datesList.value.map(item => item.passRate3 || 0)
overallPassRateChart.setOption({ overallPassRateChart.setOption({
title: { text: '整体通过率时间趋势' }, title: { text: '整体通过率时间趋势' },
tooltip: { trigger: 'axis' }, tooltip: { trigger: 'axis' },
legend: { data: ['1次通过率', '2次通过率', '3次通过率'], bottom: 0 }, legend: { data: ['1次通过率', '3次通过率'], bottom: 0 },
xAxis: { type: 'category', data: dates }, xAxis: { type: 'category', data: dates },
yAxis: { type: 'value', name: '%' }, yAxis: { type: 'value', name: '%' },
grid: { left: '3%', right: '4%', bottom: '15%', containLabel: true }, grid: { left: '3%', right: '4%', bottom: '15%', containLabel: true },
series: [ series: [
{ name: '1次通过率', type: 'line', data: dates.map(() => Math.random() * 10 + 70), smooth: true }, { name: '1次通过率', type: 'line', data: pass1Data, smooth: true },
{ name: '2次通过率', type: 'line', data: dates.map(() => Math.random() * 5 + 15), smooth: true }, { name: '3次通过率', type: 'line', data: pass3Data, smooth: true }
{ name: '3次通过率', type: 'line', data: dates.map(() => Math.random() * 5 + 5), smooth: true }
] ]
}, true) }, true)
overallPassRateChart.resize() overallPassRateChart.resize()
} }
// 2. Overall Auto Rate Trend // 2. Overall Auto Rate Trend
if (overallAutoRateTrendRef.value) { if (overallAutoRateTrendRef.value && datesList.value.length > 0) {
overallAutoRateChart = echarts.getInstanceByDom(overallAutoRateTrendRef.value) || echarts.init(overallAutoRateTrendRef.value) overallAutoRateChart = echarts.getInstanceByDom(overallAutoRateTrendRef.value) || echarts.init(overallAutoRateTrendRef.value)
const dates = datesList.value.map(item => item.date)
const autoRateData = datesList.value.map(item => item.autoRecognizeRate || 0)
overallAutoRateChart.setOption({ overallAutoRateChart.setOption({
title: { text: '整体自动识别率时间趋势' }, title: { text: '整体自动识别率时间趋势' },
tooltip: { trigger: 'axis' }, tooltip: { trigger: 'axis' },
xAxis: { type: 'category', data: dates }, xAxis: { type: 'category', data: dates },
yAxis: { type: 'value', name: '%' }, yAxis: { type: 'value', name: '%' },
grid: { left: '3%', right: '4%', bottom: '3%', containLabel: true }, grid: { left: '3%', right: '4%', bottom: '3%', containLabel: true },
series: [{ name: '自动识别率', type: 'line', data: dates.map(() => Math.random() * 10 + 80), smooth: true, itemStyle: { color: '#67C23A' } }] series: [{ name: '自动识别率', type: 'line', data: autoRateData, smooth: true, itemStyle: { color: '#67C23A' } }]
}, true) }, true)
overallAutoRateChart.resize() overallAutoRateChart.resize()
} }
} else { } else {
// 'details' Tab // 'details' Tab
// 1. Device Pass Rate Stats (New Chart requested) // 1. Device Pass Rate Stats
nextTick(() => { nextTick(() => {
if (devicePassRateChartRef.value) { if (devicePassRateChartRef.value && processList.value.length > 0) {
devicePassRateChart = echarts.getInstanceByDom(devicePassRateChartRef.value) || echarts.init(devicePassRateChartRef.value) devicePassRateChart = echarts.getInstanceByDom(devicePassRateChartRef.value) || echarts.init(devicePassRateChartRef.value)
const deviceNames = processList.value.map(item => item.processName)
const pass1Data = processList.value.map(item => item.passRate1 || 0)
const pass3Data = processList.value.map(item => item.passRate3 || 0)
devicePassRateChart.setOption({ devicePassRateChart.setOption({
title: { text: '设备识别通过率' }, title: { text: '设备识别通过率' },
tooltip: { trigger: 'axis' }, tooltip: { trigger: 'axis' },
legend: { data: ['1次通过', '2次通过', '3次通过'], bottom: 0 }, legend: { data: ['1次通过', '3次通过'], bottom: 0 },
grid: { left: '3%', right: '4%', bottom: '15%', containLabel: true }, grid: { left: '3%', right: '4%', bottom: '15%', containLabel: true },
xAxis: { type: 'category', data: deviceList, axisLabel: { interval: 0, rotate: 30 } }, xAxis: { type: 'category', data: deviceNames, axisLabel: { interval: 0, rotate: 30 } },
yAxis: { type: 'value', name: '%' }, yAxis: { type: 'value', name: '%' },
series: [ series: [
{ name: '1次通过', type: 'bar', data: deviceList.map(() => Math.random() * 20 + 60) }, { name: '1次通过', type: 'bar', data: pass1Data },
{ name: '2次通过', type: 'bar', data: deviceList.map(() => Math.random() * 10 + 10) }, { name: '3次通过', type: 'bar', data: pass3Data }
{ name: '3次通过', type: 'bar', data: deviceList.map(() => Math.random() * 5 + 5) }
] ]
}) })
devicePassRateChart.resize() devicePassRateChart.resize()
...@@ -191,19 +258,22 @@ const initMultiDayCharts = () => { ...@@ -191,19 +258,22 @@ const initMultiDayCharts = () => {
}) })
// 2. Device Auto Rate Stats // 2. Device Auto Rate Stats
// x: Device Name, y: Auto Rate
nextTick(() => { nextTick(() => {
if (deviceAutoRateChartRef.value) { if (deviceAutoRateChartRef.value && processList.value.length > 0) {
deviceAutoRateChart = echarts.getInstanceByDom(deviceAutoRateChartRef.value) || echarts.init(deviceAutoRateChartRef.value) deviceAutoRateChart = echarts.getInstanceByDom(deviceAutoRateChartRef.value) || echarts.init(deviceAutoRateChartRef.value)
const deviceNames = processList.value.map(item => item.processName)
const autoRateData = processList.value.map(item => item.autoRecognizeRate || 0)
deviceAutoRateChart.setOption({ deviceAutoRateChart.setOption({
title: { text: '设备自动识别率' }, title: { text: '设备自动识别率' },
tooltip: { trigger: 'axis' }, tooltip: { trigger: 'axis' },
legend: { data: ['自动识别率'], bottom: 0 }, legend: { data: ['自动识别率'], bottom: 0 },
xAxis: { type: 'category', data: deviceList, axisLabel: { interval: 0, rotate: 30 } }, xAxis: { type: 'category', data: deviceNames, axisLabel: { interval: 0, rotate: 30 } },
yAxis: { type: 'value', name: '%' }, yAxis: { type: 'value', name: '%' },
grid: { left: '3%', right: '4%', bottom: '15%', containLabel: true }, grid: { left: '3%', right: '4%', bottom: '15%', containLabel: true },
series: [ series: [
{ name: '自动识别率', type: 'bar', data: deviceList.map(() => Math.random() * 15 + 75), itemStyle: { color: '#409EFF' } } { name: '自动识别率', type: 'bar', data: autoRateData, itemStyle: { color: '#409EFF' } }
] ]
}) })
deviceAutoRateChart.resize() deviceAutoRateChart.resize()
...@@ -213,9 +283,7 @@ const initMultiDayCharts = () => { ...@@ -213,9 +283,7 @@ const initMultiDayCharts = () => {
} }
const handleSearch = () => { const handleSearch = () => {
nextTick(() => { fetchStats()
updateView()
})
} }
const updateView = () => { const updateView = () => {
...@@ -236,20 +304,19 @@ const handleResize = () => { ...@@ -236,20 +304,19 @@ const handleResize = () => {
passRateChart?.resize() passRateChart?.resize()
trendChart?.resize() trendChart?.resize()
overallPassRateChart?.resize() overallPassRateChart?.resize()
overallPassRateChart?.resize()
overallAutoRateChart?.resize() overallAutoRateChart?.resize()
devicePassRateChart?.resize() devicePassRateChart?.resize()
deviceAutoRateChart?.resize() deviceAutoRateChart?.resize()
} }
onMounted(() => { onMounted(() => {
updateView() fetchStats()
window.addEventListener('resize', handleResize) window.addEventListener('resize', handleResize)
}) })
</script> </script>
<template> <template>
<div class="serial-stats-page"> <div class="serial-stats-page" v-loading="loading">
<!-- Filter --> <!-- Filter -->
<el-card shadow="never" class="mb-4"> <el-card shadow="never" class="mb-4">
<el-form :inline="true" :model="queryForm"> <el-form :inline="true" :model="queryForm">
...@@ -260,7 +327,6 @@ onMounted(() => { ...@@ -260,7 +327,6 @@ onMounted(() => {
range-separator="至" range-separator="至"
start-placeholder="开始时间" start-placeholder="开始时间"
end-placeholder="结束时间" end-placeholder="结束时间"
value-format="YYYY-MM-DD"
/> />
</el-form-item> </el-form-item>
<el-form-item> <el-form-item>
...@@ -270,14 +336,36 @@ onMounted(() => { ...@@ -270,14 +336,36 @@ onMounted(() => {
</el-card> </el-card>
<!-- Summary Cards --> <!-- Summary Cards -->
<div class="grid grid-cols-7 gap-2 mb-4"> <div class="grid grid-cols-3 gap-4 mb-4">
<el-card shadow="never" body-style="padding: 10px"><div class="text-xs text-gray-500">提交总次数</div><div class="text-lg font-bold">{{ summary.total }}</div></el-card> <el-card shadow="never" class="bg-blue-50">
<el-card shadow="never" body-style="padding: 10px"><div class="text-xs text-gray-500">自动识别占比</div><div class="text-lg font-bold text-blue-500">{{ summary.autoRate }}%</div></el-card> <div class="text-gray-500 mb-2">提交总次数</div>
<el-card shadow="never" body-style="padding: 10px"><div class="text-xs text-gray-500">1次通过率</div><div class="text-lg font-bold text-green-500">{{ summary.pass1 }}%</div></el-card> <div class="text-4xl font-bold mb-3">{{ summary.totalSubmitCount }}</div>
<el-card shadow="never" body-style="padding: 10px"><div class="text-xs text-gray-500">2次通过率</div><div class="text-lg font-bold text-green-500">{{ summary.pass2 }}%</div></el-card> </el-card>
<el-card shadow="never" body-style="padding: 10px"><div class="text-xs text-gray-500">3次内通过率</div><div class="text-lg font-bold text-green-500">{{ (summary.pass1 + summary.pass2 + summary.pass3).toFixed(1) }}%</div></el-card>
<el-card shadow="never" body-style="padding: 10px"><div class="text-xs text-gray-500">平均耗时</div><div class="text-lg font-bold">{{ summary.avgTime }}s</div></el-card> <el-card shadow="never" class="bg-green-50">
<el-card shadow="never" body-style="padding: 10px"><div class="text-xs text-gray-500">平均尝试</div><div class="text-lg font-bold">{{ summary.avgAttempts }}</div></el-card> <div class="text-gray-500 mb-2">自动识别占比</div>
<div class="text-4xl font-bold text-blue-600 mb-3">{{ summary.autoRecognizeRate }}%</div>
</el-card>
<el-card shadow="never" class="bg-purple-50">
<div class="text-gray-500 mb-2">1次通过率</div>
<div class="text-4xl font-bold text-green-600 mb-3">{{ summary.passRate1 }}%</div>
</el-card>
<el-card shadow="never" class="bg-orange-50">
<div class="text-gray-500 mb-2">3次内通过率</div>
<div class="text-4xl font-bold text-green-600 mb-3">{{ summary.passRateWithin3 }}%</div>
</el-card>
<el-card shadow="never" class="bg-indigo-50">
<div class="text-gray-500 mb-2">平均耗时</div>
<div class="text-4xl font-bold text-indigo-600 mb-3">{{ summary.avgDuration }}<span class="text-xl">s</span></div>
</el-card>
<el-card shadow="never" class="bg-pink-50">
<div class="text-gray-500 mb-2">平均识别</div>
<div class="text-4xl font-bold text-pink-600 mb-3">{{ summary.avgRecognizeTimes }}<span class="text-xl"></span></div>
</el-card>
</div> </div>
<!-- Charts Area --> <!-- Charts Area -->
...@@ -315,16 +403,25 @@ onMounted(() => { ...@@ -315,16 +403,25 @@ onMounted(() => {
<div ref="deviceAutoRateChartRef" style="height: 400px; border: 1px solid #f0f0f0; border-radius: 4px; padding: 10px;" class="mb-4"></div> <div ref="deviceAutoRateChartRef" style="height: 400px; border: 1px solid #f0f0f0; border-radius: 4px; padding: 10px;" class="mb-4"></div>
<!-- Table for Device Details --> <!-- Table for Device Details -->
<el-table :data="tableData" border stripe> <el-table :data="processList" border stripe>
<el-table-column prop="name" label="设备名称" /> <el-table-column prop="processName" label="设备名称" />
<el-table-column prop="total" label="提交总次数" sortable /> <el-table-column prop="totalCount" label="提交总次数" sortable />
<el-table-column prop="pass1" label="1次通过率" sortable /> <el-table-column prop="passRate1" label="1次通过率" sortable>
<el-table-column prop="pass2" label="2次通过率" sortable /> <template #default="{ row }">{{ row.passRate1 }}%</template>
<el-table-column prop="pass3" label="3次通过率" sortable /> </el-table-column>
<el-table-column prop="pass3in" label="3次内通过率" sortable /> <el-table-column prop="passRate3" label="3次通过率" sortable>
<el-table-column prop="autoRate" label="自动识别占比" sortable /> <template #default="{ row }">{{ row.passRate3 }}%</template>
<el-table-column prop="avgCount" label="平均识别次数" sortable /> </el-table-column>
<el-table-column prop="avgTime" label="平均耗时" sortable /> <el-table-column prop="passRateWithin3" label="3次内通过率" sortable>
<template #default="{ row }">{{ row.passRateWithin3 }}%</template>
</el-table-column>
<el-table-column prop="autoRecognizeRate" label="自动识别占比" sortable>
<template #default="{ row }">{{ row.autoRecognizeRate }}%</template>
</el-table-column>
<el-table-column prop="avgRecognizeTimes" label="平均识别次数" sortable />
<el-table-column prop="avgDuration" label="平均耗时" sortable>
<template #default="{ row }">{{ row.avgDuration }}s</template>
</el-table-column>
</el-table> </el-table>
</div> </div>
</el-card> </el-card>
...@@ -338,16 +435,25 @@ onMounted(() => { ...@@ -338,16 +435,25 @@ onMounted(() => {
<el-button type="success" link :icon="Download">导出表格</el-button> <el-button type="success" link :icon="Download">导出表格</el-button>
</div> </div>
</template> </template>
<el-table :data="tableData" border stripe> <el-table :data="processList" border stripe>
<el-table-column prop="name" label="设备名称" /> <el-table-column prop="processName" label="设备名称" />
<el-table-column prop="total" label="提交总次数" sortable /> <el-table-column prop="totalCount" label="提交总次数" sortable />
<el-table-column prop="pass1" label="1次通过率" sortable /> <el-table-column prop="passRate1" label="1次通过率" sortable>
<el-table-column prop="pass2" label="2次通过率" sortable /> <template #default="{ row }">{{ row.passRate1 }}%</template>
<el-table-column prop="pass3" label="3次通过率" sortable /> </el-table-column>
<el-table-column prop="pass3in" label="3次内通过率" sortable /> <el-table-column prop="passRate3" label="3次通过率" sortable>
<el-table-column prop="autoRate" label="自动识别占比" sortable /> <template #default="{ row }">{{ row.passRate3 }}%</template>
<el-table-column prop="avgCount" label="平均识别次数" sortable /> </el-table-column>
<el-table-column prop="avgTime" label="平均耗时" sortable /> <el-table-column prop="passRateWithin3" label="3次内通过率" sortable>
<template #default="{ row }">{{ row.passRateWithin3 }}%</template>
</el-table-column>
<el-table-column prop="autoRecognizeRate" label="自动识别占比" sortable>
<template #default="{ row }">{{ row.autoRecognizeRate }}%</template>
</el-table-column>
<el-table-column prop="avgRecognizeTimes" label="平均识别次数" sortable />
<el-table-column prop="avgDuration" label="平均耗时" sortable>
<template #default="{ row }">{{ row.avgDuration }}s</template>
</el-table-column>
</el-table> </el-table>
</el-card> </el-card>
</div> </div>
......
Markdown is supported
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!