Skip to content
Toggle navigation
Projects
Groups
Snippets
Help
Toggle navigation
This project
Loading...
Sign in
李宁
/
Activity
Go to a project
Project
Repository
Issues
0
Merge Requests
0
Pipelines
Settings
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Commit 415edb68
authored
Jan 27, 2026
by
李宁
Browse Files
Options
Browse Files
Tag
Download
Email Patches
Plain Diff
1
1 parent
8d4d8b90
Show whitespace changes
Inline
Side-by-side
Showing
13 changed files
with
2745 additions
and
438 deletions
haMgr/API_DOC.md
haMgr/ORDERLIST_API_INTEGRATION.md
haMgr/ORDERLIST_RESPONSE_ADAPTATION.md
haMgr/prompt.txt
haMgr/web-admin/src/api/index.ts
haMgr/web-admin/src/types/order.ts
haMgr/web-admin/src/utils/request.ts
haMgr/web-admin/src/views/OrderDetail.vue
haMgr/web-admin/src/views/OrderList.vue
haMgr/web-admin/src/views/stats/DeviceStats.vue
haMgr/web-admin/src/views/stats/NoPhotoStats.vue
haMgr/web-admin/src/views/stats/QualityStats.vue
haMgr/web-admin/src/views/stats/SerialStats.vue
haMgr/API_DOC.md
View file @
415edb6
...
@@ -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,
uncheck
Reason]`
|
| POST |
`/zhijian/opt/unCheckByOpt`
| 淮安工维质检-无法质检提交 |
`[applyId,
checkStatus, fail
Reason]`
|
| 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 | - |
...
...
haMgr/ORDERLIST_API_INTEGRATION.md
0 → 100644
View file @
415edb6
# 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.
考虑添加请求缓存或防抖
haMgr/ORDERLIST_RESPONSE_ADAPTATION.md
0 → 100644
View file @
415edb6
# 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`
等)的返回结构与预期不同,也需要进行类似的适配调整。
haMgr/prompt.txt
View file @
415edb6
...
@@ -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
haMgr/web-admin/src/api/index.ts
View file @
415edb6
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
})
}
}
}
}
haMgr/web-admin/src/types/order.ts
View file @
415edb6
// 工单设备信息
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
;
}
haMgr/web-admin/src/utils/request.ts
View file @
415edb6
...
@@ -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
:
5
000
timeout
:
10
000
})
})
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
.
m
essage
||
'Error
'
)
ElMessage
.
error
(
res
.
m
sg
||
res
.
message
||
'请求失败
'
)
return
Promise
.
reject
(
new
Error
(
res
.
m
essage
||
'Error
'
))
return
Promise
.
reject
(
new
Error
(
res
.
m
sg
||
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
)
}
}
)
)
...
...
haMgr/web-admin/src/views/OrderDetail.vue
View file @
415edb6
...
@@ -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>
...
...
haMgr/web-admin/src/views/OrderList.vue
View file @
415edb6
<
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=
"4
00px"
>
<el-dialog
v-model=
"dialogVisible.orderIds"
title=
"工单
设备列表"
width=
"6
00px"
>
<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
>
haMgr/web-admin/src/views/stats/DeviceStats.vue
View file @
415edb6
<
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=
"12
0"
/>
<el-table-column
prop=
"
processName"
label=
"设备名称"
fixed
width=
"15
0"
/>
<el-table-column
prop=
"t1"
label=
"识别1次"
align=
"center"
sortable
>
<el-table-column
prop=
"t
ryNum
1"
label=
"识别1次"
align=
"center"
sortable
>
<template
#
default=
"
{ row }">
<template
#
default=
"
{ row }">
<div>
{{
row
.
t
1
}}
</div><div
class=
"text-xs text-gray-400"
>
{{
row
.
t1p
}}
%
</div>
<div>
{{
row
.
t
ryNum1
||
0
}}
</div>
</
template
>
</
template
>
</el-table-column>
</el-table-column>
<el-table-column
prop=
"t2"
label=
"识别2次"
align=
"center"
sortable
>
<el-table-column
prop=
"t
ryNum
2"
label=
"识别2次"
align=
"center"
sortable
>
<
template
#
default=
"{ row }"
>
<
template
#
default=
"{ row }"
>
<div>
{{
row
.
t
2
}}
</div><div
class=
"text-xs text-gray-400"
>
{{
row
.
t2p
}}
%
</div>
<div>
{{
row
.
t
ryNum2
||
0
}}
</div>
</
template
>
</
template
>
</el-table-column>
</el-table-column>
<el-table-column
prop=
"t3"
label=
"识别3次"
align=
"center"
sortable
>
<el-table-column
prop=
"t
ryNum
3"
label=
"识别3次"
align=
"center"
sortable
>
<
template
#
default=
"{ row }"
>
<
template
#
default=
"{ row }"
>
<div>
{{
row
.
t
3
}}
</div><div
class=
"text-xs text-gray-400"
>
{{
row
.
t3p
}}
%
</div>
<div>
{{
row
.
t
ryNum3
||
0
}}
</div>
</
template
>
</
template
>
</el-table-column>
</el-table-column>
<el-table-column
prop=
"t4"
label=
"识别4次"
align=
"center"
sortable
>
<el-table-column
prop=
"t
ryNum
4"
label=
"识别4次"
align=
"center"
sortable
>
<
template
#
default=
"{ row }"
>
<
template
#
default=
"{ row }"
>
<div>
{{
row
.
t
4
}}
</div><div
class=
"text-xs text-gray-400"
>
{{
row
.
t4p
}}
%
</div>
<div>
{{
row
.
t
ryNum4
||
0
}}
</div>
</
template
>
</
template
>
</el-table-column>
</el-table-column>
<el-table-column
prop=
"t5"
label=
"识别5次"
align=
"center"
sortable
>
<el-table-column
prop=
"t
ryNum
5"
label=
"识别5次"
align=
"center"
sortable
>
<
template
#
default=
"{ row }"
>
<
template
#
default=
"{ row }"
>
<div>
{{
row
.
t
5
}}
</div><div
class=
"text-xs text-gray-400"
>
{{
row
.
t5p
}}
%
</div>
<div>
{{
row
.
t
ryNum5
||
0
}}
</div>
</
template
>
</
template
>
</el-table-column>
</el-table-column>
<el-table-column
prop=
"t
6"
label=
"识别
6次"
align=
"center"
sortable
>
<el-table-column
prop=
"t
ryNumGte6"
label=
"≥
6次"
align=
"center"
sortable
>
<
template
#
default=
"{ row }"
>
<
template
#
default=
"{ row }"
>
<div>
{{
row
.
t
6
}}
</div><div
class=
"text-xs text-gray-400"
>
{{
row
.
t6p
}}
%
</div>
<div>
{{
row
.
t
ryNumGte6
||
0
}}
</div>
</
template
>
</
template
>
</el-table-column>
</el-table-column>
<el-table-column
prop=
"t
6plus"
label=
">6次
"
align=
"center"
sortable
>
<el-table-column
prop=
"t
otalCount"
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>
haMgr/web-admin/src/views/stats/NoPhotoStats.vue
View file @
415edb6
...
@@ -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>
...
...
haMgr/web-admin/src/views/stats/QualityStats.vue
View file @
415edb6
<
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,
2
5, 1000(all)
const
processLimit
=
ref
(
10
)
// 10,
1
5, 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
:
cit
yList
,
axisLabel
:
{
interval
:
0
,
rotate
:
30
}
},
xAxis
:
{
type
:
'category'
,
data
:
cit
ies
,
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
:
cit
yList
,
axisLabel
:
{
interval
:
0
,
rotate
:
30
}
},
xAxis
:
{
type
:
'category'
,
data
:
cit
ies
,
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
:
cit
yList
,
axisLabel
:
{
interval
:
0
,
rotate
:
30
}
},
xAxis
:
{
type
:
'category'
,
data
:
cit
ies
,
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
:
cit
yList
,
axisLabel
:
{
interval
:
0
,
rotate
:
30
}
},
xAxis
:
{
type
:
'category'
,
data
:
cit
ies
,
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
:
cit
yList
},
yAxis
:
{
type
:
'category'
,
data
:
cit
ies
},
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
:
cit
yList
,
axisLabel
:
{
interval
:
0
,
rotate
:
30
}
},
xAxis
:
{
type
:
'category'
,
data
:
cit
ies
,
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=
"complaint
Done
"
label=
"已完成"
/>
<el-table-column
prop=
"complaint
Finish
"
label=
"已完成"
/>
<el-table-column
prop=
"complaintUn
done
"
label=
"未完成"
/>
<el-table-column
prop=
"complaintUn
finish
"
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"
>
...
...
haMgr/web-admin/src/views/stats/SerialStats.vue
View file @
415edb6
...
@@ -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
:
device
List
,
axisLabel
:
{
interval
:
0
,
rotate
:
30
}
},
xAxis
:
{
type
:
'category'
,
data
:
device
Names
,
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
:
device
List
,
axisLabel
:
{
interval
:
0
,
rotate
:
30
}
},
xAxis
:
{
type
:
'category'
,
data
:
device
Names
,
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
:
device
List
,
axisLabel
:
{
interval
:
0
,
rotate
:
30
}
},
xAxis
:
{
type
:
'category'
,
data
:
device
Names
,
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
:
device
List
,
axisLabel
:
{
interval
:
0
,
rotate
:
30
}
},
xAxis
:
{
type
:
'category'
,
data
:
device
Names
,
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>
...
...
Write
Preview
Markdown
is supported
Attach a file
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to post a comment