yanghao před 15 hodinami
rodič
revize
12d1a43485
92 změnil soubory, kde provedl 10467 přidání a 4372 odebrání
  1. 1 1
      .env.local
  2. 1 1
      package.json
  3. 950 356
      pnpm-lock.yaml
  4. binární
      public/model/industrialEquipment2.glb
  5. binární
      public/model/test2.glb
  6. 13 3
      src/api/login/index.ts
  7. 21 0
      src/api/pms/device/index.ts
  8. 26 0
      src/api/pms/inspect/order/index.ts
  9. 1 1
      src/api/pms/iotlockstock/index.ts
  10. 11 1
      src/api/pms/iotmainworkorder/index.ts
  11. 10 5
      src/api/pms/iotopeationfill/index.ts
  12. 5 5
      src/api/pms/iotprojectinfo/index.ts
  13. 2 2
      src/api/pms/iotprojecttask/index.ts
  14. 14 0
      src/api/pms/iotrddailyreport/index.ts
  15. 6 0
      src/api/pms/iotrhdailyreport/index.ts
  16. 6 0
      src/api/pms/iotrydailyreport/index.ts
  17. 1 1
      src/api/pms/iotsapstock/index.ts
  18. 3 3
      src/api/pms/maintenance/index.ts
  19. 3 0
      src/api/pms/report/index.ts
  20. 30 0
      src/api/pms/stat/index.ts
  21. binární
      src/assets/imgs/green.png
  22. binární
      src/assets/imgs/red.png
  23. 8 7
      src/components/ZmTable/ZmTableColumn.vue
  24. 10 2
      src/components/ZmTable/index.vue
  25. 1 1
      src/components/count-to1.vue
  26. 3 3
      src/hooks/web/useMessage.ts
  27. 2 2
      src/locales/zh-CN.ts
  28. 1 0
      src/main.ts
  29. 29 9
      src/permission.ts
  30. 46 0
      src/router/modules/remaining.ts
  31. 63 0
      src/styles/index.scss
  32. 39 38
      src/utils/dict.ts
  33. 4 4
      src/views/oli-connection/monitoring/detail.vue
  34. 179 73
      src/views/pms/device/IotDeviceForm.vue
  35. 187 76
      src/views/pms/device/IotDeviceFormAdd.vue
  36. 21 14
      src/views/pms/device/allotlog/DeviceAllot.vue
  37. 28 3
      src/views/pms/device/completeSet/DeviceCompleteSet.vue
  38. 165 6
      src/views/pms/device/index.vue
  39. 17 19
      src/views/pms/device/personlog/DevicePerson.vue
  40. 24 20
      src/views/pms/device/statuslog/DeviceStatus.vue
  41. 2 2
      src/views/pms/iotdeviceallotlog/index.vue
  42. 194 113
      src/views/pms/iotlockstock/index.vue
  43. 22 28
      src/views/pms/iotmainworkorder/IotDeviceMainAlarm.vue
  44. 1 4
      src/views/pms/iotmainworkorder/index.vue
  45. 686 0
      src/views/pms/iotopeationfill/index1 copy.vue
  46. 327 96
      src/views/pms/iotopeationfill/index1.vue
  47. 2 9
      src/views/pms/iotprojectinfo/index.vue
  48. 196 139
      src/views/pms/iotprojecttask/index.vue
  49. 423 176
      src/views/pms/iotrddailyreport/FillDailyReportForm.vue
  50. 36 24
      src/views/pms/iotrddailyreport/fillDailyReport.vue
  51. 60 60
      src/views/pms/iotrddailyreport/index.vue
  52. 20 27
      src/views/pms/iotrddailyreport/statistics.vue
  53. 4 3
      src/views/pms/iotrddailyreport/summary.vue
  54. 20 379
      src/views/pms/iotrhdailyreport/approval.vue
  55. 18 348
      src/views/pms/iotrhdailyreport/fill.vue
  56. 2 2
      src/views/pms/iotrhdailyreport/index.vue
  57. 601 0
      src/views/pms/iotrhdailyreport/rh-form.vue
  58. 10 586
      src/views/pms/iotrydailyreport/approval.vue
  59. 8 520
      src/views/pms/iotrydailyreport/fill.vue
  60. 16 24
      src/views/pms/iotrydailyreport/index.vue
  61. 712 0
      src/views/pms/iotrydailyreport/ry-form.vue
  62. 690 0
      src/views/pms/iotrydailyreport/ry-xj-form.vue
  63. 22 7
      src/views/pms/iotrydailyreport/summary.vue
  64. 11 428
      src/views/pms/iotrydailyreport/xapproval.vue
  65. 9 402
      src/views/pms/iotrydailyreport/xfill.vue
  66. 15 23
      src/views/pms/iotrydailyreport/xjindex.vue
  67. 23 8
      src/views/pms/iotrydailyreport/xsummary.vue
  68. 200 112
      src/views/pms/iotsapstock/index.vue
  69. 41 41
      src/views/pms/maintenance/IotMaintenancePlanEdit.vue
  70. 0 2
      src/views/pms/maintenance/index.vue
  71. 116 0
      src/views/pms/monitor/ModelViewer.vue
  72. 116 0
      src/views/pms/monitor/ModelViewer2.vue
  73. 40 0
      src/views/pms/monitor/data-row.vue
  74. 204 0
      src/views/pms/monitor/data.txt
  75. 501 0
      src/views/pms/monitor/index.vue
  76. 442 0
      src/views/pms/monitor/kanban.vue
  77. 540 0
      src/views/pms/monitor/kanban2.vue
  78. 153 108
      src/views/pms/stat/rdkb.vue
  79. 138 0
      src/views/pms/stat/rdkb/availability.vue
  80. 144 0
      src/views/pms/stat/rdkb/constructionBriefing.vue
  81. 279 0
      src/views/pms/stat/rdkb/exception.vue
  82. 218 0
      src/views/pms/stat/rdkb/utilization.vue
  83. 361 0
      src/views/pms/stat/rdkb/workload.vue
  84. 58 9
      src/views/report-statistics/costs.vue
  85. 22 2
      src/views/report-statistics/daily-report.vue
  86. 30 6
      src/views/report-statistics/device_book/index2.vue
  87. 26 0
      src/views/report-statistics/fault_report/index.vue
  88. 37 3
      src/views/report-statistics/inspection_order/index.vue
  89. 21 0
      src/views/report-statistics/rd-daily-report.vue
  90. 27 23
      src/views/report-statistics/ry-daily-report.vue
  91. 661 0
      src/views/report-statistics/ry-xj-daily-report.vue
  92. 31 2
      src/views/report-statistics/work-order-completion.vue

+ 1 - 1
.env.local

@@ -4,7 +4,7 @@ NODE_ENV=development
 VITE_DEV=true
 
 # 请求路径  http://192.168.188.149:48080  https://iot.deepoil.cc
-VITE_BASE_URL='http://172.21.10.222:8080'
+VITE_BASE_URL='https://iot.deepoil.cc'
 
 # 文件上传类型:server - 后端上传, client - 前端直连上传,仅支持 S3 服务
 VITE_UPLOAD_TYPE=server

+ 1 - 1
package.json

@@ -60,7 +60,7 @@
     "echarts": "^5.6.0",
     "echarts-wordcloud": "^2.1.0",
     "element-china-area-data": "^6.1.0",
-    "element-plus": "2.9.1",
+    "element-plus": "2.13.0",
     "fast-xml-parser": "^4.3.2",
     "file-save": "^0.2.0",
     "file-saver": "^2.0.5",

Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 950 - 356
pnpm-lock.yaml


binární
public/model/industrialEquipment2.glb


binární
public/model/test2.glb


+ 13 - 3
src/api/login/index.ts

@@ -1,5 +1,6 @@
 import request from '@/config/axios'
 import type { RegisterVO, UserLoginVO } from './types'
+import { head } from 'lodash-es'
 
 export interface SmsCodeVO {
   mobile: string
@@ -16,12 +17,12 @@ export const login = (data: UserLoginVO) => {
   return request.post({ url: '/system/auth/login', data })
 }
 
-export const dingTalkLogin = (data:{code:string,type:number,state:number}) => {
-  return request.post({ url: '/system/auth/h5SocialLogin', data,headers:{'tenant-id':1} })
+export const dingTalkLogin = (data: { code: string; type: number; state: number }) => {
+  return request.post({ url: '/system/auth/h5SocialLogin', data, headers: { 'tenant-id': 1 } })
 }
 
 export const simpleLogin = (id: any) => {
-  return request.post({ url: '/system/auth/simple/login/'+id })
+  return request.post({ url: '/system/auth/simple/login/' + id })
 }
 // 注册
 export const register = (data: RegisterVO) => {
@@ -90,3 +91,12 @@ export const reqCheck = (data: any) => {
 export const smsResetPassword = (data: any) => {
   return request.post({ url: '/system/auth/reset-password', data })
 }
+
+// 门户用户登录
+export const portalLogin = (data: any) => {
+  return request.post({
+    url: 'system/auth/portal/login',
+    headers: { 'portal-secret': 'cc99d802-ce5c-5f62-b037-9a00726e7109' },
+    data
+  })
+}

+ 21 - 0
src/api/pms/device/index.ts

@@ -152,6 +152,22 @@ export const IotDeviceApi = {
   exportIotDevice: async (params) => {
     return await request.download({ url: `/rq/iot-device/export-excel`, params })
   },
+
+  exportIotDeviceAdjust: async (params) => {
+    return await request.download({ url: `/pms/iot-device-status-log/export-excel`, params })
+  },
+
+  exportIotDevicePerson: async (params) => {
+    return await request.download({ url: `/pms/iot-device-person-log/export-excel`, params })
+  },
+
+  exportIotDeviceAllot: async (params) => {
+    return await request.download({ url: `/pms/iot-device-allot-log/export-excel`, params })
+  },
+
+  exportIotDeviceMainAlarm: async (params) => {
+    return await request.download({ url: `/pms/iot-main-work-order/exportMaintenances`, params })
+  },
   getIotDeviceTdPage: async (params: any) => {
     return await request.get({ url: `/rq/iot-device/td/page`, params })
   },
@@ -212,5 +228,10 @@ export const IotDeviceApi = {
   // 设备报表导出
   exportDeviceReport: async (params) => {
     return await request.download({ url: `/rq/report/export-excel`, params })
+  },
+
+  // 获取成套设备参数
+  getDeviceSetParams: async (id) => {
+    return await request.get({ url: `/rq/iot-device-group/td/${id}` })
   }
 }

+ 26 - 0
src/api/pms/inspect/order/index.ts

@@ -90,5 +90,31 @@ export const IotInspectOrderApi = {
   // 异常设备列表
   getExceptionDeviceList: async (params: any) => {
     return await request.get({ url: `/rq/iot-inspect-order/exception/device`, params })
+  },
+
+  // 导出巡检报表 Excel
+  exportInspectReport: async (params) => {
+    return await request.download({ url: `/rq/iot-inspect-order/report/export-excel`, params })
+  },
+
+  // 异常巡检点导出 Excel
+  exportExceptionPointInspectReport: async (params) => {
+    return await request.download({
+      url: `/rq/iot-inspect-order/report/exception/item/export-excel`,
+      params
+    })
+  },
+
+  //  异常设备导出 Excel
+  exportExceptionDeviceInspectReport: async (params) => {
+    return await request.download({
+      url: `/rq/iot-inspect-order/report/exception/device/export-excel`,
+      params
+    })
+  },
+
+  // 故障报表导出 Excel
+  exportFaultReport: async (params) => {
+    return await request.download({ url: `/rq/report/failure/report/export-excel`, params })
   }
 }

+ 1 - 1
src/api/pms/iotlockstock/index.ts

@@ -65,5 +65,5 @@ export const IotLockStockApi = {
   // 导出PMS 本地 库存 Excel
   exportIotLockStock: async (params) => {
     return await request.download({ url: `/pms/iot-lock-stock/export-excel`, params })
-  },
+  }
 }

+ 11 - 1
src/api/pms/iotmainworkorder/index.ts

@@ -62,7 +62,10 @@ export const IotMainWorkOrderApi = {
   },
 
   getDeviceIotWorkOrder: async (orderId: number, deviceId: number) => {
-    return await request.get({ url: `/pms/iot-main-work-order/deviceWorkOrder?workOrderId=` + orderId+'&deviceId='+deviceId })
+    return await request.get({
+      url:
+        `/pms/iot-main-work-order/deviceWorkOrder?workOrderId=` + orderId + '&deviceId=' + deviceId
+    })
   },
 
   // 新增保养工单
@@ -99,4 +102,11 @@ export const IotMainWorkOrderApi = {
   exportIotMainWorkOrder: async (params) => {
     return await request.download({ url: `/pms/iot-main-work-order/export-excel`, params })
   },
+
+  exportIotMainWorkOrderIndex: async (params) => {
+    return await request.download({
+      url: `/pms/iot-main-work-order/exportMaintenanceOrders`,
+      params
+    })
+  }
 }

+ 10 - 5
src/api/pms/iotopeationfill/index.ts

@@ -2,11 +2,11 @@ import request from '@/config/axios'
 
 // 运行记录填报 VO
 export interface IotOpeationFillVO {
-  id: number // 主键id
+  id: any // 主键id
   deviceCode: string // 资产编号
   deviceCategoryId: number
   deviceName: string // 设备名称
-  fillContent: string // 填写内容
+  fillContent: any // 填写内容
   deviceType: string // 设备类别
   deviceComponent: string // 设备部件
   deptId: number // 公司id
@@ -22,16 +22,21 @@ export interface IotOpeationFillVO {
   modelAttr: string
   modelId: number
   isFill: number
-  fillContent: string
   pointCode: string
   pointName: string
   orderName: string
   orderType: string
   orderStatus: number
-  createTime: Date
+  createTime: any
   totalRunTime: number
-  userId: number
+  userId: any
   isSum: number
+  deviceId: any
+  limitType: any
+  currentSumValue: any
+  maxAllowedValue: any
+  isCollection: any
+  description: any
 }
 
 // 运行记录填报 API

+ 5 - 5
src/api/pms/iotprojectinfo/index.ts

@@ -1,5 +1,5 @@
 import request from '@/config/axios'
-import {RouteParamValue} from "vue-router";
+import { RouteParamValue } from 'vue-router'
 
 // 项目信息 VO
 export interface IotProjectInfoVO {
@@ -17,8 +17,8 @@ export interface IotProjectInfoVO {
   payment: string // 施工工艺
   userName: string // 用户名
   userId: number // 用户id
-  manufacturerId:number
-  manufactureName:string
+  manufacturerId: number
+  manufactureName: string
 }
 
 // 项目信息 API
@@ -30,7 +30,7 @@ export const IotProjectInfoApi = {
 
   //根据用户获取项目信息
   getIotProjectInfoUser: async (deptId: string | RouteParamValue[]) => {
-    return await request.get({ url: `/rq/iot-project-info/projectList?deptId=`+deptId })
+    return await request.get({ url: `/rq/iot-project-info/projectList?deptId=` + deptId })
   },
   // 查询项目信息详情
   getIotProjectInfo: async (id: string | RouteParamValue[]) => {
@@ -55,5 +55,5 @@ export const IotProjectInfoApi = {
   // 导出项目信息 Excel
   exportIotProjectInfo: async (params) => {
     return await request.download({ url: `/rq/iot-project-info/export-excel`, params })
-  },
+  }
 }

+ 2 - 2
src/api/pms/iotprojecttask/index.ts

@@ -12,7 +12,7 @@ export interface IotProjectTaskVO {
   userName: string // 用户名
   userId: number // 用户id
   remark: string // 备注
-  deptIds:[]
+  deptIds: []
 }
 
 // 项目信息任务拆分 API
@@ -47,5 +47,5 @@ export const IotProjectTaskApi = {
   // 导出项目信息任务拆分 Excel
   exportIotProjectTask: async (params) => {
     return await request.download({ url: `/rq/iot-project-task/export-excel`, params })
-  },
+  }
 }

+ 14 - 0
src/api/pms/iotrddailyreport/index.ts

@@ -99,6 +99,10 @@ export const IotRdDailyReportApi = {
     return await request.download({ url: `/pms/iot-rd-daily-report/export-excel`, params })
   },
 
+  exportIotRdDailyReportDetails: async (params) => {
+    return await request.download({ url: `/pms/iot-rd-daily-report/export-detail`, params })
+  },
+
   // 查询项目任务实际进度列表
   taskActualProgress: async (params: any) => {
     return await request.get({ url: `/pms/iot-rd-daily-report/taskActualProgress`, params })
@@ -113,5 +117,15 @@ export const IotRdDailyReportApi = {
   },
   getIotRdDailyReportSummaryPolyline: async (params: any) => {
     return await request.get({ url: `/pms/iot-rd-daily-report/polylineStatistics`, params })
+  },
+
+  exportRdDailyReportStatistics: async (params) => {
+    return await request.download({ url: `/pms/iot-rd-daily-report/exportStatistics`, params })
+  },
+  exportIotRdDailyReportWell: async (params) => {
+    return await request.download({ url: `/pms/iot-rd-daily-report/exportSingleWells`, params })
+  },
+  exportIotRdDailyReportTeam: async (params) => {
+    return await request.download({ url: `/pms/iot-rd-daily-report/exportSingleTeams`, params })
   }
 }

+ 6 - 0
src/api/pms/iotrhdailyreport/index.ts

@@ -113,5 +113,11 @@ export const IotRhDailyReportApi = {
   // 导出瑞恒日报 Excel
   exportIotRhDailyReport: async (params) => {
     return await request.download({ url: `/pms/iot-rh-daily-report/export-excel`, params })
+  },
+  exportIotRhDailyReportWell: async (params) => {
+    return await request.download({ url: `/pms/iot-rh-daily-report/exportSingleWells`, params })
+  },
+  exportIotRhDailyReportTeam: async (params) => {
+    return await request.download({ url: `/pms/iot-rh-daily-report/exportSingleTeams`, params })
   }
 }

+ 6 - 0
src/api/pms/iotrydailyreport/index.ts

@@ -123,5 +123,11 @@ export const IotRyDailyReportApi = {
 
   approvalIotRyDailyReport: async (data: { id: number; auditStatus: 20 | 30; opinion: string }) => {
     return await request.put({ url: `/pms/iot-ry-daily-report/approval`, data })
+  },
+  exportIotRyDailyReportWell: async (params) => {
+    return await request.download({ url: `/pms/iot-ry-daily-report/exportSingleWells`, params })
+  },
+  exportIotRyDailyReportTeam: async (params) => {
+    return await request.download({ url: `/pms/iot-ry-daily-report/exportSingleTeams`, params })
   }
 }

+ 1 - 1
src/api/pms/iotsapstock/index.ts

@@ -64,5 +64,5 @@ export const IotSapStockApi = {
   // 导出PMS SAP 库存(通用库存/项目部库存) Excel
   exportIotSapStock: async (params) => {
     return await request.download({ url: `/pms/iot-sap-stock/export-excel`, params })
-  },
+  }
 }

+ 3 - 3
src/api/pms/maintenance/index.ts

@@ -39,7 +39,7 @@ export const IotMaintenancePlanApi = {
     return await request.post({ url: `/rq/iot-maintenance-plan/updatePlan`, data })
   },
 
-  updatePlanStatus : (id: number, status: number) => {
+  updatePlanStatus: (id: number, status: number) => {
     const data = {
       id,
       status
@@ -54,6 +54,6 @@ export const IotMaintenancePlanApi = {
 
   // 导出保养计划 Excel
   exportIotMaintenancePlan: async (params) => {
-    return await request.download({ url: `/rq/iot-maintenance-plan/export-excel`, params })
-  },
+    return await request.download({ url: `/pms/iot-maintenance-bom/exportPlans`, params })
+  }
 }

+ 3 - 0
src/api/pms/report/index.ts

@@ -13,5 +13,8 @@ export const IotReportApi = {
   },
   getOrderPage: async (params: any) => {
     return await request.get({ url: `/rq/report/order/page`, params })
+  },
+  exportCosts: async (params) => {
+    return await request.download({ url: `/rq/report/repair/report/export-excel`, params })
   }
 }

+ 30 - 0
src/api/pms/stat/index.ts

@@ -123,6 +123,12 @@ export const IotStatApi = {
   getDeviceCount: async (params: any) => {
     return await request.get({ url: `/rq/stat/home/device/count/` + params })
   },
+  getAbnormalDevice: async (params: any) => {
+    return await request.get({ url: `/rq/stat/inspect/exception/device` + params })
+  },
+  getOutliers: async (params: any) => {
+    return await request.get({ url: `/rq/iot-inspect-order-detail/report/status` + params })
+  },
   getRhRate: async (params: any) => {
     return await request.get({ url: `/rq/stat/rh/device/utilizationRate`, params })
   },
@@ -174,5 +180,29 @@ export const IotStatApi = {
   },
   getDevSta: async (params: any) => {
     return await request.get({ url: `/rq/iot-opeation-fill/getDeviceCount`, params })
+  },
+  getRdWorkload: async (params: any) => {
+    return await request.get({ url: `/pms/iot-rd-daily-report/summaryStatistics`, params })
+  },
+  getRdWorkloadYear: async (params: any) => {
+    return await request.get({ url: `/pms/iot-rd-daily-report/workloadKanban`, params })
+  },
+  getUtilization: async (params: any) => {
+    return await request.get({ url: `/rq/stat/rd/device/utilizationRates`, params })
+  },
+  getWhl: async () => {
+    return await request.get({ url: `/rq/report/rd/whl` })
+  },
+  getDeviceException: async (params: any) => {
+    return await request.get({ url: `/rq/stat/inspect/exception/device`, params })
+  },
+  getStatusException: async (params: any) => {
+    return await request.get({ url: `/rq/iot-inspect-order-detail/report/status`, params })
+  },
+  getProductionException: async (params: any) => {
+    return await request.get({ url: `/pms/iot-rd-daily-report/abnormalAlert`, params })
+  },
+  getConstructionBriefing: async () => {
+    return await request.get({ url: `/pms/iot-rd-daily-report/constructionBriefing` })
   }
 }

binární
src/assets/imgs/green.png


binární
src/assets/imgs/red.png


+ 8 - 7
src/components/ZmTable/ZmTableColumn.vue

@@ -117,19 +117,20 @@ const calculativeWidth = () => {
   if (props.zmFilterable) labelWidth += 22
   if (props.zmSortable) labelWidth += 22
 
-  const maxWidth = Math.max(...values.map((value) => getTextWidth(value) + 38), labelWidth)
+  const maxWidth = Math.min(
+    Math.max(...values.map((value) => getTextWidth(value) + 38), labelWidth),
+    360
+  )
 
   defaultOptions.value.minWidth = maxWidth
 }
 
 watch(
   () => tableContext.loading.value,
-  (loading) => {
-    if (!loading) {
-      nextTick(() => {
-        calculativeWidth()
-      })
-    }
+  () => {
+    nextTick(() => {
+      calculativeWidth()
+    })
   },
   { immediate: true }
 )

+ 10 - 2
src/components/ZmTable/index.vue

@@ -8,6 +8,7 @@ interface Props extends /* @vue-ignore */ Partial<Omit<TableProps<T>, 'data'>> {
   handleQuery?: (payload?: FilterPayload) => void
   sortingFields?: SortField[]
   sortFn?: (prop: string, order: SortOrder | null) => void
+  customClass?: boolean
 }
 
 const props = defineProps<Props>()
@@ -25,7 +26,8 @@ const defaultOptions: Partial<Props> = {
   border: true,
   highlightCurrentRow: true,
   showOverflowTooltip: true,
-  scrollbarAlwaysOn: false
+  scrollbarAlwaysOn: false,
+  customClass: false
 }
 
 const bindProps = computed(() => {
@@ -85,7 +87,13 @@ defineExpose({
 </script>
 
 <template>
-  <el-table ref="tableRef" v-loading="loading" class="zm-table" v-bind="bindProps" :data="data">
+  <el-table
+    ref="tableRef"
+    v-loading="loading"
+    :class="{ 'zm-table': !customClass }"
+    v-bind="bindProps"
+    :data="data"
+  >
     <template v-for="(_, name) in $slots" #[name]="slotData">
       <slot :name="name" v-bind="slotData || {}"></slot>
     </template>

+ 1 - 1
src/components/count-to1.vue

@@ -88,7 +88,7 @@ defineExpose({
 
 <template>
   <span class="flex">
-    <span v-if="endVal">{{ value }}</span>
+    <span v-if="endVal !== null && endVal !== undefined">{{ value }}</span>
     <slot v-else></slot>
   </span>
 </template>

+ 3 - 3
src/hooks/web/useMessage.ts

@@ -52,10 +52,10 @@ export const useMessage = () => {
       ElNotification.warning(content)
     },
     // 确认窗体
-    confirm(content: string, tip?: string) {
+    confirm(content: string, tip?: string, confirmButtonText?: string, cancelButtonText?: string) {
       return ElMessageBox.confirm(content, tip ? tip : t('common.confirmTitle'), {
-        confirmButtonText: t('common.ok'),
-        cancelButtonText: t('common.cancel'),
+        confirmButtonText: confirmButtonText ? confirmButtonText : t('common.ok'),
+        cancelButtonText: cancelButtonText ? cancelButtonText : t('common.cancel'),
         type: 'warning'
       })
     },

+ 2 - 2
src/locales/zh-CN.ts

@@ -8,8 +8,8 @@ export default {
     maintained: '已保养',
     notInspected: '未巡检',
     inspected: '已巡检',
-    zj: '总进尺(m)',
-    xj: '总完成井数',
+    zj: '钻井总进尺(m)',
+    xj: '修井总完成井数',
     mttr: 'MTTR(平均解决时间)',
     materialsUnderInventory: '库存预警物料数量',
     deviceStatus: '设备状态统计',

+ 1 - 0
src/main.ts

@@ -67,6 +67,7 @@ const setupAll = async () => {
   setupMountedFocus(app)
 
   await router.isReady()
+  app.use(DataVVue3)
 
   app.use(DataVVue3)
 

+ 29 - 9
src/permission.ts

@@ -1,16 +1,15 @@
 import router from './router'
 import type { RouteRecordRaw } from 'vue-router'
 import { isRelogin } from '@/config/axios/service'
-import {getAccessToken} from '@/utils/auth'
+import { getAccessToken } from '@/utils/auth'
 import { useTitle } from '@/hooks/web/useTitle'
 import { useNProgress } from '@/hooks/web/useNProgress'
 import { usePageLoading } from '@/hooks/web/usePageLoading'
 import { useDictStoreWithOut } from '@/store/modules/dict'
 import { useUserStoreWithOut } from '@/store/modules/user'
 import { usePermissionStoreWithOut } from '@/store/modules/permission'
-import * as LoginApi from "@/api/login";
-import * as authUtil from "@/utils/auth";
-
+import * as LoginApi from '@/api/login'
+import * as authUtil from '@/utils/auth'
 
 const { start, done } = useNProgress()
 
@@ -87,6 +86,18 @@ router.beforeEach(async (to, from, next) => {
   if (getAccessToken()) {
     if (to.path === '/login') {
       next({ path: '/' })
+    } else if (to.fullPath.includes('portalLogin')) {
+      // authUtil.removeToken()
+      // deleteUserCache()
+      const userStore = useUserStoreWithOut()
+      await userStore.loginOut()
+      await getTenantId()
+      const res = await LoginApi.portalLogin({
+        username: to.query.username
+      })
+
+      authUtil.setToken(res)
+      next({ path: '/' })
     } else {
       // 获取所有字典
       const dictStore = useDictStoreWithOut()
@@ -116,16 +127,25 @@ router.beforeEach(async (to, from, next) => {
       }
     }
   } else {
-    if (whiteList.indexOf(to.path) !== -1) {
-      const code = to.query.code;
+    if (to.query.username) {
+      await getTenantId()
+      const res = await LoginApi.portalLogin({
+        username: to.query.username
+      })
+
+      authUtil.setToken(res)
+
+      next({ path: '/index' })
+    } else if (whiteList.indexOf(to.path) !== -1) {
+      const code = to.query.code
       if (code) {
         debugger
         await getTenantId()
-        const res = await LoginApi.socialLogin('20', typeof code === "string" ? code :"", '22')
+        const res = await LoginApi.socialLogin('20', typeof code === 'string' ? code : '', '22')
         authUtil.setToken(res)
-        next({ path: 'index' });
+        next({ path: 'index' })
       } else {
-        next(); // 正常导航
+        next() // 正常导航
       }
       // next()
     } else {

+ 46 - 0
src/router/modules/remaining.ts

@@ -1817,6 +1817,52 @@ const remainingRouter: AppRouteRecordRaw[] = [
       }
     ]
   },
+  // {
+  //   path: '/device_monitor',
+  //   component: Layout,
+  //   name: 'kanban',
+  //   meta: { hidden: true },
+  //   children: [
+  //     {
+  //       path: 'kanban',
+  //       name: 'DeviceKanban',
+  //       meta: {
+  //         title: '设备看板',
+  //         noCache: true,
+  //         hidden: true
+  //       },
+  //       component: () => import('@/views/pms/monitor/kanban.vue')
+  //     }
+  //   ]
+  // },
+  {
+    path: '/kanban',
+    component: Layout,
+    name: 'kanban',
+    meta: { hidden: true },
+    children: [
+      {
+        path: 'monitor/kanban',
+        name: 'Kanban',
+        meta: {
+          title: '设备看板',
+          noCache: true,
+          hidden: true
+        },
+        component: () => import('@/views/pms/monitor/kanban.vue')
+      },
+      {
+        path: 'monitor/kanbanOther',
+        name: 'KanbanOther',
+        meta: {
+          title: '设备看板',
+          noCache: true,
+          hidden: true
+        },
+        component: () => import('@/views/pms/monitor/kanban2.vue')
+      }
+    ]
+  },
   {
     path: '/diy',
     name: 'DiyCenter',

+ 63 - 0
src/styles/index.scss

@@ -35,3 +35,66 @@
     border-left-color: var(--el-color-primary);
   }
 }
+
+/* 全屏模式样式 */
+.app-fullscreen {
+  /* 隐藏所有布局相关元素 */
+  .layout-aside,
+  .sidebar-container,
+  .el-aside,
+  .app-header,
+  .layout-header,
+  .el-header,
+  .tags-view-container,
+  .app-footer,
+  .el-footer,
+  .layout-border__right,
+  .layout-border__top,
+  .layout-border__bottom,
+  .fixed.top-0.left-0.z-10,
+  .bg-\[var\(--top-header-bg-color\)\] {
+    display: none !important;
+  }
+
+  /* 调整主应用容器 */
+  .app-wrapper,
+  .layout-container,
+  .layout-content,
+  .app-main,
+  .main-container,
+  .layout-app-main {
+    position: fixed !important;
+    top: 0 !important;
+    left: 0 !important;
+    width: 100vw !important;
+    height: 100vh !important;
+    margin: 0 !important;
+    padding: 0 !important;
+
+    overflow: hidden !important;
+  }
+
+  /* 确保内容区域完全填充 */
+  .content-container,
+  .p-\[var\(--app-content-padding\)\].w-full {
+    height: 100% !important;
+    width: 100% !important;
+    margin: 0 !important;
+    padding: 0 !important;
+    overflow: hidden !important;
+  }
+
+  .min-h-screen {
+    height: 100vh !important;
+    width: 100vw !important;
+    margin: 0 !important;
+    padding: 0 !important;
+
+    position: fixed !important;
+    top: 0 !important;
+    left: 0 !important;
+    overflow: hidden !important;
+    max-width: 100vw !important;
+    max-height: 100vh !important;
+  }
+}

+ 39 - 38
src/utils/dict.ts

@@ -109,15 +109,15 @@ export const getDictLabel = (dictType: string, value: any): string => {
 export enum DICT_TYPE {
   PMS_MAINTAIN_METHOD = 'maintain_method',
   IOT_DEVICE_STATUS = 'iot_device_status', // IOT 设备状态
-  PMS_INSPECT_ORDER_STATUS = "pms_inspect_order_status",
-  OPERATION_FILL_ORDER_STATUS = "operation_fill_order_status",
-  OPERATION_FILL_DEVICE_STATUS = "operation_fill_device_status",
-  PMS_INSPECT_UNIT = "pms_inspect_unit",
+  PMS_INSPECT_ORDER_STATUS = 'pms_inspect_order_status',
+  OPERATION_FILL_ORDER_STATUS = 'operation_fill_order_status',
+  OPERATION_FILL_DEVICE_STATUS = 'operation_fill_device_status',
+  PMS_INSPECT_UNIT = 'pms_inspect_unit',
   PMS_MAIN_STATUS_NO = 'pms_maintain_status_no',
   PMS_MAIN_STATUS = 'pms_maintain_status',
-  PMS_MAIN_TYPE = "pms_main_type",
-  PMS_MAIN_CLASSIFY = "pms_maintain_classify",
-  PMS_BOOLEAN = "pms_boolean",
+  PMS_MAIN_TYPE = 'pms_main_type',
+  PMS_MAIN_CLASSIFY = 'pms_maintain_classify',
+  PMS_BOOLEAN = 'pms_boolean',
   PMS_FAILURE_STATUS = 'pms_failure_status',
   PMS_FILE_TYPE = 'pms_file_type',
   PMS_DEVICE_STATUS = 'pms_device_status',
@@ -128,14 +128,14 @@ export enum DICT_TYPE {
   DEPT_TYPE = 'dept_type',
   TERMINAL = 'terminal', // 终端
   DATE_INTERVAL = 'date_interval', // 数据间隔
-// ========== supplier 模块 ==========
-  SUPPLIER_TYPE = "supplier_classification",
-  PMS_SUPPLIER_NATURE = "pms_supplier_nature",
-  SUPPLIER_COMPANY_TYPE = "supplier_type",
-  SUPPLIER_NATURE = "supplier_nature",
-  SUPPLIER_STATUS = "supplier_status",
-  SUPPLIER_SIZE = "supplier_size",
-  SUPPLIER_CERT = "supplier_certificate",
+  // ========== supplier 模块 ==========
+  SUPPLIER_TYPE = 'supplier_classification',
+  PMS_SUPPLIER_NATURE = 'pms_supplier_nature',
+  SUPPLIER_COMPANY_TYPE = 'supplier_type',
+  SUPPLIER_NATURE = 'supplier_nature',
+  SUPPLIER_STATUS = 'supplier_status',
+  SUPPLIER_SIZE = 'supplier_size',
+  SUPPLIER_CERT = 'supplier_certificate',
   // ========== SYSTEM 模块 ==========
   SYSTEM_USER_SEX = 'system_user_sex',
   SYSTEM_MENU_TYPE = 'system_menu_type',
@@ -271,35 +271,35 @@ export enum DICT_TYPE {
   IOT_DATA_BRIDGE_TYPE_ENUM = 'iot_data_bridge_type_enum', // 桥梁类型
 
   // ========== PMS模块  ==========
-  PMS_INSPECT_WRITE = "inspect_wirte_normal",
+  PMS_INSPECT_WRITE = 'inspect_wirte_normal',
   PMS_BOM_NODE_EXT_ATTR = 'BOM_NODE_EXT_ATTR', // BOM节点扩展属性 维护 or 保养
   PMS_MAIN_WORK_ORDER_TYPE = 'pms_main_work_order_type', // 保养工单类型
   PMS_MAIN_WORK_ORDER_RESULT = 'pms_main_work_order_result', // 保养工单状态
-  RQ_IOT_ISCOLLECTION = 'rq_iot_isCollection',//是否数采
-  RQ_IOT_ISREPORT = 'rq_iot_isReport',//日报是否取值
+  RQ_IOT_ISCOLLECTION = 'rq_iot_isCollection', //是否数采
+  RQ_IOT_ISREPORT = 'rq_iot_isReport', //日报是否取值
   RQ_IOT_MODEL_TEMPLATE_ATTR = 'rq_iot_model_template_attr',
-  RQ_IOT_MODEL_PLUS = "rq_iot_model_plus",
-  RQ_IOT_SUM = 'rq_iot_isSum',//是否累计
-  PMS_ORDER_PROCESS_MODE = "pms_main_work_order_process_mode",  // 保养方式 0内部保养  1委外保养
+  RQ_IOT_MODEL_PLUS = 'rq_iot_model_plus',
+  RQ_IOT_SUM = 'rq_iot_isSum', //是否累计
+  PMS_ORDER_PROCESS_MODE = 'pms_main_work_order_process_mode', // 保养方式 0内部保养  1委外保养
   PMS_THING_MODEL_UNIT = 'pms_thing_model_unit', // pms属性模板单位
-  PMS_PROJECT_TASK_SCHEDULE = 'constructionStatus',  // 日报 项目管理 任务计划
-  PMS_PROJECT_TASK_RY_SCHEDULE = 'rigStatus',  // 日报 项目管理 瑞鹰项目进度
-  PMS_PROJECT_TASK_RY_REPAIR_SCHEDULE = 'repairStatus',  // 日报 瑞鹰 修井 项目进度
-  PMS_PROJECT_SETTLEMENT = 'rq_iot_project_settlement_method',  // 日报 项目管理 结算方式
-  PMS_PROJECT_OVERSEA_FLAG = 'rq_iot_project_oversea_flag',  // 日报 项目管理 是否海外项目
-  PMS_PROJECT_AREA = 'rq_iot_project_area',  // 日报 项目管理 海外项目所属区域
-  PMS_PROJECT_WELL_TYPE = 'rq_iot_project_well_type',  // 日报 项目管理 井型
-  PMS_PROJECT_WELL_CATEGORY = 'rq_iot_project_well_category',  // 日报 项目管理 井别
-  PMS_PROJECT_TECHNOLOGY = 'rq_iot_project_technology',  // 日报 项目管理 施工工艺
+  PMS_PROJECT_TASK_SCHEDULE = 'constructionStatus', // 日报 项目管理 任务计划
+  PMS_PROJECT_TASK_RY_SCHEDULE = 'rigStatus', // 日报 项目管理 瑞鹰项目进度
+  PMS_PROJECT_TASK_RY_REPAIR_SCHEDULE = 'repairStatus', // 日报 瑞鹰 修井 项目进度
+  PMS_PROJECT_SETTLEMENT = 'rq_iot_project_settlement_method', // 日报 项目管理 结算方式
+  PMS_PROJECT_OVERSEA_FLAG = 'rq_iot_project_oversea_flag', // 日报 项目管理 是否海外项目
+  PMS_PROJECT_AREA = 'rq_iot_project_area', // 日报 项目管理 海外项目所属区域
+  PMS_PROJECT_WELL_TYPE = 'rq_iot_project_well_type', // 日报 项目管理 井型
+  PMS_PROJECT_WELL_CATEGORY = 'rq_iot_project_well_category', // 日报 项目管理 井别
+  PMS_PROJECT_TECHNOLOGY = 'rq_iot_project_technology', // 日报 项目管理 施工工艺
   PMS_PROJECT_RY_TECHNOLOGY = 'rq_iot_project_technology_ry', // 瑞鹰施工工艺
-  PMS_PROJECT_WORKLOAD_UNIT = 'rq_iot_project_measure_unit',  // 日报 项目管理 工作量单位
-  PMS_PROJECT_WORK_AREA = 'rq_iot_project_work_area',     // 日报 施工区域
-  PMS_PROJECT_NPT_REASON = 'nptReason',    // 日报 非生产时间原因
-  PMS_PROJECT_RY_NPT_REASON = 'ryNptReason',  // 瑞鹰日报 非生产时间原因
-  PMS_PROJECT_WELL_CONTROL_LEVEL = 'rq_iot_well_control_level',  // 井控级别
-  PMS_PROJECT_RD_STATUS = 'rdStatus',                  // 瑞都 施工状态
-  PMS_PROJECT_CASING_PIPE_SIZE = 'rq_iot_casing_pipe_size',    // 日报 套生段产管尺寸
-  PMS_PROJECT_RD_TECHNOLOGY = 'rq_iot_project_technology_rd',   // 瑞都施工工艺
+  PMS_PROJECT_WORKLOAD_UNIT = 'rq_iot_project_measure_unit', // 日报 项目管理 工作量单位
+  PMS_PROJECT_WORK_AREA = 'rq_iot_project_work_area', // 日报 施工区域
+  PMS_PROJECT_NPT_REASON = 'nptReason', // 日报 非生产时间原因
+  PMS_PROJECT_RY_NPT_REASON = 'ryNptReason', // 瑞鹰日报 非生产时间原因
+  PMS_PROJECT_WELL_CONTROL_LEVEL = 'rq_iot_well_control_level', // 井控级别
+  PMS_PROJECT_RD_STATUS = 'rdStatus', // 瑞都 施工状态
+  PMS_PROJECT_CASING_PIPE_SIZE = 'rq_iot_casing_pipe_size', // 日报 套生段产管尺寸
+  PMS_PROJECT_RD_TECHNOLOGY = 'rq_iot_project_technology_rd', // 瑞都施工工艺
 
   // 视频中心
 
@@ -312,4 +312,5 @@ export enum DICT_TYPE {
   VIDEO_TYPE = 'video_type',
   VIDEO_PRODUCT_TYPE = 'video_product_type',
 
+  DEVICE_GROUP_TYPE = 'device_group_type'
 }

+ 4 - 4
src/views/oli-connection/monitoring/detail.vue

@@ -596,13 +596,13 @@ onUnmounted(() => {
 
 <template>
   <div
-    class="w-full bg-gradient-to-r from-blue-100 to-white rounded-lg p-6 shadow"
+    class="w-full bg-gradient-to-r from-blue-100 to-white rounded-lg p-4 shadow"
     id="td-device-info"
   >
     <h2 class="flex items-center gap-2">
       <el-icon class="text-sky!"><Odometer /></el-icon> 设备基础信息
     </h2>
-    <el-form size="large" label-position="top" class="mt-4 grid grid-cols-3 gap-4">
+    <el-form size="default" label-position="top" class="mt-4 grid grid-cols-4 gap-2">
       <el-form-item label="资产编码"> {{ data.deviceCode }} </el-form-item>
       <el-form-item label="设备类别"> {{ data.deviceName }} </el-form-item>
       <el-form-item label="所在部门"> {{ data.dept }} </el-form-item>
@@ -652,7 +652,7 @@ onUnmounted(() => {
     <div
       v-loading="dimensionLoading"
       element-loading-background="transparent"
-      class="w-full mt-4 grid grid-cols-4 gap-4 min-h-30"
+      class="w-full mt-4 grid grid-cols-5 gap-2 min-h-30"
       id="dimension"
     >
       <button
@@ -680,7 +680,7 @@ onUnmounted(() => {
     class="mt-4 w-full rounded-lg bg-gradient-to-r from-blue-100 to-white p-4 shadow"
   >
     <header class="font-medium text-center w-full">中航北斗</header>
-    <div class="w-full mt-4 grid grid-cols-4 gap-4" id="dimension">
+    <div class="w-full mt-4 grid grid-cols-5 gap-2 min-h-30" id="dimension">
       <button
         v-for="item in carDimensions"
         :key="item.identifier"

+ 179 - 73
src/views/pms/device/IotDeviceForm.vue

@@ -1,6 +1,12 @@
 <template>
   <ContentWrap v-loading="formLoading">
-    <el-form ref="formRef" :model="formData" :rules="formRules" style="margin-right: 4em;margin-left: 0.5em" label-width="130px">
+    <el-form
+      ref="formRef"
+      :model="formData"
+      :rules="formRules"
+      style="margin-right: 4em; margin-left: 0.5em"
+      label-width="130px"
+    >
       <div class="title-div">
         <el-button @click="baseInfoClick" class="title-button">
           <Icon color="black" icon="ep:set-up" :size="18" class="cursor-pointer first-icon" />
@@ -18,24 +24,33 @@
           <el-col :span="8">
             <el-form-item :label="t('iotDevice.yfClass')" prop="yfClass">
               <el-cascader
-                :disabled="formType==='update'&&formData.yfDeviceCode"
+                :disabled="formType === 'update' && formData.yfDeviceCode"
                 style="width: 100%"
                 v-model="formData.yfClass"
                 :options="yfclasses"
                 :props="{ expandTrigger: 'hover' }"
                 clearable
                 filterable
-                @change="handleYfClassChange" />
+                @change="handleYfClassChange"
+              />
             </el-form-item>
           </el-col>
           <el-col :span="8">
             <el-form-item :label="t('iotDevice.yfCode')" prop="yfDeviceCode">
-              <el-input v-model="formData.yfDeviceCode" :disabled="formData.yfDeviceCode" placeholder="请输入油服设备编码" />
+              <el-input
+                v-model="formData.yfDeviceCode"
+                :disabled="formData.yfDeviceCode"
+                placeholder="请输入油服设备编码"
+              />
             </el-form-item>
           </el-col>
           <el-col :span="8">
             <el-form-item :label="t('iotDevice.code')" prop="deviceCode">
-              <el-input v-model="formData.deviceCode" :disabled="formType==='update'" placeholder="请输入设备编码" />
+              <el-input
+                v-model="formData.deviceCode"
+                :disabled="formType === 'update'"
+                placeholder="请输入设备编码"
+              />
             </el-form-item>
           </el-col>
           <el-col :span="8">
@@ -46,7 +61,7 @@
           <el-col :span="8">
             <el-form-item :label="t('iotDevice.dept')" prop="deptId">
               <el-tree-select
-                :disabled="formType==='update'"
+                :disabled="formType === 'update'"
                 v-model="formData.deptId"
                 :data="deptList"
                 :props="defaultProps"
@@ -55,20 +70,20 @@
                 filterable
                 placeholder="请选择所在部门"
               />
-<!--              <el-tree-select-->
-<!--                v-model="formData.deptId"-->
-<!--                :data="deptList"-->
-<!--                :props="defaultProps"-->
-<!--                check-strictly-->
-<!--                node-key="id"-->
-<!--                placeholder="请选择归属部门"-->
-<!--              />-->
+              <!--              <el-tree-select-->
+              <!--                v-model="formData.deptId"-->
+              <!--                :data="deptList"-->
+              <!--                :props="defaultProps"-->
+              <!--                check-strictly-->
+              <!--                node-key="id"-->
+              <!--                placeholder="请选择归属部门"-->
+              <!--              />-->
             </el-form-item>
           </el-col>
           <el-col :span="8">
             <el-form-item :label="t('deviceForm.category')" prop="assetClass">
               <el-tree-select
-                :disabled="formType==='update'&&username!=='超级管理员'"
+                :disabled="formType === 'update' && username !== '超级管理员'"
                 v-model="formData.assetClass"
                 :data="productClassifyList"
                 :props="defaultProps"
@@ -82,7 +97,12 @@
           </el-col>
           <el-col :span="8">
             <el-form-item :label="t('iotDevice.status')" prop="deviceStatus">
-              <el-select v-model="formData.deviceStatus" :placeholder="t('deviceForm.choose')" :disabled="formType==='update'" clearable>
+              <el-select
+                v-model="formData.deviceStatus"
+                :placeholder="t('deviceForm.choose')"
+                :disabled="formType === 'update'"
+                clearable
+              >
                 <el-option
                   v-for="dict in getStrDictOptions(DICT_TYPE.PMS_DEVICE_STATUS)"
                   :key="dict.label"
@@ -115,8 +135,19 @@
               />
             </el-form-item>
           </el-col>
-          <el-col :span="8" >
-            <div style="display: flex;flex-direction: row">
+          <el-col :span="8">
+            <el-form-item label="车牌号" prop="carNo">
+              <el-input clearable v-model="formData.carNo" placeholder="请输入车牌号" />
+            </el-form-item>
+          </el-col>
+
+          <el-col :span="8">
+            <el-form-item label="设备号" prop="deviceNo">
+              <el-input clearable v-model="formData.deviceNo" placeholder="请输入设备号" />
+            </el-form-item>
+          </el-col>
+          <el-col :span="8">
+            <div style="display: flex; flex-direction: row">
               <el-form-item :label="t('deviceForm.model')" prop="model" style="width: 86%">
                 <el-input
                   clearable
@@ -137,6 +168,11 @@
               <el-input v-model="formData.assetOwnership" :disabled="isDetail" height="60px" />
             </el-form-item>
           </el-col>
+          <el-col :span="8">
+            <el-form-item label="所在地点" prop="address">
+              <el-input v-model="formData.address" height="60px" />
+            </el-form-item>
+          </el-col>
           <el-col :span="8">
             <el-form-item :label="t('deviceForm.picture')" prop="picUrl">
               <UploadImg v-model="formData.picUrl" :disabled="isDetail" height="60px" />
@@ -144,7 +180,11 @@
           </el-col>
           <el-col :span="8">
             <el-form-item :label="t('deviceForm.remark')" prop="remark">
-              <el-input v-model="formData.remark" type="textarea" :placeholder="t('deviceForm.remarkHolder')" />
+              <el-input
+                v-model="formData.remark"
+                type="textarea"
+                :placeholder="t('deviceForm.remarkHolder')"
+              />
             </el-form-item>
           </el-col>
         </el-row>
@@ -220,7 +260,11 @@
           </el-col>
           <el-col :span="8">
             <el-form-item :label="t('deviceForm.ni')" prop="nameplate">
-              <el-input v-model="formData.nameplate" type="textarea" :placeholder="t('deviceForm.niHolder')"/>
+              <el-input
+                v-model="formData.nameplate"
+                type="textarea"
+                :placeholder="t('deviceForm.niHolder')"
+              />
             </el-form-item>
           </el-col>
         </el-row>
@@ -240,56 +284,82 @@
       <div class="cw-expandable-content" :class="{ 'is-expanded': cwIsExpanded }">
         <el-row>
           <el-col :span="8">
-            <el-form-item :label="formData.assetProperty==='zy'?'采购价格':'租赁价格'" prop="plPrice">
+            <el-form-item
+              :label="formData.assetProperty === 'zy' ? '采购价格' : '租赁价格'"
+              prop="plPrice"
+            >
               <el-input
                 v-model="formData.plPrice"
                 @input="handleInput(formData.plPrice, 'plPrice')"
-                :placeholder="formData.assetProperty==='zy'?'请输入采购价格':'请输入租赁价格'"
+                :placeholder="formData.assetProperty === 'zy' ? '请输入采购价格' : '请输入租赁价格'"
               />
             </el-form-item>
           </el-col>
           <el-col :span="8">
-            <el-form-item :label="formData.assetProperty==='zy'?'采购日期':'租赁日期'" prop="plDate">
+            <el-form-item
+              :label="formData.assetProperty === 'zy' ? '采购日期' : '租赁日期'"
+              prop="plDate"
+            >
               <el-date-picker
                 style="width: 150%"
                 v-model="formData.plDate"
                 type="date"
                 value-format="x"
-                :placeholder="formData.assetProperty==='zy'?'请输入采购日期':'请输入租赁日期'"
+                :placeholder="formData.assetProperty === 'zy' ? '请输入采购日期' : '请输入租赁日期'"
               />
             </el-form-item>
           </el-col>
           <el-col :span="8">
-            <el-form-item :label="formData.assetProperty==='zy'?'折旧年限':'租赁年限'" prop="plYear">
-              <el-input v-model="formData.plYear" type="number" :placeholder="formData.assetProperty==='zy'?'请输入折旧年限':'请输入租赁年限'" />
+            <el-form-item
+              :label="formData.assetProperty === 'zy' ? '折旧年限' : '租赁年限'"
+              prop="plYear"
+            >
+              <el-input
+                v-model="formData.plYear"
+                type="number"
+                :placeholder="formData.assetProperty === 'zy' ? '请输入折旧年限' : '请输入租赁年限'"
+              />
             </el-form-item>
           </el-col>
           <el-col :span="8">
-            <el-form-item :label="formData.assetProperty==='zy'?'折旧开始日期':'租赁开始日期'" prop="plStartDate">
+            <el-form-item
+              :label="formData.assetProperty === 'zy' ? '折旧开始日期' : '租赁开始日期'"
+              prop="plStartDate"
+            >
               <el-date-picker
                 style="width: 150%"
                 v-model="formData.plStartDate"
                 type="date"
                 value-format="x"
-                :placeholder="formData.assetProperty==='zy'?'请选择折旧开始日期':'请选择租赁开始日期'"
+                :placeholder="
+                  formData.assetProperty === 'zy' ? '请选择折旧开始日期' : '请选择租赁开始日期'
+                "
               />
             </el-form-item>
           </el-col>
           <el-col :span="8">
-            <el-form-item :label="formData.assetProperty==='zy'?'已提折旧月数':'已租赁月数'" prop="plMonthed">
+            <el-form-item
+              :label="formData.assetProperty === 'zy' ? '已提折旧月数' : '已租赁月数'"
+              prop="plMonthed"
+            >
               <el-input
                 v-model="formData.plMonthed"
                 type="number"
-                :placeholder="formData.assetProperty==='zy'?'请输入已提折旧月数':'请输入已租赁月数'"
+                :placeholder="
+                  formData.assetProperty === 'zy' ? '请输入已提折旧月数' : '请输入已租赁月数'
+                "
               />
             </el-form-item>
           </el-col>
           <el-col :span="8">
-            <el-form-item :label="formData.assetProperty==='zy'?'已提折旧金额':'已租赁金额'" prop="plAmounted">
+            <el-form-item
+              :label="formData.assetProperty === 'zy' ? '已提折旧金额' : '已租赁金额'"
+              prop="plAmounted"
+            >
               <el-input
                 v-model="formData.plAmounted"
                 @input="handleInput(formData.plAmounted, 'plAmounted')"
-                :placeholder="formData.assetProperty==='zy'?'请输入已提折旧金额':'已租赁金额'"
+                :placeholder="formData.assetProperty === 'zy' ? '请输入已提折旧金额' : '已租赁金额'"
               />
             </el-form-item>
           </el-col>
@@ -319,7 +389,12 @@
       <div class="qt-expandable-content" :class="{ 'is-expanded': qtIsExpanded }">
         <el-row>
           <el-col v-for="field in list" :key="field.sort" :span="8">
-            <el-form-item label-width="180px" :label="field.name" :prop="field.code" :rules="field.rules">
+            <el-form-item
+              label-width="180px"
+              :label="field.name"
+              :prop="field.code"
+              :rules="field.rules"
+            >
               <!-- 文本输入 -->
               <el-input
                 v-if="field.type === 'text'"
@@ -331,7 +406,7 @@
               <el-select
                 v-else-if="field.type === 'enum'"
                 v-model="formData[field.code]"
-                :placeholder="'请输入'+field.name"
+                :placeholder="'请输入' + field.name"
                 clearable
                 filterable
               >
@@ -382,7 +457,7 @@
     </el-form>
   </ContentWrap>
   <BrandList ref="brandFormRef" @choose="brandChoose" />
-  <ModelList ref="modelFormRef" @choose="modelChoose" :brand = "formData.brand" />
+  <ModelList ref="modelFormRef" @choose="modelChoose" :brand="formData.brand" />
   <CustomerList ref="customerZzFormRef" @choose="customerZzChoose" />
   <CustomerList ref="customerSupplierFormRef" @choose="customerSupplierChoose" />
 </template>
@@ -396,10 +471,11 @@ import * as DeptApi from '@/api/system/dept'
 import * as ProductClassifyApi from '@/api/pms/productclassify'
 import { DICT_TYPE, getStrDictOptions } from '@/utils/dict'
 import { useTagsViewStore } from '@/store/modules/tagsView'
-import {DeviceAttrModelApi} from "@/api/pms/deviceattrmodel";
+import { DeviceAttrModelApi } from '@/api/pms/deviceattrmodel'
 import { CACHE_KEY, useCache } from '@/hooks/web/useCache'
-import {IotYfClassifyApi} from "@/api/pms/yfclass";
-import { useRefreshStore } from '@/store/modules/pms/refreshStore';
+import { IotYfClassifyApi } from '@/api/pms/yfclass'
+import { useRefreshStore } from '@/store/modules/pms/refreshStore'
+import { watch } from 'vue'
 
 /** 设备台账 表单 */
 defineOptions({ name: 'DeviceDetailAdd' })
@@ -412,7 +488,7 @@ const username = ref('')
 const deptList = ref<Tree[]>([]) // 树形结构
 const productClassifyList = ref<Tree[]>([]) // 树形结构
 const { delView } = useTagsViewStore() // 视图操作
-const { params, name,query } = useRoute() // 查询参数
+const { params, name, query } = useRoute() // 查询参数
 const { currentRoute, push } = useRouter()
 const { wsCache } = useCache()
 const id = params.id
@@ -429,7 +505,7 @@ const brandLabel = ref('') // 表单的类型:create - 新增;update - 修
 const zzLabel = ref('') // 表单的类型:create - 新增;update - 修改
 const supplierLabel = ref('') // 表单的类型:create - 新增;update - 修改
 const yfclasses = ref([])
-const refreshStore = useRefreshStore();
+const refreshStore = useRefreshStore()
 
 const formData = ref({
   id: undefined,
@@ -468,10 +544,25 @@ const formData = ref({
   infoRemark: undefined,
   infoUrl: undefined,
   templateJson: undefined,
-  assetClass: undefined
+  assetClass: undefined,
+  carNo: undefined,
+  deviceNo: undefined,
+  address: undefined
 })
 const formRules = reactive({
-  yfClass: [{ required: true, message: '编码类别不能为空', trigger: 'blur' }],
+  yfClass: [
+    {
+      validator: (rule, value, callback) => {
+        // 当资产性质为租赁('zl')时,yfClass非必填;否则必填
+        if (formData.value.assetProperty === 'zl' || value) {
+          callback() // 租赁资产或有值时通过验证
+        } else {
+          callback(new Error('编码类别不能为空')) // 非租赁资产且无值时失败
+        }
+      },
+      trigger: 'blur'
+    }
+  ],
   yfDeviceCode: [{ required: true, message: '油服编码不能为空', trigger: 'blur' }],
   assetClass: [{ required: true, message: '资产类别不能为空', trigger: 'blur' }],
   deviceCode: [{ required: true, message: '设备编码不能为空', trigger: 'blur' }],
@@ -485,19 +576,19 @@ const formRules = reactive({
 })
 
 const list = ref([])
-const handleYfClassChange = async (value) =>{
+const handleYfClassChange = async (value) => {
   console.log(value)
   const prefix = value.join('')
   const last = await IotDeviceApi.getMaxCode(prefix)
-  formData.value.yfDeviceCode = prefix+last
+  formData.value.yfDeviceCode = prefix + last
 }
 const assetclasschange = () => {
   const assetClass = formData.value.assetClass
-  DeviceAttrModelApi.getDeviceAttrModelListByDeviceCategoryId(assetClass).then(res => {
-    if (res){
+  DeviceAttrModelApi.getDeviceAttrModelListByDeviceCategoryId(assetClass).then((res) => {
+    if (res) {
       res.forEach((item) => {
         if (item.requiredFlag) {
-          const rule = {required: true, message: item.name+'不能为空', trigger: 'blur'}
+          const rule = { required: true, message: item.name + '不能为空', trigger: 'blur' }
           item.rules = []
           item.rules.push(rule)
         }
@@ -509,12 +600,25 @@ const assetclasschange = () => {
   })
 }
 
+watch(
+  () => formData.value.assetProperty,
+  (newVal) => {
+    nextTick(() => {
+      if (formRef.value) {
+        // 重新验证 yfClass 和 yfCode 字段
+        formRef.value.validateField('yfClass')
+      }
+    })
+  },
+  { immediate: true }
+)
+
 const brandChoose = (row) => {
   formData.value.brand = row.id
   // brandLabel.value = row.value
   formData.value.brandName = row.label
 }
-const brandClear = () =>{
+const brandClear = () => {
   formData.value.brand = undefined
   formData.value.brandName = undefined
 }
@@ -535,7 +639,7 @@ const customerZzChoose = (row) => {
   // zzLabel.value = row.name
   formData.value.manufacturerName = row.name
 }
-const zzClear = () =>{
+const zzClear = () => {
   formData.value.manufacturerId = undefined
   formData.value.manufacturerName = undefined
 }
@@ -545,7 +649,7 @@ const openForm = () => {
   brandFormRef.value.open()
 }
 const modelFormRef = ref()
-const openModelForm = () =>{
+const openModelForm = () => {
   modelFormRef.value.open()
 }
 const customerSupplierFormRef = ref()
@@ -594,7 +698,7 @@ const handleInput = (value, obj) => {
 
 const close = () => {
   delView(unref(currentRoute))
-  push({ name: 'IotDevicePms', params:{}})
+  push({ name: 'IotDevicePms', params: {} })
   // delView(unref(currentRoute))
   // push({
   //   name: 'IotDevicePms',
@@ -625,13 +729,13 @@ const submitForm = async () => {
   formLoading.value = true
   try {
     if (list.value) {
-      list.value = list.value.map(item => ({
+      list.value = list.value.map((item) => ({
         ...item,
         value: formData.value[item.code] // 自定义属性生成逻辑
       }))
       formData.value.templateJson = JSON.stringify(list.value)
     }
-    formData.value.yfClass = formData.value.yfClass.join(',');
+    formData.value.yfClass = formData.value.yfClass.join(',')
     const data = formData.value as unknown as IotDeviceVO
     if (formType.value === 'create') {
       await IotDeviceApi.createIotDevice(data)
@@ -643,11 +747,11 @@ const submitForm = async () => {
     dialogVisible.value = false
     // 发送操作成功的事件
     //emit('success')
-    const sourcePage = query.source as string;
+    const sourcePage = query.source as string
 
     // 如果有来源页面标识,触发原页面的刷新
     if (sourcePage) {
-      refreshStore.triggerRefresh(sourcePage);
+      refreshStore.triggerRefresh(sourcePage)
     }
     close()
   } finally {
@@ -666,20 +770,23 @@ onMounted(async () => {
   formData.value.assetProperty = 'zy'
   // 修改时,设置数据
   if (id) {
-    formType.value = 'update';
+    formType.value = 'update'
     formLoading.value = true
     try {
-      const iotDevice = await IotDeviceApi.getIotDevice(id);
+      const iotDevice = await IotDeviceApi.getIotDevice(id)
       formData.value = iotDevice
-      formData.value.brandName = iotDevice.brandName;
-      formData.value.manufacturerName = iotDevice.zzName;
-      formData.value.supplierName = iotDevice.supplierName;
+      formData.value.brandName = iotDevice.brandName
+      formData.value.manufacturerName = iotDevice.zzName
+      formData.value.supplierName = iotDevice.supplierName
+      formData.value.carNo = iotDevice.carNo
+      formData.value.deviceNo = iotDevice.deviceNo
+      formData.value.address = iotDevice.address
       if (iotDevice.yfClass) {
-        formData.value.yfClass = iotDevice.yfClass.split(',');
+        formData.value.yfClass = iotDevice.yfClass.split(',')
       }
-      list.value = JSON.parse(iotDevice.templateJson);
+      list.value = JSON.parse(iotDevice.templateJson)
       list.value.forEach((item) => {
-        formData.value[item.code] = item.value;
+        formData.value[item.code] = item.value
       })
     } finally {
       formLoading.value = false
@@ -688,11 +795,11 @@ onMounted(async () => {
     if (deptId) {
       formData.value.deptId = Number(deptId)
     }
-    formType.value = 'create';
+    formType.value = 'create'
   }
-  await IotYfClassifyApi.getChildrenList().then(res => {
+  await IotYfClassifyApi.getChildrenList().then((res) => {
     yfclasses.value = res
-  });
+  })
 })
 /** 重置表单 */
 const resetForm = () => {
@@ -725,8 +832,7 @@ const resetForm = () => {
     infoRemark: undefined,
     infoUrl: undefined,
     templateJson: undefined,
-    assetClass: undefined,
-
+    assetClass: undefined
   }
   formRef.value?.resetFields()
 }
@@ -740,7 +846,7 @@ const resetForm = () => {
 }
 
 .base-expandable-content.is-expanded {
-  min-height: 260px; /* 或者根据内容设定一个合适的最大高度 */
+  min-height: 350px; /* 或者根据内容设定一个合适的最大高度 */
 }
 .zz-expandable-content {
   max-height: 0; /* 初始高度为0 */
@@ -769,18 +875,18 @@ const resetForm = () => {
 .qt-expandable-content.is-expanded {
   max-height: 1200px; /* 或者根据内容设定一个合适的最大高度 */
 }
-.title-button{
+.title-button {
   font-size: 18px;
   border: none;
 }
-.title-div{
+.title-div {
   margin-bottom: 20px;
   margin-top: 10px;
 }
-.cursor-pointer{
+.cursor-pointer {
   vertical-align: middle;
 }
-.first-icon{
+.first-icon {
   margin-bottom: 2px;
 }
 </style>

+ 187 - 76
src/views/pms/device/IotDeviceFormAdd.vue

@@ -1,6 +1,12 @@
 <template>
   <ContentWrap v-loading="formLoading">
-    <el-form ref="formRef" :model="formData" :rules="formRules" style="margin-right: 4em;margin-left: 0.5em" label-width="130px">
+    <el-form
+      ref="formRef"
+      :model="formData"
+      :rules="formRules"
+      style="margin-right: 4em; margin-left: 0.5em"
+      label-width="130px"
+    >
       <div class="title-div">
         <el-button @click="baseInfoClick" class="title-button">
           <Icon color="black" icon="ep:set-up" :size="18" class="cursor-pointer first-icon" />
@@ -16,38 +22,51 @@
       <div class="base-expandable-content" :class="{ 'is-expanded': baseIsExpanded }">
         <el-row>
           <el-col :span="8">
-            <el-form-item :label="t('iotDevice.yfClass')" prop="yfClass">
+            <el-form-item
+              :label="t('iotDevice.yfClass')"
+              prop="yfClass"
+              :required="formData.assetProperty !== 'zl'"
+            >
               <el-cascader
-                :disabled="formType==='update'&&formData.yfDeviceCode"
+                :disabled="formType === 'update' && formData.yfDeviceCode"
                 style="width: 100%"
                 v-model="formData.yfClass"
                 :options="yfclasses"
                 :props="{ expandTrigger: 'hover' }"
                 clearable
                 filterable
-                @change="handleYfClassChange" />
+                @change="handleYfClassChange"
+              />
             </el-form-item>
           </el-col>
           <el-col :span="8">
             <el-form-item :label="t('iotDevice.yfCode')" prop="yfDeviceCode">
-              <el-input v-model="formData.yfDeviceCode" :disabled="formData.yfDeviceCode" placeholder="请输入油服设备编码" />
+              <el-input
+                v-model="formData.yfDeviceCode"
+                :disabled="formData.yfDeviceCode"
+                placeholder="请输入油服设备编码"
+              />
             </el-form-item>
           </el-col>
           <el-col :span="8">
             <el-form-item :label="t('iotDevice.code')" prop="deviceCode">
-              <el-input v-model="formData.deviceCode" :disabled="formType==='update'" placeholder="请输入设备编码" />
+              <el-input
+                v-model="formData.deviceCode"
+                :disabled="formType === 'update'"
+                placeholder="请输入设备编码"
+              />
             </el-form-item>
           </el-col>
           <el-col :span="8">
             <el-form-item :label="t('iotDevice.name')" prop="deviceName">
-<!--              <el-input v-model="formData.deviceName" placeholder="请输入设备名称" />-->
+              <!--              <el-input v-model="formData.deviceName" placeholder="请输入设备名称" />-->
               <lang-input v-model="formData.deviceName" placeholder="请输入设备名称" />
             </el-form-item>
           </el-col>
           <el-col :span="8">
             <el-form-item :label="t('iotDevice.dept')" prop="deptId">
               <el-tree-select
-                :disabled="formType==='update'"
+                :disabled="formType === 'update'"
                 v-model="formData.deptId"
                 :data="deptList"
                 :props="defaultProps"
@@ -56,20 +75,20 @@
                 filterable
                 placeholder="请选择所在部门"
               />
-<!--              <el-tree-select-->
-<!--                v-model="formData.deptId"-->
-<!--                :data="deptList"-->
-<!--                :props="defaultProps"-->
-<!--                check-strictly-->
-<!--                node-key="id"-->
-<!--                placeholder="请选择归属部门"-->
-<!--              />-->
+              <!--              <el-tree-select-->
+              <!--                v-model="formData.deptId"-->
+              <!--                :data="deptList"-->
+              <!--                :props="defaultProps"-->
+              <!--                check-strictly-->
+              <!--                node-key="id"-->
+              <!--                placeholder="请选择归属部门"-->
+              <!--              />-->
             </el-form-item>
           </el-col>
           <el-col :span="8">
             <el-form-item :label="t('deviceForm.category')" prop="assetClass">
               <el-tree-select
-                :disabled="formType==='update'"
+                :disabled="formType === 'update'"
                 v-model="formData.assetClass"
                 :data="productClassifyList"
                 :props="defaultProps"
@@ -83,7 +102,12 @@
           </el-col>
           <el-col :span="8">
             <el-form-item :label="t('iotDevice.status')" prop="deviceStatus">
-              <el-select v-model="formData.deviceStatus" :placeholder="t('deviceForm.choose')" :disabled="formType==='update'" clearable>
+              <el-select
+                v-model="formData.deviceStatus"
+                :placeholder="t('deviceForm.choose')"
+                :disabled="formType === 'update'"
+                clearable
+              >
                 <el-option
                   v-for="dict in getStrDictOptions(DICT_TYPE.PMS_DEVICE_STATUS)"
                   :key="dict.label"
@@ -116,8 +140,20 @@
               />
             </el-form-item>
           </el-col>
-          <el-col :span="8" >
-            <div style="display: flex;flex-direction: row">
+
+          <el-col :span="8">
+            <el-form-item label="车牌号" prop="carNo">
+              <el-input clearable v-model="formData.carNo" placeholder="请输入车牌号" />
+            </el-form-item>
+          </el-col>
+
+          <el-col :span="8">
+            <el-form-item label="设备号" prop="deviceNo">
+              <el-input clearable v-model="formData.deviceNo" placeholder="请输入设备号" />
+            </el-form-item>
+          </el-col>
+          <el-col :span="8">
+            <div style="display: flex; flex-direction: row">
               <el-form-item :label="t('deviceForm.model')" prop="model" style="width: 86%">
                 <el-input
                   clearable
@@ -138,18 +174,28 @@
               <el-input v-model="formData.assetOwnership" :disabled="isDetail" height="60px" />
             </el-form-item>
           </el-col>
+          <el-col :span="8">
+            <el-form-item label="所在地点" prop="address">
+              <el-input v-model="formData.address" height="60px" />
+            </el-form-item>
+          </el-col>
           <el-col :span="8">
             <el-form-item :label="t('deviceForm.picture')" prop="picUrl">
               <UploadImg v-model="formData.picUrl" :disabled="isDetail" height="60px" />
             </el-form-item>
           </el-col>
-          <el-col :span="16">
+          <el-col :span="8">
             <el-form-item :label="t('deviceForm.remark')" prop="remark">
-              <el-input v-model="formData.remark" type="textarea" :placeholder="t('deviceForm.remarkHolder')" />
+              <el-input
+                v-model="formData.remark"
+                type="textarea"
+                :placeholder="t('deviceForm.remarkHolder')"
+              />
             </el-form-item>
           </el-col>
         </el-row>
       </div>
+
       <div class="title-div">
         <el-button @click="zzInfoClick" class="title-button">
           <Icon color="black" icon="ep:set-up" :size="18" class="cursor-pointer first-icon" />
@@ -221,7 +267,11 @@
           </el-col>
           <el-col :span="8">
             <el-form-item :label="t('deviceForm.ni')" prop="nameplate">
-              <el-input v-model="formData.nameplate" type="textarea" :placeholder="t('deviceForm.niHolder')"/>
+              <el-input
+                v-model="formData.nameplate"
+                type="textarea"
+                :placeholder="t('deviceForm.niHolder')"
+              />
             </el-form-item>
           </el-col>
         </el-row>
@@ -241,56 +291,82 @@
       <div class="cw-expandable-content" :class="{ 'is-expanded': cwIsExpanded }">
         <el-row>
           <el-col :span="8">
-            <el-form-item :label="formData.assetProperty==='zy'?'采购价格':'租赁价格'" prop="plPrice">
+            <el-form-item
+              :label="formData.assetProperty === 'zy' ? '采购价格' : '租赁价格'"
+              prop="plPrice"
+            >
               <el-input
                 v-model="formData.plPrice"
                 @input="handleInput(formData.plPrice, 'plPrice')"
-                :placeholder="formData.assetProperty==='zy'?'请输入采购价格':'请输入租赁价格'"
+                :placeholder="formData.assetProperty === 'zy' ? '请输入采购价格' : '请输入租赁价格'"
               />
             </el-form-item>
           </el-col>
           <el-col :span="8">
-            <el-form-item :label="formData.assetProperty==='zy'?'采购日期':'租赁日期'" prop="plDate">
+            <el-form-item
+              :label="formData.assetProperty === 'zy' ? '采购日期' : '租赁日期'"
+              prop="plDate"
+            >
               <el-date-picker
                 style="width: 150%"
                 v-model="formData.plDate"
                 type="date"
                 value-format="x"
-                :placeholder="formData.assetProperty==='zy'?'请输入采购日期':'请输入租赁日期'"
+                :placeholder="formData.assetProperty === 'zy' ? '请输入采购日期' : '请输入租赁日期'"
               />
             </el-form-item>
           </el-col>
           <el-col :span="8">
-            <el-form-item :label="formData.assetProperty==='zy'?'折旧年限':'租赁年限'" prop="plYear">
-              <el-input v-model="formData.plYear" type="number" :placeholder="formData.assetProperty==='zy'?'请输入折旧年限':'请输入租赁年限'" />
+            <el-form-item
+              :label="formData.assetProperty === 'zy' ? '折旧年限' : '租赁年限'"
+              prop="plYear"
+            >
+              <el-input
+                v-model="formData.plYear"
+                type="number"
+                :placeholder="formData.assetProperty === 'zy' ? '请输入折旧年限' : '请输入租赁年限'"
+              />
             </el-form-item>
           </el-col>
           <el-col :span="8">
-            <el-form-item :label="formData.assetProperty==='zy'?'折旧开始日期':'租赁开始日期'" prop="plStartDate">
+            <el-form-item
+              :label="formData.assetProperty === 'zy' ? '折旧开始日期' : '租赁开始日期'"
+              prop="plStartDate"
+            >
               <el-date-picker
                 style="width: 150%"
                 v-model="formData.plStartDate"
                 type="date"
                 value-format="x"
-                :placeholder="formData.assetProperty==='zy'?'请选择折旧开始日期':'请选择租赁开始日期'"
+                :placeholder="
+                  formData.assetProperty === 'zy' ? '请选择折旧开始日期' : '请选择租赁开始日期'
+                "
               />
             </el-form-item>
           </el-col>
           <el-col :span="8">
-            <el-form-item :label="formData.assetProperty==='zy'?'已提折旧月数':'已租赁月数'" prop="plMonthed">
+            <el-form-item
+              :label="formData.assetProperty === 'zy' ? '已提折旧月数' : '已租赁月数'"
+              prop="plMonthed"
+            >
               <el-input
                 v-model="formData.plMonthed"
                 type="number"
-                :placeholder="formData.assetProperty==='zy'?'请输入已提折旧月数':'请输入已租赁月数'"
+                :placeholder="
+                  formData.assetProperty === 'zy' ? '请输入已提折旧月数' : '请输入已租赁月数'
+                "
               />
             </el-form-item>
           </el-col>
           <el-col :span="8">
-            <el-form-item :label="formData.assetProperty==='zy'?'已提折旧金额':'已租赁金额'" prop="plAmounted">
+            <el-form-item
+              :label="formData.assetProperty === 'zy' ? '已提折旧金额' : '已租赁金额'"
+              prop="plAmounted"
+            >
               <el-input
                 v-model="formData.plAmounted"
                 @input="handleInput(formData.plAmounted, 'plAmounted')"
-                :placeholder="formData.assetProperty==='zy'?'请输入已提折旧金额':'已租赁金额'"
+                :placeholder="formData.assetProperty === 'zy' ? '请输入已提折旧金额' : '已租赁金额'"
               />
             </el-form-item>
           </el-col>
@@ -320,7 +396,12 @@
       <div class="qt-expandable-content" :class="{ 'is-expanded': qtIsExpanded }">
         <el-row>
           <el-col v-for="field in list" :key="field.sort" :span="8">
-            <el-form-item label-width="180px" :label="field.name" :prop="field.code" :rules="field.rules">
+            <el-form-item
+              label-width="180px"
+              :label="field.name"
+              :prop="field.code"
+              :rules="field.rules"
+            >
               <!-- 文本输入 -->
               <el-input
                 v-if="field.type === 'text'"
@@ -332,7 +413,7 @@
               <el-select
                 v-else-if="field.type === 'enum'"
                 v-model="formData[field.code]"
-                :placeholder="'请输入'+field.name"
+                :placeholder="'请输入' + field.name"
                 clearable
                 filterable
               >
@@ -383,7 +464,7 @@
     </el-form>
   </ContentWrap>
   <BrandList ref="brandFormRef" @choose="brandChoose" />
-  <ModelList ref="modelFormRef" @choose="modelChoose" :brand = "formData.brand" />
+  <ModelList ref="modelFormRef" @choose="modelChoose" :brand="formData.brand" />
   <CustomerList ref="customerZzFormRef" @choose="customerZzChoose" />
   <CustomerList ref="customerSupplierFormRef" @choose="customerSupplierChoose" />
 </template>
@@ -397,9 +478,10 @@ import * as DeptApi from '@/api/system/dept'
 import * as ProductClassifyApi from '@/api/pms/productclassify'
 import { DICT_TYPE, getStrDictOptions } from '@/utils/dict'
 import { useTagsViewStore } from '@/store/modules/tagsView'
-import {DeviceAttrModelApi} from "@/api/pms/deviceattrmodel";
-import {IotYfClassifyApi} from "@/api/pms/yfclass";
-import { useRefreshStore } from '@/store/modules/pms/refreshStore';
+import { DeviceAttrModelApi } from '@/api/pms/deviceattrmodel'
+import { IotYfClassifyApi } from '@/api/pms/yfclass'
+import { useRefreshStore } from '@/store/modules/pms/refreshStore'
+import { watch, nextTick } from 'vue'
 
 /** 设备台账 表单 */
 defineOptions({ name: 'DeviceDetailAddd' })
@@ -428,7 +510,7 @@ const brandLabel = ref('') // 表单的类型:create - 新增;update - 修
 const zzLabel = ref('') // 表单的类型:create - 新增;update - 修改
 const supplierLabel = ref('') // 表单的类型:create - 新增;update - 修改
 const yfclasses = ref([])
-const refreshStore = useRefreshStore();
+const refreshStore = useRefreshStore()
 const formData = ref({
   id: undefined,
   deviceCode: undefined,
@@ -463,10 +545,25 @@ const formData = ref({
   infoRemark: undefined,
   infoUrl: undefined,
   templateJson: undefined,
-  assetClass: undefined
+  assetClass: undefined,
+  carNo: undefined,
+  deviceNo: undefined,
+  address: undefined
 })
 const formRules = reactive({
-  yfClass: [{ required: true, message: '编码类别不能为空', trigger: 'blur' }],
+  yfClass: [
+    {
+      validator: (rule, value, callback) => {
+        // 当资产性质为租赁('zl')时,yfClass非必填;否则必填
+        if (formData.value.assetProperty === 'zl' || value) {
+          callback() // 租赁资产或有值时通过验证
+        } else {
+          callback(new Error('编码类别不能为空')) // 非租赁资产且无值时失败
+        }
+      },
+      trigger: 'blur'
+    }
+  ],
   yfCode: [{ required: true, message: '油服编码不能为空', trigger: 'blur' }],
   assetClass: [{ required: true, message: '资产类别不能为空', trigger: 'blur' }],
   deviceCode: [{ required: true, message: '设备编码不能为空', trigger: 'blur' }],
@@ -479,15 +576,28 @@ const formRules = reactive({
   manDate: [{ required: true, message: '生产日期不能为空', trigger: 'blur' }]
 })
 
+watch(
+  () => formData.value.assetProperty,
+  (newVal) => {
+    nextTick(() => {
+      if (formRef.value) {
+        // 重新验证 yfClass 和 yfCode 字段
+        formRef.value.validateField('yfClass')
+      }
+    })
+  },
+  { immediate: true }
+)
+
 const list = ref([])
 
 const assetclasschange = () => {
   const assetClass = formData.value.assetClass
-  DeviceAttrModelApi.getDeviceAttrModelListByDeviceCategoryId(assetClass).then(res => {
-    if (res){
+  DeviceAttrModelApi.getDeviceAttrModelListByDeviceCategoryId(assetClass).then((res) => {
+    if (res) {
       res.forEach((item) => {
         if (item.requiredFlag) {
-          const rule = {required: true, message: item.name+'不能为空', trigger: 'blur'}
+          const rule = { required: true, message: item.name + '不能为空', trigger: 'blur' }
           item.rules = []
           item.rules.push(rule)
         }
@@ -498,18 +608,18 @@ const assetclasschange = () => {
     }
   })
 }
-const handleYfClassChange = async (value) =>{
+const handleYfClassChange = async (value) => {
   console.log(value)
   const prefix = value.join('')
   const last = await IotDeviceApi.getMaxCode(prefix)
-  formData.value.yfDeviceCode = prefix+last
+  formData.value.yfDeviceCode = prefix + last
 }
 const brandChoose = (row) => {
   formData.value.brand = row.id
   // brandLabel.value = row.value
   formData.value.brandName = row.label
 }
-const brandClear = () =>{
+const brandClear = () => {
   formData.value.brand = undefined
   formData.value.brandName = undefined
 }
@@ -530,7 +640,7 @@ const customerZzChoose = (row) => {
   // zzLabel.value = row.name
   formData.value.manufacturerName = row.name
 }
-const zzClear = () =>{
+const zzClear = () => {
   formData.value.manufacturerId = undefined
   formData.value.manufacturerName = undefined
 }
@@ -540,7 +650,7 @@ const openForm = () => {
   brandFormRef.value.open()
 }
 const modelFormRef = ref()
-const openModelForm = () =>{
+const openModelForm = () => {
   modelFormRef.value.open()
 }
 const customerSupplierFormRef = ref()
@@ -589,7 +699,7 @@ const handleInput = (value, obj) => {
 
 const close = () => {
   delView(unref(currentRoute))
-  push({ name: 'IotDevicePms', params:{}})
+  push({ name: 'IotDevicePms', params: {} })
   // delView(unref(currentRoute))
   // push({
   //   name: 'IotDevicePms',
@@ -620,13 +730,13 @@ const submitForm = async () => {
   formLoading.value = true
   try {
     if (list.value) {
-      list.value = list.value.map(item => ({
+      list.value = list.value.map((item) => ({
         ...item,
         value: formData.value[item.code] // 自定义属性生成逻辑
       }))
       formData.value.templateJson = JSON.stringify(list.value)
     }
-    formData.value.yfClass = formData.value.yfClass.join(',');
+    formData.value.yfClass = formData.value.yfClass ? formData.value.yfClass.join(',') : ''
     const data = formData.value as unknown as IotDeviceVO
     if (formType.value === 'create') {
       await IotDeviceApi.createIotDevice(data)
@@ -638,11 +748,11 @@ const submitForm = async () => {
     dialogVisible.value = false
     // 发送操作成功的事件
     //emit('success')
-    const sourcePage = query.source as string;
+    const sourcePage = query.source as string
 
     // 如果有来源页面标识,触发原页面的刷新
     if (sourcePage) {
-      refreshStore.triggerRefresh(sourcePage);
+      refreshStore.triggerRefresh(sourcePage)
     }
     close()
   } finally {
@@ -658,20 +768,22 @@ onMounted(async () => {
   formData.value.assetProperty = 'zy'
   // 修改时,设置数据
   if (id) {
-    formType.value = 'update';
+    formType.value = 'update'
     formLoading.value = true
     try {
-      const iotDevice = await IotDeviceApi.getIotDevice(id);
+      const iotDevice = await IotDeviceApi.getIotDevice(id)
       formData.value = iotDevice
-      formData.value.brandName = iotDevice.brandName;
-      formData.value.manufacturerName = iotDevice.zzName;
-      formData.value.supplierName = iotDevice.supplierName;
-      list.value = JSON.parse(iotDevice.templateJson);
+      formData.value.brandName = iotDevice.brandName
+      formData.value.manufacturerName = iotDevice.zzName
+      formData.value.supplierName = iotDevice.supplierName
+      formData.value.carNo = iotDevice.carNo
+      formData.value.deviceNo = iotDevice.deviceNo
+      list.value = JSON.parse(iotDevice.templateJson)
       if (iotDevice.yfClass) {
-        formData.value.yfClass = iotDevice.yfClass.split(',');
+        formData.value.yfClass = iotDevice.yfClass.split(',')
       }
       list.value.forEach((item) => {
-        formData.value[item.code] = item.value;
+        formData.value[item.code] = item.value
       })
     } finally {
       formLoading.value = false
@@ -680,11 +792,11 @@ onMounted(async () => {
     if (deptId) {
       formData.value.deptId = Number(deptId)
     }
-    formType.value = 'create';
+    formType.value = 'create'
   }
-  await IotYfClassifyApi.getChildrenList().then(res => {
+  await IotYfClassifyApi.getChildrenList().then((res) => {
     yfclasses.value = res
-  });
+  })
 })
 /** 重置表单 */
 const resetForm = () => {
@@ -717,8 +829,7 @@ const resetForm = () => {
     infoRemark: undefined,
     infoUrl: undefined,
     templateJson: undefined,
-    assetClass: undefined,
-
+    assetClass: undefined
   }
   formRef.value?.resetFields()
 }
@@ -726,13 +837,13 @@ const resetForm = () => {
 
 <style scoped lang="scss">
 .base-expandable-content {
-  max-height: 0; /* 初始高度为0 */
+  max-height: 0px; /* 初始高度为0 */
   overflow: hidden; /* 隐藏溢出的内容 */
   transition: max-height 0.3s ease; /* 平滑过渡效果 */
 }
 
 .base-expandable-content.is-expanded {
-  min-height: 260px; /* 或者根据内容设定一个合适的最大高度 */
+  min-height: 350px; /* 或者根据内容设定一个合适的最大高度 */
 }
 .zz-expandable-content {
   max-height: 0; /* 初始高度为0 */
@@ -761,18 +872,18 @@ const resetForm = () => {
 .qt-expandable-content.is-expanded {
   max-height: 1200px; /* 或者根据内容设定一个合适的最大高度 */
 }
-.title-button{
+.title-button {
   font-size: 18px;
   border: none;
 }
-.title-div{
+.title-div {
   margin-bottom: 20px;
   margin-top: 10px;
 }
-.cursor-pointer{
+.cursor-pointer {
   vertical-align: middle;
 }
-.first-icon{
+.first-icon {
   margin-bottom: 2px;
 }
 </style>

+ 21 - 14
src/views/pms/device/allotlog/DeviceAllot.vue

@@ -16,7 +16,11 @@
           :inline="true"
           label-width="68px"
         >
-          <el-form-item :label="t('devicePerson.deviceCode')" prop="deviceCode" style="margin-left: 25px">
+          <el-form-item
+            :label="t('devicePerson.deviceCode')"
+            prop="deviceCode"
+            style="margin-left: 25px"
+          >
             <el-input
               v-model="queryParams.deviceCode"
               :placeholder="t('devicePerson.codeHolder')"
@@ -49,7 +53,12 @@
               />
             </el-select>
           </el-form-item>
-          <el-form-item v-show="ifShow" :label="t('devicePerson.status')" label-width="85px" prop="deviceStatus">
+          <el-form-item
+            v-show="ifShow"
+            :label="t('devicePerson.status')"
+            label-width="85px"
+            prop="deviceStatus"
+          >
             <el-select
               v-model="queryParams.deviceStatus"
               :placeholder="t('devicePerson.status')"
@@ -93,10 +102,12 @@
 
           <el-form-item>
             <el-button v-if="!ifShow" @click="moreQuery(true)" type="warning"
-              ><Icon icon="ep:search" class="mr-5px" /> {{ t('devicePerson.moreSearch') }}</el-button
+              ><Icon icon="ep:search" class="mr-5px" />
+              {{ t('devicePerson.moreSearch') }}</el-button
             >
             <el-button v-if="ifShow" @click="moreQuery(false)" type="danger"
-              ><Icon icon="ep:search" class="mr-5px" /> {{ t('devicePerson.closeSearch') }}</el-button
+              ><Icon icon="ep:search" class="mr-5px" />
+              {{ t('devicePerson.closeSearch') }}</el-button
             >
             <el-button @click="handleQuery"
               ><Icon icon="ep:search" class="mr-5px" /> {{ t('devicePerson.search') }}</el-button
@@ -133,7 +144,7 @@
               {{ scope.$index + 1 }}
             </template>
           </el-table-column>
-          <el-table-column :label="t('monitor.deviceCode')"  align="center" prop="deviceCode" />
+          <el-table-column :label="t('monitor.deviceCode')" align="center" prop="deviceCode" />
           <el-table-column :label="t('monitor.deviceName')" align="center" prop="deviceName">
             <template #default="scope">
               <el-link :underline="false" type="primary" @click="handleDetail(scope.row.id)">
@@ -180,7 +191,7 @@
   </el-row>
   <DeviceAllotLogDrawer
     :model-value="drawerVisible"
-    @update:model-value="val => drawerVisible = val"
+    @update:model-value="(val) => (drawerVisible = val)"
     :device-id="currentDeviceId"
     ref="showDrawer"
   />
@@ -190,10 +201,9 @@
 import download from '@/utils/download'
 import { IotDeviceApi, IotDeviceVO } from '@/api/pms/device'
 import { DICT_TYPE, getStrDictOptions } from '@/utils/dict'
-import { dateFormatter } from '@/utils/formatTime'
 import DeptTree from '@/views/system/user/DeptTree.vue'
 import { CACHE_KEY, useCache } from '@/hooks/web/useCache'
-import DeviceAllotLogDrawer from "@/views/pms/device/allotlog/DeviceAllotLogDrawer.vue";
+import DeviceAllotLogDrawer from '@/views/pms/device/allotlog/DeviceAllotLogDrawer.vue'
 
 /** 设备调拨 列表 */
 defineOptions({ name: 'IotDeviceAllot' })
@@ -320,7 +330,7 @@ const resultOptions = computed(() => [
   {
     label: '否',
     value: 'N' // 空值会触发 clearable 效果
-  },
+  }
 ])
 
 const handleDetail = (id: number) => {
@@ -334,12 +344,9 @@ const handleUpload = (id: number) => {
 /** 导出按钮操作 */
 const handleExport = async () => {
   try {
-    // 导出的二次确认
-    await message.exportConfirm()
-    // 发起导出
     exportLoading.value = true
-    const data = await IotDeviceApi.exportIotDevice(queryParams)
-    download.excel(data, '设备台账.xls')
+    const data = await IotDeviceApi.exportIotDeviceAllot(queryParams)
+    download.excel(data, '设备调拨记录.xls')
   } catch {
   } finally {
     exportLoading.value = false

+ 28 - 3
src/views/pms/device/completeSet/DeviceCompleteSet.vue

@@ -58,6 +58,11 @@
           </el-table-column>
           <el-table-column label="部门名称" align="center" prop="deptName" />
           <el-table-column label="成套名称" align="center" prop="name" />
+          <el-table-column label="成套类型" align="center">
+            <template #default="scope">
+              <dict-tag :type="DICT_TYPE.DEVICE_GROUP_TYPE" :value="scope.row.type" />
+            </template>
+          </el-table-column>
 
           <el-table-column label="描述" align="center" prop="remark" />
           <el-table-column label="设备数量" align="center" prop="deviceCount">
@@ -102,13 +107,31 @@
     </template>
     <el-form ref="formRef" :model="formData" :rules="formRules" label-width="80px">
       <el-row :gutter="20">
-        <el-col :span="12">
+        <el-col :span="8">
           <el-form-item label="成套名称" prop="name">
             <el-input v-model="formData.name" placeholder="请输入成套名称" />
           </el-form-item>
         </el-col>
 
-        <el-col :span="12">
+        <el-col :span="8">
+          <el-form-item label="成套类型" prop="type">
+            <el-select
+              v-model="formData.type"
+              placeholder="请选择成套类型"
+              clearable
+              class="!w-240px"
+            >
+              <el-option
+                v-for="dict in getStrDictOptions(DICT_TYPE.DEVICE_GROUP_TYPE)"
+                :key="dict.value"
+                :label="dict.label"
+                :value="dict.value"
+              />
+            </el-select>
+          </el-form-item>
+        </el-col>
+
+        <el-col :span="8">
           <el-form-item :label="t('iotDevice.dept')" prop="deptId">
             <el-tree-select
               v-model="formData.deptId"
@@ -198,6 +221,7 @@ import DeptTree from '@/views/system/user/DeptTree.vue'
 import { defaultProps, handleTree } from '@/utils/tree'
 import * as DeptApi from '@/api/system/dept'
 import { ElMessageBox } from 'element-plus'
+import { DICT_TYPE, getStrDictOptions } from '@/utils/dict'
 const deptList = ref<Tree[]>([]) // 树形结构
 
 defineOptions({ name: 'IotDeviceComplete' })
@@ -231,7 +255,8 @@ const formData = ref({
   name: '',
   details: [],
   deptId: '',
-  remark: ''
+  remark: '',
+  type: ''
 })
 
 // 表单验证规则

+ 165 - 6
src/views/pms/device/index.vue

@@ -48,6 +48,25 @@
               class="!w-200px"
             />
           </el-form-item>
+          <el-form-item label="车牌号" prop="carNo">
+            <el-input
+              v-model="queryParams.carNo"
+              placeholder="请输入车牌号"
+              clearable
+              @keyup.enter="handleQuery"
+              class="!w-200px"
+            />
+          </el-form-item>
+
+          <el-form-item label="设备号" prop="deviceNo">
+            <el-input
+              v-model="queryParams.deviceNo"
+              placeholder="请输入设备号"
+              clearable
+              @keyup.enter="handleQuery"
+              class="!w-200px"
+            />
+          </el-form-item>
           <el-form-item :label="t('iotDevice.brand')" prop="brandName">
             <el-input
               v-model="queryParams.brandName"
@@ -155,7 +174,7 @@
           height="65vh"
           @sort-change="handleSortChange"
         >
-          <el-table-column :label="t('iotDevice.serial')" width="70" align="center">
+          <el-table-column :label="t('iotDevice.serial')" width="70" align="center" fixed="left">
             <template #default="scope">
               {{ scope.$index + 1 }}
             </template>
@@ -166,27 +185,106 @@
             align="center"
             prop="yfDeviceCode"
             width="150"
-          />
+            fixed="left"
+          >
+            <template #header>
+              <span
+                style="display: inline-block"
+                class="text-[#ad9399] w-[70px] text-[12px] cursor-pointer z-[999] justify-center flex items-center"
+              >
+                <el-popover placement="bottom" :width="250" trigger="hover">
+                  <template #reference>
+                    <div class="flex items-center">
+                      <span> 油服编码 </span> <Icon icon="ep:arrow-down" />
+                    </div>
+                  </template>
+                  <div class="flex items-center gap-2">
+                    <el-input
+                      v-model="queryParams.yfDeviceCode"
+                      placeholder="请输入油服编码"
+                      style="width: 180px"
+                      clearable
+                      @keyup.enter="handleQuery"
+                    />
+                    <el-button type="primary" :icon="Search" @click="handleQuery">搜索</el-button>
+                  </div>
+                </el-popover>
+              </span>
+            </template>
+          </el-table-column>
           <el-table-column
             :label="t('iotDevice.code')"
             sortable
             align="center"
             prop="deviceCode"
             width="150"
-          />
+            fixed="left"
+          >
+            <template #header>
+              <span
+                style="display: inline-block"
+                class="text-[#ad9399] w-[70px] text-[12px] cursor-pointer z-[999] justify-center flex items-center"
+              >
+                <el-popover placement="bottom" :width="250" trigger="hover">
+                  <template #reference>
+                    <div class="flex items-center">
+                      <span> 历史编码 </span> <Icon icon="ep:arrow-down" />
+                    </div>
+                  </template>
+                  <div class="flex items-center gap-2">
+                    <el-input
+                      v-model="queryParams.deviceCode"
+                      placeholder="请输入历史编码"
+                      style="width: 180px"
+                      clearable
+                      @keyup.enter="handleQuery"
+                    />
+                    <el-button type="primary" :icon="Search" @click="handleQuery">搜索</el-button>
+                  </div>
+                </el-popover>
+              </span>
+            </template>
+          </el-table-column>
+
           <el-table-column
             :label="t('iotDevice.name')"
             sortable
             align="center"
             prop="deviceName"
-            min-width="250"
+            min-width="280"
           >
+            <template #header>
+              <span
+                style="display: inline-block"
+                class="text-[#ad9399] w-[70px] text-[12px] cursor-pointer z-[999] justify-center flex items-center"
+              >
+                <el-popover placement="bottom" :width="250" trigger="hover">
+                  <template #reference>
+                    <div class="flex items-center">
+                      <span> 设备名称 </span> <Icon icon="ep:arrow-down" />
+                    </div>
+                  </template>
+                  <div class="flex items-center gap-2">
+                    <el-input
+                      v-model="queryParams.deviceName"
+                      placeholder="请输入设备名称"
+                      style="width: 180px"
+                      clearable
+                      @keyup.enter="handleQuery"
+                    />
+                    <el-button type="primary" :icon="Search" @click="handleQuery">搜索</el-button>
+                  </div>
+                </el-popover>
+              </span>
+            </template>
+
             <template #default="scope">
               <el-link :underline="false" type="primary" @click="handleDetail(scope.row.id)">
                 {{ scope.row.deviceName }}
               </el-link>
             </template>
           </el-table-column>
+          <el-table-column label="设备号" sortable align="center" prop="deviceNo" width="120" />
           <el-table-column
             :label="t('iotDevice.dept')"
             align="center"
@@ -197,8 +295,27 @@
             :label="t('iotDevice.status')"
             align="center"
             prop="deviceStatus"
-            min-width="90"
+            min-width="150"
           >
+            <template #header>
+              <div class="flex items-center justify-center pb-[1px]">
+                <el-dropdown @command="handleCommand">
+                  <span class="text-[#ad9399] text-[12px] cursor-pointer flex items-center">
+                    <span> 设备状态 </span> <Icon icon="ep:arrow-down" />
+                  </span>
+                  <template #dropdown>
+                    <el-dropdown-menu>
+                      <el-dropdown-item
+                        v-for="item in getStrDictOptions(DICT_TYPE.PMS_DEVICE_STATUS)"
+                        :key="item.label"
+                        :command="item.value"
+                        >{{ item.label }}</el-dropdown-item
+                      >
+                    </el-dropdown-menu>
+                  </template>
+                </el-dropdown>
+              </div>
+            </template>
             <template #default="scope">
               <dict-tag :type="DICT_TYPE.PMS_DEVICE_STATUS" :value="scope.row.deviceStatus" />
             </template>
@@ -209,6 +326,25 @@
             prop="assetProperty"
             min-width="110"
           >
+            <template #header>
+              <div class="flex items-center justify-center pb-[1px]">
+                <el-dropdown @command="handleAssetProperty">
+                  <span class="text-[#ad9399] text-[12px] cursor-pointer flex items-center">
+                    <span> 资产性质 </span> <Icon icon="ep:arrow-down" />
+                  </span>
+                  <template #dropdown>
+                    <el-dropdown-menu>
+                      <el-dropdown-item
+                        v-for="item in getStrDictOptions(DICT_TYPE.PMS_ASSET_PROPERTY)"
+                        :key="item.label"
+                        :command="item.value"
+                        >{{ item.label }}</el-dropdown-item
+                      >
+                    </el-dropdown-menu>
+                  </template>
+                </el-dropdown>
+              </div>
+            </template>
             <template #default="scope">
               <dict-tag :type="DICT_TYPE.PMS_ASSET_PROPERTY" :value="scope.row.assetProperty" />
             </template>
@@ -219,6 +355,9 @@
             prop="assetClassName"
             min-width="170"
           />
+
+          <el-table-column label="车牌号" align="center" prop="carNo" min-width="170" />
+
           <el-table-column
             :label="t('deviceForm.mfg')"
             align="center"
@@ -255,6 +394,7 @@
             prop="assetOwnership"
             min-width="170"
           />
+          <el-table-column label="所在地点" align="center" prop="address" min-width="170" />
           <el-table-column
             :label="t('operationFill.operation')"
             align="center"
@@ -308,6 +448,7 @@ import { buildSortingField } from '@/utils'
 import { defaultProps, handleTree } from '@/utils/tree'
 import * as ProductClassifyApi from '@/api/pms/productclassify'
 import { useRefreshStore } from '@/store/modules/pms/refreshStore'
+import { Search } from '@element-plus/icons-vue'
 
 /** 设备台账 列表 */
 defineOptions({ name: 'IotDevicePms' })
@@ -357,7 +498,10 @@ const queryParams = reactive({
   creator: undefined,
   sortingFields: [],
   assetClass: undefined,
-  yfDeviceCode: undefined
+  yfDeviceCode: undefined,
+  carNo: undefined,
+  deviceNo: undefined,
+  assetClassName: undefined
 })
 const queryFormRef = ref() // 搜索的表单
 const exportLoading = ref(false) // 导出的加载中
@@ -372,6 +516,16 @@ const shou = (tree) => {
   }
 }
 
+const handleCommand = (command) => {
+  queryParams.deviceStatus = command
+  getList()
+}
+
+const handleAssetProperty = (command) => {
+  queryParams.assetProperty = command
+  getList()
+}
+
 const handleSortChange = (params: any) => {
   //console.log(`排序字段: ${prop}, 排序方式: ${order}`);
   queryParams.sortingFields = []
@@ -484,4 +638,9 @@ onMounted(async () => {
   top: 0px;
   z-index: 2000;
 }
+
+::v-deep .el-tooltip__trigger {
+  border: none !important;
+  outline: none !important;
+}
 </style>

+ 17 - 19
src/views/pms/device/personlog/DevicePerson.vue

@@ -16,7 +16,11 @@
           :inline="true"
           label-width="68px"
         >
-          <el-form-item :label="t('devicePerson.deviceCode')" prop="deviceCode" style="margin-left: 20px">
+          <el-form-item
+            :label="t('devicePerson.deviceCode')"
+            prop="deviceCode"
+            style="margin-left: 20px"
+          >
             <el-input
               v-model="queryParams.deviceCode"
               :placeholder="t('devicePerson.codeHolder')"
@@ -35,7 +39,11 @@
             />
           </el-form-item>
 
-          <el-form-item :label="t('devicePerson.responsiblePerson')" prop="setFlag" label-width="140px">
+          <el-form-item
+            :label="t('devicePerson.responsiblePerson')"
+            prop="setFlag"
+            label-width="140px"
+          >
             <el-select
               v-model="queryParams.setFlag"
               :placeholder="t('devicePerson.choose')"
@@ -75,13 +83,8 @@
             >
               <Icon icon="ep:plus" class="mr-5px" /> {{ t('devicePerson.setUp') }}
             </el-button>
-            <el-button
-              type="success"
-              plain
-              @click="handleExport"
-              :loading="exportLoading"
-              v-hasPermi="['rq:iot-device:export']"
-            >
+            <!-- v-hasPermi="['rq:iot-device:export']" -->
+            <el-button type="success" plain @click="handleExport" :loading="exportLoading">
               <Icon icon="ep:download" class="mr-5px" /> 导出
             </el-button>
           </el-form-item>
@@ -131,7 +134,7 @@
   </el-row>
   <DevicePersonLogDrawer
     :model-value="drawerVisible"
-    @update:model-value="val => drawerVisible = val"
+    @update:model-value="(val) => (drawerVisible = val)"
     :device-id="currentDeviceId"
     ref="showDrawer"
   />
@@ -140,10 +143,9 @@
 <script setup lang="ts">
 import download from '@/utils/download'
 import { IotDeviceApi, IotDeviceVO } from '@/api/pms/device'
-import { DICT_TYPE, getStrDictOptions } from '@/utils/dict'
 import DeptTree from '@/views/system/user/DeptTree.vue'
 import { CACHE_KEY, useCache } from '@/hooks/web/useCache'
-import DevicePersonLogDrawer from "@/views/pms/device/personlog/DevicePersonLogDrawer.vue";
+import DevicePersonLogDrawer from '@/views/pms/device/personlog/DevicePersonLogDrawer.vue'
 
 /** 设备台账 列表 */
 defineOptions({ name: 'IotDevicePerson' })
@@ -270,7 +272,7 @@ const resultOptions = computed(() => [
   {
     label: '否',
     value: 'N' // 空值会触发 clearable 效果
-  },
+  }
 ])
 
 const handleDetail = (id: number) => {
@@ -295,12 +297,8 @@ const handleView = async (deviceId: number) => {
 /** 导出按钮操作 */
 const handleExport = async () => {
   try {
-    // 导出的二次确认
-    await message.exportConfirm()
-    // 发起导出
-    exportLoading.value = true
-    const data = await IotDeviceApi.exportIotDevice(queryParams)
-    download.excel(data, '设备台账.xls')
+    const data = await IotDeviceApi.exportIotDevicePerson(queryParams)
+    download.excel(data, '设备责任人.xls')
   } catch {
   } finally {
     exportLoading.value = false

+ 24 - 20
src/views/pms/device/statuslog/DeviceStatus.vue

@@ -16,7 +16,11 @@
           :inline="true"
           label-width="68px"
         >
-          <el-form-item :label="t('devicePerson.deviceCode')" prop="deviceCode" style="margin-left: 25px">
+          <el-form-item
+            :label="t('devicePerson.deviceCode')"
+            prop="deviceCode"
+            style="margin-left: 25px"
+          >
             <el-input
               v-model="queryParams.deviceCode"
               :placeholder="t('devicePerson.codeHolder')"
@@ -49,7 +53,12 @@
               />
             </el-select>
           </el-form-item>
-          <el-form-item v-show="ifShow" :label="t('devicePerson.status')" label-width="85px" prop="deviceStatus">
+          <el-form-item
+            v-show="ifShow"
+            :label="t('devicePerson.status')"
+            label-width="85px"
+            prop="deviceStatus"
+          >
             <el-select
               v-model="queryParams.deviceStatus"
               :label="t('devicePerson.status')"
@@ -97,7 +106,8 @@
               {{ t('devicePerson.moreSearch') }}</el-button
             >
             <el-button v-if="ifShow" @click="moreQuery(false)" type="danger"
-              ><Icon icon="ep:search" class="mr-5px" /> {{ t('devicePerson.closeSearch') }}</el-button
+              ><Icon icon="ep:search" class="mr-5px" />
+              {{ t('devicePerson.closeSearch') }}</el-button
             >
             <el-button @click="handleQuery"
               ><Icon icon="ep:search" class="mr-5px" /> {{ t('devicePerson.search') }}</el-button
@@ -113,13 +123,8 @@
             >
               <Icon icon="ep:plus" class="mr-5px" /> {{ t('deviceStatus.setUp') }}
             </el-button>
-            <el-button
-              type="success"
-              plain
-              @click="handleExport"
-              :loading="exportLoading"
-              v-hasPermi="['rq:iot-device:export']"
-            >
+            <!-- v-hasPermi="['rq:iot-device:export']" -->
+            <el-button type="success" plain @click="handleExport" :loading="exportLoading">
               <Icon icon="ep:download" class="mr-5px" /> 导出
             </el-button>
           </el-form-item>
@@ -129,7 +134,7 @@
       <!-- 列表 -->
       <ContentWrap>
         <el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
-          <el-table-column :label="t('monitor.serial')" width="70" align="center" >
+          <el-table-column :label="t('monitor.serial')" width="70" align="center">
             <template #default="scope">
               {{ scope.$index + 1 }}
             </template>
@@ -181,7 +186,7 @@
   </el-row>
   <DeviceStatusLogDrawer
     :model-value="drawerVisible"
-    @update:model-value="val => drawerVisible = val"
+    @update:model-value="(val) => (drawerVisible = val)"
     :device-id="currentDeviceId"
     ref="showDrawer"
   />
@@ -191,10 +196,9 @@
 import download from '@/utils/download'
 import { IotDeviceApi, IotDeviceVO } from '@/api/pms/device'
 import { DICT_TYPE, getStrDictOptions } from '@/utils/dict'
-import { dateFormatter } from '@/utils/formatTime'
 import DeptTree from '@/views/system/user/DeptTree.vue'
 import { CACHE_KEY, useCache } from '@/hooks/web/useCache'
-import DeviceStatusLogDrawer from "@/views/pms/device/statuslog/DeviceStatusLogDrawer.vue";
+import DeviceStatusLogDrawer from '@/views/pms/device/statuslog/DeviceStatusLogDrawer.vue'
 
 /** 设备台账 列表 */
 defineOptions({ name: 'IotDeviceStatus' })
@@ -271,7 +275,7 @@ const resultOptions = computed(() => [
   {
     label: '否',
     value: 'N' // 空值会触发 clearable 效果
-  },
+  }
 ])
 
 const showDrawer = ref()
@@ -344,12 +348,12 @@ const handleDetail = (id: number) => {
 /** 导出按钮操作 */
 const handleExport = async () => {
   try {
-    // 导出的二次确认
-    await message.exportConfirm()
-    // 发起导出
+    // // 导出的二次确认
+    // await message.exportConfirm()
+    // // 发起导出
     exportLoading.value = true
-    const data = await IotDeviceApi.exportIotDevice(queryParams)
-    download.excel(data, '设备台账.xls')
+    const data = await IotDeviceApi.exportIotDeviceAdjust(queryParams)
+    download.excel(data, '设备状态调整.xls')
   } catch {
   } finally {
     exportLoading.value = false

+ 2 - 2
src/views/pms/iotdeviceallotlog/index.vue

@@ -161,7 +161,7 @@ const queryParams = reactive({
   newDeptId: undefined,
   reason: undefined,
   remark: undefined,
-  createTime: [],
+  createTime: []
 })
 const queryFormRef = ref() // 搜索的表单
 const exportLoading = ref(false) // 导出的加载中
@@ -228,4 +228,4 @@ const handleExport = async () => {
 onMounted(() => {
   getList()
 })
-</script>
+</script>

+ 194 - 113
src/views/pms/iotlockstock/index.vue

@@ -8,8 +8,19 @@
       :inline="true"
       label-width="68px"
     >
-      <el-form-item :label="t('workOrderMaterial.factory')" prop="factoryId" v-if="!shouldHideComponents">
-        <el-select v-model="queryParams.factoryId" clearable filterable :placeholder="t('faultForm.choose')" class="!w-240px" @change="selectedFactoryChange">
+      <el-form-item
+        :label="t('workOrderMaterial.factory')"
+        prop="factoryId"
+        v-if="!shouldHideComponents"
+      >
+        <el-select
+          v-model="queryParams.factoryId"
+          clearable
+          filterable
+          :placeholder="t('faultForm.choose')"
+          class="!w-240px"
+          @change="selectedFactoryChange"
+        >
           <el-option
             v-for="item in factoryList"
             :key="item.id"
@@ -19,8 +30,18 @@
         </el-select>
       </el-form-item>
 
-      <el-form-item :label="t('workOrderMaterial.costCenter')" prop="costCenterId" v-if="!shouldHideComponents">
-        <el-select v-model="queryParams.costCenterId" clearable filterable :placeholder="t('faultForm.choose')" class="!w-240px">
+      <el-form-item
+        :label="t('workOrderMaterial.costCenter')"
+        prop="costCenterId"
+        v-if="!shouldHideComponents"
+      >
+        <el-select
+          v-model="queryParams.costCenterId"
+          clearable
+          filterable
+          :placeholder="t('faultForm.choose')"
+          class="!w-240px"
+        >
           <el-option
             v-for="item in filteredCostCenterList"
             :key="item.id"
@@ -59,8 +80,12 @@
         />
       </el-form-item>
       <el-form-item>
-        <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> {{ t('operationFill.search') }}</el-button>
-        <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> {{ t('operationFill.reset') }}</el-button>
+        <el-button @click="handleQuery"
+          ><Icon icon="ep:search" class="mr-5px" /> {{ t('operationFill.search') }}</el-button
+        >
+        <el-button @click="resetQuery"
+          ><Icon icon="ep:refresh" class="mr-5px" /> {{ t('operationFill.reset') }}</el-button
+        >
         <el-button
           type="primary"
           plain
@@ -69,13 +94,8 @@
         >
           <Icon icon="ep:plus" class="mr-5px" />{{ t('operationFill.add') }}
         </el-button>
-        <el-button
-          type="success"
-          plain
-          @click="handleExport"
-          :loading="exportLoading"
-          v-hasPermi="['pms:iot-lock-stock:export']"
-        >
+        <!-- v-hasPermi="['pms:iot-lock-stock:export']" -->
+        <el-button type="success" plain @click="handleExport" :loading="exportLoading">
           <Icon icon="ep:download" class="mr-5px" /> 导出
         </el-button>
       </el-form-item>
@@ -92,7 +112,15 @@
         </div>
         <div class="stat-item">
           <span class="stat-label">{{ t('stock.totalAmount') }}:</span>
-          <span class="stat-value">¥ {{ totalAmount.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 }) }}</span>
+          <span class="stat-value"
+            >¥
+            {{
+              totalAmount.toLocaleString(undefined, {
+                minimumFractionDigits: 2,
+                maximumFractionDigits: 2
+              })
+            }}</span
+          >
         </div>
       </div>
     </el-card>
@@ -100,16 +128,58 @@
 
   <!-- 列表 -->
   <ContentWrap ref="tableContainerRef" class="table-container">
-    <el-table ref="tableRef" v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="false" style="width: 100%">
-      <el-table-column :label="t('workOrderMaterial.factory')" align="center" prop="factory" :width="columnWidths.factory"/>
-      <el-table-column :label="t('workOrderMaterial.costCenter')" align="center" prop="costCenter" :width="columnWidths.costCenter"/>
-      <el-table-column :label="t('chooseMaintain.materialCode')" align="center" prop="materialCode" :width="columnWidths.materialCode"/>
-      <el-table-column :label="t('chooseMaintain.materialName')" align="left" prop="materialName" :width="columnWidths.materialName"/>
-      <el-table-column :label="t('route.quantity')" align="center" prop="quantity"
-                       :formatter="erpPriceTableColumnFormatter" :width="columnWidths.quantity"/>
-      <el-table-column :label="t('workOrderMaterial.unitPrice')" align="center" prop="unitPrice"
-                       :formatter="erpPriceTableColumnFormatter" :width="columnWidths.unitPrice"/>
-      <el-table-column :label="t('workOrderMaterial.unit')" align="center" prop="unit" :width="columnWidths.unit"/>
+    <el-table
+      ref="tableRef"
+      v-loading="loading"
+      :data="list"
+      :stripe="true"
+      :show-overflow-tooltip="false"
+      style="width: 100%"
+    >
+      <el-table-column
+        :label="t('workOrderMaterial.factory')"
+        align="center"
+        prop="factory"
+        :width="columnWidths.factory"
+      />
+      <el-table-column
+        :label="t('workOrderMaterial.costCenter')"
+        align="center"
+        prop="costCenter"
+        :width="columnWidths.costCenter"
+      />
+      <el-table-column
+        :label="t('chooseMaintain.materialCode')"
+        align="center"
+        prop="materialCode"
+        :width="columnWidths.materialCode"
+      />
+      <el-table-column
+        :label="t('chooseMaintain.materialName')"
+        align="left"
+        prop="materialName"
+        :width="columnWidths.materialName"
+      />
+      <el-table-column
+        :label="t('route.quantity')"
+        align="center"
+        prop="quantity"
+        :formatter="erpPriceTableColumnFormatter"
+        :width="columnWidths.quantity"
+      />
+      <el-table-column
+        :label="t('workOrderMaterial.unitPrice')"
+        align="center"
+        prop="unitPrice"
+        :formatter="erpPriceTableColumnFormatter"
+        :width="columnWidths.unitPrice"
+      />
+      <el-table-column
+        :label="t('workOrderMaterial.unit')"
+        align="center"
+        prop="unit"
+        :width="columnWidths.unit"
+      />
       <el-table-column
         :label="t('stock.storageTime')"
         align="center"
@@ -117,7 +187,6 @@
         :formatter="dateFormatter"
         :width="columnWidths.storageTime"
       />
-
     </el-table>
     <!-- 分页 -->
     <Pagination
@@ -137,9 +206,9 @@ import { dateFormatter } from '@/utils/formatTime'
 import download from '@/utils/download'
 import { IotLockStockApi, IotLockStockVO } from '@/api/pms/iotlockstock'
 import IotLockStockForm from './IotLockStockForm.vue'
-import {erpPriceTableColumnFormatter} from "@/utils";
+import { erpPriceTableColumnFormatter } from '@/utils'
 import { useUserStore } from '@/store/modules/user'
-import { SapOrgApi, SapOrgVO } from "@/api/system/saporg";
+import { SapOrgApi, SapOrgVO } from '@/api/system/saporg'
 import { checkRole } from '@/utils/permission'
 import { computed, nextTick, onMounted, onUnmounted, ref, watch } from 'vue'
 
@@ -179,18 +248,18 @@ const queryParams = reactive({
   processInstanceId: undefined,
   auditStatus: undefined,
   remark: undefined,
-  createTime: [],
+  createTime: []
 })
 const queryFormRef = ref() // 搜索的表单
 const exportLoading = ref(false) // 导出的加载中
 
-const factoryList = ref([] as SapOrgVO[])   // 工厂列表
+const factoryList = ref([] as SapOrgVO[]) // 工厂列表
 const storageLocationList = ref([] as SapOrgVO[]) // 库存地点列表
 const costCenterList = ref([] as SapOrgVO[]) // SAP成本中心列表
 
 // 统计变量
-const totalQuantity = ref(0)   // 总数量
-const totalAmount = ref(0)     // 总金额
+const totalQuantity = ref(0) // 总数量
+const totalAmount = ref(0) // 总金额
 
 // 表格容器和表格的引用
 const tableContainerRef = ref()
@@ -220,54 +289,66 @@ const columnWidths = ref({
 
 /** 获取滚动条宽度 */
 const getScrollbarWidth = () => {
-  const outer = document.createElement('div');
-  outer.style.visibility = 'hidden';
-  outer.style.overflow = 'scroll';
-  document.body.appendChild(outer);
+  const outer = document.createElement('div')
+  outer.style.visibility = 'hidden'
+  outer.style.overflow = 'scroll'
+  document.body.appendChild(outer)
 
-  const inner = document.createElement('div');
-  outer.appendChild(inner);
+  const inner = document.createElement('div')
+  outer.appendChild(inner)
 
-  const scrollbarWidth = outer.offsetWidth - inner.offsetWidth;
-  outer.parentNode?.removeChild(outer);
+  const scrollbarWidth = outer.offsetWidth - inner.offsetWidth
+  outer.parentNode?.removeChild(outer)
 
-  return scrollbarWidth;
-};
+  return scrollbarWidth
+}
 
 /** 计算文本宽度 */
 const getTextWidth = (text: string, fontSize = 14) => {
-  const span = document.createElement('span');
-  span.style.visibility = 'hidden';
-  span.style.position = 'absolute';
-  span.style.whiteSpace = 'nowrap';
-  span.style.fontSize = `${fontSize}px`;
-  span.style.fontFamily = 'inherit';
-  span.innerText = text;
-
-  document.body.appendChild(span);
-  const width = span.offsetWidth;
-  document.body.removeChild(span);
-
-  return width;
-};
+  const span = document.createElement('span')
+  span.style.visibility = 'hidden'
+  span.style.position = 'absolute'
+  span.style.whiteSpace = 'nowrap'
+  span.style.fontSize = `${fontSize}px`
+  span.style.fontFamily = 'inherit'
+  span.innerText = text
+
+  document.body.appendChild(span)
+  const width = span.offsetWidth
+  document.body.removeChild(span)
+
+  return width
+}
 
 /** 计算列宽度 */
 const calculateColumnWidths = () => {
-  const MIN_WIDTH = 80; // 最小列宽
-  const PADDING = 25; // 列内边距
-  const FLEXIBLE_COLUMN = 'materialName'; // 可伸缩列
-  const scrollbarWidth = getScrollbarWidth(); // 动态获取滚动条宽度
+  const MIN_WIDTH = 80 // 最小列宽
+  const PADDING = 25 // 列内边距
+  const FLEXIBLE_COLUMN = 'materialName' // 可伸缩列
+  const scrollbarWidth = getScrollbarWidth() // 动态获取滚动条宽度
 
-  if (!tableContainerRef.value?.$el || list.value.length === 0) return;
+  if (!tableContainerRef.value?.$el || list.value.length === 0) return
 
-  const containerWidth = tableContainerRef.value.$el.clientWidth;
+  const containerWidth = tableContainerRef.value.$el.clientWidth
 
   // 需要自适应的列配置
   const autoColumns = [
     { key: 'factory', label: t('workOrderMaterial.factory'), getValue: (row) => row.factory },
-    { key: 'costCenter', label: t('workOrderMaterial.costCenter'), getValue: (row) => row.costCenter },
-    { key: 'materialCode', label: t('chooseMaintain.materialCode'), getValue: (row) => row.materialCode },
-    { key: 'materialName', label: t('chooseMaintain.materialName'), getValue: (row) => row.materialName },
+    {
+      key: 'costCenter',
+      label: t('workOrderMaterial.costCenter'),
+      getValue: (row) => row.costCenter
+    },
+    {
+      key: 'materialCode',
+      label: t('chooseMaintain.materialCode'),
+      getValue: (row) => row.materialCode
+    },
+    {
+      key: 'materialName',
+      label: t('chooseMaintain.materialName'),
+      getValue: (row) => row.materialName
+    },
     {
       key: 'quantity',
       label: t('route.quantity'),
@@ -284,63 +365,65 @@ const calculateColumnWidths = () => {
       label: t('stock.storageTime'),
       getValue: (row) => dateFormatter(null, null, row.storageTime)
     }
-  ];
+  ]
 
-  const newWidths: Record<string, string> = {};
-  let totalFixedWidth = 0; // 所有固定列的总宽度
+  const newWidths: Record<string, string> = {}
+  let totalFixedWidth = 0 // 所有固定列的总宽度
 
   // 计算除可伸缩列外的所有列宽度
-  autoColumns.forEach(col => {
-    if (col.key === FLEXIBLE_COLUMN) return;
+  autoColumns.forEach((col) => {
+    if (col.key === FLEXIBLE_COLUMN) return
 
-    const headerText = col.label;
-    const headerWidth = getTextWidth(headerText) * 1.3; // 表头宽度(加粗效果增加20%)
+    const headerText = col.label
+    const headerWidth = getTextWidth(headerText) * 1.3 // 表头宽度(加粗效果增加20%)
 
     // 计算内容最大宽度
-    let contentMaxWidth = 0;
-    list.value.forEach(row => {
-      const text = col.getValue ? String(col.getValue(row)) : String(row[col.key] || '');
-      const textWidth = getTextWidth(text);
-      if (textWidth > contentMaxWidth) contentMaxWidth = textWidth;
-    });
+    let contentMaxWidth = 0
+    list.value.forEach((row) => {
+      const text = col.getValue ? String(col.getValue(row)) : String(row[col.key] || '')
+      const textWidth = getTextWidth(text)
+      if (textWidth > contentMaxWidth) contentMaxWidth = textWidth
+    })
 
     // 取表头宽度、内容最大宽度和最小宽度的最大值
-    const finalWidth = Math.max(headerWidth, contentMaxWidth, MIN_WIDTH) + PADDING;
-    newWidths[col.key] = `${finalWidth}px`;
-    totalFixedWidth += finalWidth;
-  });
+    const finalWidth = Math.max(headerWidth, contentMaxWidth, MIN_WIDTH) + PADDING
+    newWidths[col.key] = `${finalWidth}px`
+    totalFixedWidth += finalWidth
+  })
 
   // 处理可伸缩列(materialName)
-  const flexibleCol = autoColumns.find(col => col.key === FLEXIBLE_COLUMN);
+  const flexibleCol = autoColumns.find((col) => col.key === FLEXIBLE_COLUMN)
   if (flexibleCol) {
-    const headerText = flexibleCol.label;
-    const headerWidth = getTextWidth(headerText) * 1.3;
-
-    let contentMaxWidth = 0;
-    list.value.forEach(row => {
-      const text = flexibleCol.getValue ? String(flexibleCol.getValue(row)) : String(row[flexibleCol.key] || '');
-      const textWidth = getTextWidth(text);
-      if (textWidth > contentMaxWidth) contentMaxWidth = textWidth;
-    });
+    const headerText = flexibleCol.label
+    const headerWidth = getTextWidth(headerText) * 1.3
+
+    let contentMaxWidth = 0
+    list.value.forEach((row) => {
+      const text = flexibleCol.getValue
+        ? String(flexibleCol.getValue(row))
+        : String(row[flexibleCol.key] || '')
+      const textWidth = getTextWidth(text)
+      if (textWidth > contentMaxWidth) contentMaxWidth = textWidth
+    })
 
-    const baseWidth = Math.max(headerWidth, contentMaxWidth, MIN_WIDTH) + PADDING;
+    const baseWidth = Math.max(headerWidth, contentMaxWidth, MIN_WIDTH) + PADDING
 
     // 剩余空间 = 容器宽度 - 其他列总宽度 - 垂直滚动条宽度(17px)
-    const remainingWidth = containerWidth - totalFixedWidth - scrollbarWidth;
+    const remainingWidth = containerWidth - totalFixedWidth - scrollbarWidth
 
     // 可伸缩列的宽度取剩余空间和基础宽度的最大值
-    const flexibleWidth = Math.max(remainingWidth, baseWidth);
-    newWidths[FLEXIBLE_COLUMN] = `${flexibleWidth}px`;
+    const flexibleWidth = Math.max(remainingWidth, baseWidth)
+    newWidths[FLEXIBLE_COLUMN] = `${flexibleWidth}px`
   }
 
   // 更新列宽度
-  columnWidths.value = newWidths;
+  columnWidths.value = newWidths
 
   // 重新布局表格
   nextTick(() => {
-    tableRef.value?.doLayout();
-  });
-};
+    tableRef.value?.doLayout()
+  })
+}
 
 /** 查询列表 */
 const getList = async () => {
@@ -413,7 +496,7 @@ const openForm = (type: string, id?: number) => {
     formRef.value.open(type, id)
     return
   }
-  push({ name: 'LockStockAdd', params:{} })
+  push({ name: 'LockStockAdd', params: {} })
 }
 
 /** 删除按钮操作 */
@@ -460,24 +543,21 @@ const selectedFactoryChange = async (selectedId: number | undefined) => {
   }
 
   // 获取选中的工厂对象
-  const selectedFactory = factoryList.value.find(item => item.id === selectedId)
+  const selectedFactory = factoryList.value.find((item) => item.id === selectedId)
   if (!selectedFactory) return
 
   // 根据工厂代码过滤成本中心
   filteredCostCenterList.value = costCenterList.value.filter(
-    item => item.factoryCode === selectedFactory.factoryCode
+    (item) => item.factoryCode === selectedFactory.factoryCode
   )
 }
 
 /** 导出按钮操作 */
 const handleExport = async () => {
   try {
-    // 导出的二次确认
-    await message.exportConfirm()
-    // 发起导出
     exportLoading.value = true
     const data = await IotLockStockApi.exportIotLockStock(queryParams)
-    download.excel(data, 'PMS 本地 库存.xls')
+    download.excel(data, 'PMS 本地库存.xls')
   } catch {
   } finally {
     exportLoading.value = false
@@ -497,10 +577,13 @@ onUnmounted(() => {
 })
 
 // 监听列表数据变化,重新计算列宽
-watch(list, () => {
-  nextTick(calculateColumnWidths)
-}, { deep: true })
-
+watch(
+  list,
+  () => {
+    nextTick(calculateColumnWidths)
+  },
+  { deep: true }
+)
 </script>
 
 <style scoped>
@@ -530,7 +613,7 @@ watch(list, () => {
 .stat-value {
   font-size: 18px;
   font-weight: bold;
-  color: #409EFF;
+  color: #409eff;
 }
 
 /* 表格容器样式 - 确保可以水平滚动 */
@@ -550,7 +633,6 @@ watch(list, () => {
   white-space: nowrap;
 }
 
-
 /* 防止表格内容换行 */
 :deep(.el-table .cell) {
   white-space: nowrap !important;
@@ -577,5 +659,4 @@ watch(list, () => {
   overflow: visible;
   text-overflow: unset;
 }
-
 </style>

+ 22 - 28
src/views/pms/iotmainworkorder/IotDeviceMainAlarm.vue

@@ -37,11 +37,10 @@
 
           <el-form-item>
             <el-button @click="handleQuery"
-              ><Icon icon="ep:search" class="mr-5px" />
-              {{ t('file.search') }}</el-button
+              ><Icon icon="ep:search" class="mr-5px" /> {{ t('file.search') }}</el-button
             >
             <el-button @click="resetQuery"
-              ><Icon icon="ep:refresh" class="mr-5px" />  {{ t('file.reset') }}</el-button
+              ><Icon icon="ep:refresh" class="mr-5px" /> {{ t('file.reset') }}</el-button
             >
             <el-button
               type="success"
@@ -125,17 +124,17 @@
       </ContentWrap>
     </el-col>
   </el-row>
-  <DeviceAlarmBomList ref="modelFormRef" :flag = "flag" />
+  <DeviceAlarmBomList ref="modelFormRef" :flag="flag" />
 </template>
 
 <script setup lang="ts">
 import download from '@/utils/download'
 import { IotDeviceApi, IotDeviceVO } from '@/api/pms/device'
-import { IotMainWorkOrderApi, IotMainWorkOrderVO } from '@/api/pms/iotmainworkorder'
-import { DICT_TYPE, getStrDictOptions } from '@/utils/dict'
+import { IotMainWorkOrderApi } from '@/api/pms/iotmainworkorder'
+import { DICT_TYPE } from '@/utils/dict'
 import DeptTree from '@/views/system/user/DeptTree.vue'
-import { CACHE_KEY, useCache } from '@/hooks/web/useCache'
-import DeviceAlarmBomList from "@/views/pms/iotmainworkorder/DeviceAlarmBomList.vue";
+import { useCache } from '@/hooks/web/useCache'
+import DeviceAlarmBomList from '@/views/pms/iotmainworkorder/DeviceAlarmBomList.vue'
 
 /** 设备台账 列表 */
 defineOptions({ name: 'IotDeviceMainAlarm' })
@@ -219,35 +218,33 @@ const resetQuery = () => {
 }
 
 const getDistanceClass = (distance: number | string | null) => {
-  if (distance === null || distance === undefined) return '';
+  if (distance === null || distance === undefined) return ''
 
   // 如果是数字类型,直接处理
   if (typeof distance === 'number') {
-    return distance < 0 ? 'negative-distance' :
-      distance > 0 ? 'positive-distance' : '';
+    return distance < 0 ? 'negative-distance' : distance > 0 ? 'positive-distance' : ''
   }
 
   // 如果是字符串,提取数字部分
   if (typeof distance === 'string') {
     // 使用正则提取数字部分(包括负号、小数点和科学计数法)
-    const numericPart = distance.match(/[-+]?\d*\.?\d+(?:[eE][-+]?\d+)?/)?.[0];
+    const numericPart = distance.match(/[-+]?\d*\.?\d+(?:[eE][-+]?\d+)?/)?.[0]
 
     // 如果提取到数字部分,转换为数值
     if (numericPart) {
-      const num = parseFloat(numericPart);
-      return num < 0 ? 'negative-distance' :
-        num > 0 ? 'positive-distance' : '';
+      const num = parseFloat(numericPart)
+      return num < 0 ? 'negative-distance' : num > 0 ? 'positive-distance' : ''
     }
   }
 
-  return '';
-};
+  return ''
+}
 
 // 判断是否有保养计划
 const hasMaintenancePlan = (mainDistance: any) => {
   // 检查:非null、非undefined、非空字符串
-  return mainDistance != null && mainDistance !== '';
-};
+  return mainDistance != null && mainDistance !== ''
+}
 
 const handleDetail = (id: number) => {
   push({ name: 'DeviceDetailInfo', params: { id } })
@@ -265,10 +262,10 @@ const openBomForm = async (row) => {
     model: row.model // 新增 model 属性
   }
   if (row.workOrderId) {
-    flag.value = 'workOrder';
+    flag.value = 'workOrder'
     modelFormRef.value.open(row.workOrderId, flag.value, row.id)
   } else if (row.planId) {
-    flag.value = 'plan';
+    flag.value = 'plan'
     modelFormRef.value.open(row.planId, flag.value, deviceInfo)
   }
 }
@@ -276,12 +273,9 @@ const openBomForm = async (row) => {
 /** 导出按钮操作 */
 const handleExport = async () => {
   try {
-    // 导出的二次确认
-    await message.exportConfirm()
-    // 发起导出
     exportLoading.value = true
-    const data = await IotDeviceApi.exportIotDevice(queryParams)
-    download.excel(data, '设备台账.xls')
+    const data = await IotDeviceApi.exportIotDeviceMainAlarm(queryParams)
+    download.excel(data, '保养查询.xls')
   } catch {
   } finally {
     exportLoading.value = false
@@ -296,7 +290,7 @@ onMounted(() => {
 <style scoped>
 /* 正数样式 - 淡绿色 */
 .positive-distance {
-  color: #67c23a;  /* element-plus 成功色 */
+  color: #67c23a; /* element-plus 成功色 */
   background-color: rgba(103, 194, 58, 0.1); /* 10% 透明度的淡绿色背景 */
   padding: 2px 8px;
   border-radius: 4px;
@@ -305,7 +299,7 @@ onMounted(() => {
 
 /* 负数样式 - 淡红色 */
 .negative-distance {
-  color: #f56c6c;  /* element-plus 危险色 */
+  color: #f56c6c; /* element-plus 危险色 */
   background-color: rgba(245, 108, 108, 0.1); /* 10% 透明度的淡红色背景 */
   padding: 2px 8px;
   border-radius: 4px;

+ 1 - 4
src/views/pms/iotmainworkorder/index.vue

@@ -726,11 +726,8 @@ const detail = (id?: number) => {
 /** 导出按钮操作 */
 const handleExport = async () => {
   try {
-    // 导出的二次确认
-    await message.exportConfirm()
-    // 发起导出
     exportLoading.value = true
-    const data = await IotMainWorkOrderApi.exportIotMainWorkOrder(queryParams)
+    const data = await IotMainWorkOrderApi.exportIotMainWorkOrderIndex(queryParams)
     download.excel(data, '保养工单.xls')
   } catch {
   } finally {

+ 686 - 0
src/views/pms/iotopeationfill/index1 copy.vue

@@ -0,0 +1,686 @@
+<template>
+  <ContentWrap>
+    <el-tabs type="border-card" tab-position="left" v-loading="loading" style="height: 84vh">
+      <el-tab-pane
+        style="height: 100%"
+        v-for="(deviceItem, deviceIndex) in list"
+        :key="deviceIndex"
+      >
+        <template #label>
+          <span
+            :class="['custom-label', { 'has-border': deviceItem.deviceName === '生产日报' }]"
+            v-if="deviceItem.isFill === 1"
+            @click="
+              openFill(
+                deviceItem.deviceCategoryId,
+                deviceItem.deviceId,
+                deviceItem.deptId,
+                deviceItem.deviceName,
+                deviceItem.deviceCode
+              )
+            "
+          >
+            {{ deviceItem.deviceCode }} ({{ deviceItem.deviceName }})
+          </span>
+          <span
+            :class="['custom-label1', { 'has-border': deviceItem.deviceName === '生产日报' }]"
+            v-else
+            @click="
+              openFill(
+                deviceItem.deviceCategoryId,
+                deviceItem.deviceId,
+                deviceItem.deptId,
+                deviceItem.deviceName,
+                deviceItem.deviceCode
+              )
+            "
+          >
+            {{ deviceItem.deviceCode }} ({{ deviceItem.deviceName }})
+          </span>
+        </template>
+        <div class="form-wrapper">
+          <el-form label-width="120px" class="scrollable-form">
+            <div style="margin-left: 24px">
+              <el-form class="demo-form-inline" :inline="true">
+                <el-form-item :label="t('common.createTime')" class="custom-label1">
+                  <span style="text-decoration: underline">
+                    {{ createTime }}
+                  </span>
+                </el-form-item>
+                <el-form-item :label="t('operationFillForm.team')" class="custom-label1">
+                  <span style="text-decoration: underline">
+                    {{ deviceItem.orgName }}
+                  </span>
+                </el-form-item>
+                <el-row :gutter="20">
+                  <el-col
+                    v-for="(summaryItem, summaryIndex) in attrList1"
+                    :key="summaryIndex"
+                    :span="24"
+                  >
+                    <el-form-item :label="summaryItem.name" class="custom-label1">
+                      <span style="text-decoration: underline">
+                        {{ summaryItem.totalRunTime }}
+                      </span>
+                    </el-form-item>
+                  </el-col>
+                </el-row>
+                <!--                <el-form-item :label="t('operationFillForm.sumTime')" class="custom-label1">
+                  <span style="text-decoration: underline;">
+                  {{totalRunTime1}}h
+                  </span>
+                </el-form-item>-->
+              </el-form>
+            </div>
+
+            <div
+              v-for="(attrItem, attrIndex) in attrList"
+              :key="attrIndex"
+              style="margin-left: 24px"
+            >
+              {{ attrItem }}
+              <!-- 添加提示文字 -->
+              <div v-if="attrItem.isCollection === 1" class="plc-tip">
+                <el-alert
+                  :title="t('operationFillForm.alert')"
+                  type="warning"
+                  :closable="false"
+                  center
+                  show-icon
+                  style="width: 320px"
+                />
+              </div>
+              <el-form-item :label="attrItem.name" prop="deviceId" label-position="top">
+                <div v-if="fillStatus === '1'">
+                  <el-select
+                    disabled
+                    v-model="attrItem.fillContent"
+                    v-if="attrItem.type === 'enum' && attrItem.description !== null"
+                    style="width: 200px"
+                  >
+                    <el-option
+                      v-for="dict in attrItem.name === '非生产原因'
+                        ? getIntDictOptions(attrItem.description)
+                        : getStrDictOptions(attrItem.description)"
+                      :key="dict.label"
+                      :label="dict.label"
+                      :value="
+                        attrItem.name === '非生产原因' ? Number(dict.value) : dict.value.toString()
+                      "
+                    />
+                  </el-select>
+                  <el-input
+                    v-else
+                    v-model="attrItem.fillContent"
+                    clearable
+                    style="width: 200px; margin-right: 10px"
+                    disabled
+                  />
+                </div>
+
+                <el-input
+                  v-else-if="attrItem.type === 'textarea'"
+                  v-model="attrItem.fillContent"
+                  type="textarea"
+                  clearable
+                  style="width: 200px"
+                />
+                <el-select
+                  v-model="attrItem.fillContent"
+                  clearable
+                  v-else-if="attrItem.type === 'enum' && attrItem.description !== null"
+                  style="width: 200px"
+                  filterable
+                >
+                  <el-option
+                    v-for="dict in attrItem.name === '非生产原因'
+                      ? getIntDictOptions(attrItem.description)
+                      : getStrDictOptions(attrItem.description)"
+                    :key="dict.label"
+                    :label="dict.label"
+                    :value="
+                      attrItem.name === '非生产原因' ? Number(dict.value) : dict.value.toString()
+                    "
+                  />
+                </el-select>
+                <el-input
+                  v-else
+                  v-model="attrItem.fillContent"
+                  clearable
+                  style="width: 200px"
+                  :placeholder="
+                    attrItem.type === 'double'
+                      ? t('operationFillForm.enterNumber')
+                      : t('operationFillForm.enterContent')
+                  "
+                  @input="handleInput(attrItem)"
+                  :maxlength="attrItem.type === 'double' ? calculateMaxLength(attrItem) : undefined"
+                />
+              </el-form-item>
+            </div>
+            <el-form-item>
+              <el-button type="primary" @click="getFillInfo" v-show="showStatus">{{
+                t('operationFillForm.confirm')
+              }}</el-button>
+              <el-button type="info" @click="deleteFillInfo" v-show="showStatus">{{
+                t('operationFill.clear')
+              }}</el-button>
+            </el-form-item>
+          </el-form>
+        </div>
+      </el-tab-pane>
+    </el-tabs>
+  </ContentWrap>
+</template>
+
+<script setup lang="ts">
+import { IotOpeationFillApi, IotOpeationFillVO } from '@/api/pms/iotopeationfill'
+import { ElMessage } from 'element-plus'
+import moment from 'moment'
+import { getIntDictOptions, getStrDictOptions } from '@/utils/dict'
+import { useRoute } from 'vue-router'
+
+/** 运行记录填报 列表 */
+defineOptions({ name: 'FillOrderInfo' })
+
+const route = useRoute()
+
+const message = useMessage() // 消息弹窗
+const { t } = useI18n() // 国际化
+/** 提交表单 */
+const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
+const loading = ref(true) // 列表的加载中
+const { params } = useRoute() // 查询参数
+const deptId = params.id as string
+const list = ref<IotOpeationFillVO[]>([]) // 列表的数据
+const attrList = ref<IotOpeationFillVO[]>([]) // 非累计属性集合
+const attrList1 = ref<IotOpeationFillVO[]>([]) // 累计属性集合
+const attrList2 = ref<IotOpeationFillVO[]>([]) // 属性集合
+let companyName = ref('')
+
+let fillStatus = deptId.split(',')[4]
+let createTime = formatTimestamp(JSON.parse(deptId.split(',')[2].substring(0, 10)))
+let showStatus = true
+const queryParams = reactive<any>({
+  pageNo: 1,
+  pageSize: 10,
+  deviceCode: undefined,
+  deviceName: undefined,
+  fillContent: undefined,
+  deviceType: undefined,
+  deviceComponent: undefined,
+  deptId: undefined,
+  orgName: undefined,
+  proId: undefined,
+  proName: undefined,
+  teamId: undefined,
+  teamName: undefined,
+  dutyName: undefined,
+  creDate: [],
+  createTime: [],
+  deviceCategoryId: 1,
+  deviceId: undefined,
+  threshold: undefined,
+  defaultValue: undefined,
+  isSum: undefined
+})
+
+let cxStatus = true
+
+// 计算数字输入的最大长度(根据阈值动态计算)
+const calculateMaxLength = (item: any) => {
+  if (item.type !== 'double' || !item.threshold) return undefined
+
+  const max = parseFloat(item.threshold)
+  if (isNaN(max)) return undefined
+
+  // 整数部分长度 + 可能的小数点 + 两位小数
+  return max.toString().length + (max.toString().includes('.') ? 0 : 3)
+}
+
+// 简单的节流函数,避免提示信息过于频繁
+const throttle = <T extends (...args: any[]) => any>(fn: T, delay: number) => {
+  let lastTime = 0
+
+  return function (this: ThisParameterType<T>, ...args: Parameters<T>) {
+    const now = Date.now()
+    if (now - lastTime >= delay) {
+      fn.apply(this, args)
+      lastTime = now
+    }
+  }
+}
+
+const showComponent = () => {
+  if (JSON.parse(fillStatus) === 1 || JSON.parse(fillStatus) === 3) {
+    showStatus = false
+  }
+}
+
+/** 查询列表 */
+const getList = async () => {
+  loading.value = true
+  try {
+    queryParams.deptId = deptId.split(',')[0]
+    queryParams.userId = deptId.split(',')[1]
+    queryParams.createTime = formatTimestamp(JSON.parse(deptId.split(',')[2].substring(0, 10)))
+    queryParams.orderId = deptId.split(',')[3]
+    const data = await IotOpeationFillApi.getIotOpeationFillPage(queryParams)
+    list.value = data
+    if (cxStatus) {
+      queryParams.deviceCategoryId = list.value[0].deviceCategoryId
+      queryParams.deptId = list.value[0].deptId
+      queryParams.deviceCode = list.value[0].deviceCode
+      queryParams.deviceName = list.value[0].deviceName
+      queryParams.deviceId = list.value[0].deviceId
+    }
+    getAttrList()
+  } finally {
+    loading.value = false
+  }
+}
+function formatTimestamp(timestamp) {
+  // const date = new Date(timestamp * 1000)
+  return moment.unix(timestamp).format('YYYY-MM-DD')
+}
+
+const open = async (_type: string, id?: number) => {
+  alert(id)
+}
+defineExpose({ open }) // 提供 open 方法,用于打开弹窗
+let devName = ''
+const openFill = (
+  deviceCategoryId?: number,
+  deviceId?: number,
+  deptId?: number,
+  deviceName?: string,
+  deviceCode?: string
+) => {
+  queryParams.deviceCategoryId = deviceCategoryId
+  queryParams.deptId = deptId
+  queryParams.deviceCode = deviceCode
+  queryParams.deviceName = deviceName
+  if (queryParams.deviceName == '生产日报') {
+    devName = '生产日报'
+  }
+  queryParams.deviceId = deviceId
+  getAttrList()
+}
+// 处理输入事件,实时限制输入格式和最大值
+const handleInput = (item: any) => {
+  if (item.type === 'double') {
+    // 保存原始值用于后续比较
+    const originalValue = item.fillContent
+
+    // 1. 格式验证:只允许数字和小数点
+    item.fillContent = item.fillContent.replace(/[^\d.]/g, '')
+    // 确保只有一个小数点
+    item.fillContent = item.fillContent.replace(/\.{2,}/g, '.')
+    // 确保小数点不在开头
+    item.fillContent = item.fillContent.replace(/^\./g, '')
+    // 限制小数位数为两位
+    item.fillContent = item.fillContent.replace(/(\d+)\.(\d{2}).*/, '$1.$2')
+
+    // 2. 最大值验证
+    if (item.threshold) {
+      const value = parseFloat(item.fillContent)
+      const max = parseFloat(item.threshold)
+
+      if (!isNaN(value) && !isNaN(max) && value > max) {
+        // 输入值超过阈值时,恢复到修改前的值
+        item.fillContent = originalValue
+          .replace(/[^\d.]/g, '')
+          .replace(/\.{2,}/g, '.')
+          .replace(/^\./g, '')
+          .replace(/(\d+)\.(\d{2}).*/, '$1.$2')
+
+        if (parseFloat(item.fillContent) > max) {
+          item.fillContent = max.toString()
+        }
+
+        throttle(() => {
+          ElMessage.warning(t('operationFillForm.exceedMax', { max }))
+        }, 1000)()
+      }
+    }
+
+    if (companyName.value === 'rd') {
+      // 3. 累计值限制验证(改为弹窗提示但允许继续)
+      if (item.maxAllowedValue !== undefined) {
+        const value = parseFloat(item.fillContent)
+        if (!isNaN(value) && value > item.maxAllowedValue) {
+          // 不自动修改值,而是显示警告弹窗
+          let limitDescription = ''
+          if (item.limitType === 'km') {
+            limitDescription = `当前累计值${item.currentSumValue} + 3000`
+          } else if (item.limitType === 'time') {
+            limitDescription = `当前累计值${item.currentSumValue} + 100`
+          }
+
+          ElMessage.warning(
+            `填报值 ${value} 超过限制 ${item.maxAllowedValue} (${limitDescription}),请确认是否正确!`
+          )
+        }
+      }
+    }
+  }
+}
+
+const getAttrList = async () => {
+  loading.value = true
+  try {
+    queryParams.createTime = formatTimestamp(JSON.parse(deptId.split(',')[2].substring(0, 10)))
+    const data = await IotOpeationFillApi.getAttrs(queryParams)
+
+    attrList.value = data[0].nonSumList
+    attrList1.value = data[0].sumList
+
+    // 建立累计数据映射,用于后续验证
+    const sumMap = new Map()
+    attrList1.value.forEach((item) => {
+      // 创建匹配规则:移除"填报"字样的差异,保留核心名称
+      const coreName = item.name.replace(/填报/g, '')
+      sumMap.set(coreName, item)
+    })
+
+    // 为非累计数据添加最大值限制
+    attrList.value.forEach(function (item) {
+      if (item.fillContent !== '' && item.fillContent !== null) {
+        const num = Number(item.fillContent)
+        if (!isNaN(num)) {
+          if (item.fillContent.includes('.')) {
+            item.fillContent = Number(num.toFixed(2))
+          } else {
+            item.fillContent = Math.floor(num)
+          }
+        }
+      }
+
+      if (companyName.value === 'rd') {
+        // 添加最大值限制逻辑
+        const coreName = item.name.replace(/填报/g, '')
+        const sumItem = sumMap.get(coreName)
+        if (sumItem) {
+          // 根据字段名称判断使用哪种限制规则
+          if (item.name.includes('公里数填报')) {
+            // 公里数限制:当前累计值 + 3000
+            item.maxAllowedValue = sumItem.totalRunTime + 3000
+            item.currentSumValue = sumItem.totalRunTime // 保存当前累计值用于提示
+            item.limitType = 'km' // 标记为公里数限制
+          } else if (item.name.includes('运转时长填报')) {
+            // 运转时长限制:当前累计值 + 100
+            item.maxAllowedValue = sumItem.totalRunTime + 100
+            item.currentSumValue = sumItem.totalRunTime // 保存当前累计值用于提示
+            item.limitType = 'time' // 标记为时长限制
+          }
+        }
+      }
+
+      item.deviceCode = queryParams.deviceCode
+      item.deptId = queryParams.deptId
+      item.deviceId = queryParams.deviceId
+      item.deviceCategoryId = queryParams.deviceCategoryId
+      item.modelId = item.id
+      console.log(item.fillContent)
+    })
+
+    attrList1.value.forEach(function (item) {
+      item.deviceCode = queryParams.deviceCode
+      item.deptId = queryParams.deptId
+      item.deviceId = queryParams.deviceId
+      item.deviceCategoryId = queryParams.deviceCategoryId
+      item.modelId = item.id
+    })
+  } finally {
+    loading.value = false
+  }
+}
+/** 获取填写信息保存到后台*/
+const getFillInfo = async () => {
+  try {
+    const company = await IotOpeationFillApi.getOrgName(route.params.id.toString().split(',')[0])
+
+    if (devName != '生产日报') {
+      // 检查必填字段
+      const emptyFields = attrList.value.filter((item) => {
+        // 只检查非disabled的字段
+        return (
+          !(item.isCollection === 1 || fillStatus === '1') &&
+          (item.fillContent === undefined || item.fillContent === '')
+        )
+      })
+      if (emptyFields.length > 0) {
+        ElMessage.error(t('operationFillForm.fill'))
+        return
+      }
+    }
+
+    if (company === 'rd') {
+      // 检查是否有超出累计值限制的字段
+      const exceededFields = attrList.value.filter((item) => {
+        if (
+          item.type === 'double' &&
+          item.maxAllowedValue !== undefined &&
+          item.fillContent !== '' &&
+          item.fillContent !== null
+        ) {
+          const value = parseFloat(item.fillContent)
+          return !isNaN(value) && value > item.maxAllowedValue
+        }
+        return false
+      })
+
+      // 如果有超出限制的字段,提示用户确认
+      if (exceededFields.length > 0) {
+        let exceededMessage = ''
+        exceededFields.forEach((field) => {
+          let limitDescription = ''
+          if (field.limitType === 'km') {
+            limitDescription = `(${field.currentSumValue} + 3000)`
+          } else if (field.limitType === 'time') {
+            limitDescription = `(${field.currentSumValue} + 100)`
+          }
+          exceededMessage += `${field.name};\n`
+        })
+
+        // exceededMessage += '\n是否继续保存?'
+
+        const confirmResult = await message.confirm(
+          exceededMessage,
+          '以下填报项超出限制,是否继续保存?',
+          '继续保存',
+          '取消'
+        )
+        if (!confirmResult) {
+          return // 用户取消保存
+        }
+      }
+    }
+
+    attrList2.value = attrList.value.concat(attrList1.value)
+
+    attrList2.value.forEach(function (item) {
+      item.pointName = item.name
+      item.createTime = formatTimestamp(JSON.parse(deptId.split(',')[2].substring(0, 10)))
+      item.userId = deptId.split(',')[1]
+      item.id = deptId.split(',')[3]
+    })
+    const data = attrList2.value as unknown as IotOpeationFillVO
+    await IotOpeationFillApi.insertLog(data)
+    message.success(t('common.createSuccess'))
+    // 发送操作成功的事件
+    emit('success')
+    cxStatus = false
+    getList()
+  } catch (error) {
+    console.error('保存失败:', error)
+  }
+}
+
+/**清空填写信息*/
+const deleteFillInfo = () => {
+  attrList.value.forEach(function (item) {
+    item.fillContent = ''
+  })
+}
+
+/** 初始化 **/
+onMounted(async () => {
+  const company = await IotOpeationFillApi.getOrgName(route.params.id.toString().split(',')[0])
+  companyName.value = company
+
+  getList()
+  showComponent()
+})
+</script>
+<style scoped>
+.scrollable-form {
+  /* 设置最大高度,超过这个高度会出现滚动条 */
+  max-height: 500px; /* 根据你的需求调整 */
+
+  /* 可选:添加内边距和边框美化 */
+  padding: 16px;
+
+  /* 超出部分显示垂直滚动条 */
+  overflow-y: auto;
+  border: 1px solid #e5e7eb;
+  border-radius: 4px;
+}
+
+/* 可选:美化滚动条 */
+.scrollable-form::-webkit-scrollbar {
+  width: 6px;
+}
+
+.scrollable-form::-webkit-scrollbar-track {
+  background: #f1f1f1;
+  border-radius: 3px;
+}
+
+.scrollable-form::-webkit-scrollbar-thumb {
+  background: #c1c1c1;
+  border-radius: 3px;
+}
+
+.scrollable-form::-webkit-scrollbar-thumb:hover {
+  background: #a8a8a8;
+}
+
+.back-red {
+  /* 红色背景 */
+  background-color: red;
+}
+
+.back-blue {
+  background-color: grey;
+}
+
+.step-container {
+  display: grid;
+  grid-template-columns: 220px 1fr;
+  gap: 10px;
+  height: 100%;
+  min-height: 600px;
+}
+
+.steps-nav {
+  padding-right: 15px;
+  overflow-y: auto;
+}
+
+.form-wrapper {
+  padding: 30px;
+  background: #fff;
+  border-radius: 8px;
+  box-shadow: 0 2px 12px rgb(0 0 0 / 10%);
+}
+
+.navigation-controls {
+  margin-top: 40px;
+  text-align: center;
+}
+
+.custom-label {
+  padding: 0 10px;
+  font-size: 17px;
+  font-weight: 1000;
+  color: forestgreen;
+}
+
+.custom-label1 {
+  padding: 0 10px;
+  font-size: 17px;
+  font-weight: 1000;
+}
+
+.has-border {
+  padding: 3px 8px; /* 适当增加内边距,避免文字太贴近边框 */
+  font-size: 20px;
+  border: 2px solid #333; /* 加粗到2px,使用深灰色#333让边框更清晰 */
+  border-radius: 2px; /* 轻微圆角,可选 */
+}
+
+::v-deep .el-step__icon {
+  color: #fff;
+  background-color: #409eff;
+  border: 0;
+}
+
+.step-title-wrapper {
+  position: relative;
+  display: inline-flex;
+  padding-right: 25px;
+  align-items: center;
+  gap: 8px;
+}
+
+/* 覆盖步骤条默认样式 */
+:deep(.custom-steps) {
+  /* 调整头部位置 */
+  .el-step__head {
+    top: 3px;
+  }
+
+  /* 标题容器定位 */
+  .el-step__title {
+    display: inline-block;
+    padding-right: 0;
+    margin-left: 10px;
+  }
+
+  /* 步骤连接线 */
+  .el-step__line {
+    left: 11px;
+    background-color: #ebeef5;
+  }
+
+  /* 当前步骤样式 */
+  .is-process .title-text {
+    font-weight: 600;
+    color: #409eff;
+  }
+
+  /* 完成状态图标 */
+  .is-finish .tip-icon {
+    color: #67c23a;
+  }
+}
+
+.horizontal-container {
+  display: flex;
+  flex-wrap: wrap; /* 允许换行 */
+  gap: 20px; /* 项目间距 */
+}
+
+.form-item-container {
+  flex: 1; /* 等宽分布 */
+  min-width: 200px; /* 最小宽度防止挤压 */
+}
+
+/* 新增日报填报项的边框样式 */
+.report-border {
+  padding: 2px 4px;
+  border: 2px solid #42b983; /* 使用Vue标志性的绿色边框 */
+  border-radius: 4px;
+}
+</style>

+ 327 - 96
src/views/pms/iotopeationfill/index1.vue

@@ -1,47 +1,50 @@
 <template>
   <ContentWrap>
-    <el-tabs
-      v-model="activeTab"
-      type="border-card"
-      tab-position="left"
-      v-loading="loading"
-      style="height: 84vh"
-    >
-      <el-tab-pane style="height: 100%" v-for="(item, index) in list" :key="index">
+    <el-tabs type="border-card" tab-position="left" v-loading="loading" style="height: 84vh">
+      <el-tab-pane
+        style="height: 100%"
+        v-for="(deviceItem, deviceIndex) in list"
+        :key="deviceIndex"
+      >
         <template #label>
           <span
-            :class="['custom-label', { 'has-border': item.deviceName === '生产日报' }]"
-            v-if="item.isFill === 1"
+            :class="['custom-label', { 'has-border': deviceItem.deviceName === '生产日报' }]"
+            v-if="deviceItem.isFill === 1"
             @click="
               openFill(
-                item.deviceCategoryId,
-                item.deviceId,
-                item.deptId,
-                item.deviceName,
-                item.deviceCode
+                deviceItem.deviceCategoryId,
+                deviceItem.deviceId,
+                deviceItem.deptId,
+                deviceItem.deviceName,
+                deviceItem.deviceCode
               )
             "
           >
-            {{ item.deviceCode }} ({{ item.deviceName }})
+            {{ deviceItem.deviceCode }} ({{ deviceItem.deviceName }})
           </span>
           <span
-            :class="['custom-label1', { 'has-border': item.deviceName === '生产日报' }]"
+            :class="['custom-label1', { 'has-border': deviceItem.deviceName === '生产日报' }]"
             v-else
             @click="
               openFill(
-                item.deviceCategoryId,
-                item.deviceId,
-                item.deptId,
-                item.deviceName,
-                item.deviceCode
+                deviceItem.deviceCategoryId,
+                deviceItem.deviceId,
+                deviceItem.deptId,
+                deviceItem.deviceName,
+                deviceItem.deviceCode
               )
             "
           >
-            {{ item.deviceCode }} ({{ item.deviceName }})
+            {{ deviceItem.deviceCode }} ({{ deviceItem.deviceName }})
           </span>
         </template>
-        <div class="form-wrapper">
-          <el-form label-width="120px" class="scrollable-form">
+        <div class="form-wrapper h-full">
+          <el-form
+            size="default"
+            label-width="120px"
+            class="scrollable-form"
+            :model="{ attrList: attrList }"
+          >
             <div style="margin-left: 24px">
               <el-form class="demo-form-inline" :inline="true">
                 <el-form-item :label="t('common.createTime')" class="custom-label1">
@@ -51,14 +54,18 @@
                 </el-form-item>
                 <el-form-item :label="t('operationFillForm.team')" class="custom-label1">
                   <span style="text-decoration: underline">
-                    {{ item.orgName }}
+                    {{ deviceItem.orgName }}
                   </span>
                 </el-form-item>
                 <el-row :gutter="20">
-                  <el-col v-for="(item, index) in attrList1" :key="index" :span="24">
-                    <el-form-item :label="item.name" class="custom-label1">
+                  <el-col
+                    v-for="(summaryItem, summaryIndex) in attrList1"
+                    :key="summaryIndex"
+                    :span="24"
+                  >
+                    <el-form-item :label="summaryItem.name" class="custom-label1">
                       <span style="text-decoration: underline">
-                        {{ item.totalRunTime }}
+                        {{ summaryItem.totalRunTime }}
                       </span>
                     </el-form-item>
                   </el-col>
@@ -71,9 +78,15 @@
               </el-form>
             </div>
 
-            <div v-for="(item, index) in attrList" :key="index" style="margin-left: 24px">
+            <div
+              v-for="(attrItem, attrIndex) in attrList.filter(
+                (item) => !keys.includes(item.description)
+              )"
+              :key="attrIndex"
+              style="margin-left: 24px"
+            >
               <!-- 添加提示文字 -->
-              <div v-if="item.isCollection === 1" class="plc-tip">
+              <div v-if="attrItem.isCollection === 1" class="plc-tip">
                 <el-alert
                   :title="t('operationFillForm.alert')"
                   type="warning"
@@ -83,28 +96,28 @@
                   style="width: 320px"
                 />
               </div>
-              <el-form-item :label="item.name" prop="deviceId" label-position="top">
+              <el-form-item :label="attrItem.name" prop="deviceId" label-position="top">
                 <div v-if="fillStatus === '1'">
                   <el-select
                     disabled
-                    v-model="item.fillContent"
-                    v-if="item.type === 'enum' && item.description !== null"
+                    v-model="attrItem.fillContent"
+                    v-if="attrItem.type === 'enum' && attrItem.description !== null"
                     style="width: 200px"
                   >
                     <el-option
-                      v-for="dict in item.name === '非生产原因'
-                        ? getIntDictOptions(item.description)
-                        : getStrDictOptions(item.description)"
+                      v-for="dict in attrItem.name === '非生产原因'
+                        ? getIntDictOptions(attrItem.description)
+                        : getStrDictOptions(attrItem.description)"
                       :key="dict.label"
                       :label="dict.label"
                       :value="
-                        item.name === '非生产原因' ? Number(dict.value) : dict.value.toString()
+                        attrItem.name === '非生产原因' ? Number(dict.value) : dict.value.toString()
                       "
                     />
                   </el-select>
                   <el-input
                     v-else
-                    v-model="item.fillContent"
+                    v-model="attrItem.fillContent"
                     clearable
                     style="width: 200px; margin-right: 10px"
                     disabled
@@ -112,40 +125,85 @@
                 </div>
 
                 <el-input
-                  v-else-if="item.type === 'textarea'"
-                  v-model="item.fillContent"
+                  v-else-if="attrItem.type === 'textarea'"
+                  v-model="attrItem.fillContent"
                   type="textarea"
                   clearable
                   style="width: 200px"
                 />
                 <el-select
-                  v-model="item.fillContent"
+                  v-model="attrItem.fillContent"
                   clearable
-                  v-else-if="item.type === 'enum' && item.description !== null"
+                  v-else-if="attrItem.type === 'enum' && attrItem.description !== null"
                   style="width: 200px"
                   filterable
                 >
                   <el-option
-                    v-for="dict in item.name === '非生产原因'
-                      ? getIntDictOptions(item.description)
-                      : getStrDictOptions(item.description)"
+                    v-for="dict in attrItem.name === '非生产原因'
+                      ? getIntDictOptions(attrItem.description)
+                      : getStrDictOptions(attrItem.description)"
                     :key="dict.label"
                     :label="dict.label"
-                    :value="item.name === '非生产原因' ? Number(dict.value) : dict.value.toString()"
+                    :value="
+                      attrItem.name === '非生产原因' ? Number(dict.value) : dict.value.toString()
+                    "
                   />
                 </el-select>
                 <el-input
                   v-else
-                  v-model="item.fillContent"
+                  v-model="attrItem.fillContent"
                   clearable
                   style="width: 200px"
                   :placeholder="
-                    item.type === 'double'
+                    attrItem.type === 'double'
                       ? t('operationFillForm.enterNumber')
                       : t('operationFillForm.enterContent')
                   "
-                  @input="handleInput(item)"
-                  :maxlength="item.type === 'double' ? calculateMaxLength(item) : undefined"
+                  @input="handleInput(attrItem)"
+                  :maxlength="attrItem.type === 'double' ? calculateMaxLength(attrItem) : undefined"
+                />
+              </el-form-item>
+            </div>
+
+            <div
+              v-for="(attrItem, attrIndex) in attrList"
+              :key="attrIndex"
+              style="margin-left: 24px"
+            >
+              <el-divider v-if="attrItem.description === 'repairTime'" content-position="left"
+                >非生产时间</el-divider
+              >
+              <el-form-item
+                v-if="
+                  keys.includes(attrItem.description) && attrItem.description !== 'otherNptReason'
+                "
+                label-position="top"
+                :label="attrItem.name"
+                :prop="'attrList.' + attrIndex + '.fillContent'"
+                :rules="rules[attrItem.description]"
+              >
+                <el-input-number
+                  class="w-80!"
+                  :min="0"
+                  :max="24"
+                  v-model="attrItem.fillContent"
+                  :controls="false"
+                  align="left"
+                  placeholder="请输入数字"
+                />
+              </el-form-item>
+
+              <el-form-item
+                v-else-if="attrItem.description === 'otherNptReason'"
+                label-position="top"
+                :label="attrItem.name"
+                :prop="'attrList.' + attrIndex + '.fillContent'"
+                :rules="rules[attrItem.description]"
+              >
+                <el-input
+                  class="w-80!"
+                  v-model="attrItem.fillContent"
+                  placeholder="请输入其他非生产原因"
                 />
               </el-form-item>
             </div>
@@ -165,17 +223,10 @@
 </template>
 
 <script setup lang="ts">
-import { dateFormatter2 } from '@/utils/formatTime'
-import download from '@/utils/download'
 import { IotOpeationFillApi, IotOpeationFillVO } from '@/api/pms/iotopeationfill'
-import IotOpeationFillForm from './IotOpeationFillForm.vue'
-import Vue from '@vitejs/plugin-vue'
-import { useUserStore } from '@/store/modules/user'
-import { ElMessage } from 'element-plus'
+import { ElMessage, FormRules } from 'element-plus'
 import moment from 'moment'
-import { format } from 'date-fns'
-import { cx } from '@fullcalendar/core/internal-common'
-import { DICT_TYPE, getIntDictOptions, getStrDictOptions } from '@/utils/dict'
+import { getIntDictOptions, getStrDictOptions } from '@/utils/dict'
 import { useRoute } from 'vue-router'
 
 /** 运行记录填报 列表 */
@@ -188,18 +239,18 @@ const { t } = useI18n() // 国际化
 /** 提交表单 */
 const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
 const loading = ref(true) // 列表的加载中
-const { params, name } = useRoute() // 查询参数
-const deptId = params.id
+const { params } = useRoute() // 查询参数
+const deptId = params.id as string
 const list = ref<IotOpeationFillVO[]>([]) // 列表的数据
 const attrList = ref<IotOpeationFillVO[]>([]) // 非累计属性集合
 const attrList1 = ref<IotOpeationFillVO[]>([]) // 累计属性集合
 const attrList2 = ref<IotOpeationFillVO[]>([]) // 属性集合
 let companyName = ref('')
 
-let fillStatus = params.id.split(',')[4]
+let fillStatus = deptId.split(',')[4]
 let createTime = formatTimestamp(JSON.parse(deptId.split(',')[2].substring(0, 10)))
 let showStatus = true
-const queryParams = reactive({
+const queryParams = reactive<any>({
   pageNo: 1,
   pageSize: 10,
   deviceCode: undefined,
@@ -223,6 +274,170 @@ const queryParams = reactive({
   isSum: undefined
 })
 
+const NON_KEYS = [
+  'repairTime',
+  'selfStopTime',
+  'accidentTime',
+  'complexityTime',
+  'rectificationTime',
+  'waitingStopTime',
+  'partyaDesign',
+  'partyaPrepare',
+  'partyaResource',
+  'relocationTime',
+  'winterBreakTime',
+  'otherNptTime'
+]
+
+const keys = [
+  'repairTime',
+  'selfStopTime',
+  'accidentTime',
+  'complexityTime',
+  'rectificationTime',
+  'waitingStopTime',
+  'partyaDesign',
+  'partyaPrepare',
+  'partyaResource',
+  'relocationTime',
+  'winterBreakTime',
+  'otherNptTime',
+  'drillingWorkingTime',
+  'otherProductionTime',
+  'ratedProductionTime',
+  'productionTime',
+  'dailyInjectGasTime',
+  'otherNptReason'
+]
+
+const sumNonProdTimes = () => {
+  let sum = 0
+  NON_KEYS.forEach((field) => {
+    sum += attrList.value.find((item) => item.description === field)?.fillContent || 0
+  })
+  return sum
+}
+
+const rhValidateTotalTime =
+  (isNon: boolean = false) =>
+  (_rule: any, _value: any, callback: any) => {
+    console.log('11 :>> ', 11)
+    const gasTime =
+      attrList.value.find((item) => item.description === 'dailyInjectGasTime')?.fillContent || 0
+    const nonProdSum = sumNonProdTimes()
+
+    let total = 0
+    let msg = ''
+
+    total = parseFloat((gasTime + nonProdSum).toFixed(2))
+    msg = `运转(${gasTime})+非生产(${nonProdSum})=${total}H,必须为24H`
+
+    if (Math.abs(total - 24) > 0.01) {
+      if (!isNon) callback(new Error(msg))
+      else callback(new Error())
+    } else {
+      callback()
+    }
+  }
+
+// const ryXjValidateTotalTime =
+//   (isNon: boolean = false) =>
+//   (_rule: any, _value: any, callback: any) => {
+//     const rateTime =
+//       attrList.value.find((item) => item.description === 'ratedProductionTime')?.fillContent || 0
+//     const time =
+//       attrList.value.find((item) => item.description === 'productionTime')?.fillContent || 0
+
+//     const nonProdSum = sumNonProdTimes()
+
+//     let total = 0
+//     let msg = ''
+
+//     total = parseFloat((time + nonProdSum).toFixed(2))
+//     msg = `生产(${time})+非生产(${nonProdSum})=${total}H,必须等于额定${rateTime}H`
+
+//     if (Math.abs(total - rateTime) > 0.01) {
+//       if (!isNon) callback(new Error(msg))
+//       else callback(new Error())
+//     } else {
+//       callback()
+//     }
+//   }
+
+const ryValidateTotalTime =
+  (isNon: boolean = false) =>
+  (_rule: any, _value: any, callback: any) => {
+    const drillingTime =
+      attrList.value.find((item) => item.description === 'drillingWorkingTime')?.fillContent || 0
+    const otherTime =
+      attrList.value.find((item) => item.description === 'otherProductionTime')?.fillContent || 0
+
+    const nonProdSum = sumNonProdTimes()
+
+    let total = 0
+    let msg = ''
+
+    total = parseFloat((drillingTime + otherTime + nonProdSum).toFixed(2))
+    msg = `进尺(${drillingTime})+其他(${otherTime})+非生产(${nonProdSum})=${total}H,必须为24H`
+
+    if (Math.abs(total - 24) > 0.01) {
+      if (!isNon) callback(new Error(msg))
+      else callback(new Error())
+    } else {
+      callback()
+    }
+  }
+
+const validateOtherReason = (_rule: any, value: any, callback: any) => {
+  const time = attrList.value.find((item) => item.description === 'otherNptTime')?.fillContent || 0
+  if (time > 0 && !value) {
+    callback(new Error('填写了其他时间,必须说明原因'))
+  } else {
+    callback()
+  }
+}
+
+const rules = reactive<FormRules>({
+  dailyInjectGasTime: [
+    { required: true, message: '请输入当日运转时间', trigger: 'blur' },
+    { validator: rhValidateTotalTime(), trigger: 'blur' }
+  ],
+  drillingWorkingTime: [
+    { required: true, message: '请输入进尺工作时间', trigger: 'blur' },
+    { validator: ryValidateTotalTime(), trigger: 'blur' }
+  ],
+  otherProductionTime: [{ validator: ryValidateTotalTime(), trigger: 'blur' }],
+  // ratedProductionTime: [
+  //   { required: true, message: '请输入额定生产时间', trigger: 'blur' },
+  //   { validator: ryXjValidateTotalTime(), trigger: 'blur' }
+  // ],
+  // productionTime: [
+  //   { required: true, message: '请输入生产时间', trigger: 'blur' },
+  //   { validator: ryXjValidateTotalTime(), trigger: 'blur' }
+  // ],
+  otherNptReason: [{ validator: validateOtherReason, trigger: 'blur' }]
+})
+
+const totalValidatorComputed = computed(() => {
+  if (attrList.value.some((item) => item.description === 'dailyInjectGasTime')) {
+    return rhValidateTotalTime
+  }
+  //  else if (attrList.value.some((item) => item.description === 'ratedProductionTime')) {
+  //   return ryXjValidateTotalTime
+  // }
+  else if (attrList.value.some((item) => item.description === 'drillingWorkingTime')) {
+    return ryValidateTotalTime
+  }
+})
+
+nextTick(() => {
+  const validator = totalValidatorComputed.value
+  if (!validator) return
+  NON_KEYS.forEach((field) => {
+    rules[field] = [{ validator: validator(true), trigger: 'blur' }]
+  })
+})
+
 let cxStatus = true
 
 // 计算数字输入的最大长度(根据阈值动态计算)
@@ -237,9 +452,10 @@ const calculateMaxLength = (item: any) => {
 }
 
 // 简单的节流函数,避免提示信息过于频繁
-const throttle = (fn: Function, delay: number) => {
+const throttle = <T extends (...args: any[]) => any>(fn: T, delay: number) => {
   let lastTime = 0
-  return function (...args: any[]) {
+
+  return function (this: ThisParameterType<T>, ...args: Parameters<T>) {
     const now = Date.now()
     if (now - lastTime >= delay) {
       fn.apply(this, args)
@@ -277,11 +493,11 @@ const getList = async () => {
   }
 }
 function formatTimestamp(timestamp) {
-  const date = new Date(timestamp * 1000)
+  // const date = new Date(timestamp * 1000)
   return moment.unix(timestamp).format('YYYY-MM-DD')
 }
 
-const open = async (type: string, id?: number) => {
+const open = async (_type: string, id?: number) => {
   alert(id)
 }
 defineExpose({ open }) // 提供 open 方法,用于打开弹窗
@@ -369,7 +585,14 @@ const getAttrList = async () => {
     queryParams.createTime = formatTimestamp(JSON.parse(deptId.split(',')[2].substring(0, 10)))
     const data = await IotOpeationFillApi.getAttrs(queryParams)
 
-    attrList.value = data[0].nonSumList
+    const timeKeys = keys.filter((k) => k !== 'otherNptReason')
+
+    attrList.value = data[0].nonSumList.map((item) => {
+      if (timeKeys.includes(item.description)) {
+        item.fillContent = item.fillContent ?? 0
+      }
+      return item
+    })
     attrList1.value = data[0].sumList
 
     // 建立累计数据映射,用于后续验证
@@ -381,7 +604,7 @@ const getAttrList = async () => {
     })
 
     // 为非累计数据添加最大值限制
-    attrList.value.forEach(function (item, index) {
+    attrList.value.forEach(function (item) {
       if (item.fillContent !== '' && item.fillContent !== null) {
         const num = Number(item.fillContent)
         if (!isNaN(num)) {
@@ -421,7 +644,7 @@ const getAttrList = async () => {
       console.log(item.fillContent)
     })
 
-    attrList1.value.forEach(function (item, index) {
+    attrList1.value.forEach(function (item) {
       item.deviceCode = queryParams.deviceCode
       item.deptId = queryParams.deptId
       item.deviceId = queryParams.deviceId
@@ -485,11 +708,8 @@ const getFillInfo = async () => {
         const confirmResult = await message.confirm(
           exceededMessage,
           '以下填报项超出限制,是否继续保存?',
-          {
-            confirmButtonText: '继续保存',
-            cancelButtonText: '取消',
-            type: 'warning'
-          }
+          '继续保存',
+          '取消'
         )
         if (!confirmResult) {
           return // 用户取消保存
@@ -499,13 +719,15 @@ const getFillInfo = async () => {
 
     attrList2.value = attrList.value.concat(attrList1.value)
 
-    attrList2.value.forEach(function (item, index) {
+    attrList2.value.forEach(function (item) {
       item.pointName = item.name
       item.createTime = formatTimestamp(JSON.parse(deptId.split(',')[2].substring(0, 10)))
       item.userId = deptId.split(',')[1]
       item.id = deptId.split(',')[3]
     })
     const data = attrList2.value as unknown as IotOpeationFillVO
+
+    console.log('data :>> ', data)
     await IotOpeationFillApi.insertLog(data)
     message.success(t('common.createSuccess'))
     // 发送操作成功的事件
@@ -519,7 +741,7 @@ const getFillInfo = async () => {
 
 /**清空填写信息*/
 const deleteFillInfo = () => {
-  attrList.value.forEach(function (item, index) {
+  attrList.value.forEach(function (item) {
     item.fillContent = ''
   })
 }
@@ -536,11 +758,13 @@ onMounted(async () => {
 <style scoped>
 .scrollable-form {
   /* 设置最大高度,超过这个高度会出现滚动条 */
-  max-height: 500px; /* 根据你的需求调整 */
-  /* 超出部分显示垂直滚动条 */
-  overflow-y: auto;
+  max-height: 100%; /* 根据你的需求调整 */
+
   /* 可选:添加内边距和边框美化 */
   padding: 16px;
+
+  /* 超出部分显示垂直滚动条 */
+  overflow-y: auto;
   border: 1px solid #e5e7eb;
   border-radius: 4px;
 }
@@ -568,9 +792,11 @@ onMounted(async () => {
   /* 红色背景 */
   background-color: red;
 }
+
 .back-blue {
   background-color: grey;
 }
+
 .step-container {
   display: grid;
   grid-template-columns: 220px 1fr;
@@ -580,15 +806,15 @@ onMounted(async () => {
 }
 
 .steps-nav {
-  overflow-y: auto;
   padding-right: 15px;
+  overflow-y: auto;
 }
 
 .form-wrapper {
-  background: #fff;
   padding: 30px;
+  background: #fff;
   border-radius: 8px;
-  box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
+  box-shadow: 0 2px 12px rgb(0 0 0 / 10%);
 }
 
 .navigation-controls {
@@ -597,33 +823,37 @@ onMounted(async () => {
 }
 
 .custom-label {
-  font-weight: 1000;
-  font-size: 17px;
   padding: 0 10px;
+  font-size: 17px;
+  font-weight: 1000;
   color: forestgreen;
 }
+
 .custom-label1 {
-  font-weight: 1000;
-  font-size: 17px;
   padding: 0 10px;
+  font-size: 17px;
+  font-weight: 1000;
 }
+
 .has-border {
-  border: 2px solid #333; /* 加粗到2px,使用深灰色#333让边框更清晰 */
   padding: 3px 8px; /* 适当增加内边距,避免文字太贴近边框 */
-  border-radius: 2px; /* 轻微圆角,可选 */
   font-size: 20px;
+  border: 2px solid #333; /* 加粗到2px,使用深灰色#333让边框更清晰 */
+  border-radius: 2px; /* 轻微圆角,可选 */
 }
+
 ::v-deep .el-step__icon {
-  background-color: #409eff;
   color: #fff;
-  border: 0px;
+  background-color: #409eff;
+  border: 0;
 }
+
 .step-title-wrapper {
+  position: relative;
   display: inline-flex;
+  padding-right: 25px;
   align-items: center;
   gap: 8px;
-  position: relative;
-  padding-right: 25px;
 }
 
 /* 覆盖步骤条默认样式 */
@@ -636,8 +866,8 @@ onMounted(async () => {
   /* 标题容器定位 */
   .el-step__title {
     display: inline-block;
-    margin-left: 10px;
     padding-right: 0;
+    margin-left: 10px;
   }
 
   /* 步骤连接线 */
@@ -668,10 +898,11 @@ onMounted(async () => {
   flex: 1; /* 等宽分布 */
   min-width: 200px; /* 最小宽度防止挤压 */
 }
+
 /* 新增日报填报项的边框样式 */
 .report-border {
-  border: 2px solid #42b983; /* 使用Vue标志性的绿色边框 */
   padding: 2px 4px;
+  border: 2px solid #42b983; /* 使用Vue标志性的绿色边框 */
   border-radius: 4px;
 }
 </style>

+ 2 - 9
src/views/pms/iotprojectinfo/index.vue

@@ -73,13 +73,8 @@
         >
           <Icon icon="ep:plus" class="mr-5px" /> 新增
         </el-button>
-        <el-button
-          type="success"
-          plain
-          @click="handleExport"
-          :loading="exportLoading"
-          v-hasPermi="['rq:iot-project-info:export']"
-        >
+        <!-- v-hasPermi="['rq:iot-project-info:export']" -->
+        <el-button type="success" plain @click="handleExport" :loading="exportLoading">
           <Icon icon="ep:download" class="mr-5px" /> 导出
         </el-button>
       </el-form-item>
@@ -1001,8 +996,6 @@ const handleDelete = async (id: number) => {
 /** 导出按钮操作 */
 const handleExport = async () => {
   try {
-    // 导出的二次确认
-    await message.exportConfirm()
     // 发起导出
     exportLoading.value = true
     const data = await IotProjectInfoApi.exportIotProjectInfo(queryParams)

+ 196 - 139
src/views/pms/iotprojecttask/index.vue

@@ -50,6 +50,15 @@
           class="!w-240px"
         />
       </el-form-item>
+      <el-form-item label="施工队伍" prop="deptName">
+        <el-input
+          v-model="queryParams.deptName"
+          placeholder="请输入施工队伍"
+          clearable
+          @keyup.enter="handleQuery"
+          class="!w-240px"
+        />
+      </el-form-item>
       <el-form-item label="井号" prop="wellName">
         <el-input
           v-model="queryParams.wellName"
@@ -95,13 +104,8 @@
         >
           <Icon icon="ep:plus" class="mr-5px" /> 新增
         </el-button> -->
-        <el-button
-          type="success"
-          plain
-          @click="handleExport"
-          :loading="exportLoading"
-          v-hasPermi="['rq:iot-project-task:export']"
-        >
+        <!-- v-hasPermi="['rq:iot-project-task:export']" -->
+        <el-button type="success" plain @click="handleExport" :loading="exportLoading">
           <Icon icon="ep:download" class="mr-5px" /> 导出
         </el-button>
       </el-form-item>
@@ -109,26 +113,70 @@
   </ContentWrap>
 
   <!-- 列表 -->
-  <ContentWrap  ref="tableContainerRef">
+  <ContentWrap ref="tableContainerRef">
     <div class="table-container">
-      <el-table ref="tableRef" v-loading="loading" :data="list" :stripe="true" style="width: 100%" :cell-style="{padding: '5px'}">
-        <el-table-column :label="t('iotDevice.serial')" :width="columnWidths.serial" align="center" v-if="false">
+      <el-table
+        ref="tableRef"
+        v-loading="loading"
+        :data="list"
+        :stripe="true"
+        style="width: 100%"
+        :cell-style="{ padding: '5px' }"
+      >
+        <el-table-column
+          :label="t('iotDevice.serial')"
+          :width="columnWidths.serial"
+          align="center"
+          v-if="false"
+        >
           <template #default="scope">
             {{ scope.$index + 1 }}
           </template>
         </el-table-column>
-        <el-table-column label="客户名称" align="center" prop="manufactureName" :width="columnWidths.manufactureName" show-overflow-tooltip/>
-        <el-table-column label="合同名称" align="center" prop="contractName" :width="columnWidths.contractName" show-overflow-tooltip/>
-        <el-table-column label="合同编号" align="center" prop="contractCode" :width="columnWidths.contractCode" />
-        <el-table-column label="井号" align="center" prop="wellName" :width="columnWidths.wellName" />
+        <el-table-column
+          label="客户名称"
+          align="center"
+          prop="manufactureName"
+          :width="columnWidths.manufactureName"
+          show-overflow-tooltip
+        />
+        <el-table-column
+          label="合同名称"
+          align="center"
+          prop="contractName"
+          :width="columnWidths.contractName"
+          show-overflow-tooltip
+        />
+        <el-table-column
+          label="合同编号"
+          align="center"
+          prop="contractCode"
+          :width="columnWidths.contractCode"
+        />
+        <el-table-column
+          label="井号"
+          align="center"
+          prop="wellName"
+          :width="columnWidths.wellName"
+        />
         <!-- <el-table-column label="井型/井别" align="center" prop="wellType" />
         <el-table-column :label="t('project.wellType')" align="center" prop="wellType" :width="columnWidths.wellType">
           <template #default="scope">
             <dict-tag :type="DICT_TYPE.PMS_PROJECT_WELL_TYPE" :value="scope.row.wellType" />
           </template>
         </el-table-column> -->
-        <el-table-column label="施工地点" align="center" prop="location" :width="columnWidths.location" />
-        <el-table-column label="施工队伍" align="center" prop="deptNames" :width="columnWidths.deptNames" />
+        <el-table-column
+          label="施工地点"
+          align="center"
+          prop="location"
+          :width="columnWidths.location"
+        />
+        <el-table-column
+          label="施工队伍"
+          align="center"
+          prop="deptNames"
+          :width="columnWidths.deptNames"
+        />
         <!-- <el-table-column :label="t('project.technology')" align="center" prop="technique" :width="columnWidths.technique">
           <template #default="scope">
             <dict-tag :type="DICT_TYPE.PMS_PROJECT_TECHNOLOGY" :value="scope.row.technique" />
@@ -157,7 +205,7 @@
             <el-button
               link
               type="primary"
-              @click="openForm('update', scope.row.id,scope.row.projectId)"
+              @click="openForm('update', scope.row.id, scope.row.projectId)"
               v-hasPermi="['rq:iot-project-task:update']"
             >
               编辑
@@ -246,9 +294,7 @@
       </el-table-column>
       <el-table-column label="操作" width="100" align="center">
         <template #default="scope">
-          <el-button link type="danger" @click="removeRow(scope.$index)">
-            删除
-          </el-button>
+          <el-button link type="danger" @click="removeRow(scope.$index)"> 删除 </el-button>
         </template>
       </el-table-column>
     </el-table>
@@ -256,25 +302,22 @@
     <template #footer>
       <span class="dialog-footer">
         <el-button @click="planDialogVisible = false">取消</el-button>
-        <el-button type="primary" @click="savePlan" :loading="saveLoading">
-          保存
-        </el-button>
+        <el-button type="primary" @click="savePlan" :loading="saveLoading"> 保存 </el-button>
       </span>
     </template>
   </el-dialog>
-
 </template>
 
 <script setup lang="ts">
 import { dateFormatter } from '@/utils/formatTime'
 import download from '@/utils/download'
 import { IotProjectTaskScheduleApi } from '@/api/pms/iotprojecttaskschedule'
-import { IotProjectTaskApi, IotProjectTaskVO} from '@/api/pms/iotprojecttask'
+import { IotProjectTaskApi, IotProjectTaskVO } from '@/api/pms/iotprojecttask'
 import dayjs from 'dayjs'
 import { DICT_TYPE, getIntDictOptions, getStrDictOptions } from '@/utils/dict'
-import { ref, reactive, onMounted, computed, nextTick, watch, onUnmounted } from 'vue'
+import { ref, reactive, onMounted, nextTick, watch, onUnmounted } from 'vue'
 import { useRouter } from 'vue-router'
-import * as DeptApi from "@/api/system/dept"; // 引入部门API
+import * as DeptApi from '@/api/system/dept' // 引入部门API
 
 /** 项目信息任务拆分 列表 */
 defineOptions({ name: 'IotProjectTask' })
@@ -301,6 +344,7 @@ const queryParams = reactive({
   manufactureName: '',
   platformFlag: '',
   remark: undefined,
+  deptName: undefined
 })
 
 const dictQueryParams = reactive({
@@ -315,7 +359,7 @@ const queryFormRef = ref() // 搜索的表单
 const exportLoading = ref(false) // 导出的加载中
 const { push } = useRouter() // 路由跳转
 
-const COMPLETED_STATUS = 'wg';
+const COMPLETED_STATUS = 'wg'
 
 // 表格引用
 const tableRef = ref()
@@ -340,29 +384,31 @@ const columnWidths = ref({
 
 // 计算文本宽度
 const getTextWidth = (text: string, fontSize = 14) => {
-  const span = document.createElement('span');
-  span.style.visibility = 'hidden';
-  span.style.position = 'absolute';
-  span.style.whiteSpace = 'nowrap';
-  span.style.fontSize = `${fontSize}px`;
-  span.style.fontFamily = 'inherit';
-  span.innerText = text;
-
-  document.body.appendChild(span);
-  const width = span.offsetWidth;
-  document.body.removeChild(span);
-
-  return width;
-};
+  const span = document.createElement('span')
+  span.style.visibility = 'hidden'
+  span.style.position = 'absolute'
+  span.style.whiteSpace = 'nowrap'
+  span.style.fontSize = `${fontSize}px`
+  span.style.fontFamily = 'inherit'
+  span.innerText = text
+
+  document.body.appendChild(span)
+  const width = span.offsetWidth
+  document.body.removeChild(span)
+
+  return width
+}
 
 // 计划相关状态
 const planDialogVisible = ref(false)
 // const planList = ref<Array<{name: string, value: string, startTime: string, endTime: string}>>([])
-const planList = ref<Array<{id?: number, status: string, startTime: string, endTime: string, showEndTime: boolean}>>([])
+const planList = ref<
+  Array<{ id?: number; status: string; startTime: string; endTime: string; showEndTime: boolean }>
+>([])
 const saveLoading = ref(false)
 const currentRow = ref<IotProjectTaskVO | null>(null)
 const workProgressDictOptions = ref<any[]>([]) // 施工进度字典选项
-const companyDeptList = ref<any[]>([])  // 在公司级部门列表
+const companyDeptList = ref<any[]>([]) // 在公司级部门列表
 const wellTypeDictOptions = ref<any[]>([]) // 井型字典选项
 const technologyDictOptions = ref<any[]>([]) // 施工工艺字典选项
 
@@ -389,19 +435,19 @@ const getTechnologyDictOptions = async () => {
 /** 时间戳转换为日期时间字符串(使用dayjs处理) */
 const timestampToDateTime = (timestamp: number | string | null | undefined): string => {
   if (timestamp === null || timestamp === undefined || timestamp === '') {
-    return '';
+    return ''
   }
   // 转换为数字
-  let ts = typeof timestamp === 'string' ? parseInt(timestamp) : timestamp;
+  let ts = typeof timestamp === 'string' ? parseInt(timestamp) : timestamp
 
   // 检查是否为有效数字
   if (isNaN(ts)) {
-    console.warn('无效的时间戳:', timestamp);
-    return '';
+    console.warn('无效的时间戳:', timestamp)
+    return ''
   }
   // 如果时间戳是秒级,转换为毫秒级
   if (ts < 1000000000000) {
-    ts *= 1000;
+    ts *= 1000
   }
   return dayjs(ts).format('YYYY-MM-DD HH:mm')
 }
@@ -425,12 +471,14 @@ const openPlanDialog = async (row: IotProjectTaskVO) => {
     await getWorkProgressDictOptions()
 
     // 获取已有计划数据
-    const taskSchedules = await IotProjectTaskScheduleApi.getIotProjectTaskSchedules({ taskId: row.id })
+    const taskSchedules = await IotProjectTaskScheduleApi.getIotProjectTaskSchedules({
+      taskId: row.id
+    })
 
     if (taskSchedules && taskSchedules.length > 0) {
       // 如果有数据,则使用接口返回的数据初始化表格
       planList.value = taskSchedules.map((plan: any) => {
-        const statusNum = plan.status;
+        const statusNum = plan.status
         return {
           id: plan.id,
           status: statusNum,
@@ -451,17 +499,17 @@ const openPlanDialog = async (row: IotProjectTaskVO) => {
 
 /** 判断某一行是否应该显示结束时间列 */
 const rowShowEndTime = (row) => {
-  return row.showEndTime;
+  return row.showEndTime
 }
 
 /** 处理施工状态变化 */
 const onStatusChange = (row) => {
   // 当状态变为“完工”时,隐藏结束时间列并清空结束时间;否则显示
   if (row.status === COMPLETED_STATUS) {
-    row.showEndTime = false;
-    row.endTime = ''; // 清空结束时间
+    row.showEndTime = false
+    row.endTime = '' // 清空结束时间
   } else {
-    row.showEndTime = true;
+    row.showEndTime = true
   }
 }
 
@@ -495,12 +543,13 @@ const savePlan = async () => {
     }
 
     // 准备提交数据
-    const submitData = planList.value.map(item => ({
+    const submitData = planList.value.map((item) => ({
       id: item.id, // 更新时使用
       taskId: currentRow.value?.id,
       status: item.status,
       startTime: item.startTime ? new Date(item.startTime).getTime() : null,
-      endTime: (item.status !== COMPLETED_STATUS && item.endTime) ? new Date(item.endTime).getTime() : null
+      endTime:
+        item.status !== COMPLETED_STATUS && item.endTime ? new Date(item.endTime).getTime() : null
     }))
 
     // 调用保存接口
@@ -526,8 +575,8 @@ const getList = async () => {
 
     // 获取数据后计算列宽
     nextTick(() => {
-      calculateColumnWidths();
-    });
+      calculateColumnWidths()
+    })
   } finally {
     loading.value = false
   }
@@ -535,111 +584,118 @@ const getList = async () => {
 
 // 计算列宽度
 const calculateColumnWidths = () => {
-  const MIN_WIDTH = 80; // 最小列宽
-  const PADDING = 25; // 列内边距
-  const FLEXIBLE_COLUMNS = ['contractCode', 'wellName', 'location']; // 可伸缩列
+  const MIN_WIDTH = 80 // 最小列宽
+  const PADDING = 25 // 列内边距
+  const FLEXIBLE_COLUMNS = ['contractCode', 'wellName', 'location'] // 可伸缩列
 
   // 确保表格容器存在
-  if (!tableContainerRef.value?.$el) return;
+  if (!tableContainerRef.value?.$el) return
 
-  const container = tableContainerRef.value.$el;
-  const containerWidth = container.clientWidth;
+  const container = tableContainerRef.value.$el
+  const containerWidth = container.clientWidth
 
   // 固定列的宽度
   const FIXED_COLUMNS = {
     manufactureName: 200,
     contractName: 200
-  };
+  }
 
   // 1. 计算所有列的最小宽度
-  const minWidths: Record<string, number> = {};
-  let totalMinWidth = 0;
+  const minWidths: Record<string, number> = {}
+  let totalMinWidth = 0
 
   // 设置固定列的宽度
-  Object.keys(FIXED_COLUMNS).forEach(key => {
-    minWidths[key] = FIXED_COLUMNS[key];
-    totalMinWidth += FIXED_COLUMNS[key];
-  });
+  Object.keys(FIXED_COLUMNS).forEach((key) => {
+    minWidths[key] = FIXED_COLUMNS[key]
+    totalMinWidth += FIXED_COLUMNS[key]
+  })
 
   // 计算列最小宽度的函数
   const calculateColumnMinWidth = (key: string, label: string, getValue: Function) => {
     // 如果是固定列,跳过计算
-    if (FIXED_COLUMNS[key]) return;
+    if (FIXED_COLUMNS[key]) return
 
-    const headerWidth = getTextWidth(label) * 1.2;
-    let contentMaxWidth = 0;
+    const headerWidth = getTextWidth(label) * 1.2
+    let contentMaxWidth = 0
 
     // 计算内容最大宽度
     list.value.forEach((row, index) => {
-      const text = String(getValue ? getValue(row, index) : (row[key] || ''));
-      const textWidth = getTextWidth(text);
-      if (textWidth > contentMaxWidth) contentMaxWidth = textWidth;
-    });
-
-    const minWidth = Math.max(headerWidth, contentMaxWidth, MIN_WIDTH) + PADDING;
-    minWidths[key] = minWidth;
-    totalMinWidth += minWidth;
-    return minWidth;
-  };
+      const text = String(getValue ? getValue(row, index) : row[key] || '')
+      const textWidth = getTextWidth(text)
+      if (textWidth > contentMaxWidth) contentMaxWidth = textWidth
+    })
+
+    const minWidth = Math.max(headerWidth, contentMaxWidth, MIN_WIDTH) + PADDING
+    minWidths[key] = minWidth
+    totalMinWidth += minWidth
+    return minWidth
+  }
 
   // 计算各列最小宽度
-  calculateColumnMinWidth('serial', t('iotDevice.serial'), (row: any, index: number) => `${index + 1}`);
+  calculateColumnMinWidth(
+    'serial',
+    t('iotDevice.serial'),
+    (row: any, index: number) => `${index + 1}`
+  )
   // calculateColumnMinWidth('manufactureName', '客户名称', (row: any) => row.manufactureName);
   // calculateColumnMinWidth('contractName', '合同名称', (row: any) => row.contractName);
-  calculateColumnMinWidth('contractCode', '合同编号', (row: any) => row.contractCode);
-  calculateColumnMinWidth('wellName', '井号', (row: any) => row.wellName);
+  calculateColumnMinWidth('contractCode', '合同编号', (row: any) => row.contractCode)
+  calculateColumnMinWidth('wellName', '井号', (row: any) => row.wellName)
   calculateColumnMinWidth('wellType', t('project.wellType'), (row: any) => {
-    const dict = wellTypeDictOptions.value.find(d => d.value === row.wellType);
-    return dict ? dict.label : '';
-  });
-  calculateColumnMinWidth('location', '施工地点', (row: any) => row.location);
-  calculateColumnMinWidth('deptNames', '施工队伍', (row: any) => row.deptNames);
+    const dict = wellTypeDictOptions.value.find((d) => d.value === row.wellType)
+    return dict ? dict.label : ''
+  })
+  calculateColumnMinWidth('location', '施工地点', (row: any) => row.location)
+  calculateColumnMinWidth('deptNames', '施工队伍', (row: any) => row.deptNames)
   calculateColumnMinWidth('technique', t('project.technology'), (row: any) => {
-    const dict = technologyDictOptions.value.find(d => d.value === row.technique);
-    return dict ? dict.label : '';
-  });
-  calculateColumnMinWidth('workloadDesign', '设计工作量', (row: any) => row.workloadDesign);
-  calculateColumnMinWidth('createTime', '创建时间', (row: any) => dateFormatter(null, null, row.createTime));
+    const dict = technologyDictOptions.value.find((d) => d.value === row.technique)
+    return dict ? dict.label : ''
+  })
+  calculateColumnMinWidth('workloadDesign', '设计工作量', (row: any) => row.workloadDesign)
+  calculateColumnMinWidth('createTime', '创建时间', (row: any) =>
+    dateFormatter(null, null, row.createTime)
+  )
 
   // 操作列固定宽度
-  minWidths.operation = 200;
-  totalMinWidth += 200;
+  minWidths.operation = 200
+  totalMinWidth += 200
 
   // 2. 计算可伸缩列最终宽度
-  const newWidths: Record<string, string> = {};
-  const availableWidth = containerWidth - 17; // 减去滚动条宽度
+  const newWidths: Record<string, string> = {}
+  const availableWidth = containerWidth - 17 // 减去滚动条宽度
 
   // 应用最小宽度到所有列
-  Object.keys(minWidths).forEach(key => {
+  Object.keys(minWidths).forEach((key) => {
     if (FIXED_COLUMNS[key]) {
-      newWidths[key] = `${FIXED_COLUMNS[key]}px`;
+      newWidths[key] = `${FIXED_COLUMNS[key]}px`
     } else {
-      newWidths[key] = `${minWidths[key]}px`;
+      newWidths[key] = `${minWidths[key]}px`
     }
-  });
+  })
 
   // 计算可伸缩列需要的宽度
   if (totalMinWidth < availableWidth) {
     // 有剩余空间:按比例分配给可伸缩列
-    const extraSpace = availableWidth - totalMinWidth;
-    const flexibleColumnCount = FLEXIBLE_COLUMNS.length;
-    const spacePerColumn = Math.floor(extraSpace / flexibleColumnCount);
-
-    FLEXIBLE_COLUMNS.forEach(key => {
-      if (!FIXED_COLUMNS[key]) { // 确保不是固定列
-        newWidths[key] = `${minWidths[key] + spacePerColumn}px`;
+    const extraSpace = availableWidth - totalMinWidth
+    const flexibleColumnCount = FLEXIBLE_COLUMNS.length
+    const spacePerColumn = Math.floor(extraSpace / flexibleColumnCount)
+
+    FLEXIBLE_COLUMNS.forEach((key) => {
+      if (!FIXED_COLUMNS[key]) {
+        // 确保不是固定列
+        newWidths[key] = `${minWidths[key] + spacePerColumn}px`
       }
-    });
+    })
   }
 
   // 3. 更新列宽配置
-  columnWidths.value = newWidths;
+  columnWidths.value = newWidths
 
   // 4. 触发表格重新布局
   nextTick(() => {
-    tableRef.value?.doLayout();
-  });
-};
+    tableRef.value?.doLayout()
+  })
+}
 
 /** 搜索按钮操作 */
 const handleQuery = () => {
@@ -655,11 +711,11 @@ const resetQuery = () => {
 
 /** 添加/修改操作 */
 const formRef = ref()
-const openForm = (type: string, id?: number,projectId?:number) => {
-  if(id===undefined){
-    push({ name: 'IotProjectTaskInfo', params: { type} })
-  }else{
-    push({ name: 'IotProjectTaskInfo', params: { type,id,projectId} })
+const openForm = (type: string, id?: number, projectId?: number) => {
+  if (id === undefined) {
+    push({ name: 'IotProjectTaskInfo', params: { type } })
+  } else {
+    push({ name: 'IotProjectTaskInfo', params: { type, id, projectId } })
   }
 }
 
@@ -679,8 +735,6 @@ const handleDelete = async (id: number) => {
 /** 导出按钮操作 */
 const handleExport = async () => {
   try {
-    // 导出的二次确认
-    await message.exportConfirm()
     // 发起导出
     exportLoading.value = true
     const data = await IotProjectTaskApi.exportIotProjectTask(queryParams)
@@ -692,7 +746,7 @@ const handleExport = async () => {
 }
 
 // 声明 ResizeObserver 实例
-let resizeObserver: ResizeObserver | null = null;
+let resizeObserver: ResizeObserver | null = null
 
 /** 初始化 **/
 onMounted(async () => {
@@ -709,33 +763,36 @@ onMounted(async () => {
   if (tableContainerRef.value?.$el) {
     resizeObserver = new ResizeObserver(() => {
       // 使用防抖避免频繁触发
-      clearTimeout((window as any).resizeTimer);
-      (window as any).resizeTimer = setTimeout(() => {
-        calculateColumnWidths();
-      }, 100);
-    });
-    resizeObserver.observe(tableContainerRef.value.$el);
+      clearTimeout((window as any).resizeTimer)
+      ;(window as any).resizeTimer = setTimeout(() => {
+        calculateColumnWidths()
+      }, 100)
+    })
+    resizeObserver.observe(tableContainerRef.value.$el)
   }
 })
 
 onUnmounted(() => {
   // 清除 ResizeObserver
   if (resizeObserver && tableContainerRef.value?.$el) {
-    resizeObserver.unobserve(tableContainerRef.value.$el);
-    resizeObserver = null;
+    resizeObserver.unobserve(tableContainerRef.value.$el)
+    resizeObserver = null
   }
 
   // 清除定时器
   if ((window as any).resizeTimer) {
-    clearTimeout((window as any).resizeTimer);
+    clearTimeout((window as any).resizeTimer)
   }
 })
 
 // 监听列表数据变化重新计算列宽
-watch(list, () => {
-  nextTick(calculateColumnWidths)
-}, { deep: true })
-
+watch(
+  list,
+  () => {
+    nextTick(calculateColumnWidths)
+  },
+  { deep: true }
+)
 </script>
 
 <style scoped>

Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 423 - 176
src/views/pms/iotrddailyreport/FillDailyReportForm.vue


+ 36 - 24
src/views/pms/iotrddailyreport/fillDailyReport.vue

@@ -60,15 +60,9 @@
             >
               <Icon icon="ep:plus" class="mr-5px" /> 新增
             </el-button>
-            <el-button
-              type="success"
-              plain
-              @click="handleExport"
-              :loading="exportLoading"
-              v-hasPermi="['pms:iot-rd-daily-report:export']"
-            >
+            <!-- <el-button type="success" plain @click="handleExport" :loading="exportLoading">
               <Icon icon="ep:download" class="mr-5px" /> 导出
-            </el-button>
+            </el-button> -->
           </el-form-item>
         </el-form>
       </ContentWrap>
@@ -103,6 +97,15 @@
               >
                 查看
               </el-button>
+              <el-button
+                link
+                type="warning"
+                @click="openForm('fill', scope.row.id)"
+                v-hasPermi="['pms:iot-rd-daily-report:non-productive']"
+                v-if="scope.row.auditStatus === 20"
+              >
+                时效
+              </el-button>
             </template>
           </el-table-column>
           <el-table-column
@@ -117,9 +120,32 @@
               <dict-tag :type="DICT_TYPE.OPERATION_FILL_ORDER_STATUS" :value="scope.row.status" />
             </template>
           </el-table-column>
+          <el-table-column label="审批状态" align="center" prop="auditStatus" :min-width="84">
+            <template #default="scope">
+              <el-tag v-if="scope.row.auditStatus === 0" type="info">
+                {{ '待提交' }}
+              </el-tag>
+              <el-tag v-else-if="scope.row.auditStatus === 10">
+                {{ '待审批' }}
+              </el-tag>
+              <el-tag v-else-if="scope.row.auditStatus === 20" type="success">
+                {{ '审批通过' }}
+              </el-tag>
+              <el-tag v-else-if="scope.row.auditStatus === 30" type="danger">
+                {{ '审批拒绝' }}
+              </el-tag>
+            </template>
+          </el-table-column>
           <el-table-column label="施工队伍" align="center" prop="deptName" />
           <el-table-column label="项目" align="center" prop="contractName" />
           <el-table-column label="任务" align="center" prop="taskName" />
+          <el-table-column label="非生产时间" align="center" prop="nonProductFlag">
+            <template #default="scope">
+              <el-tag :type="scope.row.nonProductFlag ? 'success' : 'danger'">
+                {{ scope.row.nonProductFlag ? '已填写' : '未填写' }}
+              </el-tag>
+            </template>
+          </el-table-column>
           <el-table-column label="带班干部" align="center" prop="responsiblePersonNames" />
           <el-table-column label="填报人" align="center" prop="submitterNames" />
           <!--
@@ -158,7 +184,6 @@
 
 <script setup lang="ts">
 import { dateFormatter2 } from '@/utils/formatTime'
-import download from '@/utils/download'
 import { IotRdDailyReportApi, IotRdDailyReportVO } from '@/api/pms/iotrddailyreport'
 import { DICT_TYPE } from '@/utils/dict'
 import DeptTree2 from '@/views/pms/iotrhdailyreport/DeptTree2.vue'
@@ -319,7 +344,6 @@ const queryParams = reactive({
   createTime: []
 })
 const queryFormRef = ref() // 搜索的表单
-const exportLoading = ref(false) // 导出的加载中
 
 /** 查询列表 */
 const getList = async () => {
@@ -390,20 +414,8 @@ const handleDetail = async (id: number) => {
   }
 }
 
-/** 导出按钮操作 */
-const handleExport = async () => {
-  try {
-    // 导出的二次确认
-    await message.exportConfirm()
-    // 发起导出
-    exportLoading.value = true
-    const data = await IotRdDailyReportApi.exportIotRdDailyReport(queryParams)
-    download.excel(data, '瑞都日报.xls')
-  } catch {
-  } finally {
-    exportLoading.value = false
-  }
-}
+const exportLoading = ref(false)
+const handleExport = async () => {}
 
 /** 初始化 **/
 onMounted(() => {

+ 60 - 60
src/views/pms/iotrddailyreport/index.vue

@@ -60,19 +60,12 @@
             >
               <Icon icon="ep:plus" class="mr-5px" /> 新增
             </el-button>
-            <el-button
-              type="success"
-              plain
-              @click="handleExport"
-              :loading="exportLoading"
-              v-hasPermi="['pms:iot-rd-daily-report:export']"
-            >
+            <el-button type="success" plain @click="handleExport" :loading="exportLoading">
               <Icon icon="ep:download" class="mr-5px" /> 导出
             </el-button>
           </el-form-item>
         </el-form>
       </ContentWrap>
-
       <!-- 列表 -->
       <ContentWrap ref="tableContainerRef">
         <el-table
@@ -100,13 +93,7 @@
             :min-width="columnWidths.deptName.width"
             resizable
           />
-          <el-table-column
-            label="项目"
-            align="center"
-            prop="contractName"
-            :min-width="columnWidths.contractName.width"
-            resizable
-          />
+
           <el-table-column
             label="任务"
             align="center"
@@ -114,13 +101,7 @@
             :min-width="columnWidths.taskName.width"
             resizable
           />
-          <el-table-column
-            label="时间节点"
-            align="center"
-            prop="timeRange"
-            :min-width="columnWidths.timeRange.width"
-            resizable
-          />
+
           <el-table-column
             :label="t('project.status')"
             align="center"
@@ -132,20 +113,20 @@
               <dict-tag :type="DICT_TYPE.PMS_PROJECT_RD_STATUS" :value="scope.row.rdStatus" />
             </template>
           </el-table-column>
+
           <el-table-column
-            label="项目"
+            label="当日生产动态"
             align="center"
-            prop="contractName"
-            :width="columnWidths.contractName"
-            v-if="false"
+            prop="productionStatus"
+            :min-width="columnWidths.productionStatus.width"
+            resizable
           />
-
           <el-table-column
-            label="时间节点"
+            label="下步工作计划"
             align="center"
-            prop="timeRange"
-            :width="columnWidths.timeRange"
-            v-if="false"
+            prop="nextPlan"
+            :min-width="columnWidths.nextPlan.width"
+            fixed-width
           />
 
           <!--
@@ -220,20 +201,7 @@
             :min-width="columnWidths.dailyFuel.width"
             resizable
           />
-          <el-table-column
-            label="当日生产动态"
-            align="center"
-            prop="productionStatus"
-            :min-width="columnWidths.productionStatus.width"
-            resizable
-          />
-          <el-table-column
-            label="下步工作计划"
-            align="center"
-            prop="nextPlan"
-            :min-width="columnWidths.nextPlan.width"
-            fixed-width
-          />
+
           <el-table-column
             label="外租设备"
             align="center"
@@ -255,6 +223,44 @@
             :min-width="columnWidths.faultDowntime.width"
             resizable
           />
+          <el-table-column label="非生产时间" align="center" prop="nonProductFlag" :min-width="80">
+            <template #default="scope">
+              <el-tag :type="scope.row.nonProductFlag ? 'success' : 'danger'">
+                {{ scope.row.nonProductFlag ? '已填写' : '未填写' }}
+              </el-tag>
+            </template>
+          </el-table-column>
+
+          <el-table-column
+            label="项目"
+            align="center"
+            prop="contractName"
+            :min-width="columnWidths.contractName.width"
+            resizable
+          />
+          <el-table-column
+            label="时间节点"
+            align="center"
+            prop="timeRange"
+            :min-width="columnWidths.timeRange.width"
+            resizable
+          />
+          <el-table-column label="审批状态" align="center" prop="auditStatus" :min-width="84">
+            <template #default="scope">
+              <el-tag v-if="scope.row.auditStatus === 0" type="info">
+                {{ '待提交' }}
+              </el-tag>
+              <el-tag v-else-if="scope.row.auditStatus === 10">
+                {{ '待审批' }}
+              </el-tag>
+              <el-tag v-else-if="scope.row.auditStatus === 20" type="success">
+                {{ '审批通过' }}
+              </el-tag>
+              <el-tag v-else-if="scope.row.auditStatus === 30" type="danger">
+                {{ '审批拒绝' }}
+              </el-tag>
+            </template>
+          </el-table-column>
 
           <el-table-column label="操作" align="center" min-width="120px" fixed="right">
             <template #default="scope">
@@ -294,8 +300,7 @@
 </template>
 
 <script setup lang="ts">
-import { dateFormatter2 } from '@/utils/formatTime'
-import download from '@/utils/download'
+import { dateFormatter, dateFormatter2 } from '@/utils/formatTime'
 import { IotRdDailyReportApi, IotRdDailyReportVO } from '@/api/pms/iotrddailyreport'
 import IotRdDailyReportForm from './IotRdDailyReportForm.vue'
 import { DICT_TYPE } from '@/utils/dict'
@@ -305,6 +310,7 @@ import { useDebounceFn } from '@vueuse/core'
 
 import dayjs from 'dayjs'
 import quarterOfYear from 'dayjs/plugin/quarterOfYear'
+import download from '@/utils/download'
 
 dayjs.extend(quarterOfYear)
 
@@ -459,7 +465,6 @@ const queryParams = reactive({
   createTime: []
 })
 const queryFormRef = ref() // 搜索的表单
-const exportLoading = ref(false) // 导出的加载中
 
 const rootDeptId = ref(163)
 
@@ -612,6 +617,8 @@ const calculateColumnWidths = useDebounceFn(() => {
         ]
       ) + 'px'
 
+    console.log('width :>> ', width)
+
     columnWidths.value[prop].width = width
   })
 }, 1000)
@@ -705,19 +712,12 @@ const handleDelete = async (id: number) => {
   } catch {}
 }
 
-/** 导出按钮操作 */
+const exportLoading = ref(false)
+
 const handleExport = async () => {
-  try {
-    // 导出的二次确认
-    await message.exportConfirm()
-    // 发起导出
-    exportLoading.value = true
-    const data = await IotRdDailyReportApi.exportIotRdDailyReport(queryParams)
-    download.excel(data, '瑞都日报.xls')
-  } catch {
-  } finally {
-    exportLoading.value = false
-  }
+  const res = await IotRdDailyReportApi.exportIotRdDailyReportDetails(queryParams)
+
+  download.excel(res, '瑞都日报明细.xlsx')
 }
 
 // 声明 ResizeObserver 实例

+ 20 - 27
src/views/pms/iotrddailyreport/statistics.vue

@@ -46,12 +46,12 @@
             />
           </el-form-item>
           <el-form-item>
-            <el-button @click="handleQuery"
-              ><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button
-            >
-            <el-button @click="resetQuery"
-              ><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button
-            >
+            <el-button @click="handleQuery">
+              <Icon icon="ep:search" class="mr-5px" /> 搜索
+            </el-button>
+            <el-button @click="resetQuery">
+              <Icon icon="ep:refresh" class="mr-5px" /> 重置
+            </el-button>
             <el-button
               type="primary"
               plain
@@ -60,13 +60,7 @@
             >
               <Icon icon="ep:plus" class="mr-5px" /> 新增
             </el-button>
-            <el-button
-              type="success"
-              plain
-              @click="handleExport"
-              :loading="exportLoading"
-              v-hasPermi="['pms:iot-rd-daily-report:export']"
-            >
+            <el-button type="success" plain @click="handleExport" :loading="exportLoading">
               <Icon icon="ep:download" class="mr-5px" /> 导出
             </el-button>
           </el-form-item>
@@ -210,7 +204,6 @@
 
 <script setup lang="ts">
 import { dateFormatter } from '@/utils/formatTime'
-import download from '@/utils/download'
 import { IotRdDailyReportApi, IotRdDailyReportVO } from '@/api/pms/iotrddailyreport'
 import IotRdDailyReportForm from './IotRdDailyReportForm.vue'
 import { DICT_TYPE, getDictLabel } from '@/utils/dict'
@@ -219,6 +212,7 @@ import DeptTree2 from '@/views/pms/iotrhdailyreport/DeptTree2.vue'
 
 import dayjs from 'dayjs'
 import quarterOfYear from 'dayjs/plugin/quarterOfYear'
+import download from '@/utils/download'
 
 dayjs.extend(quarterOfYear)
 
@@ -369,7 +363,7 @@ const queryParams = reactive({
   createTime: []
 })
 const queryFormRef = ref() // 搜索的表单
-const exportLoading = ref(false) // 导出的加载中
+// 导出的加载中
 
 const rootDeptId = ref(163)
 
@@ -609,19 +603,18 @@ const handleDelete = async (id: number) => {
   } catch {}
 }
 
-/** 导出按钮操作 */
+const exportLoading = ref(false)
 const handleExport = async () => {
-  try {
-    // 导出的二次确认
-    await message.exportConfirm()
-    // 发起导出
-    exportLoading.value = true
-    const data = await IotRdDailyReportApi.exportIotRdDailyReport(queryParams)
-    download.excel(data, '瑞都日报.xls')
-  } catch {
-  } finally {
-    exportLoading.value = false
-  }
+  const res = await IotRdDailyReportApi.exportIotRdDailyReport({
+    createTime: queryParams.createTime,
+    contractName: queryParams.contractName,
+    taskName: queryParams.taskName,
+    // pageNo: queryParams.pageNo,
+    // pageSize: queryParams.pageSize,
+    deptId: queryParams.deptId
+  })
+
+  download.excel(res, '瑞都日报汇总.xlsx')
 }
 
 // 声明 ResizeObserver 实例

+ 4 - 3
src/views/pms/iotrddailyreport/summary.vue

@@ -11,6 +11,7 @@ import { Motion, AnimatePresence } from 'motion-v'
 import { rangeShortcuts } from '@/utils/formatTime'
 
 import { useUserStore } from '@/store/modules/user'
+import download from '@/utils/download'
 
 const deptId = useUserStore().getUser.deptId
 
@@ -220,7 +221,7 @@ const getList = useDebounceFn(async () => {
           cumulativeRunCount: other.cumulativeRunCount || 0,
           cumulativeWorkingWell: other.cumulativeWorkingWell || 0,
           cumulativeHourCount: other.cumulativeHourCount || 0,
-          totalDailyFuel: ((other.totalDailyFuel || 0) / 10000).toFixed(4),
+          totalDailyFuel: other.totalDailyFuel || 0,
           cumulativeWaterVolume: other.cumulativeWaterVolume || 0,
           cumulativeWorkingLayers: other.cumulativeWorkingLayers || 0,
           cumulativePumpTrips: other.cumulativePumpTrips || 0,
@@ -476,8 +477,8 @@ const exportChart = () => {
 }
 
 const exportData = async () => {
-  // const res = await IotRdDailyReportApi.exportRdDailyReportStatistics(query.value)
-  // download.excel(res, '瑞恒日报统计数据.xlsx')
+  const res = await IotRdDailyReportApi.exportRdDailyReportStatistics(query.value)
+  download.excel(res, '瑞都日报统计数据.xlsx')
 }
 
 const exportAll = async () => {

+ 20 - 379
src/views/pms/iotrhdailyreport/approval.vue

@@ -3,11 +3,10 @@ import { IotRhDailyReportApi } from '@/api/pms/iotrhdailyreport'
 import { rangeShortcuts } from '@/utils/formatTime'
 import { useDebounceFn } from '@vueuse/core'
 import dayjs from 'dayjs'
-import { DICT_TYPE, getStrDictOptions } from '@/utils/dict'
+import { DICT_TYPE } from '@/utils/dict'
 import { TableColumnCtx } from 'element-plus/es/components/table/src/table-column/defaults'
-import { FormInstance, FormRules } from 'element-plus'
-import Form from '@/components/Form/src/Form.vue'
 import { useUserStore } from '@/store/modules/user'
+import rhForm from './rh-form.vue'
 
 interface List {
   createTime: number // 日期
@@ -272,18 +271,18 @@ const calculateColumnWidths = (colums: Column[]) => {
   }
 }
 
-function checkTimeSumEquals24(row: List) {
-  // 获取三个字段的值,转换为数字,如果为空则视为0
-  const gasTime = row.dailyInjectGasTime || 0
-  const waterTime = row.dailyInjectWaterTime || 0
-  const nonProdTime = row.nonProductionTime || 0
+// function checkTimeSumEquals24(row: List) {
+//   // 获取三个字段的值,转换为数字,如果为空则视为0
+//   const gasTime = row.dailyInjectGasTime || 0
+//   const waterTime = row.dailyInjectWaterTime || 0
+//   const nonProdTime = row.nonProductionTime || 0
 
-  // 计算总和
-  const sum = gasTime + waterTime + nonProdTime
+//   // 计算总和
+//   const sum = gasTime + waterTime + nonProdTime
 
-  // 返回是否等于24(允许一定的浮点数误差)
-  return Math.abs(sum - 24) < 0.01 // 使用0.01作为误差范围
-}
+//   // 返回是否等于24(允许一定的浮点数误差)
+//   return Math.abs(sum - 24) < 0.01 // 使用0.01作为误差范围
+// }
 
 function cellStyle(data: {
   row: List
@@ -403,181 +402,23 @@ watch(
   { immediate: true }
 )
 
-const FORM_KEYS = [
-  'id',
-  'deptName',
-  'contractName',
-  'taskName',
-  'dailyGasInjection',
-  'dailyWaterInjection',
-  'dailyInjectGasTime',
-  'dailyInjectWaterTime',
-  'nonProductionTime',
-  'nptReason',
-  'productionStatus',
-  'remark',
-  'relocationDays',
-  'capacity',
-  'createTime',
-  'deptId',
-  'projectId',
-  'taskId',
-  'auditStatus',
-  'opinion'
-] as const
-
-type FormKey = (typeof FORM_KEYS)[number]
-type Form = Partial<Pick<List, FormKey>>
-
-const dialogVisible = ref(false)
-const formRef = ref<FormInstance>()
-const formLoading = ref(false)
-const message = useMessage()
-
-const initFormData = (): Form => ({
-  dailyGasInjection: 0,
-  dailyWaterInjection: 0,
-  dailyInjectGasTime: 0,
-  dailyInjectWaterTime: 0,
-  nonProductionTime: 0,
-  relocationDays: 0,
-  capacity: 0
-})
-
-const form = ref<Form>(initFormData())
-
-async function loadDetail(id: number) {
-  try {
-    const res = await IotRhDailyReportApi.getIotRhDailyReport(id)
-    FORM_KEYS.forEach((key) => {
-      form.value[key] = res[key] ?? form.value[key]
-    })
-    form.value.id = id
+const visible = ref(false)
 
-    if (res.auditStatus !== 10) {
-      formType.value = 'readonly'
-    }
+const formRef = ref()
 
-    // if (!form.value.capacity) {
-    //   message.error('请维护增压机产能')
-    // }
-  } finally {
+function handleOpenForm(id: number, type: 'edit' | 'readonly') {
+  if (formRef.value) {
+    formRef.value.handleOpenForm(id, type)
   }
 }
 
-const formType = ref<'approval' | 'readonly'>('approval')
-
-function handleOpenForm(id: number, type: 'approval' | 'readonly') {
-  form.value = initFormData()
-  formRef.value?.resetFields()
-
-  formType.value = type
-
-  dialogVisible.value = true
-  loadDetail(id).then(() => {
-    formRef.value?.validate()
-  })
-}
-
 const route = useRoute()
 
 onMounted(() => {
   if (Object.keys(route.query).length > 0) {
-    handleOpenForm(Number(route.query.id), 'approval')
-  }
-})
-
-const transitTime = computed(() => {
-  const cap = form.value.capacity
-  const gas = form.value.dailyGasInjection ?? 0
-
-  if (!cap) return { original: 0, value: '0%' }
-
-  const original = gas / cap
-  return { original, value: (original * 100).toFixed(2) + '%' }
-})
-
-const sumTimes = () => {
-  const { dailyInjectGasTime = 0, dailyInjectWaterTime = 0, nonProductionTime = 0 } = form.value
-  return parseFloat((dailyInjectGasTime + dailyInjectWaterTime + nonProductionTime).toFixed(2))
-}
-
-const validateTotalTime = (_rule: any, _value: any, callback: any) => {
-  const total = sumTimes()
-  if (total !== 24) {
-    callback(new Error(`当前合计 ${total} 小时,三项时间之和必须等于 24`))
-  } else {
-    callback()
-  }
-}
-
-const validateNptReason = (_rule: any, value: any, callback: any) => {
-  if ((form.value.nonProductionTime || 0) > 0 && !value) {
-    callback(new Error('非生产时间大于 0 时,必须选择原因'))
-  } else {
-    callback()
+    handleOpenForm(Number(route.query.id), 'edit')
   }
-}
-
-const timeRuleItem = [
-  { required: true, message: '请输入时间', trigger: 'blur' },
-  { validator: validateTotalTime, trigger: 'blur' }
-]
-
-const rules = reactive<FormRules>({
-  dailyGasInjection: [{ required: true, message: '请输入当日注气量', trigger: 'blur' }],
-  dailyWaterInjection: [{ required: true, message: '请输入当日注水量', trigger: 'blur' }],
-  productionStatus: [{ required: true, message: '请输入生产动态', trigger: 'blur' }],
-
-  // 复用规则
-  // dailyInjectGasTime: timeRuleItem,
-  // dailyInjectWaterTime: timeRuleItem,
-  // nonProductionTime: timeRuleItem,
-
-  nptReason: [{ validator: validateNptReason, trigger: ['change', 'blur'] }]
 })
-
-watch(
-  [
-    () => form.value.dailyInjectGasTime,
-    () => form.value.dailyInjectWaterTime,
-    () => form.value.nonProductionTime
-  ],
-  () => {
-    nextTick(() => {
-      formRef.value?.validateField('nptReason')
-      if (sumTimes() === 24) {
-        formRef.value?.clearValidate([
-          'dailyInjectGasTime',
-          'dailyInjectWaterTime',
-          'nonProductionTime'
-        ])
-      }
-    })
-  }
-)
-
-const submitForm = async (auditStatus: 20 | 30) => {
-  if (!formRef.value) return
-
-  try {
-    // await formRef.value.validate()
-    formLoading.value = true
-    const { opinion, id } = form.value
-
-    const data = { id: id, auditStatus, opinion } as any
-
-    await IotRhDailyReportApi.approvalIotRhDailyReport(data)
-    message.success(auditStatus === 20 ? '通过成功' : '拒绝成功')
-    dialogVisible.value = false
-
-    loadList()
-  } catch (error) {
-    console.warn('表单校验未通过或提交出错')
-  } finally {
-    formLoading.value = false
-  }
-}
 </script>
 
 <template>
@@ -662,7 +503,7 @@ const submitForm = async (auditStatus: 20 | 30) => {
                         v-show="row.auditStatus === 10"
                         link
                         type="primary"
-                        @click="handleOpenForm(row.id, 'approval')"
+                        @click="handleOpenForm(row.id, 'edit')"
                         v-hasPermi="['pms:iot-rh-daily-report:update']"
                       >
                         审批
@@ -690,193 +531,7 @@ const submitForm = async (auditStatus: 20 | 30) => {
         </div>
       </div>
     </div>
-    <Dialog title="编辑" v-model="dialogVisible">
-      <el-form
-        ref="formRef"
-        label-position="top"
-        size="default"
-        :rules="rules"
-        :model="form"
-        v-loading="formLoading"
-        require-asterisk-position="right"
-      >
-        <div class="flex flex-col gap-3 text-sm">
-          <div
-            class="rounded-md border border-blue-100 bg-blue-50/80 p-3 dark:border-blue-900/30 dark:bg-blue-900/10"
-          >
-            <div class="flex flex-col gap-2.5">
-              <div class="flex items-center justify-between">
-                <div class="text-gray-600 dark:text-gray-400">
-                  <span class="font-bold text-gray-800 dark:text-gray-200">运行时效:</span>
-                  当日注气量 / 产能
-                </div>
-                <span
-                  class="inline-flex items-center rounded border border-red-200 bg-red-100 px-2 py-0.5 text-xs font-medium text-red-600 dark:bg-red-900/20 dark:border-red-800 dark:text-red-400"
-                >
-                  >120% 红色预警
-                </span>
-              </div>
-              <!-- <div class="flex items-center justify-between">
-                <div class="text-gray-600 dark:text-gray-400">
-                  <span class="font-bold text-gray-800 dark:text-gray-200">时间平衡:</span>
-                  注气 + 注水 + 非生产 = 24H
-                </div>
-                <span
-                  class="inline-flex items-center rounded border border-orange-200 bg-orange-100 px-2 py-0.5 text-xs font-medium text-orange-600 dark:bg-orange-900/20 dark:border-orange-800 dark:text-orange-400"
-                >
-                  ≠24H 橙色预警
-                </span>
-              </div> -->
-            </div>
-          </div>
-          <!-- <div
-          v-if="form.opinion"
-          class="flex gap-3 rounded-md border border-yellow-200 bg-yellow-50 p-3 dark:border-yellow-800 dark:bg-yellow-900/10"
-        >
-          <Icon
-            icon="ep:warning-filled"
-            class="mt-0.5 shrink-0 text-base text-yellow-600 dark:text-yellow-500"
-          />
-          <div class="flex flex-col">
-            <h4 class="mb-1 font-bold text-yellow-800 dark:text-yellow-500"> 审核意见 </h4>
-            <p class="leading-relaxed text-gray-600 dark:text-gray-400">
-              {{ form.opinion }}
-            </p>
-          </div>
-        </div> -->
-        </div>
-        <div class="grid grid-cols-2 gap-4 mt-5">
-          <el-form-item label="施工队伍" prop="deptName">
-            <el-input v-model="form.deptName" placeholder="" disabled />
-          </el-form-item>
-          <el-form-item label="项目" prop="contractName">
-            <el-input v-model="form.contractName" placeholder="" disabled />
-          </el-form-item>
-          <el-form-item label="任务" prop="taskName">
-            <el-input v-model="form.taskName" placeholder="" disabled />
-          </el-form-item>
-          <el-form-item label="搬迁安装天数(D)" prop="relocationDays">
-            <el-input v-model="form.relocationDays" placeholder="" disabled />
-          </el-form-item>
-          <el-form-item label="运行时效" prop="transitTime">
-            <el-input
-              :model-value="transitTime.value"
-              placeholder="运行时效"
-              disabled
-              :class="{ 'warning-input': transitTime.original > 1.2 }"
-              id="transitTimeInput"
-            />
-          </el-form-item>
-          <el-form-item label="当日注气量(方)" prop="dailyGasInjection">
-            <el-input-number
-              class="w-full!"
-              :min="0"
-              v-model="form.dailyGasInjection"
-              placeholder="请输入当日注气量(方)"
-              disabled
-            />
-          </el-form-item>
-          <el-form-item label="当日注水量(方)" prop="dailyWaterInjection">
-            <el-input-number
-              class="w-full!"
-              :min="0"
-              v-model="form.dailyWaterInjection"
-              placeholder="请输入当日注水量(方)"
-              disabled
-            />
-          </el-form-item>
-          <!-- :class="{ 'orange-input': sumTimes() !== 24 }" -->
-          <el-form-item label="当日注气时间(H)" prop="dailyInjectGasTime">
-            <el-input-number
-              class="w-full!"
-              :min="0"
-              v-model="form.dailyInjectGasTime"
-              placeholder="请输入当日注气时间(H)"
-              disabled
-            />
-          </el-form-item>
-          <el-form-item label="当日注水时间(H)" prop="dailyInjectWaterTime">
-            <el-input-number
-              class="w-full!"
-              :min="0"
-              v-model="form.dailyInjectWaterTime"
-              placeholder="当日注水时间(H)"
-              disabled
-            />
-          </el-form-item>
-          <el-form-item label="非生产时间(H)" prop="nonProductionTime">
-            <el-input-number
-              class="w-full!"
-              :min="0"
-              v-model="form.nonProductionTime"
-              placeholder="非生产时间(H)"
-              disabled
-            />
-          </el-form-item>
-          <el-form-item label="非生产时间原因" prop="nptReason">
-            <el-select v-model="form.nptReason" placeholder="请选择" disabled clearable>
-              <el-option
-                v-for="(dict, index) of getStrDictOptions(DICT_TYPE.PMS_PROJECT_NPT_REASON)"
-                :key="index"
-                :label="dict.label"
-                :value="dict.value"
-              />
-            </el-select>
-          </el-form-item>
-          <div class="grid grid-cols-1 gap-4 mt-5">
-            <el-form-item label="生产动态" prop="productionStatus">
-              <el-input
-                v-model="form.productionStatus"
-                placeholder="请输入生产动态"
-                type="textarea"
-                autosize
-                :max-length="1000"
-                disabled
-              />
-            </el-form-item>
-            <el-form-item label="备注" prop="remark">
-              <el-input
-                v-model="form.remark"
-                placeholder="请输入备注"
-                :max-length="1000"
-                type="textarea"
-                autosize
-                disabled
-              />
-            </el-form-item>
-          </div>
-        </div>
-        <el-form-item class="mt-4" label="审批意见" prop="opinion">
-          <el-input
-            v-model="form.opinion"
-            placeholder="请输入审批意见"
-            :max-length="1000"
-            type="textarea"
-            autosize
-            :disabled="formType === 'readonly'"
-          />
-        </el-form-item>
-      </el-form>
-      <template #footer>
-        <el-button
-          size="default"
-          @click="submitForm(20)"
-          type="primary"
-          :disabled="formLoading || formType === 'readonly'"
-        >
-          审批通过
-        </el-button>
-        <el-button
-          size="default"
-          @click="submitForm(30)"
-          type="danger"
-          :disabled="formLoading || formType === 'readonly'"
-        >
-          审批拒绝
-        </el-button>
-        <el-button size="default" @click="dialogVisible = false">取 消</el-button>
-      </template>
-    </Dialog>
+    <rh-form v-model:visible="visible" type="approval" ref="formRef" :load-list="loadList" />
   </div>
 </template>
 
@@ -900,20 +555,6 @@ const submitForm = async (auditStatus: 20 | 30) => {
   }
 }
 
-:deep(.warning-input) {
-  .el-input__inner {
-    color: red !important;
-    -webkit-text-fill-color: red !important;
-  }
-}
-
-:deep(.orange-input) {
-  .el-input__inner {
-    color: orange !important;
-    -webkit-text-fill-color: orange !important;
-  }
-}
-
 :deep(.el-input-number__decrease) {
   display: none !important;
 }

+ 18 - 348
src/views/pms/iotrhdailyreport/fill.vue

@@ -3,11 +3,10 @@ import { IotRhDailyReportApi } from '@/api/pms/iotrhdailyreport'
 import { rangeShortcuts } from '@/utils/formatTime'
 import { useDebounceFn } from '@vueuse/core'
 import dayjs from 'dayjs'
-import { DICT_TYPE, getStrDictOptions } from '@/utils/dict'
+import { DICT_TYPE } from '@/utils/dict'
 import { TableColumnCtx } from 'element-plus/es/components/table/src/table-column/defaults'
-import { FormInstance, FormRules } from 'element-plus'
-import Form from '@/components/Form/src/Form.vue'
 import { useUserStore } from '@/store/modules/user'
+import rhForm from './rh-form.vue'
 
 interface List {
   createTime: number // 日期
@@ -272,18 +271,18 @@ const calculateColumnWidths = (colums: Column[]) => {
   }
 }
 
-function checkTimeSumEquals24(row: List) {
-  // 获取三个字段的值,转换为数字,如果为空则视为0
-  const gasTime = row.dailyInjectGasTime || 0
-  const waterTime = row.dailyInjectWaterTime || 0
-  const nonProdTime = row.nonProductionTime || 0
+// function checkTimeSumEquals24(row: List) {
+//   // 获取三个字段的值,转换为数字,如果为空则视为0
+//   const gasTime = row.dailyInjectGasTime || 0
+//   const waterTime = row.dailyInjectWaterTime || 0
+//   const nonProdTime = row.nonProductionTime || 0
 
-  // 计算总和
-  const sum = gasTime + waterTime + nonProdTime
+//   // 计算总和
+//   const sum = gasTime + waterTime + nonProdTime
 
-  // 返回是否等于24(允许一定的浮点数误差)
-  return Math.abs(sum - 24) < 0.01 // 使用0.01作为误差范围
-}
+//   // 返回是否等于24(允许一定的浮点数误差)
+//   return Math.abs(sum - 24) < 0.01 // 使用0.01作为误差范围
+// }
 
 function cellStyle(data: {
   row: List
@@ -403,79 +402,14 @@ watch(
   { immediate: true }
 )
 
-const FORM_KEYS = [
-  'id',
-  'deptId',
-  'projectId',
-  'taskId',
-  'deptName',
-  'contractName',
-  'taskName',
-  'dailyGasInjection',
-  'dailyWaterInjection',
-  'dailyInjectGasTime',
-  'dailyInjectWaterTime',
-  'nonProductionTime',
-  'nptReason',
-  'productionStatus',
-  'remark',
-  'relocationDays',
-  'capacity',
-  'createTime',
-  'opinion'
-] as const
-
-type FormKey = (typeof FORM_KEYS)[number]
-type Form = Partial<Pick<List, FormKey>>
-
-const dialogVisible = ref(false)
-const formRef = ref<FormInstance>()
-const formLoading = ref(false)
-const message = useMessage()
-
-const initFormData = (): Form => ({
-  dailyGasInjection: 0,
-  dailyWaterInjection: 0,
-  dailyInjectGasTime: 0,
-  dailyInjectWaterTime: 0,
-  nonProductionTime: 0,
-  relocationDays: 0,
-  capacity: 0
-})
-
-const form = ref<Form>(initFormData())
-
-async function loadDetail(id: number) {
-  try {
-    const res = await IotRhDailyReportApi.getIotRhDailyReport(id)
-    FORM_KEYS.forEach((key) => {
-      form.value[key] = res[key] ?? form.value[key]
-    })
-    form.value.id = id
-
-    if (res.status !== 0) {
-      formType.value = 'readonly'
-    }
-
-    if (!form.value.capacity) {
-      message.error('请维护增压机产能')
-    }
-  } finally {
-  }
-}
+const visible = ref(false)
 
-const formType = ref<'edit' | 'readonly'>('edit')
+const formRef = ref()
 
 function handleOpenForm(id: number, type: 'edit' | 'readonly') {
-  form.value = initFormData()
-  formRef.value?.resetFields()
-
-  formType.value = type
-
-  dialogVisible.value = true
-  loadDetail(id).then(() => {
-    formRef.value?.validate()
-  })
+  if (formRef.value) {
+    formRef.value.handleOpenForm(id, type)
+  }
 }
 
 const route = useRoute()
@@ -485,99 +419,6 @@ onMounted(() => {
     handleOpenForm(Number(route.query.id), 'edit')
   }
 })
-
-const transitTime = computed(() => {
-  const cap = form.value.capacity
-  const gas = form.value.dailyGasInjection ?? 0
-
-  if (!cap) return { original: 0, value: '0%' }
-
-  const original = gas / cap
-  return { original, value: (original * 100).toFixed(2) + '%' }
-})
-
-const sumTimes = () => {
-  const { dailyInjectGasTime = 0, dailyInjectWaterTime = 0, nonProductionTime = 0 } = form.value
-  return parseFloat((dailyInjectGasTime + dailyInjectWaterTime + nonProductionTime).toFixed(2))
-}
-
-const validateTotalTime = (_rule: any, _value: any, callback: any) => {
-  const total = sumTimes()
-  if (total !== 24) {
-    callback(new Error(`当前合计 ${total} 小时,三项时间之和必须等于 24`))
-  } else {
-    callback()
-  }
-}
-
-const validateNptReason = (_rule: any, value: any, callback: any) => {
-  if ((form.value.nonProductionTime || 0) > 0 && !value) {
-    callback(new Error('非生产时间大于 0 时,必须选择原因'))
-  } else {
-    callback()
-  }
-}
-
-const timeRuleItem = [
-  { required: true, message: '请输入时间', trigger: 'blur' },
-  { validator: validateTotalTime, trigger: 'blur' }
-]
-
-const rules = reactive<FormRules>({
-  dailyGasInjection: [{ required: true, message: '请输入当日注气量', trigger: ['change', 'blur'] }],
-  dailyWaterInjection: [
-    { required: true, message: '请输入当日注水量', trigger: ['change', 'blur'] }
-  ],
-  productionStatus: [{ required: true, message: '请输入生产动态', trigger: ['change', 'blur'] }],
-
-  // 复用规则
-  // dailyInjectGasTime: timeRuleItem,
-  // dailyInjectWaterTime: timeRuleItem,
-  // nonProductionTime: timeRuleItem,
-
-  nptReason: [{ validator: validateNptReason, trigger: ['change', 'blur'] }]
-})
-
-watch(
-  [
-    () => form.value.dailyInjectGasTime,
-    () => form.value.dailyInjectWaterTime,
-    () => form.value.nonProductionTime
-  ],
-  () => {
-    nextTick(() => {
-      formRef.value?.validateField('nptReason')
-      if (sumTimes() === 24) {
-        formRef.value?.clearValidate([
-          'dailyInjectGasTime',
-          'dailyInjectWaterTime',
-          'nonProductionTime'
-        ])
-      }
-    })
-  }
-)
-
-const { t } = useI18n()
-
-const submitForm = async () => {
-  if (!formRef.value) return
-
-  try {
-    await formRef.value.validate()
-    formLoading.value = true
-    const { createTime, ...other } = form.value
-    const data = { ...other, fillOrderCreateTime: createTime } as any
-    await IotRhDailyReportApi.createIotRhDailyReport(data)
-    message.success(t('common.updateSuccess'))
-    dialogVisible.value = false
-    loadList()
-  } catch (error) {
-    console.warn('表单校验未通过或提交出错')
-  } finally {
-    formLoading.value = false
-  }
-}
 </script>
 
 <template>
@@ -690,164 +531,7 @@ const submitForm = async () => {
         </div>
       </div>
     </div>
-    <Dialog title="编辑" v-model="dialogVisible">
-      <el-form
-        ref="formRef"
-        label-position="top"
-        size="default"
-        :rules="rules"
-        :model="form"
-        v-loading="formLoading"
-        require-asterisk-position="right"
-        :disabled="formType === 'readonly'"
-      >
-        <div class="flex flex-col gap-3 text-sm">
-          <div
-            class="rounded-md border border-blue-100 bg-blue-50/80 p-3 dark:border-blue-900/30 dark:bg-blue-900/10"
-          >
-            <div class="flex flex-col gap-2.5">
-              <div class="flex items-center justify-between">
-                <div class="text-gray-600 dark:text-gray-400">
-                  <span class="font-bold text-gray-800 dark:text-gray-200">运行时效:</span>
-                  当日注气量 / 产能
-                </div>
-                <span
-                  class="inline-flex items-center rounded border border-red-200 bg-red-100 px-2 py-0.5 text-xs font-medium text-red-600 dark:bg-red-900/20 dark:border-red-800 dark:text-red-400"
-                >
-                  >120% 红色预警
-                </span>
-              </div>
-              <!-- <div class="flex items-center justify-between">
-                <div class="text-gray-600 dark:text-gray-400">
-                  <span class="font-bold text-gray-800 dark:text-gray-200">时间平衡:</span>
-                  注气 + 注水 + 非生产 = 24H
-                </div>
-                <span
-                  class="inline-flex items-center rounded border border-orange-200 bg-orange-100 px-2 py-0.5 text-xs font-medium text-orange-600 dark:bg-orange-900/20 dark:border-orange-800 dark:text-orange-400"
-                >
-                  ≠24H 橙色预警
-                </span>
-              </div> -->
-            </div>
-          </div>
-          <div
-            v-if="form.opinion"
-            class="flex gap-3 rounded-md border border-yellow-200 bg-yellow-50 p-3 dark:border-yellow-800 dark:bg-yellow-900/10"
-          >
-            <Icon
-              icon="ep:warning-filled"
-              class="mt-0.5 shrink-0 text-base text-yellow-600 dark:text-yellow-500"
-            />
-            <div class="flex flex-col">
-              <h4 class="mb-1 font-bold text-yellow-800 dark:text-yellow-500"> 审核意见 </h4>
-              <p class="leading-relaxed text-gray-600 dark:text-gray-400">
-                {{ form.opinion }}
-              </p>
-            </div>
-          </div>
-        </div>
-        <div class="grid grid-cols-2 gap-4 mt-5">
-          <el-form-item label="施工队伍" prop="deptName">
-            <el-input v-model="form.deptName" placeholder="" disabled />
-          </el-form-item>
-          <el-form-item label="项目" prop="contractName">
-            <el-input v-model="form.contractName" placeholder="" disabled />
-          </el-form-item>
-          <el-form-item label="任务" prop="taskName">
-            <el-input v-model="form.taskName" placeholder="" disabled />
-          </el-form-item>
-          <el-form-item label="搬迁安装天数(D)" prop="relocationDays">
-            <el-input v-model="form.relocationDays" placeholder="" disabled />
-          </el-form-item>
-          <el-form-item label="运行时效" prop="transitTime">
-            <el-input
-              :model-value="transitTime.value"
-              placeholder="运行时效"
-              disabled
-              :class="{ 'warning-input': transitTime.original > 1.2 }"
-              id="transitTimeInput"
-            />
-          </el-form-item>
-          <el-form-item label="当日注气量(方)" prop="dailyGasInjection">
-            <el-input-number
-              class="w-full!"
-              :min="0"
-              v-model="form.dailyGasInjection"
-              placeholder="请输入当日注气量(方)"
-            />
-          </el-form-item>
-          <el-form-item label="当日注水量(方)" prop="dailyWaterInjection">
-            <el-input-number
-              class="w-full!"
-              :min="0"
-              v-model="form.dailyWaterInjection"
-              placeholder="请输入当日注水量(方)"
-            />
-          </el-form-item>
-          <!-- :class="{ 'orange-input': sumTimes() !== 24 }" -->
-          <el-form-item label="当日注气时间(H)" prop="dailyInjectGasTime">
-            <el-input-number
-              class="w-full!"
-              :min="0"
-              v-model="form.dailyInjectGasTime"
-              placeholder="请输入当日注气时间(H)"
-            />
-          </el-form-item>
-          <el-form-item label="当日注水时间(H)" prop="dailyInjectWaterTime">
-            <el-input-number
-              class="w-full!"
-              :min="0"
-              v-model="form.dailyInjectWaterTime"
-              placeholder="当日注水时间(H)"
-            />
-          </el-form-item>
-          <el-form-item label="非生产时间(H)" prop="nonProductionTime">
-            <el-input-number
-              class="w-full!"
-              :min="0"
-              v-model="form.nonProductionTime"
-              placeholder="非生产时间(H)"
-            />
-          </el-form-item>
-          <el-form-item label="非生产时间原因" prop="nptReason">
-            <el-select v-model="form.nptReason" placeholder="请选择" clearable>
-              <el-option
-                v-for="(dict, index) of getStrDictOptions(DICT_TYPE.PMS_PROJECT_NPT_REASON)"
-                :key="index"
-                :label="dict.label"
-                :value="dict.value"
-              />
-            </el-select>
-          </el-form-item>
-        </div>
-        <div class="grid grid-cols-1 gap-4 mt-5">
-          <el-form-item label="生产动态" prop="productionStatus">
-            <el-input
-              v-model="form.productionStatus"
-              placeholder="请输入生产动态"
-              type="textarea"
-              autosize
-              :max-length="1000"
-            />
-          </el-form-item>
-          <el-form-item label="备注" prop="remark">
-            <el-input
-              v-model="form.remark"
-              placeholder="请输入备注"
-              :max-length="1000"
-              type="textarea"
-              autosize
-            />
-          </el-form-item>
-        </div>
-      </el-form>
-      <template #footer>
-        <el-button size="default" @click="submitForm" type="primary" :disabled="formLoading">
-          确 定
-        </el-button>
-        <el-button size="default" @click="dialogVisible = false">取 消</el-button>
-      </template>
-    </Dialog>
+    <rh-form v-model:visible="visible" type="edit" ref="formRef" :load-list="loadList" />
   </div>
 </template>
 
@@ -871,20 +555,6 @@ const submitForm = async () => {
   }
 }
 
-:deep(.warning-input) {
-  .el-input__inner {
-    color: red !important;
-    -webkit-text-fill-color: red !important;
-  }
-}
-
-:deep(.orange-input) {
-  .el-input__inner {
-    color: orange !important;
-    -webkit-text-fill-color: orange !important;
-  }
-}
-
 :deep(.el-input-number) {
   width: 100%;
 }

+ 2 - 2
src/views/pms/iotrhdailyreport/index.vue

@@ -253,7 +253,7 @@
                 resizable
               />
               <el-table-column
-                label="用电量(kWh)"
+                label="用电量(MWh)"
                 align="center"
                 prop="dailyPowerUsage"
                 :min-width="columnWidths.dailyPowerUsage.width"
@@ -333,7 +333,7 @@
                 resizable
               />
               <el-table-column
-                label="用电量(万千瓦时)"
+                label="用电量(MWh)"
                 align="center"
                 prop="wellTotalPower"
                 :formatter="gasInjectionFormatter"

+ 601 - 0
src/views/pms/iotrhdailyreport/rh-form.vue

@@ -0,0 +1,601 @@
+<script lang="ts" setup generic="T">
+import { IotRhDailyReportApi } from '@/api/pms/iotrhdailyreport'
+import { DICT_TYPE, getStrDictOptions } from '@/utils/dict'
+import { FormInstance, FormRules } from 'element-plus'
+import { computed, reactive, ref, watch, nextTick } from 'vue'
+
+interface Props {
+  visible: boolean
+  type?: 'edit' | 'approval' | 'readonly'
+  loadList: () => void
+}
+
+const props = withDefaults(defineProps<Props>(), {
+  type: 'edit'
+})
+
+const emits = defineEmits(['update:visible'])
+
+// 1. 定义具体的 11 个非生产时间字段
+const NON_PROD_FIELDS = [
+  { key: 'repairTime', label: '设备故障' },
+  { key: 'selfStopTime', label: '设备保养' },
+  { key: 'accidentTime', label: '工程质量' },
+  { key: 'complexityTime', label: '技术受限' },
+  { key: 'rectificationTime', label: '生产组织' },
+  { key: 'waitingStopTime', label: '不可抗力' },
+  { key: 'partyaDesign', label: '甲方设计' },
+  { key: 'partyaPrepare', label: '甲方准备' },
+  { key: 'partyaResource', label: '甲方资源' },
+  { key: 'relocationTime', label: '生产配合' },
+  { key: 'winterBreakTime', label: '待命' },
+  { key: 'otherNptTime', label: '其他非生产时间' }
+] as const
+
+interface FormOriginal {
+  id: number
+  deptId: number
+  projectId: number
+  taskId: number
+  deptName: string
+  contractName: string
+  taskName: string
+  dailyGasInjection: number
+  dailyWaterInjection: number
+  dailyInjectGasTime: number
+  dailyInjectWaterTime: number
+  constructionStatus: string
+
+  // 11个非生产时间字段
+  repairTime: number
+  selfStopTime: number
+  accidentTime: number
+  complexityTime: number
+  rectificationTime: number
+  waitingStopTime: number
+  partyaDesign: number
+  partyaPrepare: number
+  partyaResource: number
+  relocationTime: number
+  winterBreakTime: number
+  otherNptTime: number
+
+  // 其他非生产时间原因(仅作为备注字段存在)
+  otherNptReason: string
+
+  productionStatus: string
+  remark: string
+  relocationDays: number
+  capacity: number
+  createTime: number
+  opinion: string
+}
+
+type Form = Partial<FormOriginal>
+
+// 字段白名单
+const FORM_KEYS: (keyof FormOriginal)[] = [
+  'id',
+  'deptId',
+  'projectId',
+  'taskId',
+  'deptName',
+  'contractName',
+  'taskName',
+  'dailyGasInjection',
+  'dailyWaterInjection',
+  'dailyInjectGasTime',
+  'dailyInjectWaterTime',
+  'constructionStatus',
+  'productionStatus',
+  'remark',
+  'relocationDays',
+  'capacity',
+  'createTime',
+  'opinion',
+  'repairTime',
+  'selfStopTime',
+  'accidentTime',
+  'complexityTime',
+  'rectificationTime',
+  'waitingStopTime',
+  'partyaDesign',
+  'partyaPrepare',
+  'partyaResource',
+  'relocationTime',
+  'winterBreakTime',
+  'otherNptTime',
+  'otherNptReason'
+]
+
+const formRef = ref<FormInstance>()
+const loading = ref(false)
+const formLoading = ref(false)
+const formType = ref<'edit' | 'readonly'>('edit')
+const message = useMessage()
+const { t } = useI18n()
+
+// 初始化表单
+const initFormData = (): Form => {
+  const base: any = {
+    dailyGasInjection: 0,
+    dailyWaterInjection: 0,
+    dailyInjectGasTime: 0,
+    dailyInjectWaterTime: 0,
+    relocationDays: 0,
+    capacity: 0,
+    otherNptReason: '',
+    opinion: ''
+  }
+  // 初始化所有非生产时间字段为 0
+  NON_PROD_FIELDS.forEach((field) => {
+    base[field.key] = 0
+  })
+  return base as Form
+}
+
+const form = ref<Form>(initFormData())
+
+const isApproval = computed(() => props.type === 'approval')
+const isEdit = computed(() => props.type === 'edit')
+const isMainFieldDisabled = computed(() => formType.value === 'readonly' || isApproval.value)
+
+async function loadDetail(id: number) {
+  loading.value = true
+  try {
+    const res = await IotRhDailyReportApi.getIotRhDailyReport(id)
+    form.value = initFormData()
+    // 按需赋值
+    FORM_KEYS.forEach((key) => {
+      if (
+        Object.prototype.hasOwnProperty.call(res, key) &&
+        res[key] !== null &&
+        res[key] !== undefined
+      ) {
+        // @ts-ignore
+        form.value[key] = res[key]
+      }
+    })
+
+    if (props.type === 'edit' && res.status !== 0) formType.value = 'readonly'
+    if (props.type === 'approval' && res.auditStatus !== 10) formType.value = 'readonly'
+    if (!form.value.capacity) message.error('请维护增压机产能')
+  } finally {
+    loading.value = false
+  }
+}
+
+function handleOpenForm(id: number, type: 'edit' | 'readonly') {
+  formType.value = type
+  emits('update:visible', true)
+  loadDetail(id).then(() => {
+    nextTick(() => formRef.value?.clearValidate())
+  })
+}
+
+defineExpose({ handleOpenForm })
+
+// --- 运行时效 ---
+const transitTime = computed(() => {
+  const cap = form.value.capacity
+  const gas = form.value.dailyGasInjection ?? 0
+  if (!cap) return { original: 0, value: '0%' }
+  const original = gas / cap
+  return { original, value: (original * 100).toFixed(2) + '%' }
+})
+
+// --- 核心校验逻辑 ---
+
+// 计算所有非生产时间总和
+const sumNonProdTimes = () => {
+  let sum = 0
+  NON_PROD_FIELDS.forEach((field) => {
+    sum += (form.value[field.key as keyof FormOriginal] as number) || 0
+  })
+  return sum
+}
+
+// 24小时平衡校验器
+const validateTotalTime =
+  (isNon: boolean = false) =>
+  (rule: any, _value: any, callback: any) => {
+    const field = rule.field
+
+    const gasTime = form.value.dailyInjectGasTime || 0
+    const waterTime = form.value.dailyInjectWaterTime || 0
+    const nonProdSum = sumNonProdTimes()
+
+    let total = 0
+    let msg = ''
+
+    if (gasTime === 0 && waterTime > 0) {
+      total = parseFloat((waterTime + nonProdSum).toFixed(2))
+      msg = `注水(${waterTime})+非生产(${nonProdSum})=${total}H,必须为24H`
+    } else {
+      total = parseFloat((gasTime + nonProdSum).toFixed(2))
+      msg = `注气(${gasTime})+非生产(${nonProdSum})=${total}H,必须为24H`
+    }
+
+    if (Math.abs(total - 24) > 0.01) {
+      if (!isNon) {
+        if (field === 'dailyInjectWaterTime' && waterTime === 0) {
+          callback()
+        } else if (field === 'dailyInjectGasTime' && gasTime === 0 && waterTime !== 0) {
+          callback()
+        } else callback(new Error(msg))
+      } else callback(new Error())
+    } else {
+      callback()
+    }
+  }
+
+const validateOtherReason = (_rule: any, value: any, callback: any) => {
+  const time = form.value.otherNptTime || 0
+  if (time > 0 && !value) {
+    callback(new Error('填写了其他时间,必须说明原因'))
+  } else {
+    callback()
+  }
+}
+
+// 动态构建校验规则
+const rules = reactive<FormRules>({
+  dailyGasInjection: [{ required: true, message: '请输入当日注气量', trigger: ['blur', 'change'] }],
+  dailyWaterInjection: [
+    { required: true, message: '请输入当日注水量', trigger: ['blur', 'change'] }
+  ],
+  constructionStatus: [{ required: true, message: '请输入施工状态', trigger: ['blur', 'change'] }],
+  productionStatus: [{ required: true, message: '请输入生产动态', trigger: ['blur', 'change'] }],
+
+  // 生产时间绑定校验
+  dailyInjectGasTime: [
+    { required: true, message: '请输入当日注气时间', trigger: ['blur', 'change'] },
+    { validator: validateTotalTime(), trigger: ['blur', 'change'] }
+  ],
+  dailyInjectWaterTime: [{ validator: validateTotalTime(), trigger: ['blur', 'change'] }],
+  otherNptReason: [{ validator: validateOtherReason, trigger: ['blur', 'change'] }]
+})
+
+// 关键步骤:为每一个非生产时间字段都绑定 validateTotalTime 规则
+// 这样当总和不对时,所有时间字段下面都会出现红色错误提示
+NON_PROD_FIELDS.forEach((field) => {
+  rules[field.key] = [{ validator: validateTotalTime(true), trigger: ['blur', 'change'] }]
+})
+
+// 监听所有时间字段
+const allTimeKeys = [
+  'dailyInjectGasTime',
+  'dailyInjectWaterTime',
+  ...NON_PROD_FIELDS.map((f) => f.key)
+]
+
+// 当任一时间变化时,触发所有时间字段的校验更新
+watch(
+  () => allTimeKeys.map((key) => form.value[key as keyof FormOriginal]),
+  () => {
+    if (!isMainFieldDisabled.value) {
+      nextTick(() => {
+        // 传入数组,同时校验所有字段
+        formRef.value?.validateField(allTimeKeys)
+      })
+    }
+  },
+  { deep: true }
+)
+
+// --- 提交 ---
+const submitForm = async () => {
+  if (!formRef.value) return
+
+  try {
+    await formRef.value.validate()
+    formLoading.value = true
+    const submitData: any = {}
+    FORM_KEYS.forEach((key) => (submitData[key] = form.value[key]))
+    submitData.fillOrderCreateTime = form.value.createTime
+
+    await IotRhDailyReportApi.createIotRhDailyReport(submitData)
+    message.success(t('common.updateSuccess'))
+    emits('update:visible', false)
+    props.loadList()
+  } catch (error) {
+    console.warn('校验失败')
+  } finally {
+    formLoading.value = false
+  }
+}
+
+const handleAudit = async (auditStatus: 20 | 30) => {
+  if (!formRef.value) return
+  try {
+    formLoading.value = true
+    await IotRhDailyReportApi.approvalIotRhDailyReport({
+      id: form.value.id!,
+      auditStatus,
+      opinion: form.value.opinion!
+    })
+    message.success(auditStatus === 20 ? '通过成功' : '拒绝成功')
+    emits('update:visible', false)
+    props.loadList()
+  } catch (error) {
+    console.warn(error)
+  } finally {
+    formLoading.value = false
+  }
+}
+</script>
+
+<template>
+  <el-drawer
+    :model-value="visible"
+    @update:model-value="emits('update:visible', $event)"
+    header-class="mb-0!"
+    size="50%"
+  >
+    <template #header>
+      <span class="text-xl font-bold text-[var(--el-text-color-primary)]">
+        {{ type === 'edit' ? '编辑日报' : '审批日报' }}
+      </span>
+    </template>
+
+    <el-form
+      ref="formRef"
+      label-position="top"
+      size="default"
+      :rules="rules"
+      :model="form"
+      v-loading="loading"
+      require-asterisk-position="right"
+      :disabled="formType === 'readonly' && type !== 'approval'"
+    >
+      <!-- 顶部提示区 -->
+      <div class="flex flex-col gap-3 text-sm mb-4">
+        <div
+          class="rounded-md border border-blue-100 bg-blue-50/80 p-3 dark:border-blue-900/30 dark:bg-blue-900/10"
+        >
+          <div class="flex flex-col gap-2.5">
+            <div class="flex items-center justify-between">
+              <div class="text-gray-600 dark:text-gray-400">
+                <span class="font-bold text-gray-800 dark:text-gray-200">运行时效:</span>
+                当日注气量 / 产能
+              </div>
+              <span
+                class="inline-flex items-center rounded border border-red-200 bg-red-100 px-2 py-0.5 text-xs font-medium text-red-600 dark:bg-red-900/20 dark:border-red-800 dark:text-red-400"
+              >
+                >120% 红色预警
+              </span>
+            </div>
+            <!-- 可以显示一个总计提示,辅助用户 -->
+            <!-- <div class="text-gray-600 dark:text-gray-400">
+              <span class="font-bold text-gray-800 dark:text-gray-200">时间校验:</span>
+              (注气 > 0 ? 注气 : 注水) + 所有非生产时间 = 24H
+            </div> -->
+          </div>
+        </div>
+        <div
+          v-if="isEdit && form.opinion"
+          class="flex gap-3 rounded-md border border-yellow-200 bg-yellow-50 p-3 dark:border-yellow-800 dark:bg-yellow-900/10"
+        >
+          <Icon
+            icon="ep:warning-filled"
+            class="mt-0.5 shrink-0 text-base text-yellow-600 dark:text-yellow-500"
+          />
+          <div class="flex flex-col">
+            <h4 class="mb-1 font-bold text-yellow-800 dark:text-yellow-500">审核意见</h4>
+            <p class="leading-relaxed text-gray-600 dark:text-gray-400">{{ form.opinion }}</p>
+          </div>
+        </div>
+      </div>
+
+      <div class="grid grid-cols-2 gap-4">
+        <!-- 基础信息 -->
+        <div class="col-span-2 flex items-center gap-2 mt-2">
+          <div class="bg-[var(--el-color-primary)] w-1 h-5 rounded-full"></div>
+          <div class="text-lg font-medium text-[var(--el-text-color-primary)]">基础信息</div>
+        </div>
+        <el-form-item label="施工队伍" prop="deptName"
+          ><el-input v-model="form.deptName" disabled
+        /></el-form-item>
+        <el-form-item label="项目" prop="contractName"
+          ><el-input v-model="form.contractName" disabled
+        /></el-form-item>
+        <el-form-item label="任务" prop="taskName"
+          ><el-input v-model="form.taskName" disabled
+        /></el-form-item>
+        <el-form-item label="搬迁安装天数(D)" prop="relocationDays"
+          ><el-input v-model="form.relocationDays" disabled
+        /></el-form-item>
+        <el-form-item label="运行时效" prop="transitTime">
+          <el-input
+            :model-value="transitTime.value"
+            disabled
+            :class="{ 'warning-input': transitTime.original > 1.2 }"
+          />
+        </el-form-item>
+        <el-form-item label="施工状态" prop="constructionStatus">
+          <el-select
+            v-model="form.constructionStatus"
+            placeholder="请选择施工状态"
+            :disabled="isMainFieldDisabled"
+            clearable
+          >
+            <el-option
+              v-for="(dict, index) in getStrDictOptions(DICT_TYPE.PMS_PROJECT_TASK_SCHEDULE)"
+              :key="index"
+              :label="dict.label"
+              :value="dict.value"
+            />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="当日注气量(方)" prop="dailyGasInjection">
+          <el-input-number
+            class="!w-full"
+            :min="0"
+            v-model="form.dailyGasInjection"
+            :controls="false"
+            align="left"
+            :disabled="isMainFieldDisabled"
+          />
+        </el-form-item>
+        <el-form-item label="当日注水量(方)" prop="dailyWaterInjection">
+          <el-input-number
+            class="!w-full"
+            :min="0"
+            v-model="form.dailyWaterInjection"
+            :controls="false"
+            align="left"
+            :disabled="isMainFieldDisabled"
+          />
+        </el-form-item>
+        <el-form-item class="col-span-2" label="生产动态" prop="productionStatus">
+          <el-input
+            v-model="form.productionStatus"
+            type="textarea"
+            autosize
+            maxlength="1000"
+            :disabled="isMainFieldDisabled"
+          />
+        </el-form-item>
+        <el-form-item class="col-span-2" label="备注" prop="remark">
+          <el-input
+            v-model="form.remark"
+            type="textarea"
+            autosize
+            maxlength="1000"
+            :disabled="isMainFieldDisabled"
+          />
+        </el-form-item>
+
+        <!-- 时间信息 -->
+        <div class="col-span-2 flex items-center gap-2 mt-4">
+          <div class="bg-[var(--el-color-primary)] w-1 h-5 rounded-full"></div>
+          <div class="text-lg font-medium text-[var(--el-text-color-primary)]"
+            >生产与非生产时间</div
+          >
+        </div>
+
+        <el-form-item label="当日注气时间(H)" prop="dailyInjectGasTime">
+          <el-input-number
+            class="!w-full"
+            :min="0"
+            :max="24"
+            v-model="form.dailyInjectGasTime"
+            :controls="false"
+            align="left"
+            placeholder="注气+非生产=24H"
+            :disabled="isMainFieldDisabled"
+          />
+        </el-form-item>
+        <el-form-item label="当日注水时间(H)" prop="dailyInjectWaterTime">
+          <el-input-number
+            class="!w-full"
+            :min="0"
+            :max="24"
+            v-model="form.dailyInjectWaterTime"
+            :controls="false"
+            align="left"
+            placeholder="注气=0时,注水+非生产=24H"
+            :disabled="isMainFieldDisabled"
+          />
+        </el-form-item>
+
+        <!-- 循环渲染11个具体非生产时间 -->
+        <el-form-item
+          v-for="field in NON_PROD_FIELDS"
+          :key="field.key"
+          :label="field.label + '(H)'"
+          :prop="field.key"
+        >
+          <el-input-number
+            class="!w-full"
+            :min="0"
+            :max="24"
+            v-model="form[field.key as keyof FormOriginal]"
+            :controls="false"
+            align="left"
+            :disabled="isMainFieldDisabled"
+          />
+        </el-form-item>
+
+        <!-- 其他非生产原因 -->
+        <el-form-item class="col-span-2" label="其他非生产原因" prop="otherNptReason">
+          <el-input
+            v-model="form.otherNptReason"
+            placeholder="请输入其他非生产原因"
+            :disabled="isMainFieldDisabled"
+          />
+        </el-form-item>
+
+        <!-- 审批意见 -->
+        <div v-if="isApproval" class="col-span-2 mt-4 border-t pt-4">
+          <el-form-item label="审批意见" prop="opinion">
+            <el-input
+              v-model="form.opinion"
+              placeholder="请输入审批意见"
+              maxlength="1000"
+              type="textarea"
+              :autosize="{ minRows: 3 }"
+              :disabled="formType === 'readonly'"
+            />
+          </el-form-item>
+        </div>
+      </div>
+    </el-form>
+
+    <template #footer>
+      <div v-if="isEdit">
+        <el-button
+          size="default"
+          type="primary"
+          @click="submitForm"
+          :loading="formLoading"
+          :disabled="formType === 'readonly'"
+          >确 定</el-button
+        >
+        <el-button size="default" @click="emits('update:visible', false)">取 消</el-button>
+      </div>
+      <div v-if="isApproval">
+        <el-button
+          size="default"
+          type="primary"
+          @click="handleAudit(20)"
+          :loading="formLoading"
+          :disabled="formType === 'readonly'"
+          >审批通过</el-button
+        >
+        <el-button
+          size="default"
+          type="danger"
+          @click="handleAudit(30)"
+          :loading="formLoading"
+          :disabled="formType === 'readonly'"
+          >审批拒绝</el-button
+        >
+        <el-button size="default" @click="emits('update:visible', false)">取 消</el-button>
+      </div>
+    </template>
+  </el-drawer>
+</template>
+
+<style scoped>
+:deep(.warning-input) {
+  .el-input__inner {
+    color: red !important;
+    -webkit-text-fill-color: red !important;
+  }
+}
+
+:deep(.blue-input) {
+  .el-input__inner {
+    color: blue !important;
+    -webkit-text-fill-color: blue !important;
+  }
+}
+
+:deep(.orange-input) {
+  .el-input__inner {
+    color: orange !important;
+    -webkit-text-fill-color: orange !important;
+  }
+}
+</style>

+ 10 - 586
src/views/pms/iotrydailyreport/approval.vue

@@ -2,12 +2,11 @@
 import { rangeShortcuts } from '@/utils/formatTime'
 import { useDebounceFn } from '@vueuse/core'
 import dayjs from 'dayjs'
-import { DICT_TYPE, getStrDictOptions } from '@/utils/dict'
+import { DICT_TYPE } from '@/utils/dict'
 import { TableColumnCtx } from 'element-plus/es/components/table/src/table-column/defaults'
-import { FormInstance, FormRules } from 'element-plus'
-import Form from '@/components/Form/src/Form.vue'
 import { useUserStore } from '@/store/modules/user'
 import { IotRyDailyReportApi } from '@/api/pms/iotrydailyreport'
+import ryForm from './ry-form.vue'
 
 interface List {
   id: number
@@ -516,235 +515,23 @@ watch(
   { immediate: true }
 )
 
-const FORM_KEYS = [
-  'id',
-  'deptName',
-  'contractName',
-  'taskName',
-  'rigStatus',
-  'designWellDepth',
-  'currentDepth',
-  'dailyPowerUsage',
-  'dailyFuel',
-  'mudDensity',
-  'mudViscosity',
-  'lateralLength',
-  'wellInclination',
-  'azimuth',
-  'designWellStruct',
-  'productionStatus',
-  'remark',
-  'createTime',
-  'deptId',
-  'projectId',
-  'taskId',
-  'opinion',
-  'personnel',
-  'accidentTime',
-  'repairTime',
-  'selfStopTime',
-  'complexityTime',
-  'relocationTime',
-  'rectificationTime',
-  'waitingStopTime',
-  'winterBreakTime',
-  'drillingWorkingTime',
-  'otherProductionTime',
-  'lastCurrentDepth'
-] as const
+const visible = ref(false)
 
-type FormKey = (typeof FORM_KEYS)[number]
-type Form = Partial<Pick<List, FormKey>>
+const formRef = ref()
 
-const dialogVisible = ref(false)
-const formRef = ref<FormInstance>()
-const formLoading = ref(false)
-const message = useMessage()
-
-const initFormData = (): Form => ({})
-
-const form = ref<Form>(initFormData())
-
-async function loadDetail(id: number) {
-  try {
-    const res = await IotRyDailyReportApi.getIotRyDailyReport(id)
-    FORM_KEYS.forEach((key) => {
-      form.value[key] = res[key] ?? form.value[key]
-    })
-    form.value.id = id
-
-    if (res.auditStatus !== 10) {
-      formType.value = 'readonly'
-    }
-  } finally {
+function handleOpenForm(id: number, type: 'edit' | 'readonly') {
+  if (formRef.value) {
+    formRef.value.handleOpenForm(id, type)
   }
 }
 
-const formType = ref<'approval' | 'readonly'>('approval')
-
-function handleOpenForm(id: number, type: 'approval' | 'readonly') {
-  form.value = initFormData()
-  formRef.value?.resetFields()
-
-  formType.value = type
-
-  dialogVisible.value = true
-  loadDetail(id).then(() => {
-    formRef.value?.validate()
-  })
-}
-
 const route = useRoute()
 
 onMounted(() => {
   if (Object.keys(route.query).length > 0) {
-    handleOpenForm(Number(route.query.id), 'approval')
-  }
-})
-
-// const transitTime = computed(() => {
-//   const cap = form.value.capacity
-//   const gas = form.value.dailyGasInjection ?? 0
-
-//   if (!cap) return { original: 0, value: '0%' }
-
-//   const original = gas / cap
-//   return { original, value: (original * 100).toFixed(2) + '%' }
-// })
-
-const sumTimes = () => {
-  const {
-    drillingWorkingTime = 0,
-    otherProductionTime = 0,
-    accidentTime = 0,
-    repairTime = 0,
-    selfStopTime = 0,
-    complexityTime = 0,
-    relocationTime = 0,
-    rectificationTime = 0,
-    waitingStopTime = 0,
-    winterBreakTime = 0
-  } = form.value
-  return parseFloat(
-    (
-      drillingWorkingTime +
-      otherProductionTime +
-      accidentTime +
-      repairTime +
-      selfStopTime +
-      complexityTime +
-      relocationTime +
-      rectificationTime +
-      waitingStopTime +
-      winterBreakTime
-    ).toFixed(2)
-  )
-}
-
-const validateTotalTime = (_rule: any, _value: any, callback: any) => {
-  const total = sumTimes()
-  if (total !== 24) {
-    callback(new Error(`当前合计 ${total} 小时,时间之和必须等于 24`))
-  } else {
-    callback()
+    handleOpenForm(Number(route.query.id), 'edit')
   }
-}
-
-// const validateNptReason = (_rule: any, value: any, callback: any) => {
-//   if ((form.value.nonProductionTime || 0) > 0 && !value) {
-//     callback(new Error('非生产时间大于 0 时,必须选择原因'))
-//   } else {
-//     callback()
-//   }
-// }
-
-const validateLastCurrentDepth = (_rule: any, value: any, callback: any) => {
-  if (value && value < (form.value.lastCurrentDepth ?? 0)) {
-    callback(new Error('当前深度需大于等于上一次填报深度'))
-  } else {
-    callback()
-  }
-}
-
-const timeRuleItem = [
-  { required: true, message: '请输入时间', trigger: 'blur' },
-  { validator: validateTotalTime, trigger: 'blur' }
-]
-
-const rules = reactive<FormRules>({
-  currentDepth: [
-    { required: true, message: '请输入当前深度', trigger: ['change', 'blur'] },
-    { validator: validateLastCurrentDepth, trigger: ['change', 'blur'] }
-  ],
-  productionStatus: [{ required: true, message: '请输入生产动态', trigger: ['change', 'blur'] }],
-
-  // 复用规则
-  drillingWorkingTime: timeRuleItem,
-  otherProductionTime: timeRuleItem,
-  accidentTime: timeRuleItem,
-  repairTime: timeRuleItem,
-  selfStopTime: timeRuleItem,
-  complexityTime: timeRuleItem,
-  relocationTime: timeRuleItem,
-  rectificationTime: timeRuleItem,
-  waitingStopTime: timeRuleItem,
-  winterBreakTime: timeRuleItem
 })
-
-watch(
-  [
-    () => form.value.drillingWorkingTime,
-    () => form.value.otherProductionTime,
-    () => form.value.accidentTime,
-    () => form.value.repairTime,
-    () => form.value.selfStopTime,
-    () => form.value.complexityTime,
-    () => form.value.relocationTime,
-    () => form.value.rectificationTime,
-    () => form.value.waitingStopTime,
-    () => form.value.winterBreakTime
-  ],
-  () => {
-    nextTick(() => {
-      if (sumTimes() === 24) {
-        formRef.value?.clearValidate([
-          'drillingWorkingTime',
-          'otherProductionTime',
-          'accidentTime',
-          'repairTime',
-          'selfStopTime',
-          'complexityTime',
-          'relocationTime',
-          'rectificationTime',
-          'waitingStopTime',
-          'winterBreakTime'
-        ])
-      }
-    })
-  }
-)
-
-const submitForm = async (auditStatus: 20 | 30) => {
-  if (!formRef.value) return
-
-  try {
-    // await formRef.value.validate()
-    formLoading.value = true
-    const { opinion, id } = form.value
-
-    const data = { id: id, auditStatus, opinion } as any
-
-    await IotRyDailyReportApi.approvalIotRyDailyReport(data)
-    message.success(auditStatus === 20 ? '通过成功' : '拒绝成功')
-    dialogVisible.value = false
-
-    loadList()
-  } catch (error) {
-    console.warn('表单校验未通过或提交出错')
-  } finally {
-    formLoading.value = false
-  }
-}
 </script>
 
 <template>
@@ -829,7 +616,7 @@ const submitForm = async (auditStatus: 20 | 30) => {
                         v-show="row.auditStatus === 10"
                         link
                         type="primary"
-                        @click="handleOpenForm(row.id, 'approval')"
+                        @click="handleOpenForm(row.id, 'edit')"
                         v-hasPermi="['pms:iot-ry-daily-report:update']"
                       >
                         审批
@@ -857,356 +644,7 @@ const submitForm = async (auditStatus: 20 | 30) => {
         </div>
       </div>
     </div>
-    <Dialog title="编辑" v-model="dialogVisible">
-      <el-form
-        ref="formRef"
-        label-position="top"
-        size="default"
-        :rules="rules"
-        :model="form"
-        v-loading="formLoading"
-        require-asterisk-position="right"
-      >
-        <div class="flex flex-col gap-3 text-sm">
-          <div
-            class="rounded-md border border-blue-100 bg-blue-50/80 p-3 dark:border-blue-900/30 dark:bg-blue-900/10"
-          >
-            <div class="flex flex-col gap-2.5">
-              <div class="flex items-center justify-between">
-                <div class="text-gray-600 dark:text-gray-400">
-                  <span class="font-bold text-gray-800 dark:text-gray-200"> 油量消耗:</span>
-                  当日油耗
-                </div>
-                <span
-                  class="inline-flex items-center rounded border border-red-200 bg-red-100 px-2 py-0.5 text-xs font-medium text-red-600 dark:bg-red-900/20 dark:border-red-800 dark:text-red-400"
-                >
-                  >9000升 红色预警
-                </span>
-              </div>
-              <div class="flex items-center justify-between">
-                <div class="text-gray-600 dark:text-gray-400">
-                  <span class="font-bold text-gray-800 dark:text-gray-200">时间平衡:</span>
-                  进尺 + 其它生产 + 非生产 = 24H
-                </div>
-                <span
-                  class="inline-flex items-center rounded border border-orange-200 bg-orange-100 px-2 py-0.5 text-xs font-medium text-orange-600 dark:bg-orange-900/20 dark:border-orange-800 dark:text-orange-400"
-                >
-                  ≠24H 橙色预警
-                </span>
-              </div>
-            </div>
-          </div>
-          <!-- <div
-          v-if="form.opinion"
-          class="flex gap-3 rounded-md border border-yellow-200 bg-yellow-50 p-3 dark:border-yellow-800 dark:bg-yellow-900/10"
-        >
-          <Icon
-            icon="ep:warning-filled"
-            class="mt-0.5 shrink-0 text-base text-yellow-600 dark:text-yellow-500"
-          />
-          <div class="flex flex-col">
-            <h4 class="mb-1 font-bold text-yellow-800 dark:text-yellow-500"> 审核意见 </h4>
-            <p class="leading-relaxed text-gray-600 dark:text-gray-400">
-              {{ form.opinion }}
-            </p>
-          </div>
-        </div> -->
-        </div>
-        <div class="grid grid-cols-2 gap-4 mt-5">
-          <el-form-item label="施工队伍" prop="deptName">
-            <el-input v-model="form.deptName" placeholder="" disabled />
-          </el-form-item>
-          <el-form-item label="项目" prop="contractName">
-            <el-input v-model="form.contractName" placeholder="" disabled />
-          </el-form-item>
-          <el-form-item label="任务" prop="taskName">
-            <el-input v-model="form.taskName" placeholder="" disabled />
-          </el-form-item>
-          <el-form-item label="施工状态" prop="rigStatus">
-            <el-select v-model="form.rigStatus" placeholder="请选择施工状态" disabled>
-              <el-option
-                v-for="(dict, index) in getStrDictOptions(DICT_TYPE.PMS_PROJECT_TASK_RY_SCHEDULE)"
-                :key="index"
-                :label="dict.label"
-                :value="dict.value"
-              />
-            </el-select>
-          </el-form-item>
-          <el-form-item label="设计井深(m)" prop="designWellDepth">
-            <el-input v-model="form.designWellDepth" placeholder="" disabled />
-          </el-form-item>
-          <el-form-item label="当前井深(m)" prop="currentDepth">
-            <el-input-number
-              class="placeholder-"
-              :min="0"
-              v-model="form.currentDepth"
-              placeholder="请输入当前井深(m)"
-              disabled
-            />
-          </el-form-item>
-          <el-form-item label="当日用电量(kWh)" prop="dailyPowerUsage">
-            <el-input-number
-              :min="0"
-              v-model="form.dailyPowerUsage"
-              placeholder="请输入当日用电量(kWh)"
-              clearable
-              disabled
-            />
-          </el-form-item>
-          <el-form-item label="当日油耗(升)" prop="dailyFuel">
-            <el-input-number
-              :min="0"
-              v-model="form.dailyFuel"
-              placeholder="请输入当日油耗(升)"
-              clearable
-              :class="{ 'warning-input': (form.dailyFuel ?? 0) > 9000 }"
-              disabled
-            />
-          </el-form-item>
-          <el-form-item label="泥浆密度(g/cm³)" prop="mudDensity">
-            <el-input-number
-              :min="0"
-              v-model="form.mudDensity"
-              placeholder="请输入泥浆性能-密度(g/cm³)"
-              clearable
-              disabled
-            />
-          </el-form-item>
-          <el-form-item label="泥浆粘度(S)" prop="mudViscosity">
-            <el-input-number
-              :min="0"
-              v-model="form.mudViscosity"
-              placeholder="请输入泥浆性能-粘度(S)"
-              clearable
-              disabled
-            />
-          </el-form-item>
-          <el-form-item label="水平段长度(m)" prop="lateralLength">
-            <el-input-number
-              :min="0"
-              v-model="form.lateralLength"
-              placeholder="请输入水平段长度(m)"
-              clearable
-              disabled
-            />
-          </el-form-item>
-          <el-form-item label="井斜(°)" prop="wellInclination">
-            <el-input-number
-              :min="0"
-              v-model="form.wellInclination"
-              placeholder="请输入井斜(°)"
-              clearable
-              disabled
-            />
-          </el-form-item>
-          <el-form-item label="方位(°)" prop="azimuth">
-            <el-input-number
-              :min="0"
-              v-model="form.azimuth"
-              placeholder="请输入方位(°)"
-              clearable
-              disabled
-            />
-          </el-form-item>
-          <el-form-item label="设计井身结构" prop="designWellStruct">
-            <el-input
-              v-model="form.designWellStruct"
-              placeholder=""
-              type="textarea"
-              disabled
-              autosize
-            />
-          </el-form-item>
-          <!-- <el-form-item label="生产动态" prop="productionStatus">
-          <el-input
-            v-model="form.productionStatus"
-            placeholder="请输入生产动态"
-            type="textarea"
-            autosize
-            :max-length="1000"
-            disabled
-          />
-        </el-form-item> -->
-          <el-form-item label="人员情况" prop="personnel">
-            <el-input
-              v-model="form.personnel"
-              placeholder="请输入人员情况"
-              type="textarea"
-              :max-length="1000"
-              autosize
-              disabled
-            />
-          </el-form-item>
-          <!-- <el-form-item label="备注" prop="remark">
-          <el-input
-            v-model="form.remark"
-            placeholder="请输入备注"
-            :max-length="1000"
-            type="textarea"
-            autosize
-            disabled
-          />
-        </el-form-item> -->
-        </div>
-        <el-divider content-position="left">生产时间</el-divider>
-        <div class="grid grid-cols-2 gap-4 mt-5">
-          <el-form-item label="进尺工作时间(H)" prop="drillingWorkingTime">
-            <el-input-number
-              class="w-full!"
-              :min="0"
-              v-model="form.drillingWorkingTime"
-              placeholder="进尺工作时间(H)"
-              disabled
-              :class="{ 'orange-input': sumTimes() !== 24 }"
-            />
-          </el-form-item>
-          <el-form-item label="其它生产时间(H)" prop="otherProductionTime">
-            <el-input-number
-              class="w-full!"
-              :min="0"
-              v-model="form.otherProductionTime"
-              placeholder="其它生产时间(H)"
-              disabled
-              :class="{ 'orange-input': sumTimes() !== 24 }"
-            />
-          </el-form-item>
-        </div>
-        <el-divider content-position="left">非生产时间</el-divider>
-        <div class="grid grid-cols-4 gap-4 mt-5">
-          <el-form-item label="事故(H)" prop="accidentTime">
-            <el-input-number
-              class="w-full!"
-              :min="0"
-              v-model="form.accidentTime"
-              placeholder="请输入事故(H)"
-              disabled
-              :class="{ 'orange-input': sumTimes() !== 24 }"
-            />
-          </el-form-item>
-          <el-form-item label="修理(H)" prop="repairTime">
-            <el-input-number
-              class="w-full!"
-              :min="0"
-              v-model="form.repairTime"
-              placeholder="请输入修理(H)"
-              disabled
-              :class="{ 'orange-input': sumTimes() !== 24 }"
-            />
-          </el-form-item>
-          <el-form-item label="自停(H)" prop="selfStopTime">
-            <el-input-number
-              class="w-full!"
-              :min="0"
-              v-model="form.selfStopTime"
-              placeholder="请输入自停(H)"
-              disabled
-              :class="{ 'orange-input': sumTimes() !== 24 }"
-            />
-          </el-form-item>
-          <el-form-item label="复杂(H)" prop="complexityTime">
-            <el-input-number
-              class="w-full!"
-              :min="0"
-              v-model="form.complexityTime"
-              placeholder="请输入复杂(H)"
-              disabled
-              :class="{ 'orange-input': sumTimes() !== 24 }"
-            />
-          </el-form-item>
-          <el-form-item label="搬迁(H)" prop="relocationTime">
-            <el-input-number
-              class="w-full!"
-              :min="0"
-              v-model="form.relocationTime"
-              placeholder="请输入搬迁(H)"
-              disabled
-              :class="{ 'orange-input': sumTimes() !== 24 }"
-            />
-          </el-form-item>
-          <el-form-item label="整改(H)" prop="rectificationTime">
-            <el-input-number
-              class="w-full!"
-              :min="0"
-              v-model="form.rectificationTime"
-              placeholder="请输入整改(H)"
-              disabled
-              :class="{ 'orange-input': sumTimes() !== 24 }"
-            />
-          </el-form-item>
-          <el-form-item label="等停(H)" prop="waitingStopTime">
-            <el-input-number
-              class="w-full!"
-              :min="0"
-              v-model="form.waitingStopTime"
-              placeholder="请输入等停(H)"
-              disabled
-              :class="{ 'orange-input': sumTimes() !== 24 }"
-            />
-          </el-form-item>
-          <el-form-item label="冬休(H)" prop="winterBreakTime">
-            <el-input-number
-              class="w-full!"
-              :min="0"
-              v-model="form.winterBreakTime"
-              placeholder="请输入冬休(H)"
-              disabled
-              :class="{ 'orange-input': sumTimes() !== 24 }"
-            />
-          </el-form-item>
-        </div>
-        <div class="grid grid-cols-1 gap-4 mt-5">
-          <el-form-item label="生产动态" prop="productionStatus">
-            <el-input
-              v-model="form.productionStatus"
-              placeholder="请输入生产动态"
-              type="textarea"
-              autosize
-              :max-length="1000"
-              disabled
-            />
-          </el-form-item>
-          <el-form-item label="备注" prop="remark">
-            <el-input
-              v-model="form.remark"
-              placeholder="请输入备注"
-              :max-length="1000"
-              type="textarea"
-              autosize
-              disabled
-            />
-          </el-form-item>
-        </div>
-        <el-form-item class="mt-4" label="审批意见" prop="opinion">
-          <el-input
-            v-model="form.opinion"
-            placeholder="请输入审批意见"
-            :max-length="1000"
-            type="textarea"
-            autosize
-            :disabled="formType === 'readonly'"
-          />
-        </el-form-item>
-      </el-form>
-      <template #footer>
-        <el-button
-          size="default"
-          @click="submitForm(20)"
-          type="primary"
-          :disabled="formLoading || formType === 'readonly'"
-        >
-          审批通过
-        </el-button>
-        <el-button
-          size="default"
-          @click="submitForm(30)"
-          type="danger"
-          :disabled="formLoading || formType === 'readonly'"
-        >
-          审批拒绝
-        </el-button>
-        <el-button size="default" @click="dialogVisible = false">取 消</el-button>
-      </template>
-    </Dialog>
+    <ry-form v-model:visible="visible" type="approval" ref="formRef" :load-list="loadList" />
   </div>
 </template>
 
@@ -1230,20 +668,6 @@ const submitForm = async (auditStatus: 20 | 30) => {
   }
 }
 
-:deep(.warning-input) {
-  .el-input__inner {
-    color: red !important;
-    -webkit-text-fill-color: red !important;
-  }
-}
-
-:deep(.orange-input) {
-  .el-input__inner {
-    color: orange !important;
-    -webkit-text-fill-color: orange !important;
-  }
-}
-
 :deep(.el-input-number) {
   width: 100%;
 }

+ 8 - 520
src/views/pms/iotrydailyreport/fill.vue

@@ -2,12 +2,11 @@
 import { rangeShortcuts } from '@/utils/formatTime'
 import { useDebounceFn } from '@vueuse/core'
 import dayjs from 'dayjs'
-import { DICT_TYPE, getStrDictOptions } from '@/utils/dict'
+import { DICT_TYPE } from '@/utils/dict'
 import { TableColumnCtx } from 'element-plus/es/components/table/src/table-column/defaults'
-import { FormInstance, FormRules } from 'element-plus'
-import Form from '@/components/Form/src/Form.vue'
 import { useUserStore } from '@/store/modules/user'
 import { IotRyDailyReportApi } from '@/api/pms/iotrydailyreport'
+import ryForm from './ry-form.vue'
 
 interface List {
   id: number
@@ -516,82 +515,14 @@ watch(
   { immediate: true }
 )
 
-const FORM_KEYS = [
-  'id',
-  'deptName',
-  'contractName',
-  'taskName',
-  'rigStatus',
-  'designWellDepth',
-  'currentDepth',
-  'dailyPowerUsage',
-  'dailyFuel',
-  'mudDensity',
-  'mudViscosity',
-  'lateralLength',
-  'wellInclination',
-  'azimuth',
-  'designWellStruct',
-  'productionStatus',
-  'remark',
-  'createTime',
-  'deptId',
-  'projectId',
-  'taskId',
-  'opinion',
-  'personnel',
-  'accidentTime',
-  'repairTime',
-  'selfStopTime',
-  'complexityTime',
-  'relocationTime',
-  'rectificationTime',
-  'waitingStopTime',
-  'winterBreakTime',
-  'drillingWorkingTime',
-  'otherProductionTime',
-  'lastCurrentDepth'
-] as const
+const visible = ref(false)
 
-type FormKey = (typeof FORM_KEYS)[number]
-type Form = Partial<Pick<List, FormKey>>
-
-const dialogVisible = ref(false)
-const formRef = ref<FormInstance>()
-const formLoading = ref(false)
-const message = useMessage()
-
-const initFormData = (): Form => ({})
-
-const form = ref<Form>(initFormData())
-
-async function loadDetail(id: number) {
-  try {
-    const res = await IotRyDailyReportApi.getIotRyDailyReport(id)
-    FORM_KEYS.forEach((key) => {
-      form.value[key] = res[key] ?? form.value[key]
-    })
-    form.value.id = id
-
-    if (res.status !== 0) {
-      formType.value = 'readonly'
-    }
-  } finally {
-  }
-}
-
-const formType = ref<'edit' | 'readonly'>('edit')
+const formRef = ref()
 
 function handleOpenForm(id: number, type: 'edit' | 'readonly') {
-  form.value = initFormData()
-  formRef.value?.resetFields()
-
-  formType.value = type
-
-  dialogVisible.value = true
-  loadDetail(id).then(() => {
-    formRef.value?.validate()
-  })
+  if (formRef.value) {
+    formRef.value.handleOpenForm(id, type)
+  }
 }
 
 const route = useRoute()
@@ -601,149 +532,6 @@ onMounted(() => {
     handleOpenForm(Number(route.query.id), 'edit')
   }
 })
-
-// const transitTime = computed(() => {
-//   const cap = form.value.capacity
-//   const gas = form.value.dailyGasInjection ?? 0
-
-//   if (!cap) return { original: 0, value: '0%' }
-
-//   const original = gas / cap
-//   return { original, value: (original * 100).toFixed(2) + '%' }
-// })
-
-const sumTimes = () => {
-  const {
-    drillingWorkingTime = 0,
-    otherProductionTime = 0,
-    accidentTime = 0,
-    repairTime = 0,
-    selfStopTime = 0,
-    complexityTime = 0,
-    relocationTime = 0,
-    rectificationTime = 0,
-    waitingStopTime = 0,
-    winterBreakTime = 0
-  } = form.value
-  return parseFloat(
-    (
-      drillingWorkingTime +
-      otherProductionTime +
-      accidentTime +
-      repairTime +
-      selfStopTime +
-      complexityTime +
-      relocationTime +
-      rectificationTime +
-      waitingStopTime +
-      winterBreakTime
-    ).toFixed(2)
-  )
-}
-
-const validateTotalTime = (_rule: any, _value: any, callback: any) => {
-  const total = sumTimes()
-  if (total !== 24) {
-    callback(new Error(`当前合计 ${total} 小时,时间之和必须等于 24`))
-  } else {
-    callback()
-  }
-}
-
-// const validateNptReason = (_rule: any, value: any, callback: any) => {
-//   if ((form.value.nonProductionTime || 0) > 0 && !value) {
-//     callback(new Error('非生产时间大于 0 时,必须选择原因'))
-//   } else {
-//     callback()
-//   }
-// }
-
-const validateLastCurrentDepth = (_rule: any, value: any, callback: any) => {
-  if (value && value < (form.value.lastCurrentDepth ?? 0)) {
-    callback(new Error(`当前深度需大于等于上一次填报深度 ${form.value.lastCurrentDepth}`))
-  } else {
-    callback()
-  }
-}
-
-const timeRuleItem = [
-  { required: true, message: '请输入时间', trigger: 'blur' },
-  { validator: validateTotalTime, trigger: 'blur' }
-]
-
-const rules = reactive<FormRules>({
-  currentDepth: [
-    { required: true, message: '请输入当前深度', trigger: ['change', 'blur'] },
-    { validator: validateLastCurrentDepth, trigger: ['change', 'blur'] }
-  ],
-  productionStatus: [{ required: true, message: '请输入生产动态', trigger: ['change', 'blur'] }],
-
-  // 复用规则
-  drillingWorkingTime: timeRuleItem,
-  otherProductionTime: timeRuleItem,
-  accidentTime: timeRuleItem,
-  repairTime: timeRuleItem,
-  selfStopTime: timeRuleItem,
-  complexityTime: timeRuleItem,
-  relocationTime: timeRuleItem,
-  rectificationTime: timeRuleItem,
-  waitingStopTime: timeRuleItem,
-  winterBreakTime: timeRuleItem
-})
-
-watch(
-  [
-    () => form.value.drillingWorkingTime,
-    () => form.value.otherProductionTime,
-    () => form.value.accidentTime,
-    () => form.value.repairTime,
-    () => form.value.selfStopTime,
-    () => form.value.complexityTime,
-    () => form.value.relocationTime,
-    () => form.value.rectificationTime,
-    () => form.value.waitingStopTime,
-    () => form.value.winterBreakTime
-  ],
-  () => {
-    nextTick(() => {
-      if (sumTimes() === 24) {
-        formRef.value?.clearValidate([
-          'drillingWorkingTime',
-          'otherProductionTime',
-          'accidentTime',
-          'repairTime',
-          'selfStopTime',
-          'complexityTime',
-          'relocationTime',
-          'rectificationTime',
-          'waitingStopTime',
-          'winterBreakTime'
-        ])
-      }
-    })
-  }
-)
-
-const { t } = useI18n()
-
-const submitForm = async () => {
-  if (!formRef.value) return
-
-  try {
-    await formRef.value.validate()
-    formLoading.value = true
-    const { createTime, ...other } = form.value
-    const data = { ...other, fillOrderCreateTime: createTime, projectClassification: '1' } as any
-    await IotRyDailyReportApi.createIotRyDailyReport(data)
-    message.success(t('common.updateSuccess'))
-    dialogVisible.value = false
-    loadList()
-  } catch (error) {
-    console.warn('表单校验未通过或提交出错')
-  } finally {
-    formLoading.value = false
-  }
-}
 </script>
 
 <template>
@@ -856,293 +644,7 @@ const submitForm = async () => {
         </div>
       </div>
     </div>
-    <Dialog title="编辑" v-model="dialogVisible">
-      <el-form
-        ref="formRef"
-        label-position="top"
-        size="default"
-        :rules="rules"
-        :model="form"
-        v-loading="formLoading"
-        require-asterisk-position="right"
-        :disabled="formType === 'readonly'"
-      >
-        <div class="flex flex-col gap-3 text-sm">
-          <div
-            class="rounded-md border border-blue-100 bg-blue-50/80 p-3 dark:border-blue-900/30 dark:bg-blue-900/10"
-          >
-            <div class="flex flex-col gap-2.5">
-              <div class="flex items-center justify-between">
-                <div class="text-gray-600 dark:text-gray-400">
-                  <span class="font-bold text-gray-800 dark:text-gray-200"> 油量消耗:</span>
-                  当日油耗
-                </div>
-                <span
-                  class="inline-flex items-center rounded border border-red-200 bg-red-100 px-2 py-0.5 text-xs font-medium text-red-600 dark:bg-red-900/20 dark:border-red-800 dark:text-red-400"
-                >
-                  >9000升 红色预警
-                </span>
-              </div>
-              <div class="flex items-center justify-between">
-                <div class="text-gray-600 dark:text-gray-400">
-                  <span class="font-bold text-gray-800 dark:text-gray-200">时间平衡:</span>
-                  进尺 + 其它生产 + 非生产 = 24H
-                </div>
-                <span
-                  class="inline-flex items-center rounded border border-orange-200 bg-orange-100 px-2 py-0.5 text-xs font-medium text-orange-600 dark:bg-orange-900/20 dark:border-orange-800 dark:text-orange-400"
-                >
-                  ≠24H 橙色预警
-                </span>
-              </div>
-            </div>
-          </div>
-          <div
-            v-if="form.opinion"
-            class="flex gap-3 rounded-md border border-yellow-200 bg-yellow-50 p-3 dark:border-yellow-800 dark:bg-yellow-900/10"
-          >
-            <Icon
-              icon="ep:warning-filled"
-              class="mt-0.5 shrink-0 text-base text-yellow-600 dark:text-yellow-500"
-            />
-            <div class="flex flex-col">
-              <h4 class="mb-1 font-bold text-yellow-800 dark:text-yellow-500"> 审核意见 </h4>
-              <p class="leading-relaxed text-gray-600 dark:text-gray-400">
-                {{ form.opinion }}
-              </p>
-            </div>
-          </div>
-        </div>
-        <div class="grid grid-cols-2 gap-4 mt-5">
-          <el-form-item label="施工队伍" prop="deptName">
-            <el-input v-model="form.deptName" placeholder="" disabled />
-          </el-form-item>
-          <el-form-item label="项目" prop="contractName">
-            <el-input v-model="form.contractName" placeholder="" disabled />
-          </el-form-item>
-          <el-form-item label="任务" prop="taskName">
-            <el-input v-model="form.taskName" placeholder="" disabled />
-          </el-form-item>
-          <el-form-item label="施工状态" prop="rigStatus">
-            <el-select v-model="form.rigStatus" placeholder="请选择施工状态">
-              <el-option
-                v-for="(dict, index) in getStrDictOptions(DICT_TYPE.PMS_PROJECT_TASK_RY_SCHEDULE)"
-                :key="index"
-                :label="dict.label"
-                :value="dict.value"
-              />
-            </el-select>
-          </el-form-item>
-          <el-form-item label="设计井深(m)" prop="designWellDepth">
-            <el-input v-model="form.designWellDepth" placeholder="" disabled />
-          </el-form-item>
-          <el-form-item label="当前井深(m)" prop="currentDepth">
-            <el-input-number
-              class="placeholder-"
-              :min="0"
-              v-model="form.currentDepth"
-              placeholder="请输入当前井深(m)"
-            />
-          </el-form-item>
-          <el-form-item label="当日用电量(kWh)" prop="dailyPowerUsage">
-            <el-input-number
-              :min="0"
-              v-model="form.dailyPowerUsage"
-              placeholder="请输入当日用电量(kWh)"
-              clearable
-            />
-          </el-form-item>
-          <el-form-item label="当日油耗(升)" prop="dailyFuel">
-            <el-input-number
-              :min="0"
-              v-model="form.dailyFuel"
-              placeholder="请输入当日油耗(升)"
-              clearable
-              :class="{ 'warning-input': (form.dailyFuel ?? 0) > 9000 }"
-            />
-          </el-form-item>
-          <el-form-item label="泥浆密度(g/cm³)" prop="mudDensity">
-            <el-input-number
-              :min="0"
-              v-model="form.mudDensity"
-              placeholder="请输入泥浆性能-密度(g/cm³)"
-              clearable
-            />
-          </el-form-item>
-          <el-form-item label="泥浆粘度(S)" prop="mudViscosity">
-            <el-input-number
-              :min="0"
-              v-model="form.mudViscosity"
-              placeholder="请输入泥浆性能-粘度(S)"
-              clearable
-            />
-          </el-form-item>
-          <el-form-item label="水平段长度(m)" prop="lateralLength">
-            <el-input-number
-              :min="0"
-              v-model="form.lateralLength"
-              placeholder="请输入水平段长度(m)"
-              clearable
-            />
-          </el-form-item>
-          <el-form-item label="井斜(°)" prop="wellInclination">
-            <el-input-number
-              :min="0"
-              v-model="form.wellInclination"
-              placeholder="请输入井斜(°)"
-              clearable
-            />
-          </el-form-item>
-          <el-form-item label="方位(°)" prop="azimuth">
-            <el-input-number
-              :min="0"
-              v-model="form.azimuth"
-              placeholder="请输入方位(°)"
-              clearable
-            />
-          </el-form-item>
-          <el-form-item label="设计井身结构" prop="designWellStruct">
-            <el-input
-              v-model="form.designWellStruct"
-              placeholder=""
-              type="textarea"
-              disabled
-              autosize
-            />
-          </el-form-item>
-          <el-form-item label="人员情况" prop="personnel">
-            <el-input
-              v-model="form.personnel"
-              placeholder="请输入人员情况"
-              type="textarea"
-              :max-length="1000"
-              autosize
-            />
-          </el-form-item>
-        </div>
-        <el-divider content-position="left">生产时间</el-divider>
-        <div class="grid grid-cols-2 gap-4 mt-5">
-          <el-form-item label="进尺工作时间(H)" prop="drillingWorkingTime">
-            <el-input-number
-              class="w-full!"
-              :min="0"
-              v-model="form.drillingWorkingTime"
-              placeholder="进尺工作时间(H)"
-              :class="{ 'orange-input': sumTimes() !== 24 }"
-            />
-          </el-form-item>
-          <el-form-item label="其它生产时间(H)" prop="otherProductionTime">
-            <el-input-number
-              class="w-full!"
-              :min="0"
-              v-model="form.otherProductionTime"
-              placeholder="其它生产时间(H)"
-              :class="{ 'orange-input': sumTimes() !== 24 }"
-            />
-          </el-form-item>
-        </div>
-        <el-divider content-position="left">非生产时间</el-divider>
-        <div class="grid grid-cols-4 gap-4 mt-5">
-          <el-form-item label="事故(H)" prop="accidentTime">
-            <el-input-number
-              class="w-full!"
-              :min="0"
-              v-model="form.accidentTime"
-              placeholder="请输入事故(H)"
-              :class="{ 'orange-input': sumTimes() !== 24 }"
-            />
-          </el-form-item>
-          <el-form-item label="修理(H)" prop="repairTime">
-            <el-input-number
-              class="w-full!"
-              :min="0"
-              v-model="form.repairTime"
-              placeholder="请输入修理(H)"
-              :class="{ 'orange-input': sumTimes() !== 24 }"
-            />
-          </el-form-item>
-          <el-form-item label="自停(H)" prop="selfStopTime">
-            <el-input-number
-              class="w-full!"
-              :min="0"
-              v-model="form.selfStopTime"
-              placeholder="请输入自停(H)"
-              :class="{ 'orange-input': sumTimes() !== 24 }"
-            />
-          </el-form-item>
-          <el-form-item label="复杂(H)" prop="complexityTime">
-            <el-input-number
-              class="w-full!"
-              :min="0"
-              v-model="form.complexityTime"
-              placeholder="请输入复杂(H)"
-              :class="{ 'orange-input': sumTimes() !== 24 }"
-            />
-          </el-form-item>
-          <el-form-item label="搬迁(H)" prop="relocationTime">
-            <el-input-number
-              class="w-full!"
-              :min="0"
-              v-model="form.relocationTime"
-              placeholder="请输入搬迁(H)"
-              :class="{ 'orange-input': sumTimes() !== 24 }"
-            />
-          </el-form-item>
-          <el-form-item label="整改(H)" prop="rectificationTime">
-            <el-input-number
-              class="w-full!"
-              :min="0"
-              v-model="form.rectificationTime"
-              placeholder="请输入整改(H)"
-              :class="{ 'orange-input': sumTimes() !== 24 }"
-            />
-          </el-form-item>
-          <el-form-item label="等停(H)" prop="waitingStopTime">
-            <el-input-number
-              class="w-full!"
-              :min="0"
-              v-model="form.waitingStopTime"
-              placeholder="请输入等停(H)"
-              :class="{ 'orange-input': sumTimes() !== 24 }"
-            />
-          </el-form-item>
-          <el-form-item label="冬休(H)" prop="winterBreakTime">
-            <el-input-number
-              class="w-full!"
-              :min="0"
-              v-model="form.winterBreakTime"
-              placeholder="请输入冬休(H)"
-              :class="{ 'orange-input': sumTimes() !== 24 }"
-            />
-          </el-form-item>
-        </div>
-        <div class="grid grid-cols-1 gap-4 mt-5">
-          <el-form-item label="生产动态" prop="productionStatus">
-            <el-input
-              v-model="form.productionStatus"
-              placeholder="请输入生产动态"
-              type="textarea"
-              autosize
-              :max-length="1000"
-            />
-          </el-form-item>
-          <el-form-item label="备注" prop="remark">
-            <el-input
-              v-model="form.remark"
-              placeholder="请输入备注"
-              :max-length="1000"
-              type="textarea"
-              autosize
-            />
-          </el-form-item>
-        </div>
-      </el-form>
-      <template #footer>
-        <el-button size="default" @click="submitForm" type="primary" :disabled="formLoading">
-          确 定
-        </el-button>
-        <el-button size="default" @click="dialogVisible = false">取 消</el-button>
-      </template>
-    </Dialog>
+    <ry-form v-model:visible="visible" type="edit" ref="formRef" :load-list="loadList" />
   </div>
 </template>
 
@@ -1166,20 +668,6 @@ const submitForm = async () => {
   }
 }
 
-:deep(.warning-input) {
-  .el-input__inner {
-    color: red !important;
-    -webkit-text-fill-color: red !important;
-  }
-}
-
-:deep(.orange-input) {
-  .el-input__inner {
-    color: orange !important;
-    -webkit-text-fill-color: orange !important;
-  }
-}
-
 :deep(.el-input-number) {
   width: 100%;
 }

+ 16 - 24
src/views/pms/iotrydailyreport/index.vue

@@ -63,21 +63,15 @@
             <el-button @click="resetQuery"
               ><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button
             >
-            <el-button
+            <!-- <el-button
               type="primary"
               plain
               @click="openForm('create')"
               v-hasPermi="['pms:iot-ry-daily-report:create']"
             >
               <Icon icon="ep:plus" class="mr-5px" /> 新增
-            </el-button>
-            <el-button
-              type="success"
-              plain
-              @click="handleExport"
-              :loading="exportLoading"
-              v-hasPermi="['pms:iot-ry-daily-report:export']"
-            >
+            </el-button> -->
+            <el-button type="success" plain @click="handleExport" :loading="exportLoading">
               <Icon icon="ep:download" class="mr-5px" /> 导出
             </el-button>
           </el-form-item>
@@ -441,7 +435,6 @@
 
 <script setup lang="ts">
 import { dateFormatter, dateFormatter2, rangeShortcuts } from '@/utils/formatTime'
-import download from '@/utils/download'
 import { IotRyDailyReportApi, IotRyDailyReportVO } from '@/api/pms/iotrydailyreport'
 import IotRyDailyReportForm from './IotRyDailyReportForm.vue'
 import { DICT_TYPE } from '@/utils/dict'
@@ -452,6 +445,7 @@ import quarterOfYear from 'dayjs/plugin/quarterOfYear'
 import { useDebounceFn } from '@vueuse/core'
 
 import { useUserStore } from '@/store/modules/user'
+import download from '@/utils/download'
 
 dayjs.extend(quarterOfYear)
 
@@ -513,7 +507,6 @@ let queryParams = reactive({
   nonProductFlag: 'N'
 })
 const queryFormRef = ref() // 搜索的表单
-const exportLoading = ref(false) // 导出的加载中
 
 // 表格引用
 const tableRef = ref()
@@ -909,21 +902,20 @@ const handleDeptNodeClick = async (row) => {
   await getList()
 }
 
-/** 导出按钮操作 */
+const exportLoading = ref(false)
 const handleExport = async () => {
-  try {
-    // 导出的二次确认
-    await message.exportConfirm()
-    // 发起导出
-    exportLoading.value = true
-    const data = await IotRyDailyReportApi.exportIotRyDailyReport(queryParams)
-    download.excel(data, '瑞鹰日报.xls')
-  } catch {
-  } finally {
-    exportLoading.value = false
-  }
-}
+  const res = await IotRyDailyReportApi.exportIotRyDailyReport({
+    createTime: queryParams.createTime,
+    contractName: queryParams.contractName,
+    taskName: queryParams.taskName,
+    // pageNo: queryParams.pageNo,
+    // pageSize: queryParams.pageSize,
+    deptId: queryParams.deptId,
+    projectClassification: queryParams.projectClassification
+  })
 
+  download.excel(res, '瑞鹰钻井日报.xlsx')
+}
 // 声明 ResizeObserver 实例
 let resizeObserver: ResizeObserver | null = null
 

+ 712 - 0
src/views/pms/iotrydailyreport/ry-form.vue

@@ -0,0 +1,712 @@
+<script lang="ts" setup generic="T">
+import { IotRyDailyReportApi } from '@/api/pms/iotrydailyreport'
+import { DICT_TYPE, getStrDictOptions } from '@/utils/dict'
+import { FormInstance, FormRules } from 'element-plus'
+import { computed, reactive, ref, watch, nextTick } from 'vue'
+
+interface Props {
+  visible: boolean
+  type?: 'edit' | 'approval' | 'readonly'
+  loadList: () => void
+}
+
+const props = withDefaults(defineProps<Props>(), {
+  type: 'edit'
+})
+
+const emits = defineEmits(['update:visible'])
+
+// 1. 定义具体的 11 个非生产时间字段
+const NON_PROD_FIELDS = [
+  { key: 'repairTime', label: '设备故障' },
+  { key: 'selfStopTime', label: '设备保养' },
+  { key: 'accidentTime', label: '工程质量' },
+  { key: 'complexityTime', label: '技术受限' },
+  { key: 'rectificationTime', label: '生产组织' },
+  { key: 'waitingStopTime', label: '不可抗力' },
+  { key: 'partyaDesign', label: '甲方设计' },
+  { key: 'partyaPrepare', label: '甲方准备' },
+  { key: 'partyaResource', label: '甲方资源' },
+  { key: 'relocationTime', label: '生产配合' },
+  { key: 'winterBreakTime', label: '待命' },
+  { key: 'otherNptTime', label: '其他非生产时间' }
+] as const
+
+interface FormOriginal {
+  id: number
+  deptId: number
+  projectId: number
+  taskId: number
+  deptName: string
+  contractName: string
+  taskName: string
+  rigStatus: string
+  designWellDepth: string
+  currentDepth: number
+  dailyPowerUsage: number
+  dailyFuel: number
+  mudDensity: number
+  mudViscosity: number
+  lateralLength: number
+  wellInclination: number
+  azimuth: number
+  designWellStruct: string
+  personnel: string
+  drillingWorkingTime: number
+  otherProductionTime: number
+  lastCurrentDepth: number
+
+  // 11个非生产时间字段
+  repairTime: number
+  selfStopTime: number
+  accidentTime: number
+  complexityTime: number
+  rectificationTime: number
+  waitingStopTime: number
+  partyaDesign: number
+  partyaPrepare: number
+  partyaResource: number
+  relocationTime: number
+  winterBreakTime: number
+  otherNptTime: number
+
+  // 其他非生产时间原因(仅作为备注字段存在)
+  otherNptReason: string
+
+  productionStatus: string
+  remark: string
+  createTime: string
+  opinion: string
+}
+
+type Form = Partial<FormOriginal>
+
+// 字段白名单
+const FORM_KEYS: (keyof FormOriginal)[] = [
+  'id',
+  'deptId',
+  'projectId',
+  'taskId',
+  'deptName',
+  'contractName',
+  'taskName',
+  'rigStatus',
+  'designWellDepth',
+  'currentDepth',
+  'dailyPowerUsage',
+  'dailyFuel',
+  'mudDensity',
+  'mudViscosity',
+  'lateralLength',
+  'wellInclination',
+  'azimuth',
+  'designWellStruct',
+  'personnel',
+  'drillingWorkingTime',
+  'otherProductionTime',
+  'lastCurrentDepth',
+
+  'productionStatus',
+  'remark',
+  'createTime',
+
+  'opinion',
+  'repairTime',
+  'selfStopTime',
+  'accidentTime',
+  'complexityTime',
+  'rectificationTime',
+  'waitingStopTime',
+  'partyaDesign',
+  'partyaPrepare',
+  'partyaResource',
+  'relocationTime',
+  'winterBreakTime',
+  'otherNptTime',
+  'otherNptReason'
+]
+
+const formRef = ref<FormInstance>()
+const loading = ref(false)
+const formLoading = ref(false)
+const formType = ref<'edit' | 'readonly'>('edit')
+const message = useMessage()
+const { t } = useI18n()
+
+// 初始化表单
+const initFormData = (): Form => {
+  const base: any = {
+    drillingWorkingTime: 0,
+    otherProductionTime: 0
+  }
+  // 初始化所有非生产时间字段为 0
+  NON_PROD_FIELDS.forEach((field) => {
+    base[field.key] = 0
+  })
+  return base as Form
+}
+
+const form = ref<Form>(initFormData())
+
+const isApproval = computed(() => props.type === 'approval')
+const isEdit = computed(() => props.type === 'edit')
+const isMainFieldDisabled = computed(() => formType.value === 'readonly' || isApproval.value)
+
+async function loadDetail(id: number) {
+  loading.value = true
+  try {
+    const res = await IotRyDailyReportApi.getIotRyDailyReport(id)
+    form.value = initFormData()
+    // 按需赋值
+    FORM_KEYS.forEach((key) => {
+      if (
+        Object.prototype.hasOwnProperty.call(res, key) &&
+        res[key] !== null &&
+        res[key] !== undefined
+      ) {
+        // @ts-ignore
+        form.value[key] = res[key]
+      }
+    })
+
+    if (props.type === 'edit' && res.status !== 0) formType.value = 'readonly'
+    if (props.type === 'approval' && res.auditStatus !== 10) formType.value = 'readonly'
+  } finally {
+    loading.value = false
+  }
+}
+
+function handleOpenForm(id: number, type: 'edit' | 'readonly') {
+  formType.value = type
+  emits('update:visible', true)
+  loadDetail(id).then(() => {
+    nextTick(() => formRef.value?.clearValidate())
+  })
+}
+
+defineExpose({ handleOpenForm })
+
+// --- 运行时效 ---
+// const transitTime = computed(() => {
+//   const cap = form.value.capacity
+//   const gas = form.value.dailyGasInjection ?? 0
+//   if (!cap) return { original: 0, value: '0%' }
+//   const original = gas / cap
+//   return { original, value: (original * 100).toFixed(2) + '%' }
+// })
+
+// --- 核心校验逻辑 ---
+
+// 计算所有非生产时间总和
+const sumNonProdTimes = () => {
+  let sum = 0
+  NON_PROD_FIELDS.forEach((field) => {
+    sum += (form.value[field.key as keyof FormOriginal] as number) || 0
+  })
+
+  return sum
+}
+
+// 24小时平衡校验器
+const validateTotalTime =
+  (isNon: boolean = false) =>
+  (_rule: any, _value: any, callback: any) => {
+    const drillingTime = form.value.drillingWorkingTime || 0
+    const otherTime = form.value.otherProductionTime || 0
+
+    const nonProdSum = sumNonProdTimes()
+
+    let total = 0
+    let msg = ''
+
+    total = parseFloat((drillingTime + otherTime + nonProdSum).toFixed(2))
+    msg = `进尺(${drillingTime})+其他(${otherTime})+非生产(${nonProdSum})=${total}H,必须为24H`
+
+    if (Math.abs(total - 24) > 0.01) {
+      if (!isNon) callback(new Error(msg))
+      else callback(new Error())
+    } else {
+      callback()
+    }
+  }
+
+const validateOtherReason = (_rule: any, value: any, callback: any) => {
+  const time = form.value.otherNptTime || 0
+  if (time > 0 && !value) {
+    callback(new Error('填写了其他时间,必须说明原因'))
+  } else {
+    callback()
+  }
+}
+
+const validateLastCurrentDepth = (_rule: any, value: any, callback: any) => {
+  if (value && value < (form.value.lastCurrentDepth ?? 0)) {
+    callback(new Error(`当前深度需大于等于上一次填报深度 ${form.value.lastCurrentDepth}`))
+  } else {
+    callback()
+  }
+}
+
+// 动态构建校验规则
+const rules = reactive<FormRules>({
+  currentDepth: [
+    { required: true, message: '请输入当前深度', trigger: ['change', 'blur'] },
+    { validator: validateLastCurrentDepth, trigger: ['change', 'blur'] }
+  ],
+  productionStatus: [{ required: true, message: '请输入生产动态', trigger: ['blur', 'change'] }],
+
+  // 生产时间绑定校验
+  drillingWorkingTime: [
+    { required: true, message: '请输入进尺工作时间', trigger: ['blur', 'change'] },
+    { validator: validateTotalTime(), trigger: ['blur', 'change'] }
+  ],
+  otherProductionTime: [{ validator: validateTotalTime(), trigger: ['blur', 'change'] }],
+  otherNptReason: [{ validator: validateOtherReason, trigger: ['blur', 'change'] }]
+})
+
+// 关键步骤:为每一个非生产时间字段都绑定 validateTotalTime 规则
+// 这样当总和不对时,所有时间字段下面都会出现红色错误提示
+NON_PROD_FIELDS.forEach((field) => {
+  rules[field.key] = [{ validator: validateTotalTime(true), trigger: ['blur', 'change'] }]
+})
+
+// 监听所有时间字段
+const allTimeKeys = [
+  'drillingWorkingTime',
+  'otherProductionTime',
+  ...NON_PROD_FIELDS.map((f) => f.key)
+]
+
+// 当任一时间变化时,触发所有时间字段的校验更新
+watch(
+  () => allTimeKeys.map((key) => form.value[key as keyof FormOriginal]),
+  () => {
+    if (!isMainFieldDisabled.value) {
+      nextTick(() => {
+        // 传入数组,同时校验所有字段
+        formRef.value?.validateField(allTimeKeys)
+      })
+    }
+  },
+  { deep: true }
+)
+
+// --- 提交 ---
+const submitForm = async () => {
+  if (!formRef.value) return
+
+  try {
+    await formRef.value.validate()
+    formLoading.value = true
+    const submitData: any = {}
+    FORM_KEYS.forEach((key) => (submitData[key] = form.value[key]))
+    submitData.fillOrderCreateTime = form.value.createTime
+    submitData.projectClassification = '1'
+
+    await IotRyDailyReportApi.createIotRyDailyReport(submitData)
+    message.success(t('common.updateSuccess'))
+    emits('update:visible', false)
+    props.loadList()
+  } catch (error) {
+    console.warn('校验失败')
+  } finally {
+    formLoading.value = false
+  }
+}
+
+const handleAudit = async (auditStatus: 20 | 30) => {
+  if (!formRef.value) return
+  try {
+    formLoading.value = true
+    await IotRyDailyReportApi.approvalIotRyDailyReport({
+      id: form.value.id!,
+      auditStatus,
+      opinion: form.value.opinion!
+    })
+    message.success(auditStatus === 20 ? '通过成功' : '拒绝成功')
+    emits('update:visible', false)
+    props.loadList()
+  } catch (error) {
+    console.warn(error)
+  } finally {
+    formLoading.value = false
+  }
+}
+
+const orange = computed(() => {
+  const drillingTime = form.value.drillingWorkingTime || 0
+  const otherTime = form.value.otherProductionTime || 0
+
+  const nonProdSum = sumNonProdTimes()
+
+  let total = 0
+
+  total = parseFloat((drillingTime + otherTime + nonProdSum).toFixed(2))
+
+  if (Math.abs(total - 24) > 0.01) return true
+  return false
+})
+</script>
+
+<template>
+  <el-drawer
+    :model-value="visible"
+    @update:model-value="emits('update:visible', $event)"
+    header-class="mb-0!"
+    size="50%"
+  >
+    <template #header>
+      <span class="text-xl font-bold text-[var(--el-text-color-primary)]">
+        {{ type === 'edit' ? '编辑日报' : '审批日报' }}
+      </span>
+    </template>
+
+    <el-form
+      ref="formRef"
+      label-position="top"
+      size="default"
+      :rules="rules"
+      :model="form"
+      v-loading="loading"
+      require-asterisk-position="right"
+      :disabled="formType === 'readonly' && type !== 'approval'"
+    >
+      <!-- 顶部提示区 -->
+      <div class="flex flex-col gap-3 text-sm mb-4">
+        <div
+          class="rounded-md border border-blue-100 bg-blue-50/80 p-3 dark:border-blue-900/30 dark:bg-blue-900/10"
+        >
+          <div class="flex flex-col gap-2.5">
+            <div class="flex items-center justify-between">
+              <div class="text-gray-600 dark:text-gray-400">
+                <span class="font-bold text-gray-800 dark:text-gray-200"> 油量消耗:</span>
+                当日油耗
+              </div>
+              <span
+                class="inline-flex items-center rounded border border-red-200 bg-red-100 px-2 py-0.5 text-xs font-medium text-red-600 dark:bg-red-900/20 dark:border-red-800 dark:text-red-400"
+              >
+                >9000升 红色预警
+              </span>
+            </div>
+            <div class="flex items-center justify-between">
+              <div class="text-gray-600 dark:text-gray-400">
+                <span class="font-bold text-gray-800 dark:text-gray-200">时间平衡:</span>
+                进尺 + 其它生产 + 非生产 = 24H
+              </div>
+              <span
+                class="inline-flex items-center rounded border border-orange-200 bg-orange-100 px-2 py-0.5 text-xs font-medium text-orange-600 dark:bg-orange-900/20 dark:border-orange-800 dark:text-orange-400"
+              >
+                ≠24H 橙色预警
+              </span>
+            </div>
+          </div>
+        </div>
+        <div
+          v-if="isEdit && form.opinion"
+          class="flex gap-3 rounded-md border border-yellow-200 bg-yellow-50 p-3 dark:border-yellow-800 dark:bg-yellow-900/10"
+        >
+          <Icon
+            icon="ep:warning-filled"
+            class="mt-0.5 shrink-0 text-base text-yellow-600 dark:text-yellow-500"
+          />
+          <div class="flex flex-col">
+            <h4 class="mb-1 font-bold text-yellow-800 dark:text-yellow-500">审核意见</h4>
+            <p class="leading-relaxed text-gray-600 dark:text-gray-400">{{ form.opinion }}</p>
+          </div>
+        </div>
+      </div>
+
+      <div class="grid grid-cols-2 gap-4">
+        <!-- 基础信息 -->
+        <div class="col-span-2 flex items-center gap-2 mt-2">
+          <div class="bg-[var(--el-color-primary)] w-1 h-5 rounded-full"></div>
+          <div class="text-lg font-medium text-[var(--el-text-color-primary)]">基础信息</div>
+        </div>
+        <el-form-item label="施工队伍" prop="deptName"
+          ><el-input v-model="form.deptName" disabled
+        /></el-form-item>
+        <el-form-item label="项目" prop="contractName"
+          ><el-input v-model="form.contractName" disabled
+        /></el-form-item>
+        <el-form-item label="任务" prop="taskName"
+          ><el-input v-model="form.taskName" disabled
+        /></el-form-item>
+        <el-form-item label="施工状态" prop="rigStatus">
+          <el-select
+            v-model="form.rigStatus"
+            placeholder="请选择施工状态"
+            :disabled="isMainFieldDisabled"
+          >
+            <el-option
+              v-for="(dict, index) in getStrDictOptions(DICT_TYPE.PMS_PROJECT_TASK_RY_SCHEDULE)"
+              :key="index"
+              :label="dict.label"
+              :value="dict.value"
+            />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="设计井深(m)" prop="designWellDepth">
+          <el-input v-model="form.designWellDepth" placeholder="" disabled />
+        </el-form-item>
+        <el-form-item label="当前井深(m)" prop="currentDepth">
+          <el-input-number
+            :min="0"
+            v-model="form.currentDepth"
+            placeholder="请输入当前井深(m)"
+            :disabled="isMainFieldDisabled"
+            :controls="false"
+            align="left"
+          />
+        </el-form-item>
+        <el-form-item label="当日用电量(kWh)" prop="dailyPowerUsage">
+          <el-input-number
+            :min="0"
+            v-model="form.dailyPowerUsage"
+            placeholder="请输入当日用电量(kWh)"
+            clearable
+            :disabled="isMainFieldDisabled"
+            :controls="false"
+            align="left"
+          />
+        </el-form-item>
+        <el-form-item label="当日油耗(升)" prop="dailyFuel">
+          <el-input-number
+            :min="0"
+            v-model="form.dailyFuel"
+            placeholder="请输入当日油耗(升)"
+            clearable
+            :class="{ 'warning-input': (form.dailyFuel ?? 0) > 9000 }"
+            :disabled="isMainFieldDisabled"
+            :controls="false"
+            align="left"
+          />
+        </el-form-item>
+        <el-form-item label="泥浆密度(g/cm³)" prop="mudDensity">
+          <el-input-number
+            :min="0"
+            v-model="form.mudDensity"
+            placeholder="请输入泥浆性能-密度(g/cm³)"
+            clearable
+            :disabled="isMainFieldDisabled"
+            :controls="false"
+            align="left"
+          />
+        </el-form-item>
+        <el-form-item label="泥浆粘度(S)" prop="mudViscosity">
+          <el-input-number
+            :min="0"
+            v-model="form.mudViscosity"
+            placeholder="请输入泥浆性能-粘度(S)"
+            clearable
+            :disabled="isMainFieldDisabled"
+            :controls="false"
+            align="left"
+          />
+        </el-form-item>
+        <el-form-item label="水平段长度(m)" prop="lateralLength">
+          <el-input-number
+            :min="0"
+            v-model="form.lateralLength"
+            placeholder="请输入水平段长度(m)"
+            clearable
+            :disabled="isMainFieldDisabled"
+            :controls="false"
+            align="left"
+          />
+        </el-form-item>
+        <el-form-item label="井斜(°)" prop="wellInclination">
+          <el-input-number
+            :min="0"
+            v-model="form.wellInclination"
+            placeholder="请输入井斜(°)"
+            clearable
+            :disabled="isMainFieldDisabled"
+            :controls="false"
+            align="left"
+          />
+        </el-form-item>
+        <el-form-item label="方位(°)" prop="azimuth">
+          <el-input-number
+            :min="0"
+            v-model="form.azimuth"
+            placeholder="请输入方位(°)"
+            clearable
+            :disabled="isMainFieldDisabled"
+            :controls="false"
+            align="left"
+          />
+        </el-form-item>
+        <el-form-item label="设计井身结构" prop="designWellStruct">
+          <el-input
+            v-model="form.designWellStruct"
+            placeholder=""
+            type="textarea"
+            disabled
+            autosize
+          />
+        </el-form-item>
+        <el-form-item label="人员情况" prop="personnel">
+          <el-input
+            v-model="form.personnel"
+            placeholder="请输入人员情况"
+            type="textarea"
+            :max-length="1000"
+            autosize
+            :disabled="isMainFieldDisabled"
+          />
+        </el-form-item>
+        <el-form-item class="col-span-2" label="生产动态" prop="productionStatus">
+          <el-input
+            v-model="form.productionStatus"
+            type="textarea"
+            autosize
+            maxlength="1000"
+            :disabled="isMainFieldDisabled"
+          />
+        </el-form-item>
+        <el-form-item class="col-span-2" label="备注" prop="remark">
+          <el-input
+            v-model="form.remark"
+            type="textarea"
+            autosize
+            maxlength="1000"
+            :disabled="isMainFieldDisabled"
+          />
+        </el-form-item>
+
+        <!-- 时间信息 -->
+        <div class="col-span-2 flex items-center gap-2 mt-4">
+          <div class="bg-[var(--el-color-primary)] w-1 h-5 rounded-full"></div>
+          <div class="text-lg font-medium text-[var(--el-text-color-primary)]"
+            >生产与非生产时间</div
+          >
+        </div>
+
+        <el-form-item label="进尺工作时间(H)" prop="drillingWorkingTime">
+          <el-input-number
+            class="!w-full"
+            :min="0"
+            :max="24"
+            v-model="form.drillingWorkingTime"
+            :controls="false"
+            align="left"
+            placeholder="进尺+其他+非生产=24H"
+            :disabled="isMainFieldDisabled"
+            :class="{ 'orange-input': orange }"
+          />
+        </el-form-item>
+        <el-form-item label="其它生产时间(H)" prop="otherProductionTime">
+          <el-input-number
+            class="!w-full"
+            :min="0"
+            :max="24"
+            v-model="form.otherProductionTime"
+            :controls="false"
+            align="left"
+            placeholder="请输入其它生产时间(H)"
+            :disabled="isMainFieldDisabled"
+            :class="{ 'orange-input': orange }"
+          />
+        </el-form-item>
+
+        <!-- 循环渲染11个具体非生产时间 -->
+        <el-form-item
+          v-for="field in NON_PROD_FIELDS"
+          :key="field.key"
+          :label="field.label + '(H)'"
+          :prop="field.key"
+        >
+          <el-input-number
+            class="!w-full"
+            :min="0"
+            :max="24"
+            v-model="form[field.key as keyof FormOriginal]"
+            :controls="false"
+            align="left"
+            :disabled="isMainFieldDisabled"
+            :class="{ 'orange-input': orange }"
+          />
+        </el-form-item>
+
+        <!-- 其他非生产原因 -->
+        <el-form-item class="col-span-2" label="其他非生产原因" prop="otherNptReason">
+          <el-input
+            v-model="form.otherNptReason"
+            placeholder="请输入其他非生产原因"
+            :disabled="isMainFieldDisabled"
+          />
+        </el-form-item>
+
+        <!-- 审批意见 -->
+        <div v-if="isApproval" class="col-span-2 mt-4 border-t pt-4">
+          <el-form-item label="审批意见" prop="opinion">
+            <el-input
+              v-model="form.opinion"
+              placeholder="请输入审批意见"
+              maxlength="1000"
+              type="textarea"
+              :autosize="{ minRows: 3 }"
+              :disabled="formType === 'readonly'"
+            />
+          </el-form-item>
+        </div>
+      </div>
+    </el-form>
+
+    <template #footer>
+      <div v-if="isEdit">
+        <el-button
+          size="default"
+          type="primary"
+          @click="submitForm"
+          :loading="formLoading"
+          :disabled="formType === 'readonly'"
+          >确 定</el-button
+        >
+        <el-button size="default" @click="emits('update:visible', false)">取 消</el-button>
+      </div>
+      <div v-if="isApproval">
+        <el-button
+          size="default"
+          type="primary"
+          @click="handleAudit(20)"
+          :loading="formLoading"
+          :disabled="formType === 'readonly'"
+          >审批通过</el-button
+        >
+        <el-button
+          size="default"
+          type="danger"
+          @click="handleAudit(30)"
+          :loading="formLoading"
+          :disabled="formType === 'readonly'"
+          >审批拒绝</el-button
+        >
+        <el-button size="default" @click="emits('update:visible', false)">取 消</el-button>
+      </div>
+    </template>
+  </el-drawer>
+</template>
+
+<style scoped>
+:deep(.warning-input) {
+  .el-input__inner {
+    color: red !important;
+    -webkit-text-fill-color: red !important;
+  }
+}
+
+:deep(.blue-input) {
+  .el-input__inner {
+    color: blue !important;
+    -webkit-text-fill-color: blue !important;
+  }
+}
+
+:deep(.orange-input) {
+  .el-input__inner {
+    color: orange !important;
+    -webkit-text-fill-color: orange !important;
+  }
+}
+</style>

+ 690 - 0
src/views/pms/iotrydailyreport/ry-xj-form.vue

@@ -0,0 +1,690 @@
+<script lang="ts" setup generic="T">
+import { IotRyDailyReportApi } from '@/api/pms/iotrydailyreport'
+import { DICT_TYPE, getStrDictOptions } from '@/utils/dict'
+import { FormInstance, FormRules } from 'element-plus'
+import { computed, reactive, ref, watch, nextTick } from 'vue'
+
+interface Props {
+  visible: boolean
+  type?: 'edit' | 'approval' | 'readonly'
+  loadList: () => void
+}
+
+const props = withDefaults(defineProps<Props>(), {
+  type: 'edit'
+})
+
+const emits = defineEmits(['update:visible'])
+
+// 1. 定义具体的 11 个非生产时间字段
+const NON_PROD_FIELDS = [
+  { key: 'repairTime', label: '设备故障' },
+  { key: 'selfStopTime', label: '设备保养' },
+  { key: 'accidentTime', label: '工程质量' },
+  { key: 'complexityTime', label: '技术受限' },
+  { key: 'rectificationTime', label: '生产组织' },
+  { key: 'waitingStopTime', label: '不可抗力' },
+  { key: 'partyaDesign', label: '甲方设计' },
+  { key: 'partyaPrepare', label: '甲方准备' },
+  { key: 'partyaResource', label: '甲方资源' },
+  { key: 'relocationTime', label: '生产配合' },
+  { key: 'winterBreakTime', label: '待命' },
+  { key: 'otherNptTime', label: '其他非生产时间' }
+] as const
+
+interface FormOriginal {
+  id: number
+  deptId: number
+  projectId: number
+  taskId: number
+  deptName: string
+  contractName: string
+  taskName: string
+  repairStatus: string
+  technique: string
+  wellCategory: string
+  designWellDepth: string
+  wellControlLevel: string
+  casingPipeSize: string
+  dailyFuel: number
+  currentOperation: string
+  nextPlan: string
+  ratedProductionTime: number
+  productionTime: number
+  totalStaffNum: number
+  onDutyStaffNum: number
+  leaveStaffNum: number
+
+  // 11个非生产时间字段
+  repairTime: number
+  selfStopTime: number
+  accidentTime: number
+  complexityTime: number
+  rectificationTime: number
+  waitingStopTime: number
+  partyaDesign: number
+  partyaPrepare: number
+  partyaResource: number
+  relocationTime: number
+  winterBreakTime: number
+  otherNptTime: number
+
+  // 其他非生产时间原因(仅作为备注字段存在)
+  otherNptReason: string
+
+  productionStatus: string
+  remark: string
+  createTime: string
+  opinion: string
+}
+
+type Form = Partial<FormOriginal>
+
+// 字段白名单
+const FORM_KEYS: (keyof FormOriginal)[] = [
+  'id',
+  'deptId',
+  'projectId',
+  'taskId',
+  'deptName',
+  'contractName',
+  'taskName',
+  'repairStatus',
+  'technique',
+  'wellCategory',
+  'designWellDepth',
+  'wellControlLevel',
+  'casingPipeSize',
+  'dailyFuel',
+  'currentOperation',
+  'nextPlan',
+  'ratedProductionTime',
+  'productionTime',
+  'totalStaffNum',
+  'onDutyStaffNum',
+  'leaveStaffNum',
+  'productionStatus',
+  'remark',
+  'createTime',
+  'opinion',
+  'repairTime',
+  'selfStopTime',
+  'accidentTime',
+  'complexityTime',
+  'rectificationTime',
+  'waitingStopTime',
+  'partyaDesign',
+  'partyaPrepare',
+  'partyaResource',
+  'relocationTime',
+  'winterBreakTime',
+  'otherNptTime',
+  'otherNptReason'
+]
+
+const formRef = ref<FormInstance>()
+const loading = ref(false)
+const formLoading = ref(false)
+const formType = ref<'edit' | 'readonly'>('edit')
+const message = useMessage()
+const { t } = useI18n()
+
+// 初始化表单
+const initFormData = (): Form => {
+  const base: any = {
+    ratedProductionTime: 0,
+    productionTime: 0
+  }
+  // 初始化所有非生产时间字段为 0
+  NON_PROD_FIELDS.forEach((field) => {
+    base[field.key] = 0
+  })
+  return base as Form
+}
+
+const form = ref<Form>(initFormData())
+
+const isApproval = computed(() => props.type === 'approval')
+const isEdit = computed(() => props.type === 'edit')
+const isMainFieldDisabled = computed(() => formType.value === 'readonly' || isApproval.value)
+
+async function loadDetail(id: number) {
+  loading.value = true
+  try {
+    const res = await IotRyDailyReportApi.getIotRyDailyReport(id)
+    form.value = initFormData()
+    // 按需赋值
+    FORM_KEYS.forEach((key) => {
+      if (
+        Object.prototype.hasOwnProperty.call(res, key) &&
+        res[key] !== null &&
+        res[key] !== undefined
+      ) {
+        // @ts-ignore
+        form.value[key] = res[key]
+      }
+    })
+
+    if (props.type === 'edit' && res.status !== 0) formType.value = 'readonly'
+    if (props.type === 'approval' && res.auditStatus !== 10) formType.value = 'readonly'
+  } finally {
+    loading.value = false
+  }
+}
+
+function handleOpenForm(id: number, type: 'edit' | 'readonly') {
+  formType.value = type
+  emits('update:visible', true)
+  loadDetail(id).then(() => {
+    nextTick(() => formRef.value?.clearValidate())
+  })
+}
+
+defineExpose({ handleOpenForm })
+
+// --- 运行时效 ---
+const transitTime = computed(() => {
+  const cap = form.value.productionTime ?? 0
+  const gas = form.value.ratedProductionTime ?? 0
+
+  if (!gas) return { original: 0, value: '0%' }
+
+  const original = cap / gas
+  return { original, value: (original * 100).toFixed(2) + '%' }
+})
+// 在线人数 = 总人数 - 离职人数
+const onDutyStaffNum = computed(() => {
+  return (form.value.totalStaffNum ?? 0) - (form.value.leaveStaffNum ?? 0)
+})
+
+// --- 核心校验逻辑 ---
+
+// 计算所有非生产时间总和
+const sumNonProdTimes = () => {
+  let sum = 0
+  NON_PROD_FIELDS.forEach((field) => {
+    sum += (form.value[field.key as keyof FormOriginal] as number) || 0
+  })
+
+  return sum
+}
+
+// 24小时平衡校验器
+const validateTotalTime =
+  (isNon: boolean = false) =>
+  (_rule: any, _value: any, callback: any) => {
+    const rateTime = form.value.ratedProductionTime || 0
+    const time = form.value.productionTime || 0
+
+    const nonProdSum = sumNonProdTimes()
+
+    let total = 0
+    let msg = ''
+
+    total = parseFloat((time + nonProdSum).toFixed(2))
+    msg = `生产(${time})+非生产(${nonProdSum})=${total}H,必须等于额定${rateTime}H`
+
+    if (Math.abs(total - rateTime) > 0.01) {
+      if (!isNon) callback(new Error(msg))
+      else callback(new Error())
+    } else {
+      callback()
+    }
+  }
+
+const validateOtherReason = (_rule: any, value: any, callback: any) => {
+  const time = form.value.otherNptTime || 0
+  if (time > 0 && !value) {
+    callback(new Error('填写了其他时间,必须说明原因'))
+  } else {
+    callback()
+  }
+}
+
+// 动态构建校验规则
+const rules = reactive<FormRules>({
+  repairStatus: [{ required: true, message: '请选择施工状态', trigger: ['change', 'blur'] }],
+  productionStatus: [{ required: true, message: '请输入生产动态', trigger: ['blur', 'change'] }],
+
+  // 生产时间绑定校验
+  ratedProductionTime: [
+    { required: true, message: '请输入额定生产时间', trigger: ['blur', 'change'] },
+    { validator: validateTotalTime(), trigger: ['blur', 'change'] }
+  ],
+  productionTime: [
+    { required: true, message: '请输入生产时间', trigger: ['blur', 'change'] },
+    { validator: validateTotalTime(), trigger: ['blur', 'change'] }
+  ],
+  otherProductionTime: [{ validator: validateTotalTime(), trigger: ['blur', 'change'] }],
+  otherNptReason: [{ validator: validateOtherReason, trigger: ['blur', 'change'] }]
+})
+
+// 关键步骤:为每一个非生产时间字段都绑定 validateTotalTime 规则
+// 这样当总和不对时,所有时间字段下面都会出现红色错误提示
+NON_PROD_FIELDS.forEach((field) => {
+  rules[field.key] = [{ validator: validateTotalTime(true), trigger: ['blur', 'change'] }]
+})
+
+// 监听所有时间字段
+const allTimeKeys = ['ratedProductionTime', 'productionTime', ...NON_PROD_FIELDS.map((f) => f.key)]
+
+// 当任一时间变化时,触发所有时间字段的校验更新
+watch(
+  () => allTimeKeys.map((key) => form.value[key as keyof FormOriginal]),
+  () => {
+    if (!isMainFieldDisabled.value) {
+      nextTick(() => {
+        // 传入数组,同时校验所有字段
+        formRef.value?.validateField(allTimeKeys)
+      })
+    }
+  },
+  { deep: true }
+)
+
+// --- 提交 ---
+const submitForm = async () => {
+  if (!formRef.value) return
+
+  try {
+    await formRef.value.validate()
+    formLoading.value = true
+    const submitData: any = {}
+    FORM_KEYS.forEach((key) => (submitData[key] = form.value[key]))
+    submitData.fillOrderCreateTime = form.value.createTime
+    submitData.projectClassification = '2'
+
+    await IotRyDailyReportApi.createIotRyDailyReport(submitData)
+    message.success(t('common.updateSuccess'))
+    emits('update:visible', false)
+    props.loadList()
+  } catch (error) {
+    console.warn('校验失败')
+  } finally {
+    formLoading.value = false
+  }
+}
+
+const handleAudit = async (auditStatus: 20 | 30) => {
+  if (!formRef.value) return
+  try {
+    formLoading.value = true
+    await IotRyDailyReportApi.approvalIotRyDailyReport({
+      id: form.value.id!,
+      auditStatus,
+      opinion: form.value.opinion!
+    })
+    message.success(auditStatus === 20 ? '通过成功' : '拒绝成功')
+    emits('update:visible', false)
+    props.loadList()
+  } catch (error) {
+    console.warn(error)
+  } finally {
+    formLoading.value = false
+  }
+}
+
+const orange = computed(() => {
+  const rateTime = form.value.ratedProductionTime || 0
+  const time = form.value.productionTime || 0
+
+  const nonProdSum = sumNonProdTimes()
+
+  let total = 0
+
+  total = parseFloat((time + nonProdSum).toFixed(2))
+
+  if (Math.abs(total - rateTime) > 0.01) return true
+  return false
+})
+</script>
+
+<template>
+  <el-drawer
+    :model-value="visible"
+    @update:model-value="emits('update:visible', $event)"
+    header-class="mb-0!"
+    size="50%"
+  >
+    <template #header>
+      <span class="text-xl font-bold text-[var(--el-text-color-primary)]">
+        {{ type === 'edit' ? '编辑日报' : '审批日报' }}
+      </span>
+    </template>
+
+    <el-form
+      ref="formRef"
+      label-position="top"
+      size="default"
+      :rules="rules"
+      :model="form"
+      v-loading="loading"
+      require-asterisk-position="right"
+      :disabled="formType === 'readonly' && type !== 'approval'"
+    >
+      <!-- 顶部提示区 -->
+      <div class="flex flex-col gap-3 text-sm mb-4">
+        <div
+          class="rounded-md border border-blue-100 bg-blue-50/80 p-3 dark:border-blue-900/30 dark:bg-blue-900/10"
+        >
+          <div class="flex flex-col gap-2.5">
+            <div class="flex items-center justify-between">
+              <div class="text-gray-600 dark:text-gray-400">
+                <span class="font-bold text-gray-800 dark:text-gray-200">运行时效:</span>
+                生产时间/额定生产时间
+              </div>
+              <span
+                class="inline-flex items-center rounded border border-red-200 bg-red-100 px-2 py-0.5 text-xs font-medium text-red-600 dark:bg-red-900/20 dark:border-red-800 dark:text-red-400"
+              >
+                >100% 红色预警
+              </span>
+            </div>
+            <div class="flex items-center justify-between">
+              <div class="text-gray-600 dark:text-gray-400">
+                <span class="font-bold text-gray-800 dark:text-gray-200">时间平衡:</span>
+                生产 + 非生产 = 额定生产
+              </div>
+              <span
+                class="inline-flex items-center rounded border border-orange-200 bg-orange-100 px-2 py-0.5 text-xs font-medium text-orange-600 dark:bg-orange-900/20 dark:border-orange-800 dark:text-orange-400"
+              >
+                ≠额定生产 橙色预警
+              </span>
+            </div>
+            <div class="flex items-center justify-between">
+              <div class="text-gray-600 dark:text-gray-400">
+                <span class="font-bold text-gray-800 dark:text-gray-200"> 油量消耗:</span>
+                当日油耗
+              </div>
+              <span
+                class="inline-flex items-center rounded border border-blue-200 bg-blue-100 px-2 py-0.5 text-xs font-medium text-blue-600 dark:bg-blue-900/20 dark:border-blue-800 dark:text-blue-400"
+              >
+                >3500升 蓝色预警
+              </span>
+            </div>
+          </div>
+        </div>
+        <div
+          v-if="isEdit && form.opinion"
+          class="flex gap-3 rounded-md border border-yellow-200 bg-yellow-50 p-3 dark:border-yellow-800 dark:bg-yellow-900/10"
+        >
+          <Icon
+            icon="ep:warning-filled"
+            class="mt-0.5 shrink-0 text-base text-yellow-600 dark:text-yellow-500"
+          />
+          <div class="flex flex-col">
+            <h4 class="mb-1 font-bold text-yellow-800 dark:text-yellow-500">审核意见</h4>
+            <p class="leading-relaxed text-gray-600 dark:text-gray-400">{{ form.opinion }}</p>
+          </div>
+        </div>
+      </div>
+
+      <div class="grid grid-cols-2 gap-4">
+        <!-- 基础信息 -->
+        <div class="col-span-2 flex items-center gap-2 mt-2">
+          <div class="bg-[var(--el-color-primary)] w-1 h-5 rounded-full"></div>
+          <div class="text-lg font-medium text-[var(--el-text-color-primary)]">基础信息</div>
+        </div>
+        <el-form-item label="施工队伍" prop="deptName"
+          ><el-input v-model="form.deptName" disabled
+        /></el-form-item>
+        <el-form-item label="项目" prop="contractName"
+          ><el-input v-model="form.contractName" disabled
+        /></el-form-item>
+        <el-form-item label="任务" prop="taskName"
+          ><el-input v-model="form.taskName" disabled
+        /></el-form-item>
+        <el-form-item :label="t('project.status')" prop="repairStatus">
+          <el-select
+            v-model="form.repairStatus"
+            placeholder="请选择"
+            clearable
+            :disabled="isMainFieldDisabled"
+          >
+            <el-option
+              v-for="(dict, index) in getStrDictOptions(
+                DICT_TYPE.PMS_PROJECT_TASK_RY_REPAIR_SCHEDULE
+              )"
+              :key="index"
+              :label="dict.label"
+              :value="dict.value"
+            />
+          </el-select>
+        </el-form-item>
+        <el-form-item :label="t('project.technology')" prop="technique">
+          <el-select v-model="form.technique" placeholder="请选择" disabled>
+            <el-option
+              v-for="(dict, index) in getStrDictOptions(DICT_TYPE.PMS_PROJECT_RY_TECHNOLOGY)"
+              :key="index"
+              :label="dict.label"
+              :value="dict.value"
+            />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="井别" prop="wellCategory">
+          <el-input v-model="form.wellCategory" placeholder="" disabled />
+        </el-form-item>
+        <el-form-item label="设计井深(m)" prop="designWellDepth">
+          <el-input v-model="form.designWellDepth" placeholder="" disabled />
+        </el-form-item>
+        <el-form-item label="井控级别" prop="wellControlLevel">
+          <el-input v-model="form.wellControlLevel" placeholder="" disabled />
+        </el-form-item>
+        <el-form-item label="套生段产管尺寸(mm)" prop="casingPipeSize">
+          <el-input v-model="form.casingPipeSize" placeholder="" disabled />
+        </el-form-item>
+        <el-form-item label="当日油耗(升)" prop="dailyFuel">
+          <el-input-number
+            class="w-full!"
+            :min="0"
+            v-model="form.dailyFuel"
+            :class="{ 'blue-input': (form.dailyFuel ?? 0) > 3500 }"
+            placeholder="请输入当日油耗(升)"
+            :disabled="isMainFieldDisabled"
+            :controls="false"
+            align="left"
+          />
+        </el-form-item>
+        <el-form-item :label="t('project.currentOperation')" prop="currentOperation">
+          <el-input
+            v-model="form.currentOperation"
+            placeholder="请输入目前工序"
+            type="textarea"
+            autosize
+            :maxlength="1000"
+            :disabled="isMainFieldDisabled"
+          />
+        </el-form-item>
+        <el-form-item :label="t('project.nextPlan')" prop="nextPlan">
+          <el-input
+            v-model="form.nextPlan"
+            placeholder="请输入下步工序"
+            type="textarea"
+            autosize
+            :maxlength="1000"
+            :disabled="isMainFieldDisabled"
+          />
+        </el-form-item>
+        <el-form-item label="运行时效" prop="transitTime">
+          <el-input
+            :model-value="transitTime.value"
+            placeholder="运行时效"
+            disabled
+            :class="{ 'warning-input': transitTime.original >= 1.0 }"
+            id="transitTimeInput"
+          />
+        </el-form-item>
+        <el-form-item label="全员数量" prop="totalStaffNum">
+          <el-input-number
+            :disabled="isMainFieldDisabled"
+            :controls="false"
+            align="left"
+            :min="0"
+            v-model="form.totalStaffNum"
+            placeholder="请输入全员数量"
+          />
+        </el-form-item>
+        <el-form-item label="在岗人数" prop="onDutyStaffNum">
+          <el-input-number :min="0" v-model="onDutyStaffNum" placeholder="" disabled />
+        </el-form-item>
+        <el-form-item label="休假人员数量" prop="leaveStaffNum">
+          <el-input-number
+            :disabled="isMainFieldDisabled"
+            :controls="false"
+            align="left"
+            :min="0"
+            v-model="form.leaveStaffNum"
+            placeholder="请输入休假人员数量"
+          />
+        </el-form-item>
+        <el-form-item class="col-span-2" label="生产动态" prop="productionStatus">
+          <el-input
+            v-model="form.productionStatus"
+            type="textarea"
+            autosize
+            maxlength="1000"
+            :disabled="isMainFieldDisabled"
+          />
+        </el-form-item>
+        <el-form-item class="col-span-2" label="备注" prop="remark">
+          <el-input
+            v-model="form.remark"
+            type="textarea"
+            autosize
+            maxlength="1000"
+            :disabled="isMainFieldDisabled"
+          />
+        </el-form-item>
+
+        <!-- 时间信息 -->
+        <div class="col-span-2 flex items-center gap-2 mt-4">
+          <div class="bg-[var(--el-color-primary)] w-1 h-5 rounded-full"></div>
+          <div class="text-lg font-medium text-[var(--el-text-color-primary)]"
+            >生产与非生产时间</div
+          >
+        </div>
+
+        <el-form-item label="额定生产时间(H)" prop="ratedProductionTime">
+          <el-input-number
+            class="!w-full"
+            :min="0"
+            v-model="form.ratedProductionTime"
+            :controls="false"
+            align="left"
+            placeholder="请输入额定生产时间(H)"
+            :disabled="isMainFieldDisabled"
+            :class="{ 'orange-input': orange }"
+          />
+        </el-form-item>
+        <el-form-item label="生产时间(H)" prop="productionTime">
+          <el-input-number
+            class="!w-full"
+            :min="0"
+            v-model="form.productionTime"
+            :controls="false"
+            align="left"
+            placeholder="请输入生产时间(H)"
+            :disabled="isMainFieldDisabled"
+            :class="{ 'orange-input': orange }"
+          />
+        </el-form-item>
+
+        <!-- 循环渲染11个具体非生产时间 -->
+        <el-form-item
+          v-for="field in NON_PROD_FIELDS"
+          :key="field.key"
+          :label="field.label + '(H)'"
+          :prop="field.key"
+        >
+          <el-input-number
+            class="!w-full"
+            :min="0"
+            v-model="form[field.key as keyof FormOriginal]"
+            :controls="false"
+            align="left"
+            :disabled="isMainFieldDisabled"
+            :class="{ 'orange-input': orange }"
+          />
+        </el-form-item>
+
+        <!-- 其他非生产原因 -->
+        <el-form-item class="col-span-2" label="其他非生产原因" prop="otherNptReason">
+          <el-input
+            v-model="form.otherNptReason"
+            placeholder="请输入其他非生产原因"
+            :disabled="isMainFieldDisabled"
+          />
+        </el-form-item>
+
+        <!-- 审批意见 -->
+        <div v-if="isApproval" class="col-span-2 mt-4 border-t pt-4">
+          <el-form-item label="审批意见" prop="opinion">
+            <el-input
+              v-model="form.opinion"
+              placeholder="请输入审批意见"
+              maxlength="1000"
+              type="textarea"
+              :autosize="{ minRows: 3 }"
+              :disabled="formType === 'readonly'"
+            />
+          </el-form-item>
+        </div>
+      </div>
+    </el-form>
+
+    <template #footer>
+      <div v-if="isEdit">
+        <el-button
+          size="default"
+          type="primary"
+          @click="submitForm"
+          :loading="formLoading"
+          :disabled="formType === 'readonly'"
+          >确 定</el-button
+        >
+        <el-button size="default" @click="emits('update:visible', false)">取 消</el-button>
+      </div>
+      <div v-if="isApproval">
+        <el-button
+          size="default"
+          type="primary"
+          @click="handleAudit(20)"
+          :loading="formLoading"
+          :disabled="formType === 'readonly'"
+          >审批通过</el-button
+        >
+        <el-button
+          size="default"
+          type="danger"
+          @click="handleAudit(30)"
+          :loading="formLoading"
+          :disabled="formType === 'readonly'"
+          >审批拒绝</el-button
+        >
+        <el-button size="default" @click="emits('update:visible', false)">取 消</el-button>
+      </div>
+    </template>
+  </el-drawer>
+</template>
+
+<style scoped>
+:deep(.warning-input) {
+  .el-input__inner {
+    color: red !important;
+    -webkit-text-fill-color: red !important;
+  }
+}
+
+:deep(.blue-input) {
+  .el-input__inner {
+    color: blue !important;
+    -webkit-text-fill-color: blue !important;
+  }
+}
+
+:deep(.orange-input) {
+  .el-input__inner {
+    color: orange !important;
+    -webkit-text-fill-color: orange !important;
+  }
+}
+</style>

+ 22 - 7
src/views/pms/iotrydailyreport/summary.vue

@@ -47,6 +47,13 @@ const totalWorkKeys: [string, string, string, string, number][] = [
     0
   ],
   ['notReported', '个', '未填报', 'i-material-symbols:cancel-outline-rounded text-rose', 0],
+  [
+    'averageFuelConsumption',
+    '升',
+    '平均油耗',
+    'i-material-symbols:directions-car-outline-rounded text-sky',
+    2
+  ],
   [
     'totalFuelConsumption',
     '升',
@@ -70,7 +77,8 @@ const totalWork = ref({
   notReported: 0,
   totalFuelConsumption: 0,
   totalPowerConsumption: 0,
-  totalFootage: 0
+  totalFootage: 0,
+  averageFuelConsumption: 0
 })
 
 const totalLoading = ref(false)
@@ -101,7 +109,8 @@ const getTotal = useDebounceFn(async () => {
       ...res2,
       totalPowerConsumption: (res2.totalPowerConsumption || 0) / 1000,
       totalGasInjection: (res2.totalGasInjection || 0) / 10000,
-      totalFuelConsumption: res2.totalFuelConsumption || 0
+      totalFuelConsumption: res2.totalFuelConsumption || 0,
+      averageFuelConsumption: res2.averageFuelConsumption || 0
     }
   } finally {
     totalLoading.value = false
@@ -118,6 +127,7 @@ interface List {
   cumulativeFuelConsumption: number | null
   transitTime: number | null
   nonProductiveTime: number | null
+  averageFuelConsumption: number | null
 }
 
 const list = ref<List[]>([])
@@ -142,6 +152,10 @@ const columns = (type: string) => {
       label: '累计油耗(升)',
       prop: 'cumulativeFuelConsumption'
     },
+    {
+      label: '平均油耗(升)',
+      prop: 'averageFuelConsumption'
+    },
     {
       label: '平均时效(%)',
       prop: 'transitTime'
@@ -178,7 +192,8 @@ const getList = useDebounceFn(async () => {
         name: type === '2' ? projectDeptName : teamName,
         ...other,
         cumulativePowerConsumption: ((other.cumulativePowerConsumption || 0) / 1000).toFixed(2),
-        cumulativeFuelConsumption: (other.cumulativeFuelConsumption || 0).toFixed(2)
+        cumulativeFuelConsumption: (other.cumulativeFuelConsumption || 0).toFixed(2),
+        averageFuelConsumption: (other.averageFuelConsumption || 0).toFixed(2)
       })
     )
   } finally {
@@ -212,9 +227,9 @@ const xAxisData = ref<string[]>([])
 
 const legend = ref<string[][]>([
   // ['累计油耗 (万升)', 'cumulativeFuelConsumption'],
-  ['累计油耗 (升)', 'cumulativeFuelConsumption'],
-  ['累计进尺 (M)', 'cumulativeFootage'],
-  ['累计用电量 (KWh)', 'cumulativePowerConsumption'],
+  ['油耗 (升)', 'cumulativeFuelConsumption'],
+  ['进尺 (M)', 'cumulativeFootage'],
+  ['用电量 (KWh)', 'cumulativePowerConsumption'],
   // ['累计用电量 (MWh)', 'cumulativePowerConsumption'],
   ['平均时效 (%)', 'transitTime']
 ])
@@ -527,7 +542,7 @@ const tolist = (id: number, non: boolean = false) => {
           <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
         </el-form-item>
       </el-form>
-      <div class="grid grid-cols-6 gap-8">
+      <div class="grid grid-cols-7 gap-8">
         <div
           v-for="info in totalWorkKeys"
           :key="info[0]"

+ 11 - 428
src/views/pms/iotrydailyreport/xapproval.vue

@@ -2,12 +2,11 @@
 import { rangeShortcuts } from '@/utils/formatTime'
 import { useDebounceFn } from '@vueuse/core'
 import dayjs from 'dayjs'
-import { DICT_TYPE, getStrDictOptions } from '@/utils/dict'
+import { DICT_TYPE } from '@/utils/dict'
 import { TableColumnCtx } from 'element-plus/es/components/table/src/table-column/defaults'
-import { FormInstance, FormRules } from 'element-plus'
-import Form from '@/components/Form/src/Form.vue'
 import { useUserStore } from '@/store/modules/user'
 import { IotRyDailyReportApi } from '@/api/pms/iotrydailyreport'
+import ryXjForm from './ry-xj-form.vue'
 
 interface List {
   id: number
@@ -433,178 +432,23 @@ watch(
   { immediate: true }
 )
 
-const FORM_KEYS = [
-  'id',
-  'deptId',
-  'projectId',
-  'taskId',
-  'deptName',
-  'contractName',
-  'taskName',
-  'repairStatus',
-  'technique',
-  'wellCategory',
-  'designWellDepth',
-  'casingPipeSize',
-  'wellControlLevel',
-  'currentOperation',
-  'nextPlan',
-  'transitTime',
-  'ratedProductionTime',
-  'productionTime',
-  'nonProductionTime',
-  'ryNptReason',
-  'productionStatus',
-  'totalStaffNum',
-  'onDutyStaffNum',
-  'leaveStaffNum',
-  'remark',
-  'opinion',
-  'createTime',
-  'dailyFuel'
-] as const
+const visible = ref(false)
 
-type FormKey = (typeof FORM_KEYS)[number]
-type Form = Partial<Pick<List, FormKey>>
+const formRef = ref()
 
-const dialogVisible = ref(false)
-const formRef = ref<FormInstance>()
-const formLoading = ref(false)
-const message = useMessage()
-
-const initFormData = (): Form => ({})
-
-const form = ref<Form>(initFormData())
-
-async function loadDetail(id: number) {
-  try {
-    const res = await IotRyDailyReportApi.getIotRyDailyReport(id)
-    FORM_KEYS.forEach((key) => {
-      form.value[key] = res[key] ?? form.value[key]
-    })
-    form.value.id = id
-
-    if (res.auditStatus !== 10) {
-      formType.value = 'readonly'
-    }
-  } finally {
+function handleOpenForm(id: number, type: 'edit' | 'readonly') {
+  if (formRef.value) {
+    formRef.value.handleOpenForm(id, type)
   }
 }
 
-const formType = ref<'approval' | 'readonly'>('approval')
-
-function handleOpenForm(id: number, type: 'approval' | 'readonly') {
-  form.value = initFormData()
-  formRef.value?.resetFields()
-
-  formType.value = type
-
-  dialogVisible.value = true
-  loadDetail(id).then(() => {
-    formRef.value?.validate()
-  })
-}
-
 const route = useRoute()
 
 onMounted(() => {
   if (Object.keys(route.query).length > 0) {
-    handleOpenForm(Number(route.query.id), 'approval')
+    handleOpenForm(Number(route.query.id), 'edit')
   }
 })
-
-const transitTime = computed(() => {
-  const cap = form.value.productionTime ?? 0
-  const gas = form.value.ratedProductionTime ?? 0
-
-  if (!gas) return { original: 0, value: '0%' }
-
-  const original = cap / gas
-  return { original, value: (original * 100).toFixed(2) + '%' }
-})
-
-const onDutyStaffNum = computed(() => {
-  return (form.value.totalStaffNum ?? 0) - (form.value.leaveStaffNum ?? 0)
-})
-
-const sumTimes = () => {
-  const { productionTime = 0, nonProductionTime = 0 } = form.value
-  return productionTime + nonProductionTime
-}
-
-const validateTotalTime = (_rule: any, _value: any, callback: any) => {
-  const total = sumTimes()
-  if (total !== form.value.ratedProductionTime) {
-    callback(new Error(`生产时间和非生产时间之和必须等于额定生产时间`))
-  } else {
-    callback()
-  }
-}
-
-const validateNptReason = (_rule: any, value: any, callback: any) => {
-  if ((form.value.nonProductionTime || 0) > 0 && !value) {
-    callback(new Error('非生产时间大于 0 时,必须选择原因'))
-  } else {
-    callback()
-  }
-}
-
-const timeRuleItem = [
-  { required: true, message: '请输入时间', trigger: 'blur' },
-  { validator: validateTotalTime, trigger: 'blur' }
-]
-
-const rules = reactive<FormRules>({
-  repairStatus: [{ required: true, message: '请输入施工状态', trigger: ['change', 'blur'] }],
-  productionStatus: [{ required: true, message: '请输入生产动态', trigger: ['change', 'blur'] }],
-
-  // 复用规则
-  productionTime: timeRuleItem,
-  nonProductionTime: timeRuleItem,
-  ratedProductionTime: timeRuleItem,
-
-  ryNptReason: [{ validator: validateNptReason, trigger: ['change', 'blur'] }]
-})
-
-watch(
-  [
-    () => form.value.productionTime,
-    () => form.value.nonProductionTime,
-    () => form.value.ratedProductionTime
-  ],
-  () => {
-    nextTick(() => {
-      formRef.value?.validateField('nptReason')
-      if (sumTimes() === form.value.ratedProductionTime) {
-        formRef.value?.clearValidate(['productionTime', 'nonProductionTime', 'ratedProductionTime'])
-      }
-    })
-  }
-)
-
-const { t } = useI18n()
-
-const submitForm = async (auditStatus: 20 | 30) => {
-  if (!formRef.value) return
-
-  try {
-    // await formRef.value.validate()
-    formLoading.value = true
-    const { opinion, id } = form.value
-
-    const data = { id: id, auditStatus, opinion } as any
-
-    await IotRyDailyReportApi.approvalIotRyDailyReport(data)
-    message.success(auditStatus === 20 ? '通过成功' : '拒绝成功')
-    dialogVisible.value = false
-
-    loadList()
-  } catch (error) {
-    console.warn('表单校验未通过或提交出错')
-  } finally {
-    formLoading.value = false
-  }
-}
 </script>
 
 <template>
@@ -689,7 +533,7 @@ const submitForm = async (auditStatus: 20 | 30) => {
                         v-show="row.auditStatus === 10"
                         link
                         type="primary"
-                        @click="handleOpenForm(row.id, 'approval')"
+                        @click="handleOpenForm(row.id, 'edit')"
                         v-hasPermi="['pms:iot-ry-daily-report:update']"
                       >
                         审批
@@ -717,269 +561,8 @@ const submitForm = async (auditStatus: 20 | 30) => {
         </div>
       </div>
     </div>
-    <Dialog title="编辑" v-model="dialogVisible">
-      <el-form
-        ref="formRef"
-        label-position="top"
-        size="default"
-        :rules="rules"
-        :model="form"
-        v-loading="formLoading"
-        require-asterisk-position="right"
-        :disabled="formType === 'readonly'"
-      >
-        <div class="flex flex-col gap-3 text-sm">
-          <div
-            class="rounded-md border border-blue-100 bg-blue-50/80 p-3 dark:border-blue-900/30 dark:bg-blue-900/10"
-          >
-            <div class="flex flex-col gap-2.5">
-              <div class="flex items-center justify-between">
-                <div class="text-gray-600 dark:text-gray-400">
-                  <span class="font-bold text-gray-800 dark:text-gray-200">运行时效:</span>
-                  生产时间/额定生产时间
-                </div>
-                <span
-                  class="inline-flex items-center rounded border border-red-200 bg-red-100 px-2 py-0.5 text-xs font-medium text-red-600 dark:bg-red-900/20 dark:border-red-800 dark:text-red-400"
-                >
-                  >100% 红色预警
-                </span>
-              </div>
-              <div class="flex items-center justify-between">
-                <div class="text-gray-600 dark:text-gray-400">
-                  <span class="font-bold text-gray-800 dark:text-gray-200">时间平衡:</span>
-                  生产 + 非生产 = 额定生产
-                </div>
-                <span
-                  class="inline-flex items-center rounded border border-orange-200 bg-orange-100 px-2 py-0.5 text-xs font-medium text-orange-600 dark:bg-orange-900/20 dark:border-orange-800 dark:text-orange-400"
-                >
-                  ≠额定生产 橙色预警
-                </span>
-              </div>
-              <div class="flex items-center justify-between">
-                <div class="text-gray-600 dark:text-gray-400">
-                  <span class="font-bold text-gray-800 dark:text-gray-200"> 油量消耗:</span>
-                  当日油耗
-                </div>
-                <span
-                  class="inline-flex items-center rounded border border-blue-200 bg-blue-100 px-2 py-0.5 text-xs font-medium text-blue-600 dark:bg-blue-900/20 dark:border-blue-800 dark:text-blue-400"
-                >
-                  >3500升 蓝色预警
-                </span>
-              </div>
-            </div>
-          </div>
-          <!-- <div
-          v-if="form.opinion"
-          class="flex gap-3 rounded-md border border-yellow-200 bg-yellow-50 p-3 dark:border-yellow-800 dark:bg-yellow-900/10"
-        >
-          <Icon
-            icon="ep:warning-filled"
-            class="mt-0.5 shrink-0 text-base text-yellow-600 dark:text-yellow-500"
-          />
-          <div class="flex flex-col">
-            <h4 class="mb-1 font-bold text-yellow-800 dark:text-yellow-500"> 审核意见 </h4>
-            <p class="leading-relaxed text-gray-600 dark:text-gray-400">
-              {{ form.opinion }}
-            </p>
-          </div>
-        </div> -->
-        </div>
-        <div class="grid grid-cols-2 gap-4 mt-5">
-          <el-form-item label="施工队伍" prop="deptName">
-            <el-input v-model="form.deptName" placeholder="" disabled />
-          </el-form-item>
-          <el-form-item label="项目" prop="contractName">
-            <el-input v-model="form.contractName" placeholder="" disabled />
-          </el-form-item>
-          <el-form-item label="任务" prop="taskName">
-            <el-input v-model="form.taskName" placeholder="" disabled />
-          </el-form-item>
-          <el-form-item :label="t('project.status')" prop="repairStatus">
-            <el-select v-model="form.repairStatus" placeholder="请选择" clearable disabled>
-              <el-option
-                v-for="(dict, index) in getStrDictOptions(
-                  DICT_TYPE.PMS_PROJECT_TASK_RY_REPAIR_SCHEDULE
-                )"
-                :key="index"
-                :label="dict.label"
-                :value="dict.value"
-              />
-            </el-select>
-          </el-form-item>
-          <el-form-item :label="t('project.technology')" prop="technique">
-            <el-select v-model="form.technique" placeholder="请选择" disabled>
-              <el-option
-                v-for="(dict, index) in getStrDictOptions(DICT_TYPE.PMS_PROJECT_RY_TECHNOLOGY)"
-                :key="index"
-                :label="dict.label"
-                :value="dict.value"
-              />
-            </el-select>
-          </el-form-item>
-          <el-form-item label="井别" prop="wellCategory">
-            <el-input v-model="form.wellCategory" placeholder="" disabled />
-          </el-form-item>
-          <el-form-item label="设计井深(m)" prop="designWellDepth">
-            <el-input v-model="form.designWellDepth" placeholder="" disabled />
-          </el-form-item>
-          <el-form-item label="井控级别" prop="wellControlLevel">
-            <el-input v-model="form.wellControlLevel" placeholder="" disabled />
-          </el-form-item>
-          <el-form-item label="套生段产管尺寸(mm)" prop="casingPipeSize">
-            <el-input v-model="form.casingPipeSize" placeholder="" disabled />
-          </el-form-item>
-          <el-form-item label="当日油耗(升)" prop="dailyFuel">
-            <el-input-number
-              class="w-full!"
-              :min="0"
-              v-model="form.dailyFuel"
-              :class="{ 'blue-input': (form.dailyFuel ?? 0) > 3500 }"
-              placeholder="请输入当日油耗(升)"
-            />
-          </el-form-item>
-          <el-form-item :label="t('project.currentOperation')" prop="currentOperation">
-            <el-input
-              v-model="form.currentOperation"
-              placeholder="请输入目前工序"
-              type="textarea"
-              autosize
-              :maxlength="1000"
-              disabled
-            />
-          </el-form-item>
-          <el-form-item :label="t('project.nextPlan')" prop="nextPlan">
-            <el-input
-              v-model="form.nextPlan"
-              placeholder="请输入下步工序"
-              type="textarea"
-              autosize
-              :maxlength="1000"
-              disabled
-            />
-          </el-form-item>
-          <el-form-item label="运行时效" prop="transitTime">
-            <el-input
-              :model-value="transitTime.value"
-              placeholder="运行时效"
-              disabled
-              :class="{ 'warning-input': transitTime.original >= 1.0 }"
-              id="transitTimeInput"
-            />
-          </el-form-item>
-          <el-form-item label="额定生产时间(H)" prop="ratedProductionTime">
-            <el-input-number
-              class="w-full!"
-              :min="0"
-              v-model="form.ratedProductionTime"
-              placeholder="请输入额定生产时间(H)"
-              :class="{ 'orange-input': sumTimes() !== Number(form.ratedProductionTime ?? 0) }"
-              disabled
-            />
-          </el-form-item>
-          <el-form-item label="生产时间(H)" prop="productionTime">
-            <el-input-number
-              class="w-full!"
-              :min="0"
-              v-model="form.productionTime"
-              placeholder="请输入生产时间(H)"
-              :class="{ 'orange-input': sumTimes() !== Number(form.ratedProductionTime ?? 0) }"
-              disabled
-            />
-          </el-form-item>
-          <el-form-item label="非生产时间(H)" prop="nonProductionTime">
-            <el-input-number
-              class="w-full!"
-              :min="0"
-              v-model="form.nonProductionTime"
-              placeholder="非生产时间(H)"
-              :class="{ 'orange-input': sumTimes() !== Number(form.ratedProductionTime ?? 0) }"
-              disabled
-            />
-          </el-form-item>
-          <el-form-item label="非生产时间原因" prop="nptReason">
-            <el-select v-model="form.ryNptReason" placeholder="请选择" clearable disabled>
-              <el-option
-                v-for="(dict, index) of getStrDictOptions(DICT_TYPE.PMS_PROJECT_RY_NPT_REASON)"
-                :key="index"
-                :label="dict.label"
-                :value="dict.value"
-              />
-            </el-select>
-          </el-form-item>
-          <el-form-item label="全员数量" prop="totalStaffNum">
-            <el-input-number
-              :min="0"
-              v-model="form.totalStaffNum"
-              placeholder="请输入全员数量"
-              disabled
-            />
-          </el-form-item>
-          <el-form-item label="在岗人数" prop="onDutyStaffNum">
-            <el-input-number :min="0" v-model="onDutyStaffNum" placeholder="" disabled />
-          </el-form-item>
-          <el-form-item label="休假人员数量" prop="leaveStaffNum">
-            <el-input-number
-              :min="0"
-              v-model="form.leaveStaffNum"
-              placeholder="请输入休假人员数量"
-              disabled
-            />
-          </el-form-item>
-        </div>
-        <div class="grid grid-cols-1 gap-4 mt-5">
-          <el-form-item label="生产动态" prop="productionStatus">
-            <el-input
-              v-model="form.productionStatus"
-              placeholder="请输入生产动态"
-              type="textarea"
-              autosize
-              :max-length="1000"
-              disabled
-            />
-          </el-form-item>
-          <el-form-item label="备注" prop="remark">
-            <el-input
-              v-model="form.remark"
-              placeholder="请输入备注"
-              :max-length="1000"
-              type="textarea"
-              autosize
-              disabled
-            />
-          </el-form-item>
-        </div>
-        <el-form-item class="mt-4" label="审批意见" prop="opinion">
-          <el-input
-            v-model="form.opinion"
-            placeholder="请输入审批意见"
-            :max-length="1000"
-            type="textarea"
-            autosize
-            :disabled="formType === 'readonly'"
-          />
-        </el-form-item>
-      </el-form>
-      <template #footer>
-        <el-button
-          size="default"
-          @click="submitForm(20)"
-          type="primary"
-          :disabled="formLoading || formType === 'readonly'"
-        >
-          审批通过
-        </el-button>
-        <el-button
-          size="default"
-          @click="submitForm(30)"
-          type="danger"
-          :disabled="formLoading || formType === 'readonly'"
-        >
-          审批拒绝
-        </el-button>
-        <el-button size="default" @click="dialogVisible = false">取 消</el-button>
-      </template>
-    </Dialog></div
-  >
+    <ry-xj-form v-model:visible="visible" type="approval" ref="formRef" :load-list="loadList" />
+  </div>
 </template>
 
 <style scoped>

+ 9 - 402
src/views/pms/iotrydailyreport/xfill.vue

@@ -2,12 +2,11 @@
 import { rangeShortcuts } from '@/utils/formatTime'
 import { useDebounceFn } from '@vueuse/core'
 import dayjs from 'dayjs'
-import { DICT_TYPE, getStrDictOptions } from '@/utils/dict'
+import { DICT_TYPE } from '@/utils/dict'
 import { TableColumnCtx } from 'element-plus/es/components/table/src/table-column/defaults'
-import { FormInstance, FormRules } from 'element-plus'
-import Form from '@/components/Form/src/Form.vue'
 import { useUserStore } from '@/store/modules/user'
 import { IotRyDailyReportApi } from '@/api/pms/iotrydailyreport'
+import ryXjForm from './ry-xj-form.vue'
 
 interface List {
   id: number
@@ -433,76 +432,14 @@ watch(
   { immediate: true }
 )
 
-const FORM_KEYS = [
-  'id',
-  'deptId',
-  'projectId',
-  'taskId',
-  'deptName',
-  'contractName',
-  'taskName',
-  'repairStatus',
-  'technique',
-  'wellCategory',
-  'designWellDepth',
-  'casingPipeSize',
-  'wellControlLevel',
-  'currentOperation',
-  'nextPlan',
-  'transitTime',
-  'ratedProductionTime',
-  'productionTime',
-  'nonProductionTime',
-  'ryNptReason',
-  'productionStatus',
-  'totalStaffNum',
-  'onDutyStaffNum',
-  'leaveStaffNum',
-  'remark',
-  'opinion',
-  'createTime',
-  'dailyFuel'
-] as const
-
-type FormKey = (typeof FORM_KEYS)[number]
-type Form = Partial<Pick<List, FormKey>>
-
-const dialogVisible = ref(false)
-const formRef = ref<FormInstance>()
-const formLoading = ref(false)
-const message = useMessage()
-
-const initFormData = (): Form => ({})
-
-const form = ref<Form>(initFormData())
-
-async function loadDetail(id: number) {
-  try {
-    const res = await IotRyDailyReportApi.getIotRyDailyReport(id)
-    FORM_KEYS.forEach((key) => {
-      form.value[key] = res[key] ?? form.value[key]
-    })
-    form.value.id = id
-
-    if (res.status !== 0) {
-      formType.value = 'readonly'
-    }
-  } finally {
-  }
-}
+const visible = ref(false)
 
-const formType = ref<'edit' | 'readonly'>('edit')
+const formRef = ref()
 
 function handleOpenForm(id: number, type: 'edit' | 'readonly') {
-  form.value = initFormData()
-  formRef.value?.resetFields()
-
-  formType.value = type
-
-  dialogVisible.value = true
-  loadDetail(id).then(() => {
-    formRef.value?.validate()
-  })
+  if (formRef.value) {
+    formRef.value.handleOpenForm(id, type)
+  }
 }
 
 const route = useRoute()
@@ -512,103 +449,6 @@ onMounted(() => {
     handleOpenForm(Number(route.query.id), 'edit')
   }
 })
-
-const transitTime = computed(() => {
-  const cap = form.value.productionTime ?? 0
-  const gas = form.value.ratedProductionTime ?? 0
-
-  if (!gas) return { original: 0, value: '0%' }
-
-  const original = cap / gas
-  return { original, value: (original * 100).toFixed(2) + '%' }
-})
-
-const onDutyStaffNum = computed(() => {
-  return (form.value.totalStaffNum ?? 0) - (form.value.leaveStaffNum ?? 0)
-})
-
-const sumTimes = () => {
-  const { productionTime = 0, nonProductionTime = 0 } = form.value
-  return productionTime + nonProductionTime
-}
-
-const validateTotalTime = (_rule: any, _value: any, callback: any) => {
-  const total = sumTimes()
-  if (total !== form.value.ratedProductionTime) {
-    callback(new Error(`生产时间和非生产时间之和必须等于额定生产时间`))
-  } else {
-    callback()
-  }
-}
-
-const validateNptReason = (_rule: any, value: any, callback: any) => {
-  if ((form.value.nonProductionTime || 0) > 0 && !value) {
-    callback(new Error('非生产时间大于 0 时,必须选择原因'))
-  } else {
-    callback()
-  }
-}
-
-const _timeRuleItem = [
-  { required: true, message: '请输入时间', trigger: 'blur' },
-  { validator: validateTotalTime, trigger: 'blur' }
-]
-
-const rules = reactive<FormRules>({
-  repairStatus: [{ required: true, message: '请输入施工状态', trigger: ['change', 'blur'] }],
-  productionStatus: [{ required: true, message: '请输入生产动态', trigger: ['change', 'blur'] }],
-
-  // 复用规则
-  // productionTime: timeRuleItem,
-  // nonProductionTime: timeRuleItem,
-  // ratedProductionTime: timeRuleItem,
-
-  ryNptReason: [{ validator: validateNptReason, trigger: ['change', 'blur'] }]
-})
-
-watch(
-  [
-    () => form.value.productionTime,
-    () => form.value.nonProductionTime,
-    () => form.value.ratedProductionTime
-  ],
-  () => {
-    nextTick(() => {
-      formRef.value?.validateField('nptReason')
-      if (sumTimes() === form.value.ratedProductionTime) {
-        formRef.value?.clearValidate(['productionTime', 'nonProductionTime', 'ratedProductionTime'])
-      }
-    })
-  }
-)
-
-const { t } = useI18n()
-
-const submitForm = async () => {
-  if (!formRef.value) return
-
-  try {
-    await formRef.value.validate()
-    formLoading.value = true
-
-    const total = sumTimes()
-    if (total !== form.value.ratedProductionTime) {
-      message.error(`生产时间和非生产时间之和必须等于额定生产时间`)
-    }
-
-    const { createTime, ...other } = form.value
-
-    const data = { ...other, fillOrderCreateTime: createTime, projectClassification: '2' } as any
-    await IotRyDailyReportApi.createIotRyDailyReport(data)
-    message.success(t('common.updateSuccess'))
-    dialogVisible.value = false
-    loadList()
-  } catch (error) {
-    console.warn('表单校验未通过或提交出错')
-  } finally {
-    formLoading.value = false
-  }
-}
 </script>
 
 <template>
@@ -721,241 +561,8 @@ const submitForm = async () => {
         </div>
       </div>
     </div>
-    <Dialog title="编辑" v-model="dialogVisible">
-      <el-form
-        ref="formRef"
-        label-position="top"
-        size="default"
-        :rules="rules"
-        :model="form"
-        v-loading="formLoading"
-        require-asterisk-position="right"
-        :disabled="formType === 'readonly'"
-      >
-        <div class="flex flex-col gap-3 text-sm">
-          <div
-            class="rounded-md border border-blue-100 bg-blue-50/80 p-3 dark:border-blue-900/30 dark:bg-blue-900/10"
-          >
-            <div class="flex flex-col gap-2.5">
-              <div class="flex items-center justify-between">
-                <div class="text-gray-600 dark:text-gray-400">
-                  <span class="font-bold text-gray-800 dark:text-gray-200">运行时效:</span>
-                  生产时间/额定生产时间
-                </div>
-                <span
-                  class="inline-flex items-center rounded border border-red-200 bg-red-100 px-2 py-0.5 text-xs font-medium text-red-600 dark:bg-red-900/20 dark:border-red-800 dark:text-red-400"
-                >
-                  >100% 红色预警
-                </span>
-              </div>
-              <div class="flex items-center justify-between">
-                <div class="text-gray-600 dark:text-gray-400">
-                  <span class="font-bold text-gray-800 dark:text-gray-200">时间平衡:</span>
-                  生产 + 非生产 = 额定生产
-                </div>
-                <span
-                  class="inline-flex items-center rounded border border-orange-200 bg-orange-100 px-2 py-0.5 text-xs font-medium text-orange-600 dark:bg-orange-900/20 dark:border-orange-800 dark:text-orange-400"
-                >
-                  ≠额定生产 橙色预警
-                </span>
-              </div>
-              <div class="flex items-center justify-between">
-                <div class="text-gray-600 dark:text-gray-400">
-                  <span class="font-bold text-gray-800 dark:text-gray-200"> 油量消耗:</span>
-                  当日油耗
-                </div>
-                <span
-                  class="inline-flex items-center rounded border border-blue-200 bg-blue-100 px-2 py-0.5 text-xs font-medium text-blue-600 dark:bg-blue-900/20 dark:border-blue-800 dark:text-blue-400"
-                >
-                  >3500升 蓝色预警
-                </span>
-              </div>
-            </div>
-          </div>
-          <div
-            v-if="form.opinion"
-            class="flex gap-3 rounded-md border border-yellow-200 bg-yellow-50 p-3 dark:border-yellow-800 dark:bg-yellow-900/10"
-          >
-            <Icon
-              icon="ep:warning-filled"
-              class="mt-0.5 shrink-0 text-base text-yellow-600 dark:text-yellow-500"
-            />
-            <div class="flex flex-col">
-              <h4 class="mb-1 font-bold text-yellow-800 dark:text-yellow-500"> 审核意见 </h4>
-              <p class="leading-relaxed text-gray-600 dark:text-gray-400">
-                {{ form.opinion }}
-              </p>
-            </div>
-          </div>
-        </div>
-        <div class="grid grid-cols-2 gap-4 mt-5">
-          <el-form-item label="施工队伍" prop="deptName">
-            <el-input v-model="form.deptName" placeholder="" disabled />
-          </el-form-item>
-          <el-form-item label="项目" prop="contractName">
-            <el-input v-model="form.contractName" placeholder="" disabled />
-          </el-form-item>
-          <el-form-item label="任务" prop="taskName">
-            <el-input v-model="form.taskName" placeholder="" disabled />
-          </el-form-item>
-          <el-form-item :label="t('project.status')" prop="repairStatus">
-            <el-select v-model="form.repairStatus" placeholder="请选择" clearable>
-              <el-option
-                v-for="(dict, index) in getStrDictOptions(
-                  DICT_TYPE.PMS_PROJECT_TASK_RY_REPAIR_SCHEDULE
-                )"
-                :key="index"
-                :label="dict.label"
-                :value="dict.value"
-              />
-            </el-select>
-          </el-form-item>
-          <el-form-item :label="t('project.technology')" prop="technique">
-            <el-select v-model="form.technique" placeholder="请选择" disabled>
-              <el-option
-                v-for="(dict, index) in getStrDictOptions(DICT_TYPE.PMS_PROJECT_RY_TECHNOLOGY)"
-                :key="index"
-                :label="dict.label"
-                :value="dict.value"
-              />
-            </el-select>
-          </el-form-item>
-          <!-- <el-form-item label="运行时效" prop="transitTime">
-          <el-input
-            :model-value="transitTime.value"
-            placeholder="运行时效"
-            disabled
-            :class="{ 'warning-input': transitTime.original > 1.0 }"
-            id="transitTimeInput"
-          />
-        </el-form-item> -->
-          <el-form-item label="井别" prop="wellCategory">
-            <el-input v-model="form.wellCategory" placeholder="" disabled />
-          </el-form-item>
-          <el-form-item label="设计井深(m)" prop="designWellDepth">
-            <el-input v-model="form.designWellDepth" placeholder="" disabled />
-          </el-form-item>
-          <el-form-item label="井控级别" prop="wellControlLevel">
-            <el-input v-model="form.wellControlLevel" placeholder="" disabled />
-          </el-form-item>
-          <el-form-item label="套生段产管尺寸(mm)" prop="casingPipeSize">
-            <el-input v-model="form.casingPipeSize" placeholder="" disabled />
-          </el-form-item>
-          <el-form-item label="当日油耗(升)" prop="dailyFuel">
-            <el-input-number
-              class="w-full!"
-              :min="0"
-              v-model="form.dailyFuel"
-              :class="{ 'blue-input': (form.dailyFuel ?? 0) > 3500 }"
-              placeholder="请输入当日油耗(升)"
-            />
-          </el-form-item>
-          <el-form-item :label="t('project.currentOperation')" prop="currentOperation">
-            <el-input
-              v-model="form.currentOperation"
-              placeholder="请输入目前工序"
-              type="textarea"
-              autosize
-              :maxlength="1000"
-            />
-          </el-form-item>
-          <el-form-item :label="t('project.nextPlan')" prop="nextPlan">
-            <el-input
-              v-model="form.nextPlan"
-              placeholder="请输入下步工序"
-              type="textarea"
-              autosize
-              :maxlength="1000"
-            />
-          </el-form-item>
-          <el-form-item label="运行时效" prop="transitTime">
-            <el-input
-              :model-value="transitTime.value"
-              placeholder="运行时效"
-              disabled
-              :class="{ 'warning-input': transitTime.original >= 1.0 }"
-              id="transitTimeInput"
-            />
-          </el-form-item>
-          <el-form-item label="额定生产时间(H)" prop="ratedProductionTime">
-            <el-input-number
-              class="w-full!"
-              :min="0"
-              v-model="form.ratedProductionTime"
-              :class="{ 'orange-input': sumTimes() !== Number(form.ratedProductionTime ?? 0) }"
-              placeholder="请输入额定生产时间(H)"
-            />
-          </el-form-item>
-          <el-form-item label="生产时间(H)" prop="productionTime">
-            <el-input-number
-              class="w-full!"
-              :min="0"
-              v-model="form.productionTime"
-              :class="{ 'orange-input': sumTimes() !== Number(form.ratedProductionTime ?? 0) }"
-              placeholder="请输入生产时间(H)"
-            />
-          </el-form-item>
-          <el-form-item label="非生产时间(H)" prop="nonProductionTime">
-            <el-input-number
-              class="w-full!"
-              :min="0"
-              v-model="form.nonProductionTime"
-              :class="{ 'orange-input': sumTimes() !== Number(form.ratedProductionTime ?? 0) }"
-              placeholder="非生产时间(H)"
-            />
-          </el-form-item>
-          <el-form-item label="非生产时间原因" prop="nptReason">
-            <el-select v-model="form.ryNptReason" placeholder="请选择" clearable>
-              <el-option
-                v-for="(dict, index) of getStrDictOptions(DICT_TYPE.PMS_PROJECT_RY_NPT_REASON)"
-                :key="index"
-                :label="dict.label"
-                :value="dict.value"
-              />
-            </el-select>
-          </el-form-item>
-          <el-form-item label="全员数量" prop="totalStaffNum">
-            <el-input-number :min="0" v-model="form.totalStaffNum" placeholder="请输入全员数量" />
-          </el-form-item>
-          <el-form-item label="在岗人数" prop="onDutyStaffNum">
-            <el-input-number :min="0" v-model="onDutyStaffNum" placeholder="" disabled />
-          </el-form-item>
-          <el-form-item label="休假人员数量" prop="leaveStaffNum">
-            <el-input-number
-              :min="0"
-              v-model="form.leaveStaffNum"
-              placeholder="请输入休假人员数量"
-            />
-          </el-form-item>
-        </div>
-        <div class="grid grid-cols-1 gap-4 mt-5">
-          <el-form-item label="生产动态" prop="productionStatus">
-            <el-input
-              v-model="form.productionStatus"
-              placeholder="请输入生产动态"
-              type="textarea"
-              autosize
-              :max-length="1000"
-            />
-          </el-form-item>
-          <el-form-item label="备注" prop="remark">
-            <el-input
-              v-model="form.remark"
-              placeholder="请输入备注"
-              :max-length="1000"
-              type="textarea"
-              autosize
-            /> </el-form-item
-        ></div>
-      </el-form>
-      <template #footer>
-        <el-button size="default" @click="submitForm" type="primary" :disabled="formLoading">
-          确 定
-        </el-button>
-        <el-button size="default" @click="dialogVisible = false">取 消</el-button>
-      </template>
-    </Dialog></div
-  >
+    <ry-xj-form v-model:visible="visible" type="edit" ref="formRef" :load-list="loadList" />
+  </div>
 </template>
 
 <style scoped>

+ 15 - 23
src/views/pms/iotrydailyreport/xjindex.vue

@@ -63,21 +63,15 @@
             <el-button @click="resetQuery"
               ><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button
             >
-            <el-button
+            <!-- <el-button
               type="primary"
               plain
               @click="openForm('create')"
               v-hasPermi="['pms:iot-rh-daily-report:create']"
             >
               <Icon icon="ep:plus" class="mr-5px" /> 新增
-            </el-button>
-            <el-button
-              type="success"
-              plain
-              @click="handleExport"
-              :loading="exportLoading"
-              v-hasPermi="['pms:iot-rh-daily-report:export']"
-            >
+            </el-button> -->
+            <el-button type="success" plain @click="handleExport" :loading="exportLoading">
               <Icon icon="ep:download" class="mr-5px" /> 导出
             </el-button>
           </el-form-item>
@@ -470,7 +464,6 @@ let queryParams = reactive({
   nonProductFlag: 'N'
 })
 const queryFormRef = ref() // 搜索的表单
-const exportLoading = ref(false) // 导出的加载中
 
 const rootDeptId = ref(useUserStore().getUser.deptId)
 
@@ -922,21 +915,20 @@ const handleDeptNodeClick = async (row) => {
   await getList()
 }
 
-/** 导出按钮操作 */
+const exportLoading = ref(false)
 const handleExport = async () => {
-  try {
-    // 导出的二次确认
-    await message.exportConfirm()
-    // 发起导出
-    exportLoading.value = true
-    const data = await IotRyDailyReportApi.exportIotRyDailyReport(queryParams)
-    download.excel(data, '瑞鹰日报.xls')
-  } catch {
-  } finally {
-    exportLoading.value = false
-  }
-}
+  const res = await IotRyDailyReportApi.exportIotRyDailyReport({
+    createTime: queryParams.createTime,
+    contractName: queryParams.contractName,
+    taskName: queryParams.taskName,
+    // pageNo: queryParams.pageNo,
+    // pageSize: queryParams.pageSize,
+    deptId: queryParams.deptId,
+    projectClassification: queryParams.projectClassification
+  })
 
+  download.excel(res, '瑞鹰修井日报.xlsx')
+}
 // 声明 ResizeObserver 实例
 let resizeObserver: ResizeObserver | null = null
 

+ 23 - 8
src/views/pms/iotrydailyreport/xsummary.vue

@@ -54,6 +54,13 @@ const totalWorkKeys: [string, string, string, string, number][] = [
     'i-material-symbols:directions-car-outline-rounded text-sky',
     2
   ],
+  [
+    'averageFuelConsumption',
+    '升',
+    '平均油耗',
+    'i-material-symbols:directions-car-outline-rounded text-sky',
+    2
+  ],
   [
     'totalPowerConsumption',
     'MWh',
@@ -72,7 +79,8 @@ const totalWork = ref({
   totalFuelConsumption: 0,
   totalPowerConsumption: 0,
   constructionWells: 0,
-  completedWells: 0
+  completedWells: 0,
+  averageFuelConsumption: 0
 })
 
 const totalLoading = ref(false)
@@ -103,7 +111,8 @@ const getTotal = useDebounceFn(async () => {
       completedWells: 0,
       ...res2,
       totalPowerConsumption: (res2.totalPowerConsumption || 0) / 1000,
-      totalFuelConsumption: res2.totalFuelConsumption || 0
+      totalFuelConsumption: res2.totalFuelConsumption || 0,
+      averageFuelConsumption: res2.averageFuelConsumption || 0
     }
   } finally {
     totalLoading.value = false
@@ -120,6 +129,7 @@ interface List {
   cumulativeFuelConsumption: number | null
   transitTime: number | null
   nonProductiveTime: number | null
+  averageFuelConsumption: number | null
 }
 
 const list = ref<List[]>([])
@@ -148,6 +158,10 @@ const columns = (type: string) => {
       label: '累计油耗(升)',
       prop: 'cumulativeFuelConsumption'
     },
+    {
+      label: '平均油耗(升)',
+      prop: 'averageFuelConsumption'
+    },
     {
       label: '平均时效(%)',
       prop: 'transitTime'
@@ -184,7 +198,8 @@ const getList = useDebounceFn(async () => {
         name: type === '2' ? projectDeptName : teamName,
         ...other,
         cumulativePowerConsumption: ((other.cumulativePowerConsumption || 0) / 1000).toFixed(2),
-        cumulativeFuelConsumption: (other.cumulativeFuelConsumption || 0).toFixed(2)
+        cumulativeFuelConsumption: (other.cumulativeFuelConsumption || 0).toFixed(2),
+        averageFuelConsumption: (other.averageFuelConsumption || 0).toFixed(2)
       })
     )
   } finally {
@@ -217,12 +232,12 @@ let chart: echarts.ECharts | null = null
 const xAxisData = ref<string[]>([])
 
 const legend = ref<string[][]>([
-  ['累计施工井数 (个)', 'cumulativeConstructWells'],
-  ['累计完工井数 (个)', 'cumulativeCompletedWells'],
+  ['施工井数 (个)', 'cumulativeConstructWells'],
+  ['完工井数 (个)', 'cumulativeCompletedWells'],
   // ['累计油耗 (万升)', 'cumulativeFuelConsumption'],
-  ['累计油耗 (升)', 'cumulativeFuelConsumption'],
+  ['油耗 (升)', 'cumulativeFuelConsumption'],
   // ['累计用电量 (MWh)', 'cumulativePowerConsumption'],
-  ['累计用电量 (KWh)', 'cumulativePowerConsumption'],
+  ['用电量 (KWh)', 'cumulativePowerConsumption'],
   ['平均时效 (%)', 'transitTime']
 ])
 
@@ -525,7 +540,7 @@ const tolist = (id: number, non: boolean = false) => {
           <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
         </el-form-item>
       </el-form>
-      <div class="grid grid-cols-7 gap-8">
+      <div class="grid grid-cols-8 gap-8">
         <div
           v-for="info in totalWorkKeys"
           :key="info[0]"

+ 200 - 112
src/views/pms/iotsapstock/index.vue

@@ -9,7 +9,14 @@
       label-width="68px"
     >
       <el-form-item :label="t('workOrderMaterial.factory')" prop="factoryId">
-        <el-select v-model="queryParams.factoryId" clearable filterable :placeholder="t('faultForm.choose')" class="!w-240px" @change="selectedFactoryChange">
+        <el-select
+          v-model="queryParams.factoryId"
+          clearable
+          filterable
+          :placeholder="t('faultForm.choose')"
+          class="!w-240px"
+          @change="selectedFactoryChange"
+        >
           <el-option
             v-for="item in factoryList"
             :key="item.id"
@@ -18,8 +25,18 @@
           />
         </el-select>
       </el-form-item>
-      <el-form-item :label="t('workOrderMaterial.storageLocation')" prop="storageLocationId" style="margin-left: 28px">
-        <el-select v-model="queryParams.storageLocationId" clearable filterable :placeholder="t('faultForm.choose')" class="!w-240px">
+      <el-form-item
+        :label="t('workOrderMaterial.storageLocation')"
+        prop="storageLocationId"
+        style="margin-left: 28px"
+      >
+        <el-select
+          v-model="queryParams.storageLocationId"
+          clearable
+          filterable
+          :placeholder="t('faultForm.choose')"
+          class="!w-240px"
+        >
           <el-option
             v-for="item in storageLocationList"
             :key="item.id"
@@ -73,15 +90,14 @@
         </el-select>
       </el-form-item>
       <el-form-item>
-        <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> {{ t('operationFill.search') }}</el-button>
-        <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" />  {{ t('operationFill.reset') }}</el-button>
-        <el-button
-          type="success"
-          plain
-          @click="handleExport"
-          :loading="exportLoading"
-          v-hasPermi="['pms:iot-sap-stock:export']"
+        <el-button @click="handleQuery"
+          ><Icon icon="ep:search" class="mr-5px" /> {{ t('operationFill.search') }}</el-button
+        >
+        <el-button @click="resetQuery"
+          ><Icon icon="ep:refresh" class="mr-5px" /> {{ t('operationFill.reset') }}</el-button
         >
+        <!-- v-hasPermi="['pms:iot-sap-stock:export']" -->
+        <el-button type="success" plain @click="handleExport" :loading="exportLoading">
           <Icon icon="ep:download" class="mr-5px" /> 导出
         </el-button>
       </el-form-item>
@@ -98,7 +114,15 @@
         </div>
         <div class="stat-item">
           <span class="stat-label">{{ t('stock.totalAmount') }}:</span>
-          <span class="stat-value">¥ {{ totalAmount.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 }) }}</span>
+          <span class="stat-value"
+            >¥
+            {{
+              totalAmount.toLocaleString(undefined, {
+                minimumFractionDigits: 2,
+                maximumFractionDigits: 2
+              })
+            }}</span
+          >
         </div>
       </div>
     </el-card>
@@ -106,18 +130,62 @@
 
   <!-- 列表 -->
   <ContentWrap ref="tableContainerRef" class="table-container">
-    <el-table ref="tableRef" v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="false" style="width: 100%">
-      <el-table-column :label="t('workOrderMaterial.factory')" align="center" prop="factory" :width="columnWidths.factory"/>
-      <el-table-column :label="t('workOrderMaterial.storageLocation')" align="center"
-                       prop="projectDepartment" :width="columnWidths.projectDepartment"/>
-      <el-table-column :label="t('chooseMaintain.materialCode')" align="center"
-                       prop="materialCode" :width="columnWidths.materialCode"/>
-      <el-table-column :label="t('chooseMaintain.materialName')" align="left"
-                       prop="materialName" :width="columnWidths.materialName"/>
-      <el-table-column :label="t('route.quantity')" align="center" prop="quantity" :width="columnWidths.quantity"/>
-      <el-table-column :label="t('workOrderMaterial.unitPrice')" align="center" prop="unitPrice" :width="columnWidths.unitPrice"/>
-      <el-table-column :label="t('workOrderMaterial.unit')" align="center" prop="unit" :width="columnWidths.unit"/>
-      <el-table-column :label="t('route.safetyStock')" align="center" prop="safetyStock" :width="columnWidths.safetyStock"/>
+    <el-table
+      ref="tableRef"
+      v-loading="loading"
+      :data="list"
+      :stripe="true"
+      :show-overflow-tooltip="false"
+      style="width: 100%"
+    >
+      <el-table-column
+        :label="t('workOrderMaterial.factory')"
+        align="center"
+        prop="factory"
+        :width="columnWidths.factory"
+      />
+      <el-table-column
+        :label="t('workOrderMaterial.storageLocation')"
+        align="center"
+        prop="projectDepartment"
+        :width="columnWidths.projectDepartment"
+      />
+      <el-table-column
+        :label="t('chooseMaintain.materialCode')"
+        align="center"
+        prop="materialCode"
+        :width="columnWidths.materialCode"
+      />
+      <el-table-column
+        :label="t('chooseMaintain.materialName')"
+        align="left"
+        prop="materialName"
+        :width="columnWidths.materialName"
+      />
+      <el-table-column
+        :label="t('route.quantity')"
+        align="center"
+        prop="quantity"
+        :width="columnWidths.quantity"
+      />
+      <el-table-column
+        :label="t('workOrderMaterial.unitPrice')"
+        align="center"
+        prop="unitPrice"
+        :width="columnWidths.unitPrice"
+      />
+      <el-table-column
+        :label="t('workOrderMaterial.unit')"
+        align="center"
+        prop="unit"
+        :width="columnWidths.unit"
+      />
+      <el-table-column
+        :label="t('route.safetyStock')"
+        align="center"
+        prop="safetyStock"
+        :width="columnWidths.safetyStock"
+      />
       <el-table-column
         :label="t('chooseMaintain.createTime')"
         align="center"
@@ -144,7 +212,7 @@ import { dateFormatter } from '@/utils/formatTime'
 import download from '@/utils/download'
 import { IotSapStockApi, IotSapStockVO } from '@/api/pms/iotsapstock'
 import IotSapStockForm from './IotSapStockForm.vue'
-import * as SapOrgApi from "@/api/system/saporg";
+import * as SapOrgApi from '@/api/system/saporg'
 
 /** PMS SAP 库存(通用库存/项目部库存) 列表 */
 defineOptions({ name: 'IotSapStock' })
@@ -152,11 +220,11 @@ defineOptions({ name: 'IotSapStock' })
 const message = useMessage() // 消息弹窗
 const { t } = useI18n() // 国际化
 
-const factoryList = ref([] as SapOrgApi.SapOrgVO[])   // 工厂列表
+const factoryList = ref([] as SapOrgApi.SapOrgVO[]) // 工厂列表
 const storageLocationList = ref([] as SapOrgApi.SapOrgVO[]) // 库存地点列表
 // 新增统计变量
-const totalQuantity = ref(0)   // 总数量
-const totalAmount = ref(0)     // 总金额
+const totalQuantity = ref(0) // 总数量
+const totalAmount = ref(0) // 总金额
 
 const loading = ref(true) // 列表的加载中
 const list = ref<IotSapStockVO[]>([]) // 列表的数据
@@ -187,7 +255,7 @@ const queryParams = reactive({
   status: undefined,
   remark: undefined,
   configFlag: 'A',
-  createTime: [],
+  createTime: []
 })
 const queryFormRef = ref() // 搜索的表单
 const exportLoading = ref(false) // 导出的加载中
@@ -211,120 +279,142 @@ const columnWidths = ref({
 
 /** 获取滚动条宽度 */
 const getScrollbarWidth = () => {
-  const outer = document.createElement('div');
-  outer.style.visibility = 'hidden';
-  outer.style.overflow = 'scroll';
-  document.body.appendChild(outer);
+  const outer = document.createElement('div')
+  outer.style.visibility = 'hidden'
+  outer.style.overflow = 'scroll'
+  document.body.appendChild(outer)
 
-  const inner = document.createElement('div');
-  outer.appendChild(inner);
+  const inner = document.createElement('div')
+  outer.appendChild(inner)
 
-  const scrollbarWidth = outer.offsetWidth - inner.offsetWidth;
-  outer.parentNode?.removeChild(outer);
+  const scrollbarWidth = outer.offsetWidth - inner.offsetWidth
+  outer.parentNode?.removeChild(outer)
 
-  return scrollbarWidth;
-};
+  return scrollbarWidth
+}
 
 /** 计算文本宽度 */
 const getTextWidth = (text: string, fontSize = 14) => {
-  const span = document.createElement('span');
-  span.style.visibility = 'hidden';
-  span.style.position = 'absolute';
-  span.style.whiteSpace = 'nowrap';
-  span.style.fontSize = `${fontSize}px`;
-  span.style.fontFamily = 'inherit';
-  span.innerText = text;
-
-  document.body.appendChild(span);
-  const width = span.offsetWidth;
-  document.body.removeChild(span);
-
-  return width;
-};
+  const span = document.createElement('span')
+  span.style.visibility = 'hidden'
+  span.style.position = 'absolute'
+  span.style.whiteSpace = 'nowrap'
+  span.style.fontSize = `${fontSize}px`
+  span.style.fontFamily = 'inherit'
+  span.innerText = text
+
+  document.body.appendChild(span)
+  const width = span.offsetWidth
+  document.body.removeChild(span)
+
+  return width
+}
 
 /** 计算列宽度 */
 const calculateColumnWidths = () => {
-  const MIN_WIDTH = 80; // 最小列宽
-  const PADDING = 25; // 列内边距
-  const FLEXIBLE_COLUMN = 'materialName'; // 可伸缩列
-  const scrollbarWidth = getScrollbarWidth(); // 动态获取滚动条宽度
+  const MIN_WIDTH = 80 // 最小列宽
+  const PADDING = 25 // 列内边距
+  const FLEXIBLE_COLUMN = 'materialName' // 可伸缩列
+  const scrollbarWidth = getScrollbarWidth() // 动态获取滚动条宽度
 
-  if (!tableContainerRef.value?.$el || list.value.length === 0) return;
+  if (!tableContainerRef.value?.$el || list.value.length === 0) return
 
-  const containerWidth = tableContainerRef.value.$el.clientWidth;
+  const containerWidth = tableContainerRef.value.$el.clientWidth
 
   // 需要自适应的列配置
   const autoColumns = [
     { key: 'factory', label: t('workOrderMaterial.factory'), getValue: (row) => row.factory },
-    { key: 'projectDepartment', label: t('workOrderMaterial.storageLocation'), getValue: (row) => row.projectDepartment },
-    { key: 'materialCode', label: t('chooseMaintain.materialCode'), getValue: (row) => row.materialCode },
-    { key: 'materialName', label: t('chooseMaintain.materialName'), getValue: (row) => row.materialName },
+    {
+      key: 'projectDepartment',
+      label: t('workOrderMaterial.storageLocation'),
+      getValue: (row) => row.projectDepartment
+    },
+    {
+      key: 'materialCode',
+      label: t('chooseMaintain.materialCode'),
+      getValue: (row) => row.materialCode
+    },
+    {
+      key: 'materialName',
+      label: t('chooseMaintain.materialName'),
+      getValue: (row) => row.materialName
+    },
     { key: 'quantity', label: t('route.quantity'), getValue: (row) => String(row.quantity) },
-    { key: 'unitPrice', label: t('workOrderMaterial.unitPrice'), getValue: (row) => String(row.unitPrice) },
+    {
+      key: 'unitPrice',
+      label: t('workOrderMaterial.unitPrice'),
+      getValue: (row) => String(row.unitPrice)
+    },
     { key: 'unit', label: t('workOrderMaterial.unit'), getValue: (row) => row.unit },
-    { key: 'safetyStock', label: t('route.safetyStock'), getValue: (row) => String(row.safetyStock) },
+    {
+      key: 'safetyStock',
+      label: t('route.safetyStock'),
+      getValue: (row) => String(row.safetyStock)
+    },
     {
       key: 'createTime',
       label: t('chooseMaintain.createTime'),
       getValue: (row) => dateFormatter(null, null, row.createTime)
     }
-  ];
+  ]
 
-  const newWidths: Record<string, string> = {};
-  let totalFixedWidth = 0; // 所有固定列的总宽度
+  const newWidths: Record<string, string> = {}
+  let totalFixedWidth = 0 // 所有固定列的总宽度
 
   // 计算除可伸缩列外的所有列宽度
-  autoColumns.forEach(col => {
-    if (col.key === FLEXIBLE_COLUMN) return;
+  autoColumns.forEach((col) => {
+    if (col.key === FLEXIBLE_COLUMN) return
 
-    const headerText = col.label;
-    const headerWidth = getTextWidth(headerText) * 1.3; // 表头宽度(加粗效果增加30%)
+    const headerText = col.label
+    const headerWidth = getTextWidth(headerText) * 1.3 // 表头宽度(加粗效果增加30%)
 
     // 计算内容最大宽度
-    let contentMaxWidth = 0;
-    list.value.forEach(row => {
-      const text = col.getValue ? String(col.getValue(row)) : String(row[col.key] || '');
-      const textWidth = getTextWidth(text);
-      if (textWidth > contentMaxWidth) contentMaxWidth = textWidth;
-    });
+    let contentMaxWidth = 0
+    list.value.forEach((row) => {
+      const text = col.getValue ? String(col.getValue(row)) : String(row[col.key] || '')
+      const textWidth = getTextWidth(text)
+      if (textWidth > contentMaxWidth) contentMaxWidth = textWidth
+    })
 
     // 取表头宽度、内容最大宽度和最小宽度的最大值
-    const finalWidth = Math.max(headerWidth, contentMaxWidth, MIN_WIDTH) + PADDING;
-    newWidths[col.key] = `${finalWidth}px`;
-    totalFixedWidth += finalWidth;
-  });
+    const finalWidth = Math.max(headerWidth, contentMaxWidth, MIN_WIDTH) + PADDING
+    newWidths[col.key] = `${finalWidth}px`
+    totalFixedWidth += finalWidth
+  })
 
   // 处理可伸缩列(materialName)
-  const flexibleCol = autoColumns.find(col => col.key === FLEXIBLE_COLUMN);
+  const flexibleCol = autoColumns.find((col) => col.key === FLEXIBLE_COLUMN)
   if (flexibleCol) {
-    const headerText = flexibleCol.label;
-    const headerWidth = getTextWidth(headerText) * 1.3;
-
-    let contentMaxWidth = 0;
-    list.value.forEach(row => {
-      const text = flexibleCol.getValue ? String(flexibleCol.getValue(row)) : String(row[flexibleCol.key] || '');
-      const textWidth = getTextWidth(text);
-      if (textWidth > contentMaxWidth) contentMaxWidth = textWidth;
-    });
+    const headerText = flexibleCol.label
+    const headerWidth = getTextWidth(headerText) * 1.3
+
+    let contentMaxWidth = 0
+    list.value.forEach((row) => {
+      const text = flexibleCol.getValue
+        ? String(flexibleCol.getValue(row))
+        : String(row[flexibleCol.key] || '')
+      const textWidth = getTextWidth(text)
+      if (textWidth > contentMaxWidth) contentMaxWidth = textWidth
+    })
 
-    const baseWidth = Math.max(headerWidth, contentMaxWidth, MIN_WIDTH) + PADDING;
+    const baseWidth = Math.max(headerWidth, contentMaxWidth, MIN_WIDTH) + PADDING
 
     // 剩余空间 = 容器宽度 - 其他列总宽度 - 垂直滚动条宽度
-    const remainingWidth = containerWidth - totalFixedWidth - scrollbarWidth;
+    const remainingWidth = containerWidth - totalFixedWidth - scrollbarWidth
 
     // 可伸缩列的宽度取剩余空间和基础宽度的最大值
-    const flexibleWidth = Math.max(remainingWidth, baseWidth);
-    newWidths[FLEXIBLE_COLUMN] = `${flexibleWidth}px`;
+    const flexibleWidth = Math.max(remainingWidth, baseWidth)
+    newWidths[FLEXIBLE_COLUMN] = `${flexibleWidth}px`
   }
 
   // 更新列宽度
-  columnWidths.value = newWidths;
+  columnWidths.value = newWidths
 
   // 重新布局表格
   nextTick(() => {
-    tableRef.value?.doLayout();
-  });
-};
+    tableRef.value?.doLayout()
+  })
+}
 
 /** 查询列表 */
 const getList = async () => {
@@ -381,9 +471,8 @@ const selectedFactoryReqVO = ref({
 
 /** 已经选择了 SAP工厂 */
 const selectedFactoryChange = async (selectedId: number | undefined) => {
-
   // 获取选中的factoryCode数组
-  const selectedFactory = factoryList.value.find(item => item.id === selectedId)
+  const selectedFactory = factoryList.value.find((item) => item.id === selectedId)
   const selectedFactoryCodes = selectedFactory ? [selectedFactory.factoryCode] : []
 
   // 获得已经选择的 SAP 工厂 数组
@@ -408,7 +497,7 @@ const resultOptions = computed(() => [
   {
     label: '否',
     value: 'N' // 空值会触发 clearable 效果
-  },
+  }
 ])
 
 /** 删除按钮操作 */
@@ -427,12 +516,9 @@ const handleDelete = async (id: number) => {
 /** 导出按钮操作 */
 const handleExport = async () => {
   try {
-    // 导出的二次确认
-    await message.exportConfirm()
-    // 发起导出
     exportLoading.value = true
     const data = await IotSapStockApi.exportIotSapStock(queryParams)
-    download.excel(data, 'PMS SAP 库存(通用库存/项目部库存).xls')
+    download.excel(data, 'SPA库存.xls')
   } catch {
   } finally {
     exportLoading.value = false
@@ -452,10 +538,13 @@ onUnmounted(() => {
 })
 
 // 监听列表数据变化,重新计算列宽
-watch(list, () => {
-  nextTick(calculateColumnWidths)
-}, { deep: true })
-
+watch(
+  list,
+  () => {
+    nextTick(calculateColumnWidths)
+  },
+  { deep: true }
+)
 </script>
 
 <style scoped>
@@ -485,7 +574,7 @@ watch(list, () => {
 .stat-value {
   font-size: 18px;
   font-weight: bold;
-  color: #409EFF;
+  color: #409eff;
 }
 
 /* 表格容器样式 - 确保可以水平滚动 */
@@ -526,5 +615,4 @@ watch(list, () => {
   overflow: visible;
   text-overflow: unset;
 }
-
 </style>

+ 41 - 41
src/views/pms/maintenance/IotMaintenancePlanEdit.vue

@@ -106,37 +106,38 @@
           </template>
         </el-table-column>
         <el-table-column
-          :label="t('main.mileage')"
+          :label="t('main.runTime')"
           align="center"
-          key="mileageRule"
-          prop="mileageRule"
-          :min-width="columnWidths.mileageRule"
+          key="runningTimeRule"
+          prop="runningTimeRule"
+          :min-width="columnWidths.runningTimeRule"
         >
           <template #default="scope">
             <el-switch
-              v-model="scope.row.mileageRule"
+              v-model="scope.row.runningTimeRule"
               :active-value="0"
               :inactive-value="1"
-              @change="handleRuleChange(scope.row, 'mileage')"
+              @change="handleRuleChange(scope.row, 'runningTime')"
             />
           </template>
         </el-table-column>
         <el-table-column
-          :label="t('main.runTime')"
+          :label="t('main.mileage')"
           align="center"
-          key="runningTimeRule"
-          prop="runningTimeRule"
-          :min-width="columnWidths.runningTimeRule"
+          key="mileageRule"
+          prop="mileageRule"
+          :min-width="columnWidths.mileageRule"
         >
           <template #default="scope">
             <el-switch
-              v-model="scope.row.runningTimeRule"
+              v-model="scope.row.mileageRule"
               :active-value="0"
               :inactive-value="1"
-              @change="handleRuleChange(scope.row, 'runningTime')"
+              @change="handleRuleChange(scope.row, 'mileage')"
             />
           </template>
         </el-table-column>
+
         <el-table-column
           :label="t('main.date')"
           align="center"
@@ -201,72 +202,71 @@
             </div>
           </template>
         </el-table-column>
-        <!-- 保养里程 分组 -->
-        <el-table-column v-if="hasMileageRuleInCurrentPage" label="保养里程" align="center">
+        <!-- 保养时长 分组 -->
+        <el-table-column v-if="hasTimeRuleInCurrentPage" label="保养时长" align="center">
           <el-table-column
-            :label="t('mainPlan.lastMaintenanceMileage')"
+            :label="t('mainPlan.lastMaintenanceOperationTime')"
             align="center"
-            prop="lastRunningKilometers"
+            prop="lastRunningTime"
             :formatter="erpPriceTableColumnFormatter"
-            :min-width="columnWidths.lastRunningKilometers"
+            :min-width="columnWidths.lastRunningTime"
           >
             <template #default="{ row }">
-              {{ row.lastRunningKilometers }}
+              {{ row.lastRunningTime }}
             </template>
           </el-table-column>
           <el-table-column
-            :label="t('mainPlan.nextMaintenanceKm')"
+            :label="t('mainPlan.nextMaintenanceH')"
             align="center"
-            prop="nextMaintenanceKm"
-            :min-width="columnWidths.nextMaintenanceKm"
+            prop="nextMaintenanceH"
+            :min-width="columnWidths.nextMaintenanceH"
           >
             <template #default="{ row }">
-              {{ row.nextMaintenanceKm ?? '-' }}
+              {{ row.nextMaintenanceH ?? '-' }}
             </template>
           </el-table-column>
           <el-table-column
-            :label="t('mainPlan.remainKm')"
+            :label="t('mainPlan.remainH')"
             align="center"
-            prop="remainKm"
-            :min-width="columnWidths.remainKm"
+            prop="remainH"
+            :min-width="columnWidths.remainH"
           >
             <template #default="{ row }">
-              {{ row.remainKm ?? '-' }}
+              {{ row.remainH ?? '-' }}
             </template>
           </el-table-column>
         </el-table-column>
-
-        <!-- 保养时长 分组 -->
-        <el-table-column v-if="hasTimeRuleInCurrentPage" label="保养时长" align="center">
+        <!-- 保养里程 分组 -->
+        <el-table-column v-if="hasMileageRuleInCurrentPage" label="保养里程" align="center">
           <el-table-column
-            :label="t('mainPlan.lastMaintenanceOperationTime')"
+            :label="t('mainPlan.lastMaintenanceMileage')"
             align="center"
-            prop="lastRunningTime"
+            prop="lastRunningKilometers"
             :formatter="erpPriceTableColumnFormatter"
-            :min-width="columnWidths.lastRunningTime"
+            :min-width="columnWidths.lastRunningKilometers"
           >
             <template #default="{ row }">
-              {{ row.lastRunningTime }}
+              {{ row.lastRunningKilometers }}
             </template>
           </el-table-column>
           <el-table-column
-            :label="t('mainPlan.nextMaintenanceH')"
+            :label="t('mainPlan.nextMaintenanceKm')"
             align="center"
-            prop="nextMaintenanceH"
-            :min-width="columnWidths.nextMaintenanceH"
+            prop="nextMaintenanceKm"
+            :min-width="columnWidths.nextMaintenanceKm"
           >
             <template #default="{ row }">
-              {{ row.nextMaintenanceH ?? '-' }}
+              {{ row.nextMaintenanceKm ?? '-' }}
             </template>
           </el-table-column>
           <el-table-column
-            :label="t('mainPlan.remainH')"
+            :label="t('mainPlan.remainKm')"
             align="center"
-            prop="remainH"
-            :min-width="columnWidths.remainH"
+            prop="remainKm"
+            :min-width="columnWidths.remainKm"
           >
             <template #default="{ row }">
-              {{ row.remainH ?? '-' }}
+              {{ row.remainKm ?? '-' }}
             </template>
           </el-table-column>
         </el-table-column>

+ 0 - 2
src/views/pms/maintenance/index.vue

@@ -252,8 +252,6 @@ const handleDelete = async (id: number) => {
 /** 导出按钮操作 */
 const handleExport = async () => {
   try {
-    // 导出的二次确认
-    await message.exportConfirm()
     // 发起导出
     exportLoading.value = true
     const data = await IotMaintenancePlanApi.exportIotMaintenancePlan(queryParams)

+ 116 - 0
src/views/pms/monitor/ModelViewer.vue

@@ -0,0 +1,116 @@
+<template>
+  <div ref="container" class="w-full h-full z-999 cursor-pointer"></div>
+</template>
+
+<script setup lang="ts">
+import { ref, onMounted, onUnmounted } from 'vue'
+import * as THREE from 'three'
+import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js'
+import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'
+
+const container = ref<HTMLDivElement>()
+
+let scene: THREE.Scene
+let camera: THREE.PerspectiveCamera
+let renderer: THREE.WebGLRenderer
+let controls: OrbitControls
+let model: THREE.Group
+
+const init = () => {
+  scene = new THREE.Scene()
+  // scene.background = new THREE.Color(0x0a0a0a) // 移除深色背景
+
+  camera = new THREE.PerspectiveCamera(75, 1, 0.1, 1000)
+  camera.position.set(3, 3, 3)
+  camera.lookAt(0, 0, 0)
+
+  renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true })
+  renderer.setSize(2000, 1000)
+  renderer.shadowMap.enabled = true
+  renderer.shadowMap.type = THREE.PCFSoftShadowMap
+  container.value?.appendChild(renderer.domElement)
+
+  controls = new OrbitControls(camera, renderer.domElement)
+  controls.enableDamping = true
+  controls.dampingFactor = 0.05
+  controls.enableZoom = true
+  controls.enablePan = false
+
+  const hemisphereLight = new THREE.HemisphereLight(0xffffbb, 0x080820, 2) // 增加强度从1到2
+  scene.add(hemisphereLight)
+
+  const pointLight = new THREE.PointLight(0xffffff, 2, 100) // 增加强度从1到2
+  pointLight.position.set(0, 5, 5)
+  scene.add(pointLight)
+
+  const directionalLight = new THREE.DirectionalLight(0xffffff, 1.5) // 增加强度从0.8到1.5
+  directionalLight.position.set(10, 10, 5)
+  directionalLight.castShadow = true
+  scene.add(directionalLight)
+
+  // 添加额外的环境光
+  const ambientLight = new THREE.AmbientLight(0x404040, 0.6) // 添加环境光
+  scene.add(ambientLight)
+
+  const modelUrl = '/model/test2.glb'
+  console.log('模型URL:', modelUrl)
+  const loader = new GLTFLoader()
+  loader.load(
+    modelUrl,
+    (gltf) => {
+      console.log('模型加载成功:', gltf)
+      model = gltf.scene
+      model.scale.set(7, 7, 7) // 增加缩放
+      model.position.set(0, 0, 0)
+      scene.add(model)
+    },
+    (progress) => {
+      console.log('模型加载进度:', progress) // 加载进度
+    },
+    (error) => {
+      console.error('模型加载失败:', error)
+    }
+  )
+
+  const animate = () => {
+    requestAnimationFrame(animate)
+    controls.update()
+    if (model) {
+      model.rotation.y += 0.005
+    } else {
+      scene.children.forEach((child) => {
+        if (child instanceof THREE.Mesh && child.geometry instanceof THREE.BoxGeometry) {
+          child.rotation.y += 0.01
+        }
+      })
+    }
+    renderer.render(scene, camera)
+  }
+  animate()
+
+  const resize = () => {
+    if (container.value) {
+      const width = container.value.clientWidth
+      const height = container.value.clientHeight
+      camera.aspect = width / height
+      camera.updateProjectionMatrix()
+      renderer.setSize(width, height)
+    }
+  }
+  window.addEventListener('resize', resize)
+  resize()
+}
+
+onMounted(() => {
+  init()
+})
+
+onUnmounted(() => {
+  if (renderer) {
+    renderer.dispose()
+  }
+  if (controls) {
+    controls.dispose()
+  }
+})
+</script>

+ 116 - 0
src/views/pms/monitor/ModelViewer2.vue

@@ -0,0 +1,116 @@
+<template>
+  <div ref="container" class="w-full h-full z-999 cursor-pointer"></div>
+</template>
+
+<script setup lang="ts">
+import { ref, onMounted, onUnmounted } from 'vue'
+import * as THREE from 'three'
+import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js'
+import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'
+
+const container = ref<HTMLDivElement>()
+
+let scene: THREE.Scene
+let camera: THREE.PerspectiveCamera
+let renderer: THREE.WebGLRenderer
+let controls: OrbitControls
+let model: THREE.Group
+
+const init = () => {
+  scene = new THREE.Scene()
+  // scene.background = new THREE.Color(0x0a0a0a) // 移除深色背景
+
+  camera = new THREE.PerspectiveCamera(75, 1, 0.1, 1000)
+  camera.position.set(3, 3, 3)
+  camera.lookAt(0, 0, 0)
+
+  renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true })
+  renderer.setSize(2000, 1000)
+  renderer.shadowMap.enabled = true
+  renderer.shadowMap.type = THREE.PCFSoftShadowMap
+  container.value?.appendChild(renderer.domElement)
+
+  controls = new OrbitControls(camera, renderer.domElement)
+  controls.enableDamping = true
+  controls.dampingFactor = 0.05
+  controls.enableZoom = true
+  controls.enablePan = false
+
+  const hemisphereLight = new THREE.HemisphereLight(0xffffbb, 0x080820, 2) // 增加强度从1到2
+  scene.add(hemisphereLight)
+
+  const pointLight = new THREE.PointLight(0xffffff, 2, 100) // 增加强度从1到2
+  pointLight.position.set(0, 5, 5)
+  scene.add(pointLight)
+
+  const directionalLight = new THREE.DirectionalLight(0xffffff, 1.5) // 增加强度从0.8到1.5
+  directionalLight.position.set(10, 10, 5)
+  directionalLight.castShadow = true
+  scene.add(directionalLight)
+
+  // 添加额外的环境光
+  const ambientLight = new THREE.AmbientLight(0x404040, 0.6) // 添加环境光
+  scene.add(ambientLight)
+
+  const modelUrl = '/model/industrialEquipment2.glb'
+  console.log('模型URL:', modelUrl)
+  const loader = new GLTFLoader()
+  loader.load(
+    modelUrl,
+    (gltf) => {
+      console.log('模型加载成功:', gltf)
+      model = gltf.scene
+      model.scale.set(7, 7, 7) // 增加缩放
+      model.position.set(0, 0, 0)
+      scene.add(model)
+    },
+    (progress) => {
+      console.log('模型加载进度:', progress) // 加载进度
+    },
+    (error) => {
+      console.error('模型加载失败:', error)
+    }
+  )
+
+  const animate = () => {
+    requestAnimationFrame(animate)
+    controls.update()
+    if (model) {
+      model.rotation.y += 0.005
+    } else {
+      scene.children.forEach((child) => {
+        if (child instanceof THREE.Mesh && child.geometry instanceof THREE.BoxGeometry) {
+          child.rotation.y += 0.01
+        }
+      })
+    }
+    renderer.render(scene, camera)
+  }
+  animate()
+
+  const resize = () => {
+    if (container.value) {
+      const width = container.value.clientWidth
+      const height = container.value.clientHeight
+      camera.aspect = width / height
+      camera.updateProjectionMatrix()
+      renderer.setSize(width, height)
+    }
+  }
+  window.addEventListener('resize', resize)
+  resize()
+}
+
+onMounted(() => {
+  init()
+})
+
+onUnmounted(() => {
+  if (renderer) {
+    renderer.dispose()
+  }
+  if (controls) {
+    controls.dispose()
+  }
+})
+</script>

+ 40 - 0
src/views/pms/monitor/data-row.vue

@@ -0,0 +1,40 @@
+<template>
+  <div
+    style="border-bottom: 0.5px solid rgba(255, 255, 255, 0.1); border-radius: 5px"
+    :class="[
+      'flex items-center justify-between border-b border-cyan-500/20 hover:bg-cyan-500/10 transition-colors px-2 -mx-2 rounded',
+      compact ? 'py-1' : 'py-2'
+    ]"
+  >
+    <span class="text-cyan-200">{{ label }}</span>
+    <div class="flex items-center gap-2">
+      <span class="font-mono text-white font-bold">{{ value }}</span>
+      <button
+        v-if="button"
+        style="border-radius: 5px"
+        :class="[
+          'px-3 py-1 rounded text-white text-sm transition-all',
+          buttonColor === 'green'
+            ? 'bg-green-600 hover:bg-green-500 shadow-[0_0_10px_rgba(22,163,74,0.5)]'
+            : 'bg-red-600 hover:bg-red-500 shadow-[0_0_10px_rgba(220,38,38,0.5)]'
+        ]"
+      >
+        {{ button }}
+      </button>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+interface Props {
+  label: string
+  value: string
+  button?: string
+  buttonColor?: 'green' | 'red'
+  compact?: boolean
+}
+
+withDefaults(defineProps<Props>(), {
+  compact: false
+})
+</script>

+ 204 - 0
src/views/pms/monitor/data.txt

@@ -0,0 +1,204 @@
+{
+    "deptName": "HY-A41",
+    "groupName": "塔河A41",
+    "totalRuntime": 25838.2,
+    "增压机": [
+        {
+            "modelName": "急停状态",
+            "value": "",
+            "sort": null
+        },
+        {
+            "modelName": "二套",
+            "value": "",
+            "sort": null
+        },
+        {
+            "modelName": "瞬时流量",
+            "value": "",
+            "sort": null
+        },
+        {
+            "modelName": "表套",
+            "value": "",
+            "sort": null
+        },
+        {
+            "modelName": "油泵状态",
+            "value": "",
+            "sort": null
+        },
+        {
+            "modelName": "累计流量",
+            "value": "",
+            "sort": null
+        },
+        {
+            "modelName": "无油1#",
+            "value": "",
+            "sort": null
+        },
+        {
+            "modelName": "无油2#",
+            "value": "",
+            "sort": null
+        },
+        {
+            "modelName": "风机状态",
+            "value": "",
+            "sort": null
+        },
+        {
+            "modelName": "氮气纯度",
+            "value": "",
+            "sort": null
+        },
+        {
+            "modelName": "曲轴油位",
+            "value": "",
+            "sort": null
+        },
+        {
+            "modelName": "振动",
+            "value": "",
+            "sort": null
+        },
+        {
+            "modelName": "油压",
+            "value": "",
+            "sort": null
+        },
+        {
+            "modelName": "套压",
+            "value": "",
+            "sort": null
+        },
+        {
+            "modelName": "润滑油温度",
+            "value": "",
+            "sort": null
+        },
+        {
+            "modelName": "轴瓦温度6",
+            "value": "",
+            "sort": null
+        },
+        {
+            "modelName": "轴瓦温度5",
+            "value": "",
+            "sort": null
+        },
+        {
+            "modelName": "轴瓦温度4",
+            "value": "",
+            "sort": null
+        },
+        {
+            "modelName": "轴瓦温度3",
+            "value": "",
+            "sort": null
+        },
+        {
+            "modelName": "轴瓦温度2",
+            "value": "",
+            "sort": null
+        },
+        {
+            "modelName": "轴瓦温度1",
+            "value": "",
+            "sort": null
+        },
+        {
+            "modelName": "冷却水出口压力",
+            "value": "",
+            "sort": null
+        },
+        {
+            "modelName": "冷却水进口压力",
+            "value": "",
+            "sort": null
+        },
+        {
+            "modelName": "润滑油压力",
+            "value": "",
+            "sort": null
+        },
+        {
+            "modelName": "五级排气压力",
+            "value": "",
+            "sort": null
+        },
+        {
+            "modelName": "四级排气压力",
+            "value": "",
+            "sort": null
+        },
+        {
+            "modelName": "三级排气压力",
+            "value": "",
+            "sort": null
+        },
+        {
+            "modelName": "二级排气压力",
+            "value": "",
+            "sort": null
+        },
+        {
+            "modelName": "一级排气压力",
+            "value": "",
+            "sort": null
+        },
+        {
+            "modelName": "一级进气压力",
+            "value": "",
+            "sort": null
+        },
+        {
+            "modelName": "冷却水温度",
+            "value": "",
+            "sort": null
+        },
+        {
+            "modelName": "最终排气温度",
+            "value": "",
+            "sort": null
+        },
+        {
+            "modelName": "五级排气温度",
+            "value": "",
+            "sort": null
+        },
+        {
+            "modelName": "四级排气温度3",
+            "value": "",
+            "sort": null
+        },
+        {
+            "modelName": "四级排气温度2",
+            "value": "",
+            "sort": null
+        },
+        {
+            "modelName": "四级排气温度1",
+            "value": "",
+            "sort": null
+        },
+        {
+            "modelName": "三级排气温度",
+            "value": "",
+            "sort": null
+        },
+        {
+            "modelName": "二级排气温度",
+            "value": "",
+            "sort": null
+        },
+        {
+            "modelName": "一级排气温度",
+            "value": "",
+            "sort": null
+        }
+    ],
+    "wellName": "YD1-5CX,YD1-5",
+    "deviceCode": "YF520"
+}

+ 501 - 0
src/views/pms/monitor/index.vue

@@ -0,0 +1,501 @@
+<template>
+  <el-row :gutter="20">
+    <!-- 左侧部门树 -->
+    <el-col :span="4" :xs="24">
+      <ContentWrap class="h-1/1" style="border: 0" v-if="treeShow">
+        <DeptTree @node-click="handleDeptNodeClick" />
+      </ContentWrap>
+    </el-col>
+    <el-col :span="contentSpan" :xs="24">
+      <ContentWrap style="border: 0">
+        <!-- 搜索工作栏 -->
+        <el-form
+          class="-mb-15px"
+          :model="queryParams"
+          ref="queryFormRef"
+          :inline="true"
+          label-width="68px"
+        >
+          <el-form-item label="成套名称" prop="name">
+            <el-input
+              v-model="queryParams.name"
+              placeholder="请输入成套名称"
+              clearable
+              @keyup.enter="handleQuery"
+              class="!w-200px"
+            />
+          </el-form-item>
+          <el-form-item>
+            <el-button @click="handleQuery"
+              ><Icon icon="ep:search" class="mr-5px" /> {{ t('devicePerson.search') }}</el-button
+            >
+            <el-button @click="resetQuery"
+              ><Icon icon="ep:refresh" class="mr-5px" /> {{ t('devicePerson.reset') }}</el-button
+            >
+          </el-form-item>
+        </el-form>
+      </ContentWrap>
+
+      <!-- 列表 -->
+      <ContentWrap style="border: 0">
+        <el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
+          <el-table-column :label="t('monitor.serial')" width="70" align="center">
+            <template #default="scope">
+              {{ scope.$index + 1 }}
+            </template>
+          </el-table-column>
+          <el-table-column label="部门名称" align="center" prop="deptName" />
+          <el-table-column label="成套名称" align="center" prop="name" />
+          <el-table-column label="成套类型" align="center">
+            <template #default="scope">
+              <dict-tag :type="DICT_TYPE.DEVICE_GROUP_TYPE" :value="scope.row.type" />
+            </template>
+          </el-table-column>
+
+          <el-table-column label="描述" align="center" prop="remark" />
+          <el-table-column label="设备数量" align="center" prop="deviceCount">
+            <template #default="scope">
+              {{ (scope.row.details && scope.row.details.length) || 0 }}
+            </template>
+          </el-table-column>
+          <el-table-column label="主设备" align="center" prop="mainDeviceName">
+            <template #default="scope">
+              {{
+                scope.row.details.filter((item) => item.ifMaster)[0]?.deviceName +
+                  ' ' +
+                  scope.row.details.filter((item) => item.ifMaster)[0]?.deviceCode || '无'
+              }}
+            </template>
+          </el-table-column>
+
+          <el-table-column :label="t('devicePerson.operation')" align="center" min-width="120px">
+            <template #default="scope">
+              <el-button link type="primary" @click="handleEdit(scope.row)"> 查看详情 </el-button>
+            </template>
+          </el-table-column>
+        </el-table>
+        <!-- 分页 -->
+        <Pagination
+          :total="total"
+          v-model:page="queryParams.pageNo"
+          v-model:limit="queryParams.pageSize"
+          @pagination="getList"
+        />
+      </ContentWrap>
+    </el-col>
+  </el-row>
+
+  <!-- 新增/编辑成套设备对话框 -->
+  <el-dialog :title="dialogTitle" v-model="dialogVisible" width="800px" @close="cancel">
+    <template #header>
+      <div class="my-header" style="padding-bottom: 20px">
+        <span>{{ dialogTitle }}</span>
+      </div>
+    </template>
+    <el-form ref="formRef" :model="formData" :rules="formRules" label-width="80px">
+      <el-row :gutter="20">
+        <el-col :span="12">
+          <el-form-item label="成套名称" prop="name">
+            <el-input v-model="formData.name" placeholder="请输入成套名称" />
+          </el-form-item>
+        </el-col>
+
+        <el-col :span="12">
+          <el-form-item :label="t('iotDevice.dept')" prop="deptId">
+            <el-tree-select
+              v-model="formData.deptId"
+              :data="deptList"
+              :props="defaultProps"
+              check-strictly
+              node-key="id"
+              filterable
+              placeholder="请选择所在部门"
+              @change="handleDeptChange"
+            />
+          </el-form-item>
+        </el-col>
+      </el-row>
+
+      <el-row :gutter="20">
+        <el-col :span="24">
+          <el-form-item label="描述" prop="remark">
+            <el-input
+              v-model="formData.remark"
+              type="textarea"
+              placeholder="请输入描述"
+              :rows="2"
+            />
+          </el-form-item>
+        </el-col>
+      </el-row>
+
+      <el-row :gutter="20">
+        <el-col :span="24">
+          <el-form-item label="选择设备" prop="devices">
+            <div class="transfer-container">
+              <el-transfer
+                v-model="selectedDeviceIds"
+                :data="deviceOptions"
+                :titles="['设备列表', '已选择设备']"
+                :button-texts="['移除', '添加']"
+                filterable
+                :filter-method="filterDeviceMethod"
+                filter-placeholder="请输入设备名称"
+                @change="rightDeviceChange"
+              >
+                <template #left-empty>
+                  <el-empty :image-size="60" :description="isEdit ? '加载中...' : '请选择设备'" />
+                </template>
+                <template #right-empty>
+                  <el-empty :image-size="60" :description="isEdit ? '加载中...' : '请选择设备'" />
+                </template>
+              </el-transfer>
+            </div>
+          </el-form-item>
+        </el-col>
+      </el-row>
+
+      <el-row :gutter="20" v-if="selectedDevices.length > 0">
+        <el-col :span="12">
+          <el-form-item label="设置主设备" prop="mainDevice" label-width="100">
+            <el-select
+              v-model="mainDeviceId"
+              placeholder="请选择主设备"
+              clearable
+              filterable
+              @change="setMainDevice"
+            >
+              <el-option
+                v-for="device in selectedDevices"
+                :key="device.id"
+                :label="device.label"
+                :value="device.id"
+              />
+            </el-select>
+          </el-form-item>
+        </el-col>
+      </el-row>
+    </el-form>
+
+    <template #footer>
+      <el-button @click="cancel">取 消</el-button>
+      <el-button type="primary" @click="submit">确 定</el-button>
+    </template>
+  </el-dialog>
+</template>
+
+<script setup lang="ts">
+import { IotDeviceApi } from '@/api/pms/device'
+import DeptTree from '@/views/system/user/DeptTree.vue'
+import { defaultProps, handleTree } from '@/utils/tree'
+import * as DeptApi from '@/api/system/dept'
+import { DICT_TYPE, getDictLabel, getStrDictOptions } from '@/utils/dict'
+const deptList = ref<Tree[]>([]) // 树形结构
+import { useRouter } from 'vue-router'
+const router = useRouter()
+
+defineOptions({ name: 'IotDeviceComplete' })
+
+const loading = ref(true) // 列表的加载中
+
+const { t } = useI18n()
+
+const list = ref([]) // 列表的数据
+const total = ref(0) // 列表的总页数
+const queryParams = reactive({
+  pageNo: 1,
+  pageSize: 10,
+  deptName: undefined,
+  name: undefined,
+  deptId: undefined
+})
+const queryFormRef = ref(null) // 搜索的表单
+
+const contentSpan = ref(20)
+const treeShow = ref(true)
+
+// 对话框相关
+const dialogVisible = ref(false)
+const dialogTitle = ref('')
+const isEdit = ref(false)
+
+// 表单相关
+const formRef = ref()
+const formData = ref({
+  name: '',
+  details: [],
+  deptId: '',
+  remark: ''
+})
+
+// 表单验证规则
+const formRules = {
+  name: [{ required: true, message: '成套名称不能为空', trigger: 'blur' }],
+  code: [{ required: true, message: '成套编码不能为空', trigger: 'blur' }],
+  deptId: [{ required: true, message: '请选择部门', trigger: 'change' }],
+  devices: [
+    {
+      required: true,
+      validator: (rule: any, value: any, callback: any) => {
+        if (selectedDeviceIds.value.length === 0) {
+          callback(new Error('请至少选择一个设备'))
+        } else {
+          callback()
+        }
+      },
+      trigger: 'change'
+    }
+  ],
+  mainDevice: [
+    {
+      required: true,
+      validator: (rule: any, value: any, callback: any) => {
+        if (!mainDeviceId.value) {
+          callback(new Error('请选择主设备'))
+        } else {
+          callback()
+        }
+      },
+      trigger: 'change'
+    }
+  ]
+}
+
+// 部门树数据
+const selectedDeptId = ref<number | string>('')
+
+// 新增时部门改变时获取设备列表
+const handleDeptChange = async (deptId) => {
+  if (deptId) {
+    selectedDeptId.value = deptId
+    getDeviceList(deptId)
+  }
+
+  // 清空主设备she 设备列表
+  selectedDevices.value = []
+  mainDeviceId.value = ''
+  selectedDeviceIds.value = []
+}
+// 获取设备列表
+const getDeviceList = async (deptId) => {
+  try {
+    const res = await IotDeviceApi.getIotDeviceSetOptions(deptId)
+    deviceOptions.value = res.map((item) => ({
+      key: item.id, // 始终使用id作为key
+      label: `${item.deviceName} (${item.deviceCode})`,
+      ...item
+    }))
+  } catch (err) {
+    console.error(err)
+  }
+}
+
+// 设备选择相关
+const deviceOptions = ref<any[]>([])
+const selectedDeviceIds = ref([])
+const selectedDevices = ref([])
+const mainDeviceId = ref('')
+
+/** 查询列表 */
+const getList = async () => {
+  loading.value = true
+  try {
+    const data = await IotDeviceApi.getIotDeviceSetList(queryParams)
+    list.value = data.list
+    total.value = data.total
+  } finally {
+    loading.value = false
+  }
+}
+
+/** 首页处理部门被点击 */
+const handleDeptNodeClick = async (row) => {
+  queryParams.deptId = row.id
+  await getList()
+}
+
+/** 搜索按钮操作 */
+const handleQuery = () => {
+  queryParams.pageNo = 1
+  getList()
+}
+
+/** 重置按钮操作 */
+const resetQuery = () => {
+  queryFormRef.value.resetFields()
+  handleQuery()
+}
+
+// 设备过滤方法
+const filterDeviceMethod = (query, item) => {
+  return item.label.toLowerCase().includes(query.toLowerCase())
+}
+
+// 更新已选择的设备列表
+const updateSelectedDevices = () => {
+  // 根据 selectedDeviceIds 从 deviceOptions 中找到对应的设备
+  selectedDevices.value = deviceOptions.value.filter((item) =>
+    selectedDeviceIds.value.includes(item.key)
+  )
+
+  console.log('selectedDevices>>>>>>>>>>>>>>>>', selectedDevices.value)
+
+  // 构建 details 数据
+  formData.value.details = selectedDevices.value.map((item) => ({
+    deviceId: item.id,
+    deviceName: item.deviceName,
+    deviceCode: item.deviceCode,
+    deptId: item.deptId,
+    ifMaster: item.id === mainDeviceId.value ? true : false
+  }))
+
+  // 如果主设备不在当前选择中,则清空主设备
+  if (
+    mainDeviceId.value &&
+    !selectedDevices.value.some((device) => device.id === mainDeviceId.value)
+  ) {
+    mainDeviceId.value = ''
+  }
+}
+
+const rightDeviceChange = (val) => {
+  selectedDeviceIds.value = val
+  updateSelectedDevices()
+
+  // 手动触发验证
+  if (formRef.value) {
+    formRef.value.validateField('devices')
+    if (val.length > 0) {
+      formRef.value.validateField('mainDevice')
+    }
+  }
+}
+
+// 设置主设备
+const setMainDevice = (val) => {
+  mainDeviceId.value = val
+  // 更新 details 中的 ifMaster 字段
+  console.log('selectedDevices.value>>>>>>>>>>>>>>>>', selectedDevices.value)
+
+  formData.value.details = selectedDevices.value.map((item) => ({
+    deviceId: item.id,
+    deviceName: item.deviceName,
+    deviceCode: item.deviceCode,
+    deptId: item.deptId,
+    ifMaster: item.id === mainDeviceId.value ? true : false
+  }))
+
+  console.log('formData.value.details>>>>>>>>>>>>>>>>', formData.value.details)
+
+  // 手动触发验证
+  if (formRef.value) {
+    formRef.value.validateField('mainDevice')
+  }
+}
+
+// 显示新增对话框
+const handleAdd = () => {
+  isEdit.value = false
+  dialogTitle.value = '新增成套设备'
+  resetForm()
+  dialogVisible.value = true
+}
+
+// 显示编辑对话框
+const handleEdit = (row) => {
+  if (row.type === '1') {
+    router.push({ path: '/kanban/monitor/kanban', query: { deviceSetId: row.id } })
+  } else {
+    router.push({ path: '/kanban/monitor/kanbanOther', query: { deviceSetId: row.id } })
+  }
+}
+
+// 重置表单
+const resetForm = () => {
+  formData.value = {
+    name: '',
+    details: [],
+    deptId: '',
+    remark: ''
+  }
+  selectedDeviceIds.value = []
+  selectedDevices.value = []
+  deviceOptions.value = []
+  mainDeviceId.value = ''
+}
+
+// 取消操作
+const cancel = () => {
+  dialogVisible.value = false
+  resetForm()
+}
+
+// 提交表单
+const submit = async () => {
+  if (!formRef.value) return
+
+  await formRef.value.validate(async (valid) => {
+    if (!valid) return
+    try {
+      const data = {
+        ...formData.value
+      }
+
+      console.log('提交数据:', data)
+
+      if (isEdit.value) {
+        await IotDeviceApi.updateIotDeviceSet(data)
+        ElMessage.success('编辑成功')
+      } else {
+        await IotDeviceApi.createIotDeviceSet(data)
+        ElMessage.success('新增成功')
+      }
+
+      dialogVisible.value = false
+      getList()
+    } catch (error) {
+      console.error(error)
+    }
+  })
+}
+
+onMounted(async () => {
+  getList()
+
+  deptList.value = handleTree(await DeptApi.getSimpleDeptList())
+})
+</script>
+
+<style scoped>
+.transfer-container {
+  display: flex;
+  flex-direction: column;
+}
+
+.transfer-footer {
+  margin-top: 8px;
+  text-align: right;
+}
+
+::deep(.el-transfer-panel) {
+  width: 300px;
+}
+
+::deep(.el-tree--highlight-current) {
+  height: 200px !important;
+}
+
+/* 调整穿梭框面板主体高度 */
+::deep(.el-transfer-panel__body) {
+  height: 850px !important;
+}
+
+/* 如果需要进一步调整,可以分别设置左右面板的高度 */
+::deep(.el-transfer-panel .el-transfer-panel__list) {
+  height: 850px !important; /* 调整列表区域高度 */
+}
+
+.el-transfer {
+  --el-transfer-panel-body-height: 500px;
+}
+</style>

+ 442 - 0
src/views/pms/monitor/kanban.vue

@@ -0,0 +1,442 @@
+<template>
+  <div
+    class="bg-gradient-to-br from-slate-950 via-blue-950 to-slate-900 text-cyan-100 p-6 relative"
+    :class="{ 'fullscreen-layout': isFullscreen }"
+  >
+    <!-- Animated background grid -->
+    <div class="absolute inset-0 opacity-20">
+      <div class="absolute inset-0 grid-pattern"></div>
+    </div>
+
+    <!-- Scanning line effect -->
+    <div class="absolute inset-0 pointer-events-none">
+      <div class="scan-line"></div>
+    </div>
+
+    <!-- Header -->
+    <div class="relative z-10 mb-6">
+      <div class="flex items-center justify-center border-b-2 border-cyan-500/30 pb-4 gap-8">
+        <h1 class="text-2xl font-bold text-cyan-300 flex-6 tracking-wider">
+          {{ dataInfo.groupName }}
+        </h1>
+
+        <div class="flex flex-4 items-center gap-8 text-sm">
+          <div
+            style="border: 0.5px solid #085b77"
+            class="px-4 py-2 bg-cyan-500/10 border border-cyan-500/30 skew-x-[-12deg] hover:bg-cyan-500/20 transition-all cursor-pointer"
+          >
+            <span class="pr-4">部门名称</span>
+            <span class="inline-block skew-x-[12deg]">{{ dataInfo.deptName }}</span>
+          </div>
+          <div
+            style="border: 0.5px solid #085b77"
+            class="px-4 py-2 bg-cyan-500/10 border border-cyan-500/30 skew-x-[-12deg] hover:bg-cyan-500/20 transition-all cursor-pointer"
+          >
+            <span class="pr-4">井号</span>
+            <span class="inline-block skew-x-[12deg]">{{ dataInfo.wellName }}</span>
+          </div>
+          <div
+            @click="toggleFullscreen"
+            style="border: 0.5px solid #085b77"
+            class="px-4 py-2 bg-cyan-500/10 border border-cyan-500/30 skew-x-[-12deg] hover:bg-cyan-500/20 transition-all cursor-pointer"
+          >
+            <span class="inline-block skew-x-[12deg]">{{
+              isFullscreen ? '退出全屏' : '全屏'
+            }}</span>
+          </div>
+        </div>
+      </div>
+    </div>
+
+    <div class="relative z-10 grid grid-cols-12 gap-4 mt-5 p-6">
+      <!-- Left panels - 合并为一个大面板 -->
+      <div class="col-span-3">
+        <div class="panel h-full flex flex-col">
+          <!-- PSA核心参数 -->
+          <div class="flex-1 border-b border-cyan-500/30 pb-2 mb-2">
+            <div class="flex justify-between items-center panel-title mb-2">
+              <h4>PSA核心参数</h4>
+              <dv-decoration3 style="width: 120px; height: 30px" />
+            </div>
+
+            <div class="space-y-3 overflow-y-auto overflow-x-hidden flex-grow">
+              <DataRow
+                v-for="item in psaData"
+                :key="item.modelName"
+                :label="item.modelName"
+                :value="formatValue(item.value, getUnitFromModelName(item.modelName))"
+              />
+            </div>
+          </div>
+
+          <!-- 空气处理参数 -->
+          <div class="flex-1 border-b border-cyan-500/30 pb-2 mb-2">
+            <div class="flex justify-between items-center panel-title mb-2">
+              <h4>空气处理撬参数</h4>
+              <dv-decoration3 style="width: 120px; height: 30px" />
+            </div>
+            <div class="space-y-3 overflow-y-auto overflow-x-hidden flex-grow">
+              <DataRow
+                v-for="item in airTreatmentData"
+                :key="item.modelName"
+                :label="item.modelName"
+                :value="formatValue(item.value, getUnitFromModelName(item.modelName))"
+              />
+            </div>
+          </div>
+
+          <!-- 1800中压机参数 -->
+          <div class="flex-1">
+            <div class="flex justify-between items-center panel-title mb-2">
+              <h4>中压机参数</h4>
+              <dv-decoration3 style="width: 120px; height: 30px" />
+            </div>
+
+            <div class="space-y-5 overflow-y-auto overflow-x-hidden flex-grow">
+              <DataRow
+                v-for="item in mediumPressureData"
+                :key="item.modelName"
+                :label="item.modelName"
+                :value="formatValue(item.value, getUnitFromModelName(item.modelName))"
+              />
+            </div>
+          </div>
+        </div>
+      </div>
+
+      <div class="col-span-6 space-y-4">
+        <div class="panel relative overflow-hidden">
+          <div class="relative h-[450px] flex items-center justify-center">
+            <ModelViewer />
+          </div>
+        </div>
+
+        <!-- 1050空压机参数 -->
+        <div class="panel">
+          <div class="mb-2">
+            <dv-decoration7>
+              <h3 class="text-cyan-300 text-lg font-bold px-1"> 空压机参数 </h3>
+            </dv-decoration7>
+          </div>
+          <div class="grid grid-cols-5 gap-4">
+            <div v-for="(compressor, i) in compressorData" :key="i" class="space-y-2 text-center">
+              <div class="text-cyan-300 font-bold mb-2">{{ `空压机${i + 1}` }}</div>
+              <div class="text-sm" v-for="item in compressor" :key="item.modelName">
+                <div class="text-cyan-200">{{ item.modelName }}</div>
+                <div
+                  v-if="item.modelName.includes('运行状态') || item.modelName.includes('加载状态')"
+                  class="text-white font-mono flex items-center justify-center"
+                >
+                  <span
+                    :class="['w-3 h-3 rounded-full inline-block mr-2', getStatusClass(item.value)]"
+                  ></span>
+                  {{ getStatusText(item.value) }}
+                </div>
+                <div v-else class="text-white font-mono">
+                  {{ formatValue(item.value, getUnitFromModelName(item.modelName)) }}
+                </div>
+              </div>
+            </div>
+          </div>
+        </div>
+      </div>
+
+      <!-- Right panel -->
+      <div class="col-span-3">
+        <div class="panel h-full">
+          <div class="flex items-center justify-between mb-4 border-b border-cyan-500/30 pb-2">
+            <h3 class="text-cyan-300 text-lg font-bold">液驱压缩机参数</h3>
+            <dv-decoration8 :reverse="true" style="width: 110px; height: 20px" />
+          </div>
+          <div class="space-y-2 text-sm overflow-y-auto overflow-x-hidden">
+            <DataRow
+              v-for="item in liquidDrivenData"
+              :key="item.modelName"
+              :label="item.modelName"
+              :value="formatValue(item.value, getUnitFromModelName(item.modelName))"
+              :compact="true"
+            />
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, onMounted, nextTick } from 'vue'
+import DataRow from './data-row.vue'
+import ModelViewer from './ModelViewer.vue'
+import { IotDeviceApi } from '@/api/pms/device'
+import { useRoute } from 'vue-router'
+
+const route = useRoute()
+
+defineOptions({
+  name: 'Kanban'
+})
+
+let dataInfo = ref<any>({})
+let psaData = ref<any[]>([])
+let airTreatmentData = ref<any[]>([])
+let mediumPressureData = ref<any[]>([])
+let liquidDrivenData = ref<any[]>([])
+let compressorData = ref<any[][]>([])
+const isFullscreen = ref(false)
+
+// 从modelName中提取单位
+const getUnitFromModelName = (modelName: string) => {
+  if (!modelName) return ''
+
+  // 根据不同的参数类型返回单位
+  if (modelName.includes('压力') || modelName.includes('压力P')) return 'MPa'
+  if (modelName.includes('温度') || modelName.includes('T')) return '℃'
+  if (modelName.includes('流量')) return 'Nm³'
+  if (modelName.includes('时间')) return '小时'
+  if (modelName.includes('氮气纯度')) return '%'
+  if (modelName.includes('环境温度')) return '℃'
+  if (modelName.includes('喷油')) return modelName.includes('温度') ? '℃' : 'MPa'
+
+  return '' // 默认无单位
+}
+
+// 获取状态类名
+const getStatusClass = (statusValue: string | number) => {
+  return statusValue === '1' || statusValue === 1 || statusValue === 'true'
+    ? 'status-active'
+    : 'status-inactive'
+}
+
+// 格式化数值显示
+const formatValue = (value: string, unit: string) => {
+  if (
+    value === undefined ||
+    value === null ||
+    value === '' ||
+    value === 'null' ||
+    (value === '0' && unit === '')
+  ) {
+    return '--'
+  }
+  return unit ? `${value} ${unit}` : value
+}
+
+// 根据modelName获取值
+const getValueByModelName = (data: any[], modelName: string) => {
+  const item = data.find((i) => i.modelName === modelName)
+  return item ? item.value : '0'
+}
+
+const toggleFullscreen = async () => {
+  if (!document.fullscreenElement) {
+    // 进入全屏
+    document.body.classList.add('app-fullscreen')
+
+    // 等待 DOM 更新后请求全屏
+    await nextTick()
+    document.documentElement.requestFullscreen()
+  } else {
+    // 退出全屏
+    document
+      .exitFullscreen()
+      .then(() => {
+        document.body.classList.remove('app-fullscreen')
+      })
+      .catch((err) => {
+        console.error('退出全屏失败:', err)
+        document.body.classList.remove('app-fullscreen')
+      })
+  }
+}
+
+const handleFullscreenChange = () => {
+  isFullscreen.value = !!document.fullscreenElement
+  if (!document.fullscreenElement) {
+    document.body.classList.remove('app-fullscreen')
+  }
+}
+
+onMounted(() => {
+  document.addEventListener('fullscreenchange', handleFullscreenChange)
+})
+
+// 添加 onUnmounted 来清理事件监听器
+onUnmounted(() => {
+  document.removeEventListener('fullscreenchange', handleFullscreenChange)
+})
+
+const getStatusText = (statusValue: string | number) => {
+  return statusValue === '1' || statusValue === 1 || statusValue === 'true' ? '运行中' : '停止'
+}
+
+onMounted(async () => {
+  try {
+    const res = await IotDeviceApi.getDeviceSetParams(route.query.deviceSetId)
+
+    // 设置基础信息
+    dataInfo.value = {
+      groupName: res.groupName,
+      deptName: res.deptName,
+      wellName: res.wellName
+    }
+
+    // 设置各个设备的数据
+    psaData.value = res.PSA
+    airTreatmentData.value = res['空气处理撬']
+    mediumPressureData.value = res['中压机']
+    liquidDrivenData.value = res['液驱机']
+
+    // 设置空压机数据(空压机1到空压机5)
+    compressorData.value = []
+    for (let i = 1; i <= 5; i++) {
+      const compressorKey = `空压${i}`
+      if (res[compressorKey]) {
+        compressorData.value.push(res[compressorKey])
+      } else {
+        compressorData.value.push([])
+      }
+    }
+  } catch (error) {
+    console.error('获取设备参数失败:', error)
+  }
+})
+</script>
+
+<style scoped>
+/* @keyframes scan {
+  0% {
+    top: 0;
+  }
+  100% {
+    top: 100%;
+  }
+} */
+
+@keyframes gridMove {
+  0% {
+    transform: translate(0, 0);
+  }
+  100% {
+    transform: translate(50px, 50px);
+  }
+}
+
+.grid-pattern {
+  background-image: linear-gradient(rgba(6, 182, 212, 0.3) 1px, transparent 1px),
+    linear-gradient(90deg, rgba(6, 182, 212, 0.3) 1px, transparent 1px);
+  background-size: 50px 50px;
+  animation: gridMove 20s linear infinite;
+}
+
+.scan-line {
+  position: absolute;
+  width: 100%;
+  height: 2px;
+  background: linear-gradient(to right, transparent, rgb(6, 182, 212), transparent);
+  opacity: 0.3;
+  animation: scan 4s linear infinite;
+}
+
+.scan-line-fast {
+  position: absolute;
+  width: 100%;
+  height: 4px;
+  background: linear-gradient(to right, transparent, rgba(6, 182, 212, 0.6), transparent);
+  animation: scan 3s linear infinite;
+}
+
+.panel {
+  background: linear-gradient(to bottom right, rgba(15, 23, 42, 0.8), rgba(30, 58, 138, 0.5));
+  backdrop-filter: blur(4px);
+  border: 2px solid rgba(6, 182, 212, 0.4);
+  padding: 1rem;
+  border-radius: 0.5rem;
+  box-shadow: 0 0 20px rgba(6, 182, 212, 0.3);
+  transition: all 0.3s;
+  min-height: 0; /* 允许flex项目收缩到其内容以下 */
+}
+
+.panel:hover {
+  box-shadow: 0 0 30px rgba(6, 182, 212, 0.5);
+}
+
+.panel-title {
+  color: rgb(103, 232, 249);
+  font-size: 1.125rem;
+  font-weight: bold;
+  border-bottom: 1px solid rgba(6, 182, 212, 0.3);
+  padding-bottom: 0.5rem;
+  flex-shrink: 0;
+}
+
+.status-indicator {
+  width: 2rem;
+  height: 2rem;
+  border-radius: 50%;
+  animation: pulse 1.5s ease-in-out infinite;
+}
+
+.status-active {
+  background: rgb(34, 197, 94);
+  box-shadow: 0 0 15px rgba(34, 197, 94, 0.8);
+}
+
+.status-inactive {
+  background: rgb(239, 68, 68);
+  box-shadow: 0 0 15px rgba(239, 68, 68, 0.8);
+}
+
+.btn {
+  padding: 0.25rem 0.75rem;
+  border-radius: 0.25rem;
+  color: white;
+  transition: all 0.3s;
+  border: none;
+  cursor: pointer;
+  font-weight: 500;
+}
+
+.btn-green {
+  background: rgb(22, 163, 74);
+  box-shadow: 0 0 10px rgba(22, 163, 74, 0.5);
+}
+
+.btn-green:hover {
+  background: rgb(34, 197, 94);
+  box-shadow: 0 0 20px rgba(22, 163, 74, 0.8);
+  transform: translateY(-1px);
+}
+
+.btn-red {
+  background: rgb(220, 38, 38);
+  box-shadow: 0 0 10px rgba(220, 38, 38, 0.5);
+}
+
+.btn-red:hover {
+  background: rgb(239, 68, 68);
+  box-shadow: 0 0 20px rgba(220, 38, 38, 0.8);
+  transform: translateY(-1px);
+}
+
+/* 全屏模式下组件特定样式 */
+.fullscreen-layout {
+  height: 100vh !important;
+  width: 100vw !important;
+  margin: 0 !important;
+  padding: 0 !important;
+  padding-top: 20px !important;
+  padding-left: 10px !important;
+  padding-right: 10px !important;
+  padding-bottom: 150px !important;
+  position: fixed !important;
+  top: 0 !important;
+  left: 0 !important;
+  overflow: auto !important;
+  overflow-x: hidden !important;
+  max-width: 100vw !important;
+  max-height: 100vh !important;
+
+  scrollbar-width: none; /* Firefox */
+  -ms-overflow-style: none; /* Internet Explorer 10+ */
+}
+</style>

+ 540 - 0
src/views/pms/monitor/kanban2.vue

@@ -0,0 +1,540 @@
+<template>
+  <div
+    class="bg-gradient-to-br from-slate-950 via-blue-950 to-slate-900 text-cyan-100 p-6 relative"
+    :class="{ 'fullscreen-layout': isFullscreen }"
+  >
+    <!-- Animated background grid -->
+    <div class="absolute inset-0 opacity-20">
+      <div class="absolute inset-0 grid-pattern"></div>
+    </div>
+
+    <!-- Scanning line effect -->
+    <div class="absolute inset-0 pointer-events-none">
+      <div class="scan-line"></div>
+    </div>
+
+    <!-- Header -->
+    <div class="relative z-10 mb-6">
+      <div class="flex items-center justify-center border-b-2 border-cyan-500/30 pb-4 gap-8">
+        <h1 class="text-2xl font-bold text-cyan-300 flex-6 tracking-wider">
+          {{ dataInfo.groupName }}
+        </h1>
+
+        <div class="flex flex-4 items-center gap-8 text-sm">
+          <div
+            style="border: 0.5px solid #085b77"
+            class="px-4 py-2 bg-cyan-500/10 border border-cyan-500/30 skew-x-[-12deg] hover:bg-cyan-500/20 transition-all cursor-pointer"
+          >
+            <span class="pr-4">部门名称</span>
+            <span class="inline-block skew-x-[12deg]">{{ dataInfo.deptName }}</span>
+          </div>
+          <div
+            style="border: 0.5px solid #085b77"
+            class="px-4 py-2 bg-cyan-500/10 border border-cyan-500/30 skew-x-[-12deg] hover:bg-cyan-500/20 transition-all cursor-pointer"
+          >
+            <span class="pr-4">井号</span>
+            <span class="inline-block skew-x-[12deg]">{{ dataInfo.wellName }}</span>
+          </div>
+          <div
+            style="border: 0.5px solid #085b77"
+            class="px-4 py-2 bg-cyan-500/10 border border-cyan-500/30 skew-x-[-12deg] hover:bg-cyan-500/20 transition-all cursor-pointer"
+          >
+            <span class="pr-4">增加机编号</span>
+            <span class="inline-block skew-x-[12deg]">{{ dataInfo.deviceCode }}</span>
+          </div>
+          <div
+            @click="toggleFullscreen"
+            style="border: 0.5px solid #085b77"
+            class="px-4 py-2 bg-cyan-500/10 border border-cyan-500/30 skew-x-[-12deg] hover:bg-cyan-500/20 transition-all cursor-pointer"
+          >
+            <span class="inline-block skew-x-[12deg]">{{
+              isFullscreen ? '退出全屏' : '全屏'
+            }}</span>
+          </div>
+        </div>
+      </div>
+    </div>
+
+    <div class="relative z-10 grid grid-cols-12 gap-4 mt-5 p-6">
+      <!-- Left panels - 固定高度 -->
+      <div class="col-span-3">
+        <div class="panel h-full flex flex-col">
+          <!-- 增压机参数 -->
+          <div class="flex-1 border-b border-cyan-500/30 pb-2 mb-2">
+            <div class="flex justify-between items-center panel-title mb-2">
+              <h4>增压机参数</h4>
+              <dv-decoration3 style="width: 120px; height: 30px" />
+            </div>
+
+            <div class="space-y-3 overflow-y-auto overflow-x-hidden flex-grow">
+              <DataRow
+                v-for="item in psaData"
+                :key="item.modelName"
+                :label="item.modelName"
+                :value="formatValue(item.value, getUnitFromModelName(item.modelName))"
+              />
+            </div>
+          </div>
+
+          <!-- 累计运行时间 -->
+          <div class="flex-1">
+            <div class="flex justify-between items-center panel-title mb-2">
+              <h4>累计运行时间</h4>
+              <dv-decoration3 style="width: 120px; height: 30px" />
+            </div>
+            <div class="space-y-3 overflow-y-auto overflow-x-hidden flex-grow">
+              <DataRow
+                v-for="item in airTreatmentData"
+                :key="item.modelName"
+                :label="item.modelName"
+                :value="formatValue(item.value, getUnitFromModelName(item.modelName))"
+              />
+            </div>
+          </div>
+        </div>
+      </div>
+
+      <div class="col-span-6 space-y-4">
+        <!-- 3D模型面板 -->
+        <div class="panel h-[60%] relative overflow-hidden">
+          <div class="relative h-full flex items-center justify-center">
+            <ModelViewer />
+          </div>
+        </div>
+
+        <!-- 状态参数面板 -->
+        <div class="panel h-[40%]">
+          <div class="mb-2">
+            <dv-decoration7>
+              <h3 class="text-cyan-300 text-lg font-bold px-1"> 状态参数 </h3>
+            </dv-decoration7>
+          </div>
+          <div class="grid grid-cols-5 gap-4 h-[calc(100%-2rem)]">
+            <div
+              v-for="(group, i) in compressorData"
+              :key="i"
+              class="space-y-2 text-center h-full overflow-y-auto"
+            >
+              <div class="text-sm" v-for="item in group" :key="item.modelName">
+                <div class="text-cyan-200">{{ item.modelName }}</div>
+                <div class="text-white font-mono flex items-center justify-center">
+                  <span
+                    :class="['w-3 h-3 rounded-full inline-block mr-2', getStatusClass(item.value)]"
+                  ></span>
+                  {{ getStatusDisplayText(item.modelName, item.value) }}
+                </div>
+              </div>
+            </div>
+          </div>
+        </div>
+      </div>
+
+      <!-- Right panel - 氮气参数和井口参数平分高度 -->
+      <div class="col-span-3">
+        <div class="panel h-[50%] mb-2">
+          <div class="flex items-center justify-between mb-4 border-b border-cyan-500/30 pb-2">
+            <h3 class="text-cyan-300 text-lg font-bold">氮气参数</h3>
+            <dv-decoration8 :reverse="true" style="width: 110px; height: 20px" />
+          </div>
+          <div class="space-y-2 text-sm overflow-y-auto overflow-x-hidden h-[calc(100%-3rem)]">
+            <DataRow
+              v-for="item in liquidDrivenData"
+              :key="item.modelName"
+              :label="item.modelName"
+              :value="formatValue(item.value, getUnitFromModelName(item.modelName))"
+              :compact="true"
+            />
+          </div>
+        </div>
+        <div class="panel h-[50%]">
+          <div class="flex items-center justify-between mb-4 border-b border-cyan-500/30 pb-2">
+            <h3 class="text-cyan-300 text-lg font-bold">井口参数 </h3>
+            <dv-decoration8 :reverse="true" style="width: 110px; height: 20px" />
+          </div>
+          <div class="space-y-2 text-sm overflow-y-auto overflow-x-hidden h-[calc(100%-3rem)]">
+            <DataRow
+              v-for="item in wellheadData"
+              :key="item.modelName"
+              :label="item.modelName"
+              :value="formatValue(item.value, getUnitFromModelName(item.modelName))"
+              :compact="true"
+            />
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, onMounted, nextTick } from 'vue'
+import DataRow from './data-row.vue'
+import ModelViewer from './ModelViewer2.vue'
+import { IotDeviceApi } from '@/api/pms/device'
+import { useRoute } from 'vue-router'
+
+const route = useRoute()
+
+defineOptions({
+  name: 'Kanban'
+})
+
+let dataInfo = ref<any>({})
+let psaData = ref<any[]>([])
+let airTreatmentData = ref<any[]>([])
+let mediumPressureData = ref<any[]>([])
+let liquidDrivenData = ref<any[]>([])
+let wellheadData = ref<any[]>([]) // 井口参数
+let compressorData = ref<any[][]>([])
+const isFullscreen = ref(false)
+
+// 从modelName中提取单位
+const getUnitFromModelName = (modelName: string) => {
+  if (!modelName) return ''
+
+  // 根据不同的参数类型返回单位
+  if (modelName.includes('压力') || modelName.includes('压力P')) return 'MPa'
+  if (modelName.includes('温度') || modelName.includes('T')) return '℃'
+  if (modelName.includes('流量')) return 'Nm³'
+  if (modelName.includes('时间')) return '小时'
+  if (modelName.includes('氮气纯度')) return '%'
+  if (modelName.includes('环境温度')) return '℃'
+  if (modelName.includes('喷油')) return modelName.includes('温度') ? '℃' : 'MPa'
+  if (modelName.includes('轴瓦温度')) return '℃'
+  if (modelName.includes('润滑油温度')) return '℃'
+  if (modelName.includes('排气温度')) return '℃'
+  if (modelName.includes('进气压力')) return 'MPa'
+  if (modelName.includes('排气压力')) return 'MPa'
+  if (modelName.includes('累计流量')) return 'm³'
+  if (modelName.includes('瞬时流量')) return 'm³/h'
+
+  return '' // 默认无单位
+}
+
+// 判断是否为状态类参数
+const isStatusParameter = (paramName: string) => {
+  const statusKeywords = ['无油1#', '无油2#', '曲轴油位', '油泵状态', '风机状态', '急停状态']
+  return statusKeywords.some((keyword) => paramName.includes(keyword))
+}
+
+// 获取状态类参数的显示文本
+const getStatusDisplayText = (paramName: string, value: string | number) => {
+  if (value === '' || value === null || value === undefined) {
+    return '--'
+  }
+
+  // 根据不同参数类型返回不同的状态文本
+  if (paramName.includes('急停状态')) {
+    return value === '1' || value === 1 || value === 'true' ? '正常' : '急停'
+  } else if (paramName.includes('油泵状态') || paramName.includes('风机状态')) {
+    return value === '1' || value === 1 || value === 'true' ? '运行' : '停止'
+  } else if (paramName.includes('无油')) {
+    return value === '1' || value === 1 || value === 'true' ? '正常' : '缺油'
+  } else if (paramName.includes('曲轴油位')) {
+    return value === '1' || value === 1 || value === 'true' ? '正常' : '低油位'
+  } else {
+    return value === '1' || value === 1 || value === 'true' ? '开启' : '关闭'
+  }
+}
+
+// 获取状态类名
+const getStatusClass = (statusValue: string | number) => {
+  return statusValue === '1' || statusValue === 1 || statusValue === 'true'
+    ? 'status-active'
+    : 'status-inactive'
+}
+
+// 格式化数值显示
+const formatValue = (value: string, unit: string) => {
+  if (
+    value === undefined ||
+    value === null ||
+    value === '' ||
+    value === 'null' ||
+    (value === '0' && unit === '')
+  ) {
+    return '--'
+  }
+  return unit ? `${value} ${unit}` : value
+}
+
+const toggleFullscreen = async () => {
+  if (!document.fullscreenElement) {
+    // 进入全屏
+    document.body.classList.add('app-fullscreen')
+
+    // 等待 DOM 更新后请求全屏
+    await nextTick()
+    document.documentElement.requestFullscreen()
+  } else {
+    // 退出全屏
+    document
+      .exitFullscreen()
+      .then(() => {
+        document.body.classList.remove('app-fullscreen')
+      })
+      .catch((err) => {
+        console.error('退出全屏失败:', err)
+        document.body.classList.remove('app-fullscreen')
+      })
+  }
+}
+
+const handleFullscreenChange = () => {
+  isFullscreen.value = !!document.fullscreenElement
+  if (!document.fullscreenElement) {
+    document.body.classList.remove('app-fullscreen')
+  }
+}
+
+onMounted(() => {
+  document.addEventListener('fullscreenchange', handleFullscreenChange)
+})
+
+// 添加 onUnmounted 来清理事件监听器
+onUnmounted(() => {
+  document.removeEventListener('fullscreenchange', handleFullscreenChange)
+})
+
+const getStatusText = (statusValue: string | number) => {
+  return statusValue === '1' || statusValue === 1 || statusValue === 'true' ? '运行中' : '停止'
+}
+
+onMounted(async () => {
+  try {
+    const res = await IotDeviceApi.getDeviceSetParams(route.query.deviceSetId)
+
+    // 设置基础信息
+    dataInfo.value = {
+      groupName: res.groupName,
+      deptName: res.deptName,
+      wellName: res.wellName,
+      deviceCode: res.deviceCode
+    }
+
+    // 分离增压机参数、状态参数、氮气参数和井口参数
+    const allCompressorParams = res['增压机'] || []
+
+    // 定义状态参数关键词
+    const statusKeywords = [
+      '无油1#',
+      '无油2#',
+      '曲轴油位',
+      '油泵状态',
+      '风机状态',
+      '急停状态',
+      '振动'
+    ]
+
+    // 定义氮气参数关键词
+    const gasKeywords = ['氮气纯度', '瞬时流量', '累计流量']
+
+    // 定义井口参数关键词 - 使用精确匹配
+    const wellheadKeywords = ['油压', '套压', '表套', '二套']
+
+    // 过滤出状态参数
+    const statusParams = allCompressorParams.filter((item) =>
+      statusKeywords.includes(item.modelName)
+    )
+
+    // 过滤出氮气参数
+    const gasParams = allCompressorParams.filter((item) => gasKeywords.includes(item.modelName))
+
+    // 过滤出井口参数 - 使用精确匹配
+    const wellheadParams = allCompressorParams.filter((item) =>
+      wellheadKeywords.includes(item.modelName)
+    )
+
+    // 过滤出非状态参数、非氮气参数和非井口参数(真正的增压机参数)
+    const nonStatusGasWellheadParams = allCompressorParams.filter(
+      (item) =>
+        !statusKeywords.includes(item.modelName) &&
+        !gasKeywords.includes(item.modelName) &&
+        !wellheadKeywords.includes(item.modelName)
+    )
+
+    // 设置各部分数据
+    psaData.value = nonStatusGasWellheadParams // 增压机参数(排除状态参数、氮气参数和井口参数)
+    mediumPressureData.value = statusParams // 状态参数
+
+    // 显示累计运行时间
+    airTreatmentData.value = [
+      {
+        modelName: '累计运行时间',
+        value: res.totalRuntime || 0
+      }
+    ]
+
+    // 氮气参数
+    liquidDrivenData.value = gasParams
+
+    // 井口参数
+    wellheadData.value = wellheadParams
+
+    // 将状态参数分组显示
+    const chunkArray = (arr: any[], size: number) => {
+      const result = []
+      for (let i = 0; i < arr.length; i += size) {
+        result.push(arr.slice(i, i + size))
+      }
+      return result
+    }
+
+    // 将状态参数分成多组
+    compressorData.value = chunkArray(statusParams, 5)
+  } catch (error) {
+    console.error('获取设备参数失败:', error)
+  }
+})
+</script>
+
+<style scoped>
+/* @keyframes scan {
+  0% {
+    top: 0;
+  }
+  100% {
+    top: 100%;
+  }
+} */
+
+@keyframes gridMove {
+  0% {
+    transform: translate(0, 0);
+  }
+  100% {
+    transform: translate(50px, 50px);
+  }
+}
+
+.grid-pattern {
+  background-image: linear-gradient(rgba(6, 182, 212, 0.3) 1px, transparent 1px),
+    linear-gradient(90deg, rgba(6, 182, 212, 0.3) 1px, transparent 1px);
+  background-size: 50px 50px;
+  animation: gridMove 20s linear infinite;
+}
+
+.scan-line {
+  position: absolute;
+  width: 100%;
+  height: 2px;
+  background: linear-gradient(to right, transparent, rgb(6, 182, 212), transparent);
+  opacity: 0.3;
+  animation: scan 4s linear infinite;
+}
+
+.scan-line-fast {
+  position: absolute;
+  width: 100%;
+  height: 4px;
+  background: linear-gradient(to right, transparent, rgba(6, 182, 212, 0.6), transparent);
+  animation: scan 3s linear infinite;
+}
+
+.panel {
+  background: linear-gradient(to bottom right, rgba(15, 23, 42, 0.8), rgba(30, 58, 138, 0.5));
+  backdrop-filter: blur(4px);
+  border: 2px solid rgba(6, 182, 212, 0.4);
+  padding: 1rem;
+  border-radius: 0.5rem;
+  box-shadow: 0 0 20px rgba(6, 182, 212, 0.3);
+  transition: all 0.3s;
+  min-height: 0; /* 允许flex项目收缩到其内容以下 */
+}
+
+.panel:hover {
+  box-shadow: 0 0 30px rgba(6, 182, 212, 0.5);
+}
+
+.panel-title {
+  color: rgb(103, 232, 249);
+  font-size: 1.125rem;
+  font-weight: bold;
+  border-bottom: 1px solid rgba(6, 182, 212, 0.3);
+  padding-bottom: 0.5rem;
+  flex-shrink: 0;
+}
+
+.status-indicator {
+  width: 2rem;
+  height: 2rem;
+  border-radius: 50%;
+  animation: pulse 1.5s ease-in-out infinite;
+}
+
+.status-active {
+  background: rgb(34, 197, 94);
+  box-shadow: 0 0 15px rgba(34, 197, 94, 0.8);
+}
+
+.status-inactive {
+  background: rgb(239, 68, 68);
+  box-shadow: 0 0 15px rgba(239, 68, 68, 0.8);
+}
+
+.btn {
+  padding: 0.25rem 0.75rem;
+  border-radius: 0.25rem;
+  color: white;
+  transition: all 0.3s;
+  border: none;
+  cursor: pointer;
+  font-weight: 500;
+}
+
+.btn-green {
+  background: rgb(22, 163, 74);
+  box-shadow: 0 0 10px rgba(22, 163, 74, 0.5);
+}
+
+.btn-green:hover {
+  background: rgb(34, 197, 94);
+  box-shadow: 0 0 20px rgba(22, 163, 74, 0.8);
+  transform: translateY(-1px);
+}
+
+.btn-red {
+  background: rgb(220, 38, 38);
+  box-shadow: 0 0 10px rgba(220, 38, 38, 0.5);
+}
+
+.btn-red:hover {
+  background: rgb(239, 68, 68);
+  box-shadow: 0 0 20px rgba(220, 38, 38, 0.8);
+  transform: translateY(-1px);
+}
+
+/* 全屏模式下组件特定样式 */
+.fullscreen-layout {
+  height: 100vh !important;
+  width: 100vw !important;
+  margin: 0 !important;
+  padding: 0 !important;
+  padding-top: 20px !important;
+  padding-left: 10px !important;
+  padding-right: 10px !important;
+  padding-bottom: 150px !important;
+  position: fixed !important;
+  top: 0 !important;
+  left: 0 !important;
+  overflow: auto !important;
+  overflow-x: hidden !important;
+  max-width: 100vw !important;
+  max-height: 100vh !important;
+
+  scrollbar-width: none; /* Firefox */
+  -ms-overflow-style: none; /* Internet Explorer 10+ */
+}
+
+/* 在现有样式中添加 */
+.panel-container {
+  display: flex;
+  flex-direction: column;
+  height: 100%;
+}
+
+.panel-content {
+  flex: 1;
+  overflow-y: auto;
+}
+</style>

+ 153 - 108
src/views/pms/stat/rdkb.vue

@@ -1,5 +1,5 @@
 <template>
-  <div class="page-container">
+  <div class="min-h-screen p-4 bg-[#3a6fa3] flex flex-col">
     <el-row :gutter="16" class="summary">
       <!-- 原有的统计卡片部分保持不变 -->
       <el-col v-loading="loading" :sm="3" :xs="24">
@@ -96,7 +96,7 @@
               gap: 6px;
             "
           >
-            <div ref="statusChartRef" style="width: 100%; max-width: 200px; height: 170px"></div>
+            <div ref="statusChartRef" style="width: 100%; height: 170px; max-width: 200px"></div>
             <div class="text-[12px] h-[100px] w-[90%] flex flex-col justify-between items-center">
               <div v-for="item in legendData" :key="item.name" class="flex">
                 <div class="flex items-center gap-1">
@@ -123,25 +123,23 @@
           </template>
           <div
             style="
-              min-height: 295px;
               display: flex;
+              min-height: 295px;
               flex-direction: column;
               align-items: center;
               gap: 8px;
             "
           >
-            <div ref="domesticChartRef" style="width: 100%; max-width: 420px; height: 200px"></div>
+            <div ref="domesticChartRef" style="width: 100%; height: 200px; max-width: 420px"></div>
             <div
               class="domestic-legend"
               style="
+                display: flex;
                 width: 100%;
                 max-width: 520px;
-                display: flex;
+                font-size: 12px;
                 flex-wrap: wrap;
-
                 gap: 1px;
-
-                font-size: 12px;
               "
             >
               <div
@@ -163,12 +161,12 @@
                 <span
                   class="legend-name"
                   style="
-                    color: #fff;
+                    display: inline-block;
                     max-width: 150px;
                     overflow: hidden;
+                    color: #fff;
                     text-overflow: ellipsis;
                     white-space: nowrap;
-                    display: inline-block;
                   "
                   >{{ item.dept }}</span
                 >
@@ -178,7 +176,8 @@
         </el-card>
       </el-col>
       <el-col :span="8" :xs="24">
-        <el-card class="chart-card" shadow="never">
+        <WorkloadChart />
+        <!-- <el-card class="chart-card" shadow="never">
           <template #header>
             <div class="flex items-center justify-between">
               <span class="text-base font-medium" style="color: #b6c8da">{{
@@ -187,14 +186,22 @@
             </div>
           </template>
           <div ref="qxRef" class="h-[290px]"></div>
-        </el-card>
+        </el-card> -->
+      </el-col>
+    </el-row>
+    <el-row :gutter="16" class="mb-4">
+      <el-col class="mb-4" :span="12" :xs="24">
+        <AvailabilityChart />
+      </el-col>
+      <el-col class="mb-4" :span="12" :xs="24">
+        <ExceptionChart />
       </el-col>
     </el-row>
-
     <el-row :gutter="16" class="mb-4">
-      <!-- 备件更换情况部分保持不变 -->
       <el-col :span="12" :xs="24">
-        <el-card class="chart-card" shadow="never">
+        <UtilizationChart />
+      </el-col>
+      <!-- <el-card class="chart-card" shadow="never">
           <template #header>
             <div style="display: flex; flex-direction: row; justify-content: space-between">
               <span class="text-base font-medium" style="color: #b6c8da">{{
@@ -214,37 +221,37 @@
               </div>
             </div>
           </template>
-          <!-- 添加两个卡片 -->
-          <!--          <div class="flex justify-between mb-2">-->
-          <!--            <el-card class="stat-card">-->
-          <!--              <div class="flex flex-row justify-evenly">-->
-          <!--                <div>-->
-          <!--                  <Icon icon="fa-solid:award" size="30" color="blue" />-->
-          <!--                </div>-->
-          <!--                <div class="flex flex-col items-center">-->
-          <!--                  <span class="text-sm " style="color: #101010">{{t('stat.spareCount')}}</span>-->
-          <!--                  <span class="text-lg font-bold">{{ totalMaterialCount }}</span>-->
-          <!--                </div>-->
-          <!--              </div>-->
-          <!--            </el-card>-->
-          <!--            <el-card class="stat-card">-->
-          <!--              <div class="flex flex-row justify-evenly">-->
-          <!--                <div>-->
-          <!--                  <Icon icon="fa-solid:yen-sign" size="30" color="orange" />-->
-          <!--                </div>-->
-          <!--                <div class="flex flex-col items-center">-->
-          <!--                  <span class="text-sm " style="color: #101010">{{t('stat.spareAmount')}}</span>-->
-          <!--                  <span class="text-lg font-bold">{{ totalMaterialCost }}</span>-->
-          <!--                </div>-->
-          <!--              </div>-->
-          <!--            </el-card>-->
-          <!--          </div>-->
-          <!--          <div ref="sparePartRef" class="h-[330px]"></div>-->
+
           <div class="table-container">
             <div ref="utilizationRef" style="width: 100%; height: 360px"></div>
           </div>
-        </el-card>
-      </el-col>
+        </el-card> -->
+      <!-- 添加两个卡片 -->
+      <!--          <div class="flex justify-between mb-2">-->
+      <!--            <el-card class="stat-card">-->
+      <!--              <div class="flex flex-row justify-evenly">-->
+      <!--                <div>-->
+      <!--                  <Icon icon="fa-solid:award" size="30" color="blue" />-->
+      <!--                </div>-->
+      <!--                <div class="flex flex-col items-center">-->
+      <!--                  <span class="text-sm " style="color: #101010">{{t('stat.spareCount')}}</span>-->
+      <!--                  <span class="text-lg font-bold">{{ totalMaterialCount }}</span>-->
+      <!--                </div>-->
+      <!--              </div>-->
+      <!--            </el-card>-->
+      <!--            <el-card class="stat-card">-->
+      <!--              <div class="flex flex-row justify-evenly">-->
+      <!--                <div>-->
+      <!--                  <Icon icon="fa-solid:yen-sign" size="30" color="orange" />-->
+      <!--                </div>-->
+      <!--                <div class="flex flex-col items-center">-->
+      <!--                  <span class="text-sm " style="color: #101010">{{t('stat.spareAmount')}}</span>-->
+      <!--                  <span class="text-lg font-bold">{{ totalMaterialCost }}</span>-->
+      <!--                </div>-->
+      <!--              </div>-->
+      <!--            </el-card>-->
+      <!--          </div>-->
+      <!--          <div ref="sparePartRef" class="h-[330px]"></div>-->
       <!-- 月度工作量表 -->
       <el-col :span="12" :xs="24">
         <el-card class="chart-card" shadow="never">
@@ -353,6 +360,12 @@
       </el-col>
       <!-- 月度工作量表结束 -->
     </el-row>
+    <ConstructionBriefing />
+    <!-- <el-row :gutter="16" class="mb-4">
+      <el-col :span="24" :xs="24">
+
+      </el-col>
+    </el-row> -->
   </div>
   <el-dialog
     v-model="teamDialogVisible"
@@ -511,6 +524,11 @@ import { CanvasRenderer } from 'echarts/renderers'
 import { IotStatApi } from '@/api/pms/stat'
 import { ref, onMounted, computed, watch, nextTick, reactive } from 'vue'
 import { useLocaleStore } from '@/store/modules/locale'
+import WorkloadChart from './rdkb/workload.vue'
+import UtilizationChart from './rdkb/utilization.vue'
+import AvailabilityChart from './rdkb/availability.vue'
+import ExceptionChart from './rdkb/exception.vue'
+import ConstructionBriefing from './rdkb/constructionBriefing.vue'
 
 /** 会员统计 */
 defineOptions({ name: 'IotRdStat' })
@@ -640,6 +658,14 @@ const fill = ref({
   filledCount: undefined,
   unfilledCount: undefined
 })
+const abnormalDevice = ref({
+  total: undefined,
+  today: undefined
+})
+const outliers = ref({
+  total: undefined,
+  today: undefined
+})
 const inspect = ref({
   finished: 0,
   todo: 0
@@ -1462,23 +1488,23 @@ const domesticData = ref<any[]>([
     count: 13,
     orig_value: 1506.88,
     net_value: 559.95,
-    orig_ratio: '3%'
+    orig_ratio: '2.77%'
   },
   {
     index: 2,
     dept: '新疆项目部',
     count: 58,
     orig_value: 5118.1,
-    net_value: 1182.91,
-    orig_ratio: '11%'
+    net_value: 1182.49,
+    orig_ratio: '9.40%'
   },
   {
     index: 3,
     dept: '青海项目',
     count: 33,
-    orig_value: 14082.33,
-    net_value: 736.94,
-    orig_ratio: '29%'
+    orig_value: 7004.23,
+    net_value: 1758.8,
+    orig_ratio: '12.86%'
   },
   {
     index: 4,
@@ -1486,55 +1512,55 @@ const domesticData = ref<any[]>([
     count: 49,
     orig_value: 5273.54,
     net_value: 683.58,
-    orig_ratio: '11%'
+    orig_ratio: '9.68%'
   },
   {
     index: 5,
     dept: '西南连油项目部',
     count: 24,
-    orig_value: 4070.12,
-    net_value: 743.99,
-    orig_ratio: '8%'
+    orig_value: 4059.36,
+    net_value: 743.24,
+    orig_ratio: '7.45%'
   },
   {
     index: 6,
     dept: '西南压裂项目部',
     count: 47,
-    orig_value: 14602.44,
-    net_value: 6031.39,
-    orig_ratio: '30'
+    orig_value: 14591.62,
+    net_value: 6030.84,
+    orig_ratio: '26.79%'
   },
   {
     index: 7,
     dept: '伊拉克 哈法亚连油',
     count: 120,
     orig_value: 694.78,
-    net_value: 94.44,
-    orig_ratio: '1%'
+    net_value: 91.26,
+    orig_ratio: '9.07%'
   },
   {
     index: 8,
     dept: '伊拉克 哈法亚压裂',
     count: 132,
     orig_value: 1008.92,
-    net_value: 587.07,
-    orig_ratio: '2%'
+    net_value: 575.91,
+    orig_ratio: '13.17%%'
   },
   {
     index: 9,
     dept: '伊拉克 B9增产',
     count: 27,
     orig_value: 304.72,
-    net_value: 128.63,
-    orig_ratio: '1%'
+    net_value: 124.1,
+    orig_ratio: '3.98%'
   },
   {
     index: 10,
     dept: '利比亚连油8队',
     count: 22,
     orig_value: 2025.52,
-    net_value: 1961.69,
-    orig_ratio: '4%'
+    net_value: 1731.79,
+    orig_ratio: '4.85%'
   }
 ])
 
@@ -1744,11 +1770,29 @@ onMounted(async () => {
 })
 </script>
 <style lang="scss" scoped>
-/*最外层透明*/
+@media (width <= 768px) {
+  .page-container {
+    padding: 10px;
+  }
+}
+
+@media (width <= 520px) {
+  .status-legend-item {
+    min-width: 100%;
+  }
+
+  .status-legend {
+    justify-content: flex-start;
+    max-height: none;
+    overflow: visible;
+  }
+}
+
 ::v-deep .el-table,
 ::v-deep .el-table__expanded-cell {
   background-color: transparent !important;
 }
+
 /* 表格内背景颜色 */
 
 ::v-deep .el-table tr,
@@ -1769,13 +1813,15 @@ onMounted(async () => {
     margin-bottom: 1rem;
   }
 }
+
 .stat-card {
   width: 48%;
 }
+
 .page-container {
-  background-color: #3a6fa3;
   min-height: 100vh;
   padding: 20px;
+  background-color: #3a6fa3;
 }
 
 .summary {
@@ -1783,32 +1829,32 @@ onMounted(async () => {
 }
 
 ::v-deep .chart-card {
-  background-color: rgba(0, 0, 0, 0.3);
+  background-color: rgb(0 0 0 / 30%);
+  border: none;
   border-radius: 8px;
-  box-shadow: 0 2px 12px rgba(0, 0, 0, 0.5);
+  box-shadow: 0 2px 12px rgb(0 0 0 / 50%);
   transition: all 0.3s ease;
-  border: none;
 
   &:hover {
-    box-shadow: 0 4px 16px rgba(0, 0, 0, 0.08);
+    box-shadow: 0 4px 16px rgb(0 0 0 / 8%);
   }
 }
 
 // 安全生产天数卡片样式
 .safety-days-card {
   .safety-days-content {
+    position: relative;
     display: flex;
+    height: 150px;
     flex-direction: column;
     align-items: center;
     justify-content: center;
-    height: 150px;
-    position: relative;
 
     .days-number {
       font-size: 58px;
       font-weight: bold;
-      color: darkorange;
       line-height: 1;
+      color: darkorange;
       transition: all 0.3s ease;
     }
 
@@ -1817,36 +1863,32 @@ onMounted(async () => {
     }
 
     .days-label {
+      margin-top: 8px;
       font-size: 20px;
       color: white;
-      margin-top: 8px;
     }
 
     .safety-desc {
-      font-size: 14px;
-      color: #999;
       position: absolute;
       bottom: 10px;
-      text-align: center;
       width: 90%;
+      font-size: 14px;
+      color: #999;
+      text-align: center;
     }
   }
 }
 
-@media (max-width: 768px) {
-  .page-container {
-    padding: 10px;
-  }
-}
 ::v-deep .el-card__header {
-  border-bottom: none !important;
   padding-bottom: 0;
+  border-bottom: none !important;
 }
+
 .table-container {
-  padding: 16px;
   height: 420px;
-  box-sizing: border-box;
+  padding: 16px;
   overflow: auto;
+  box-sizing: border-box;
 
   // 滚动条样式优化
   &::-webkit-scrollbar {
@@ -1855,7 +1897,7 @@ onMounted(async () => {
   }
 
   &::-webkit-scrollbar-thumb {
-    background-color: rgba(255, 255, 255, 0.2);
+    background-color: rgb(255 255 255 / 20%);
     border-radius: 3px;
   }
 
@@ -1866,63 +1908,71 @@ onMounted(async () => {
 
 // 修复表格hover样式
 ::v-deep .el-table__row:hover > td {
-  background-color: rgba(255, 255, 255, 0.05) !important;
+  background-color: rgb(255 255 255 / 5%) !important;
 }
+
 .custom-scroll-dialog {
   /* 可选:限制对话框整体最大高度(避免超出屏幕) */
   max-height: 90vh;
   overflow: hidden; /* 隐藏整体溢出,避免出现双重滚动条 */
 }
+
 /* 滚动内容容器:核心样式 */
 .dialog-scroll-content {
   max-height: 60vh; /* 固定最大高度(可根据需求调整,如500px) */
-  overflow-y: auto; /* 垂直方向溢出时显示滚动条 */
   padding-right: 8px; /* 避免滚动条遮挡内容(可选) */
+  overflow-y: auto; /* 垂直方向溢出时显示滚动条 */
 }
 
 /* 优化滚动条样式(可选,提升UI体验) */
 .dialog-scroll-content::-webkit-scrollbar {
   width: 6px; /* 滚动条宽度 */
 }
+
 .dialog-scroll-content::-webkit-scrollbar-thumb {
   background-color: #e5e7eb; /* 滚动条滑块颜色 */
   border-radius: 3px; /* 滚动条圆角 */
 }
+
 .dialog-scroll-content::-webkit-scrollbar-thumb:hover {
   background-color: #d1d5db; /*  hover时滑块颜色 */
 }
+
 .custom-table :deep .el-table__row {
   height: 50px !important; /* 高度根据需求调整 */
 }
 
 /* 设备状态图例自适应样式 */
 .status-legend {
-  width: 100%;
   display: flex;
+  width: 100%;
+  max-height: 90px; /* 限制高度,超出显示滚动 */
+  padding: 6px 0;
+  overflow-y: auto;
+  box-sizing: border-box;
   flex-wrap: wrap;
   justify-content: center;
   gap: 8px;
-  padding: 6px 0;
-  box-sizing: border-box;
-  max-height: 90px; /* 限制高度,超出显示滚动 */
-  overflow-y: auto;
 }
+
 .status-legend-item {
   display: flex;
+  max-width: 100%;
+  min-width: 120px;
+  padding: 6px 10px;
+  box-sizing: border-box;
   align-items: center;
   justify-content: space-between;
   gap: 12px;
-  padding: 6px 10px;
-  min-width: 120px;
-  max-width: 100%;
-  box-sizing: border-box;
 }
+
 .status-legend-left {
   display: flex;
   align-items: center;
   gap: 8px;
   min-width: 0;
 }
+
 .status-legend-color {
   display: inline-block;
   width: 12px;
@@ -1930,35 +1980,30 @@ onMounted(async () => {
   border-radius: 50%;
   flex: 0 0 12px;
 }
+
 .status-legend-name {
   max-width: calc(100% - 60px);
   overflow: hidden;
+  color: #fff;
   text-overflow: ellipsis;
   white-space: nowrap;
-  color: #fff;
 }
+
 .status-legend-right {
   display: flex;
   align-items: center;
   gap: 8px;
   flex-shrink: 0;
 }
+
 .status-legend-value {
   font-weight: 700;
   color: #fff;
 }
+
 .status-legend-percent {
   color: #fff;
 }
 
-@media (max-width: 520px) {
-  .status-legend-item {
-    min-width: 100%;
-  }
-  .status-legend {
-    justify-content: flex-start;
-    max-height: none;
-    overflow: visible;
-  }
-}
+/* 最外层透明 */
 </style>

+ 138 - 0
src/views/pms/stat/rdkb/availability.vue

@@ -0,0 +1,138 @@
+<script lang="ts" setup>
+import { ref, onMounted } from 'vue'
+import { IotStatApi } from '@/api/pms/stat'
+
+// 定义数据接口结构
+interface DeviceStat {
+  percent: number
+  total: number
+}
+
+interface ApiData {
+  a: DeviceStat
+  b: DeviceStat
+  c: DeviceStat
+}
+
+// 响应式数据,设置初始默认值防止页面报错
+const stats = ref<ApiData>({
+  a: { percent: 0, total: 0 },
+  b: { percent: 0, total: 0 },
+  c: { percent: 0, total: 0 }
+})
+
+const loading = ref(false)
+
+// 配置项:定义每一项的名称、颜色和对应接口的key
+// 你可以在这里修改 name 为实际业务名称
+const config = [
+  { key: 'a', name: 'A(关键)', color: '#00E5FF' }, // 亮青
+  { key: 'b', name: 'B(重要)', color: '#FFD740' }, // 亮黄
+  { key: 'c', name: 'C(一般)', color: '#69F0AE' } // 荧光绿
+] as const
+
+const fetchData = async () => {
+  loading.value = true
+  try {
+    const res = await IotStatApi.getWhl()
+
+    if (res) {
+      stats.value = res
+    }
+  } catch (error) {
+    console.error('Whl API Error:', error)
+  } finally {
+    loading.value = false
+  }
+}
+
+onMounted(() => {
+  fetchData()
+})
+</script>
+
+<template>
+  <div
+    class="card size-full rounded-lg p-4 flex flex-col"
+    v-loading="loading"
+    element-loading-background="rgba(0, 0, 0, 0.3)"
+  >
+    <div class="flex items-center gap-2 mb-6">
+      <div class="w-1 h-4 bg-[#00E5FF] rounded-full shadow-[0_0_8px_#00E5FF]"></div>
+      <div class="text-[#e0e0e0] text-lg font-bold">完好率</div>
+    </div>
+
+    <!-- 内容区域:三列布局 -->
+    <div class="flex-1 flex items-center justify-around w-full">
+      <div v-for="item in config" :key="item.key" class="flex flex-col items-center group">
+        <!-- 进度环 -->
+        <!-- stroke-width: 进度条宽度 -->
+        <!-- width: 圆环直径 -->
+        <div class="relative relative-glow">
+          <el-progress
+            type="dashboard"
+            :percentage="stats[item.key]?.percent || 0"
+            :color="item.color"
+            :width="130"
+            :stroke-width="10"
+            stroke-linecap="round"
+          >
+            <!-- 自定义圆环中间的内容 -->
+            <template #default="{ percentage }">
+              <div class="flex flex-col items-center">
+                <span
+                  class="text-2xl font-bold font-mono"
+                  :style="{ color: item.color, textShadow: `0 0 10px ${item.color}40` }"
+                >
+                  {{ percentage }}%
+                </span>
+                <span class="text-[#9ca3af] text-xs mt-1">{{ item.name }}</span>
+              </div>
+            </template>
+          </el-progress>
+        </div>
+
+        <!-- 底部文字:总数 -->
+        <div class="mt-2 text-center">
+          <!-- <div class="text-[#b6c8da] text-sm">在线总数</div> -->
+          <div class="text-xl font-bold text-white mt-1">
+            {{ stats[item.key]?.total || 0 }}
+            <span class="text-xs text-[#6b7280] font-normal">台</span>
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<style scoped>
+/* 卡片背景 - 保持统一风格 */
+.card {
+  background-color: rgb(0 0 0 / 30%);
+  transition: all 0.3s ease;
+
+  &:hover {
+    box-shadow: 0 4px 16px rgb(0 0 0 / 8%);
+  }
+}
+
+/* 强制修改 Element Progress 的轨道背景色,使其适应深色主题 */
+:deep(.el-progress-circle__track) {
+  stroke: rgb(255 255 255 / 10%) !important;
+}
+
+/* 进度条文字居中修正 */
+:deep(.el-progress__text) {
+  top: 50%;
+  transform: translateY(-50%);
+}
+
+/* 简单的光晕动效 */
+.relative-glow {
+  transition: transform 0.3s ease;
+}
+
+.group:hover .relative-glow {
+  transform: scale(1.05);
+}
+</style>

+ 144 - 0
src/views/pms/stat/rdkb/constructionBriefing.vue

@@ -0,0 +1,144 @@
+<script lang="ts" setup>
+import { IotStatApi } from '@/api/pms/stat'
+import { useTableComponents } from '@/components/ZmTable/useTableComponents'
+
+const loading = ref(false)
+
+interface ConstructionBriefing {
+  projectDeptName: string // 项目部
+  deptName: string // 队伍
+  rdStatus: string // 状态
+  wellName: string // 井号
+  techniques: string //  工艺
+  deviceNames: string // 使用设备
+  cumulativeWorkingLayers: number // 当日施工层
+  cumulativeWorkingWell: string // 当日施工井
+  productionBrief: string //  当日施工简要
+  yesterdayStatus: string //   上一天状态
+  yesterdayWorkingLayers: number // 上一天施工层
+  yesterdayWorkingWell: string //   上一天施工井
+  yesterdayProduct: string //  上一天施工简要
+}
+
+const list = ref<ConstructionBriefing[]>([])
+
+const loadList = async () => {
+  try {
+    loading.value = true
+    const res = await IotStatApi.getConstructionBriefing()
+    list.value = res.list ?? []
+  } finally {
+    loading.value = false
+  }
+}
+
+onMounted(() => {
+  loadList()
+})
+
+const { ZmTable, ZmTableColumn } = useTableComponents<ConstructionBriefing>()
+</script>
+<template>
+  <div class="card min-h-80 rounded-lg p-4 flex flex-col overflow-hidden" v-loading="loading">
+    <div class="flex justify-between items-center mb-4">
+      <div class="flex items-center gap-2">
+        <!-- 红色装饰条,代表异常/警示 -->
+        <div class="w-1 h-4 bg-[#00E5FF] rounded-full shadow-[0_0_8px_#00E5FF]"></div>
+        <div class="text-[#e0e0e0] text-lg font-bold">施工简报</div>
+      </div>
+    </div>
+    <div class="flex-1 relative w-full">
+      <el-auto-resizer class="absolute">
+        <template #default="{ width, height }">
+          <ZmTable
+            class="custom-dark-table"
+            :data="list"
+            :loading="loading"
+            :width="width"
+            :max-height="height"
+            custom-class
+          >
+            <ZmTableColumn prop="projectDeptName" label="项目部" />
+            <ZmTableColumn prop="deptName" label="队伍" />
+            <ZmTableColumn prop="rdStatus" label="状态" />
+            <ZmTableColumn prop="wellName" label="井号" />
+            <ZmTableColumn prop="techniques" label="工艺" />
+            <ZmTableColumn prop="deviceNames" label="使用设备" />
+            <ZmTableColumn prop="cumulativeWorkingLayers" label="当日施工层" />
+            <ZmTableColumn prop="cumulativeWorkingWell" label="当日施工井" />
+            <ZmTableColumn prop="productionBrief" label="当日施工简要" />
+            <ZmTableColumn prop="yesterdayStatus" label="上一天状态" />
+            <ZmTableColumn prop="yesterdayWorkingLayers" label="上一天施工层" />
+            <ZmTableColumn prop="yesterdayWorkingWell" label="上一天施工井" />
+            <ZmTableColumn prop="yesterdayProduct" label="上一天施工简要" />
+          </ZmTable>
+        </template>
+      </el-auto-resizer>
+    </div>
+  </div>
+</template>
+<style scoped>
+.card {
+  background-color: rgb(0 0 0 / 30%);
+  box-shadow: 0 2px 12px rgb(0 0 0 / 50%);
+  transition: all 0.3s ease;
+
+  &:hover {
+    box-shadow: 0 4px 16px rgb(0 0 0 / 8%);
+  }
+}
+
+/* --- 表格深度定制 --- */
+:deep(.custom-dark-table) {
+  --el-table-border-color: rgb(255 255 255 / 5%);
+  --el-table-header-bg-color: rgb(0 229 255 / 8%);
+  --el-table-row-hover-bg-color: rgb(0 229 255 / 10%);
+  --el-table-tr-bg-color: transparent;
+
+  color: #e5e7eb; /* 文字颜色:灰白 */
+  background-color: transparent !important;
+}
+
+/* 移除表格底部和周围的白线 */
+:deep(.el-table__inner-wrapper::before),
+:deep(.el-table__border-left-patch) {
+  display: none;
+}
+
+/* 表头样式 */
+:deep(.el-table th.el-table__cell) {
+  font-weight: 600;
+  color: #00e5ff; /* 表头文字荧光色 */
+  background-color: var(--el-table-header-bg-color) !important;
+  border-bottom: 1px solid rgb(0 229 255 / 20%);
+}
+
+/* 单元格样式 */
+:deep(.el-table td.el-table__cell) {
+  border-bottom: 1px solid rgb(255 255 255 / 5%);
+}
+
+/* 斑马纹 - 偶数行稍微亮一点点 */
+:deep(.el-table--striped .el-table__body tr.el-table__row--striped td.el-table__cell) {
+  background-color: rgb(255 255 255 / 2%);
+}
+
+/* Hover 效果 - 这一行亮起 */
+:deep(.el-table--enable-row-hover .el-table__body tr:hover > td.el-table__cell) {
+  background-color: var(--el-table-row-hover-bg-color) !important;
+}
+
+/* 滚动条美化 */
+:deep(.el-scrollbar__bar.is-horizontal),
+:deep(.el-scrollbar__bar.is-vertical) {
+  background-color: rgb(0 0 0 / 30%);
+}
+
+:deep(.el-scrollbar__thumb) {
+  background-color: rgb(0 229 255 / 20%);
+
+  &:hover {
+    background-color: rgb(0 229 255 / 40%);
+  }
+}
+</style>

+ 279 - 0
src/views/pms/stat/rdkb/exception.vue

@@ -0,0 +1,279 @@
+<script lang="ts" setup>
+import { ref, onMounted } from 'vue'
+import dayjs from 'dayjs'
+import CountTo from '@/components/count-to1.vue'
+import { IotStatApi } from '@/api/pms/stat'
+
+// --- 类型定义 ---
+type TimeType = 'quarter' | 'month' | 'year'
+
+// 模拟的生产异常数据接口
+interface ProductionExceptions {
+  standby: number // 井场待命
+  maintenance: number // 维修设备
+  exception: number // 异常/复杂
+  material: number // 物资影响
+  noWorkload: number // 无工作量
+}
+
+// 模拟的巡检异常数据接口
+interface InspectionExceptions {
+  equipment: number // 异常设备
+  point: number // 异常点
+}
+
+// --- 状态管理 ---
+const currentTimeType = ref<TimeType>('month')
+const loading = ref(false)
+
+// 时间选项:日、月、年
+const timeOptions = [
+  { label: '本月', value: 'month' },
+  { label: '本季度', value: 'quarter' },
+  { label: '本年', value: 'year' }
+]
+
+// 数据响应式对象
+const productionData = ref<ProductionExceptions>({
+  standby: 0,
+  maintenance: 0,
+  exception: 0,
+  material: 0,
+  noWorkload: 0
+})
+
+const inspectionData = ref<InspectionExceptions>({
+  equipment: 0,
+  point: 0
+})
+
+// --- 配置项:用于 v-for 循环渲染 ---
+const productionConfig = [
+  { key: 'standby', name: '井场待命' },
+  { key: 'maintenance', name: '维修设备' },
+  { key: 'noWorkload', name: '无工作量' },
+  { key: 'material', name: '物资影响' },
+  { key: 'exception', name: '异常/复杂' }
+]
+
+const inspectionConfig = [
+  { key: 'equipment', name: '异常设备' },
+  { key: 'point', name: '异常点' }
+]
+
+const getDateRange = (type: 'year' | 'quarter' | 'month') => {
+  const now = dayjs()
+  let start: dayjs.Dayjs, end: dayjs.Dayjs
+
+  if (type === 'year') {
+    start = now.startOf('year')
+    end = now.endOf('year')
+  } else if (type === 'quarter') {
+    start = now.startOf('quarter')
+    end = now.endOf('quarter')
+  } else {
+    start = now.startOf('month')
+    end = now.endOf('month')
+  }
+
+  return {
+    start: start.format('YYYY-MM-DD HH:mm:ss'),
+    end: end.format('YYYY-MM-DD HH:mm:ss')
+  }
+}
+
+// --- 模拟 API 请求 ---
+const fetchData = async () => {
+  loading.value = true
+
+  // 模拟获取时间范围
+  const { start, end } = getDateRange(currentTimeType.value as 'year' | 'quarter' | 'month')
+
+  const params = {
+    'createTime[0]': start,
+    'createTime[1]': end
+  }
+
+  const equipmentRes = await IotStatApi.getDeviceException(params)
+  const pointRes = await IotStatApi.getStatusException(params)
+
+  const productionRes = await IotStatApi.getProductionException(params)
+
+  inspectionData.value.equipment = equipmentRes.exceptionNum || 0
+  inspectionData.value.point = pointRes.value || 0
+
+  productionConfig.forEach((item) => {
+    productionData.value[item.key] =
+      productionRes.find((resItem) => resItem.name === item.name)?.count ?? 0
+  })
+
+  console.log('inspectionData :>> ', inspectionData)
+  console.log('productionData :>> ', productionData)
+
+  loading.value = false
+}
+
+const handleTimeChange = () => {
+  fetchData()
+}
+
+onMounted(() => {
+  fetchData()
+})
+</script>
+
+<template>
+  <div
+    class="card size-full rounded-lg p-4 flex flex-col"
+    v-loading="loading"
+    element-loading-background="rgba(0, 0, 0, 0.3)"
+  >
+    <!-- 头部:标题 + 时间切换 -->
+    <div class="flex justify-between items-center mb-4">
+      <div class="flex items-center gap-2">
+        <!-- 红色装饰条,代表异常/警示 -->
+        <div class="w-1 h-4 bg-[#FF5252] rounded-full shadow-[0_0_8px_#FF5252]"></div>
+        <div class="text-[#e0e0e0] text-lg font-bold">异常统计看板</div>
+      </div>
+      <el-segmented
+        size="default"
+        v-model="currentTimeType"
+        :options="timeOptions"
+        @change="handleTimeChange"
+        class="dark-segmented w-50!"
+        block
+      />
+    </div>
+
+    <!-- 内容区域:上下布局 -->
+    <div class="flex-1 flex flex-col gap-4 min-h-0 overflow-y-auto pr-1">
+      <!-- 1. 生产异常模块 -->
+      <div class="section-container">
+        <div class="text-[#FFD740] text-sm font-bold mb-3 flex items-center gap-2">
+          <span class="w-1.5 h-1.5 rounded-full bg-[#FFD740]"></span>
+          生产异常
+        </div>
+
+        <!-- 3x3 网格 -->
+        <div class="grid grid-cols-5 gap-3">
+          <div v-for="item in productionConfig" :key="item.key" class="stat-box group">
+            <div class="text-[#9ca3af] text-xs mb-1">{{ item.name }}</div>
+
+            <CountTo
+              class="text-xl font-bold font-mono text-white group-hover:text-[#FFD740] transition-colors"
+              :start-val="0"
+              :end-val="productionData[item.key as keyof ProductionExceptions]"
+            >
+              <!-- <span class="text-xs leading-8 text-[var(--el-text-color-regular)]">暂无数据</span> -->
+            </CountTo>
+            <!-- <div
+              class="text-xl font-bold font-mono text-white group-hover:text-[#FFD740] transition-colors"
+            >
+              {{ productionData[item.key as keyof ProductionExceptions] }}
+            </div> -->
+            <!-- 底部微光条 -->
+            <div
+              class="absolute bottom-0 left-0 w-full h-[2px] bg-gradient-to-r from-transparent via-[#FFD740] to-transparent opacity-20 group-hover:opacity-100 transition-opacity"
+            ></div>
+          </div>
+        </div>
+      </div>
+
+      <!-- 2. 巡检异常模块 -->
+      <div class="section-container">
+        <div class="text-[#00E5FF] text-sm font-bold mb-3 flex items-center gap-2">
+          <span class="w-1.5 h-1.5 rounded-full bg-[#00E5FF]"></span>
+          巡检异常
+        </div>
+
+        <!-- 2列 网格 -->
+        <div class="grid grid-cols-2 gap-3">
+          <div v-for="item in inspectionConfig" :key="item.key" class="stat-box group">
+            <div class="flex justify-between items-center">
+              <div class="text-[#9ca3af] text-xs">{{ item.name }}</div>
+              <!-- 可以加个小图标占位 -->
+              <div class="w-1 h-1 bg-[#00E5FF] rounded-full opacity-50"></div>
+            </div>
+
+            <CountTo
+              class="text-2xl font-bold font-mono text-white mt-1 group-hover:text-[#00E5FF] transition-colors"
+              :start-val="0"
+              :end-val="inspectionData[item.key as keyof InspectionExceptions]"
+            >
+              <!-- <span class="text-xs leading-8 text-[var(--el-text-color-regular)]">暂无数据</span> -->
+            </CountTo>
+            <!-- <div
+              class="text-2xl font-bold font-mono text-white mt-1 group-hover:text-[#00E5FF] transition-colors"
+            >
+              {{ inspectionData[item.key as keyof InspectionExceptions] }}
+            </div> -->
+            <!-- 底部微光条 -->
+            <div
+              class="absolute bottom-0 left-0 w-full h-[2px] bg-gradient-to-r from-transparent via-[#00E5FF] to-transparent opacity-20 group-hover:opacity-100 transition-opacity"
+            ></div>
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<style scoped>
+/* 主卡片背景 */
+.card {
+  background-color: rgb(0 0 0 / 30%);
+  box-shadow: 0 2px 12px rgb(0 0 0 / 50%);
+  transition: all 0.3s ease;
+
+  &:hover {
+    box-shadow: 0 4px 16px rgb(0 0 0 / 8%);
+  }
+}
+
+/* 子模块容器 */
+.section-container {
+  padding: 12px;
+  background: rgb(255 255 255 / 2%);
+  border: 1px solid rgb(255 255 255 / 3%);
+  border-radius: 8px;
+}
+
+/* 单个数字盒子 */
+.stat-box {
+  position: relative;
+  display: flex;
+  padding: 8px 12px;
+  overflow: hidden;
+  cursor: default;
+  background: rgb(0 0 0 / 20%);
+  border-radius: 6px;
+  transition: all 0.3s ease;
+  flex-direction: column;
+  justify-content: center;
+}
+
+.stat-box:hover {
+  background: rgb(255 255 255 / 5%);
+  transform: translateY(-2px);
+}
+
+/* 分段控制器样式覆盖 */
+.dark-segmented {
+  --el-segmented-item-selected-color: #e5eaf3;
+  --el-border-radius-base: 16px;
+  --el-segmented-color: #cfd3dc;
+  --el-segmented-bg-color: #262727;
+  --el-segmented-item-selected-bg-color: #ff5252;
+  --el-segmented-item-selected-disabled-bg-color: rgb(42 89 138);
+  --el-segmented-item-hover-color: #e5eaf3;
+  --el-segmented-item-hover-bg-color: #39393a;
+  --el-segmented-item-active-bg-color: #424243;
+  --el-segmented-item-disabled-color: #8d9095;
+}
+
+:deep(.el-segmented__item) {
+  display: flex;
+  justify-content: center;
+  align-items: center;
+}
+</style>

+ 218 - 0
src/views/pms/stat/rdkb/utilization.vue

@@ -0,0 +1,218 @@
+<script lang="ts" setup>
+import { ref, onMounted, onUnmounted, nextTick } from 'vue'
+import { IotStatApi } from '@/api/pms/stat'
+import dayjs from 'dayjs'
+import quarterOfYear from 'dayjs/plugin/quarterOfYear'
+import * as echarts from 'echarts'
+
+dayjs.extend(quarterOfYear)
+
+const chartRef = ref(null)
+let myChart: echarts.ECharts | null = null
+const currentTimeType = ref('month')
+
+const timeOptions = [
+  { label: '本月', value: 'month' },
+  { label: '本季度', value: 'quarter' },
+  { label: '本年', value: 'year' }
+]
+
+const getDateRange = (type: 'year' | 'quarter' | 'month') => {
+  const now = dayjs()
+  let start: dayjs.Dayjs, end: dayjs.Dayjs
+
+  if (type === 'year') {
+    start = now.startOf('year')
+    end = now.endOf('year')
+  } else if (type === 'quarter') {
+    start = now.startOf('quarter')
+    end = now.endOf('quarter')
+  } else {
+    start = now.startOf('month')
+    end = now.endOf('month')
+  }
+
+  return {
+    start: start.format('YYYY-MM-DD HH:mm:ss'),
+    end: end.format('YYYY-MM-DD HH:mm:ss')
+  }
+}
+
+const fetchData = async () => {
+  if (myChart) {
+    myChart.showLoading({
+      text: '加载中 ...',
+      color: '#409eff',
+      textColor: '#B6C8DA',
+      maskColor: 'rgba(0, 0, 0, 0.2)'
+    })
+  }
+
+  const { start, end } = getDateRange(currentTimeType.value as 'year' | 'quarter' | 'month')
+
+  const params = {
+    'createTime[0]': start,
+    'createTime[1]': end,
+    timeType: currentTimeType.value
+  }
+
+  try {
+    let list: any[] = []
+
+    const res = await IotStatApi.getUtilization(params)
+    if (res && Array.isArray(res)) list = res
+
+    renderChart(list)
+  } catch (error) {
+    console.error('Workload API Error:', error)
+  } finally {
+    myChart?.hideLoading()
+  }
+}
+
+const renderChart = (data: any[]) => {
+  if (!myChart) return
+
+  const xAxisData = data.map((item) => item.projectDeptName.replace('项目部', ''))
+
+  const seriesData = data.map((item) => {
+    const val = item.utilizationRate
+    if (val === null || val === undefined || isNaN(val)) return 0
+    return parseFloat((val * 100).toFixed(2))
+  })
+
+  const option: echarts.EChartsOption = {
+    tooltip: {
+      trigger: 'axis',
+      backgroundColor: 'rgba(0,0,0,0.7)',
+      borderColor: '#409eff',
+      textStyle: { color: '#fff' },
+      axisPointer: {
+        type: 'shadow',
+        shadowStyle: { color: 'rgba(255, 255, 255, 0.1)' }
+      },
+      // 自定义 Tooltip 内容,加上 %
+      formatter: (params: any) => {
+        const item = params[0]
+        return `${item.name}<br/>
+                <span style="display:inline-block;margin-right:4px;border-radius:10px;width:10px;height:10px;background-color:${item.color.colorStops ? item.color.colorStops[0].color : item.color};"></span>
+                ${item.seriesName}: <span style="font-weight:bold; color: #fff">${item.value}%</span>`
+      }
+    },
+    grid: { top: '15%', left: '3%', right: '3%', bottom: '5%', containLabel: true },
+    xAxis: {
+      data: xAxisData,
+      type: 'category',
+      boundaryGap: true,
+      axisLabel: { color: '#B6C8DA', interval: 0, fontSize: 12 },
+      axisLine: { lineStyle: { color: '#B6C8DA' } },
+      axisTick: { show: false }
+    },
+    yAxis: {
+      type: 'value',
+      name: '利用率 (%)',
+      axisLabel: { color: '#B6C8DA', formatter: '{value}' },
+      nameTextStyle: { color: '#B6C8DA', padding: [0, 0, 0, 10] },
+      axisLine: { lineStyle: { color: '#B6C8DA' } },
+      splitLine: { lineStyle: { color: '#457794', type: 'dashed' } }
+    },
+    series: {
+      name: '设备利用率',
+      type: 'bar',
+      showBackground: true,
+      backgroundStyle: {
+        color: 'rgba(180, 180, 180, 0.1)',
+        borderRadius: [4, 4, 0, 0]
+      },
+      itemStyle: {
+        borderRadius: [4, 4, 0, 0],
+        color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
+          { offset: 0, color: '#23D0F6' },
+          { offset: 1, color: '#1A7BF8' }
+        ])
+      },
+      emphasis: {
+        itemStyle: {
+          color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
+            { offset: 0, color: '#4FFBDF' },
+            { offset: 1, color: '#23D0F6' }
+          ])
+        }
+      },
+      data: seriesData
+    }
+  }
+
+  myChart.setOption(option)
+}
+
+const handleTimeChange = () => {
+  fetchData()
+}
+
+const resizeChart = () => myChart?.resize()
+
+onMounted(() => {
+  nextTick(() => {
+    myChart = echarts.init(chartRef.value, undefined, { renderer: 'canvas' })
+    fetchData()
+    window.addEventListener('resize', resizeChart)
+  })
+})
+
+onUnmounted(() => {
+  window.removeEventListener('resize', resizeChart)
+  myChart?.dispose()
+})
+</script>
+
+<template>
+  <div class="card size-full rounded-lg p-4 flex flex-col">
+    <div class="flex justify-between items-center mb-4">
+      <div class="flex items-center gap-2 items-center">
+        <div class="w-1 h-4 bg-[#00E5FF] rounded-full shadow-[0_0_8px_#00E5FF]"></div>
+        <div class="text-[#e0e0e0] text-lg font-bold">设备利用率</div>
+      </div>
+      <el-segmented
+        size="default"
+        v-model="currentTimeType"
+        :options="timeOptions"
+        @change="handleTimeChange"
+        class="dark-segmented w-50!"
+        block
+      />
+    </div>
+    <div ref="chartRef" class="flex-1 w-full min-h-0"></div>
+  </div>
+</template>
+
+<style scoped>
+.card {
+  background-color: rgb(0 0 0 / 30%);
+  box-shadow: 0 2px 12px rgb(0 0 0 / 50%);
+  transition: all 0.3s ease;
+
+  &:hover {
+    box-shadow: 0 4px 16px rgb(0 0 0 / 8%);
+  }
+}
+
+.dark-segmented {
+  --el-segmented-item-selected-color: #e5eaf3;
+  --el-border-radius-base: 16px;
+  --el-segmented-color: #cfd3dc;
+  --el-segmented-bg-color: #262727;
+  --el-segmented-item-selected-bg-color: #409eff;
+  --el-segmented-item-selected-disabled-bg-color: rgb(42 89 138);
+  --el-segmented-item-hover-color: #e5eaf3;
+  --el-segmented-item-hover-bg-color: #39393a;
+  --el-segmented-item-active-bg-color: #424243;
+  --el-segmented-item-disabled-color: #8d9095;
+}
+
+:deep(.el-segmented__item) {
+  display: flex;
+  justify-content: center;
+  align-items: center;
+}
+</style>

+ 361 - 0
src/views/pms/stat/rdkb/workload.vue

@@ -0,0 +1,361 @@
+<script lang="ts" setup>
+import { ref, onMounted, onUnmounted, nextTick } from 'vue'
+import { IotStatApi } from '@/api/pms/stat'
+import dayjs from 'dayjs'
+import quarterOfYear from 'dayjs/plugin/quarterOfYear'
+import * as echarts from 'echarts'
+
+dayjs.extend(quarterOfYear)
+
+const chartRef = ref(null)
+let myChart: echarts.ECharts | null = null
+const currentTimeType = ref('month')
+
+const timeOptions = [
+  { label: '本月', value: 'month' },
+  { label: '本季度', value: 'quarter' },
+  { label: '本年', value: 'year' }
+]
+
+const fieldConfig = [
+  { key: 'ylWellCount', name: '压裂井数' },
+  { key: 'lyWellCount', name: '连油井数' },
+  { key: 'cumulativePumpTrips', name: '泵车台次' },
+  { key: 'cumulativeWorkingLayers', name: '压裂层数' }
+]
+
+const getDateRange = (type: 'year' | 'quarter' | 'month') => {
+  const now = dayjs()
+  let start: dayjs.Dayjs, end: dayjs.Dayjs
+
+  if (type === 'year') {
+    start = now.startOf('year')
+    end = now.endOf('year')
+  } else if (type === 'quarter') {
+    start = now.startOf('quarter')
+    end = now.endOf('quarter')
+  } else {
+    start = now.startOf('month')
+    end = now.endOf('month')
+  }
+
+  return {
+    start: start.format('YYYY-MM-DD HH:mm:ss'),
+    end: end.format('YYYY-MM-DD HH:mm:ss')
+  }
+}
+
+const fetchData = async () => {
+  if (myChart) {
+    myChart.showLoading({
+      text: '加载中 ...',
+      color: '#409eff',
+      textColor: '#B6C8DA',
+      maskColor: 'rgba(0, 0, 0, 0.2)'
+    })
+  }
+
+  const { start, end } = getDateRange(currentTimeType.value as 'year' | 'quarter' | 'month')
+
+  const params = {
+    deptId: 163,
+    'createTime[0]': start,
+    'createTime[1]': end,
+    timeType: currentTimeType.value
+  }
+
+  try {
+    let list: any[] = []
+
+    if (currentTimeType.value === 'year') {
+      const res = await IotStatApi.getRdWorkloadYear(params)
+      if (res && Array.isArray(res)) list = res
+    } else {
+      const res = await IotStatApi.getRdWorkload(params)
+      if (res && res.list) list = res.list
+    }
+
+    renderChart(list)
+  } catch (error) {
+    console.error('Workload API Error:', error)
+  } finally {
+    myChart?.hideLoading()
+  }
+}
+
+const renderChart = (data: any[]) => {
+  if (!myChart) return
+  const isYear = currentTimeType.value === 'year'
+
+  // --- 高亮配色方案 ---
+  const colorPalettes = [
+    // 1. 冰蓝霓虹 (压裂井数) - 极亮青色 -> 深蓝
+    {
+      line: '#00E5FF',
+      start: '#00E5FF',
+      end: '#2979FF'
+    },
+    // 2. 日落流金 (连油井数) - 亮黄 -> 亮橙
+    {
+      line: '#FFD740',
+      start: '#FFD740',
+      end: '#FF6D00'
+    },
+    // 3. 赛博紫 (泵车台次) - 亮粉紫 -> 深紫
+    {
+      line: '#EA80FC',
+      start: '#EA80FC',
+      end: '#651FFF'
+    },
+    // 4. 极光绿 (压裂层数) - 荧光绿 -> 青绿
+    {
+      line: '#69F0AE',
+      start: '#69F0AE',
+      end: '#00C853'
+    }
+  ]
+
+  const xAxisData = data.map((item) =>
+    isYear ? item.reportDate : item.projectDeptName.replace('项目部', '')
+  )
+
+  const option: echarts.EChartsOption = {
+    tooltip: {
+      trigger: 'axis',
+      backgroundColor: 'rgba(18, 26, 44, 0.9)', // 更深色的背景,对比更强
+      borderColor: '#409eff',
+      textStyle: { color: '#fff' },
+      padding: [10, 15],
+      axisPointer: {
+        type: isYear ? 'line' : 'shadow',
+        lineStyle: { color: '#fff', type: 'dashed' },
+        shadowStyle: { color: 'rgba(255, 255, 255, 0.1)' }
+      }
+    },
+    legend: {
+      data: fieldConfig.map((item) => item.name),
+      textStyle: { color: '#E0E0E0' }, // 图例文字调亮
+      top: 0,
+      itemWidth: 14,
+      itemHeight: 14
+    },
+    grid: {
+      top: '18%', // 留出更多空间给图例
+      left: '2%',
+      right: '2%',
+      bottom: '2%',
+      containLabel: true
+    },
+    xAxis: {
+      data: xAxisData,
+      type: 'category',
+      boundaryGap: !isYear,
+      axisLabel: {
+        color: '#D1D5DB', // X轴文字调亮
+        interval: 0,
+        fontSize: 12,
+        formatter: (value: string) => {
+          // 如果名字太长换行显示
+          return value.length > 4 ? value.slice(0, 4) + '\n' + value.slice(4) : value
+        }
+      },
+      axisLine: { lineStyle: { color: '#4B5563' } },
+      axisTick: { show: false }
+    },
+    yAxis: {
+      type: 'value',
+      axisLabel: { color: '#D1D5DB' },
+      splitLine: {
+        lineStyle: { color: '#457794', type: 'dashed' } // 网格线稍微亮一点
+      }
+    },
+    series: fieldConfig.map((item, index) => {
+      const palette = colorPalettes[index % colorPalettes.length]
+
+      const seriesBase = {
+        name: item.name,
+        data: data.map((d) => {
+          const val = d[item.key]
+          return val === null || val === undefined || isNaN(val) ? 0 : val
+        })
+      }
+
+      if (isYear) {
+        // --- 折线图 (年) ---
+        return {
+          ...seriesBase,
+          type: 'line',
+          smooth: true,
+          showSymbol: false,
+          symbol: 'circle',
+          symbolSize: 8,
+          // 线条非常亮
+          lineStyle: { width: 3, color: palette.line, shadowColor: palette.line, shadowBlur: 10 },
+          itemStyle: { color: palette.line, borderColor: '#fff', borderWidth: 2 },
+          areaStyle: {
+            opacity: 0.5, // 区域透明度适中
+            color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
+              { offset: 0, color: palette.start }, // 顶部颜色同线条
+              { offset: 1, color: 'rgba(0,0,0,0)' }
+            ])
+          },
+          emphasis: { focus: 'series' }
+        }
+      } else {
+        // --- 柱状图 (月/季) ---
+        return {
+          ...seriesBase,
+          type: 'bar',
+          barMaxWidth: 14,
+          barGap: '30%',
+          itemStyle: {
+            borderRadius: [4, 4, 0, 0],
+            // 柱子使用实色渐变,不再过度透明,显得“脏”
+            color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
+              { offset: 0, color: palette.start }, // 100% 亮色
+              { offset: 1, color: palette.end } // 底部颜色,透明度由 hex 决定(这里是实色)
+            ]),
+            // 给柱子加一点光晕
+            shadowColor: palette.end,
+            shadowBlur: 5
+          },
+          emphasis: {
+            focus: 'series',
+            itemStyle: {
+              // 鼠标悬停时变得更亮
+              color: palette.line
+            }
+          }
+        }
+      }
+    }) as any
+  }
+
+  myChart.setOption(option)
+}
+// const renderChart = (data: any[]) => {
+//   if (!myChart) return
+//   const isYear = currentTimeType.value === 'year'
+
+//   const color = ['#5470c6', '#f1d209', '#e14f0f', '#91cc75']
+
+//   const xAxisData = data.map((item) =>
+//     isYear ? item.reportDate : item.projectDeptName.replace('项目部', '')
+//   )
+
+//   const option: echarts.EChartsOption = {
+//     tooltip: {
+//       trigger: 'axis',
+//       axisPointer: {
+//         type: isYear ? 'cross' : 'shadow',
+//         label: { backgroundColor: '#6a7985' }
+//       }
+//     },
+//     legend: {
+//       data: fieldConfig.map((item) => item.name),
+//       textStyle: { color: '#B6C8DA' }
+//     },
+//     grid: { top: '15%', left: '2%', right: '2%', bottom: '2%', containLabel: true },
+//     xAxis: {
+//       data: xAxisData,
+//       type: 'category',
+//       boundaryGap: !isYear,
+//       axisLabel: { color: '#B6C8DA', interval: 0 },
+//       axisLine: { lineStyle: { color: '#B6C8DA' } }
+//     },
+//     yAxis: {
+//       type: 'value',
+//       axisLabel: { color: '#B6C8DA' },
+//       splitLine: { lineStyle: { color: '#457794', type: 'dashed' } },
+//       axisLine: { lineStyle: { color: '#B6C8DA' } }
+//     },
+//     series: fieldConfig.map((item, index) => ({
+//       name: item.name,
+//       type: isYear ? 'line' : 'bar',
+//       smooth: true,
+//       barMaxWidth: 30,
+//       symbol: 'circle',
+//       symbolSize: 8,
+//       itemStyle: { color: color[index] },
+//       emphasis: { focus: 'series' },
+//       lineStyle: { width: 3 },
+//       data: data.map((d) => {
+//         const val = d[item.key]
+//         return val === null || val === undefined || isNaN(val) ? 0 : val
+//       })
+//     }))
+//   }
+
+//   myChart.setOption(option)
+// }
+
+const handleTimeChange = () => {
+  fetchData()
+}
+
+const resizeChart = () => myChart?.resize()
+
+onMounted(() => {
+  nextTick(() => {
+    myChart = echarts.init(chartRef.value, undefined, { renderer: 'canvas' })
+    fetchData()
+    window.addEventListener('resize', resizeChart)
+  })
+})
+
+onUnmounted(() => {
+  window.removeEventListener('resize', resizeChart)
+  myChart?.dispose()
+})
+</script>
+
+<template>
+  <div class="card size-full rounded-lg p-4 flex flex-col">
+    <div class="flex justify-between items-center mb-4">
+      <div class="flex items-center gap-2 items-center">
+        <div class="w-1 h-4 bg-[#00E5FF] rounded-full shadow-[0_0_8px_#00E5FF]"></div>
+        <div class="text-[#e0e0e0] text-lg font-bold">工作量汇总</div>
+      </div>
+      <el-segmented
+        size="default"
+        v-model="currentTimeType"
+        :options="timeOptions"
+        @change="handleTimeChange"
+        class="dark-segmented w-50!"
+        block
+      />
+    </div>
+    <div ref="chartRef" class="flex-1 w-full min-h-0"></div>
+  </div>
+</template>
+
+<style scoped>
+.card {
+  background-color: rgb(0 0 0 / 30%);
+  box-shadow: 0 2px 12px rgb(0 0 0 / 50%);
+  transition: all 0.3s ease;
+
+  &:hover {
+    box-shadow: 0 4px 16px rgb(0 0 0 / 8%);
+  }
+}
+
+.dark-segmented {
+  --el-segmented-item-selected-color: #e5eaf3;
+  --el-border-radius-base: 16px;
+  --el-segmented-color: #cfd3dc;
+  --el-segmented-bg-color: #262727;
+  --el-segmented-item-selected-bg-color: #409eff;
+  --el-segmented-item-selected-disabled-bg-color: rgb(42 89 138);
+  --el-segmented-item-hover-color: #e5eaf3;
+  --el-segmented-item-hover-bg-color: #39393a;
+  --el-segmented-item-active-bg-color: #424243;
+  --el-segmented-item-disabled-color: #8d9095;
+}
+
+:deep(.el-segmented__item) {
+  display: flex;
+  justify-content: center;
+  align-items: center;
+}
+</style>

+ 58 - 9
src/views/report-statistics/costs.vue

@@ -4,6 +4,8 @@ import dayjs from 'dayjs'
 import CountTo from '@/components/count-to1.vue'
 import { IotReportApi } from '@/api/pms/report'
 import { useDebounceFn } from '@vueuse/core'
+import download from '@/utils/download'
+import { rangeShortcuts } from '@/utils/formatTime'
 
 // 定义时间类型
 type TimeType = 'year' | 'month' | 'day'
@@ -23,7 +25,7 @@ const timeOptions: { label: string; value: TimeType }[] = [
   { label: '日', value: 'day' }
 ]
 
-const activeTimeType = ref<TimeType>('year')
+const activeTimeType = ref<TimeType | undefined>('year')
 const query = ref<Query>({
   pageNo: 1,
   pageSize: 10
@@ -201,6 +203,26 @@ function handleReset() {
   handleTimeChange('year')
   selectType(undefined)
 }
+
+const exportLoading = ref(false)
+
+const handleExport = async () => {
+  exportLoading.value = true
+  try {
+    const data = await IotReportApi.exportCosts(query.value)
+    download.excel(data, '运维成本.xls')
+  } finally {
+    exportLoading.value = false
+  }
+}
+
+const handleClear = () => {
+  handleTimeChange('year')
+}
+
+const handleChange = () => {
+  activeTimeType.value = undefined
+}
 </script>
 
 <template>
@@ -290,17 +312,44 @@ function handleReset() {
         </section>
       </div>
       <div class="flex justify-between gap-4">
-        <el-button-group size="default">
+        <div class="flex items-center gap-4">
+          <el-date-picker
+            size="default"
+            v-model="query.createTime"
+            value-format="YYYY-MM-DD HH:mm:ss"
+            type="daterange"
+            start-placeholder="开始日期"
+            end-placeholder="结束日期"
+            :shortcuts="rangeShortcuts"
+            class="!w-220px"
+            @clear="handleClear"
+            @change="handleChange"
+            :clearable="false"
+            :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
+          />
+          <el-button-group size="default">
+            <el-button
+              v-for="item in timeOptions"
+              :key="item.value"
+              :type="activeTimeType === item.value ? 'primary' : ''"
+              @click="handleTimeChange(item.value)"
+            >
+              {{ item.label }}
+            </el-button>
+          </el-button-group>
+        </div>
+        <div class="flex items-center gap-2">
+          <el-button size="default" @click="handleReset">重置</el-button>
           <el-button
-            v-for="item in timeOptions"
-            :key="item.value"
-            :type="activeTimeType === item.value ? 'primary' : ''"
-            @click="handleTimeChange(item.value)"
+            size="default"
+            plain
+            type="success"
+            @click="handleExport"
+            :loading="exportLoading"
           >
-            {{ item.label }}
+            <Icon icon="ep:download" class="mr-5px" /> 导出
           </el-button>
-        </el-button-group>
-        <el-button size="default" @click="handleReset">重置</el-button>
+        </div>
       </div>
     </div>
     <div class="bg-white dark:bg-[#1d1e1f] shadow rounded-lg p-4 flex flex-col mt-4">

+ 22 - 2
src/views/report-statistics/daily-report.vue

@@ -6,6 +6,7 @@ import dayjs from 'dayjs'
 import { DICT_TYPE, getDictOptions } from '@/utils/dict'
 import { TableColumnCtx } from 'element-plus/es/components/table/src/table-column/defaults'
 import { useUserStore } from '@/store/modules/user'
+import download from '@/utils/download'
 
 defineOptions({ name: 'DailyReport' })
 
@@ -130,8 +131,7 @@ const columns = ref<Column[]>([
       {
         label: '注气量(万方)',
         prop: 'dailyGasInjection',
-        'min-width': '120px',
-        formatter: (row: List) => (row.dailyGasInjection / 10000).toFixed(2)
+        'min-width': '120px'
       },
       {
         label: '注水量(方)',
@@ -430,6 +430,23 @@ watch(
 const expandRowKeys = computed(() => {
   return list.value.filter((item) => item.lastGroupIdFlag).map((item) => item.id.toString())
 })
+
+const exportLoading = ref(false)
+
+const handleExport = () => {
+  exportLoading.value = true
+  if (tab.value === '井') {
+    IotRhDailyReportApi.exportIotRhDailyReportWell(query.value).then((data) => {
+      download.excel(data, '瑞恒井日报统计.xls')
+      exportLoading.value = false
+    })
+  } else {
+    IotRhDailyReportApi.exportIotRhDailyReportTeam(query.value).then((data) => {
+      download.excel(data, '瑞恒队伍日报统计.xls')
+      exportLoading.value = false
+    })
+  }
+}
 </script>
 
 <template>
@@ -480,6 +497,9 @@ const expandRowKeys = computed(() => {
           <Icon icon="ep:search" class="mr-5px" /> 搜索
         </el-button>
         <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" />重置</el-button>
+        <el-button plain type="success" @click="handleExport" :loading="exportLoading">
+          <Icon icon="ep:download" class="mr-5px" /> 导出
+        </el-button>
       </el-form-item>
     </el-form>
 

+ 30 - 6
src/views/report-statistics/device_book/index2.vue

@@ -78,7 +78,7 @@
                 >
               </el-form-item>
               <el-form-item>
-                <el-button type="success" plain @click="handleExport"
+                <el-button type="success" plain @click="handleExport" :loading="exportLoading"
                   ><Icon icon="ep:download" class="mr-3px" /> 导出</el-button
                 >
               </el-form-item>
@@ -239,7 +239,7 @@
         />
       </ContentWrap>
 
-      <el-dialog
+      <!-- <el-dialog
         v-model="exportDialogVisible"
         title="导出设置"
         width="600px"
@@ -281,7 +281,7 @@
             </el-button>
           </div>
         </template>
-      </el-dialog>
+      </el-dialog> -->
     </el-col>
   </el-row>
 </template>
@@ -297,6 +297,7 @@ import echarts from '@/plugins/echarts'
 import { formatDate } from '@/utils/formatTime'
 import { nextTick, watch } from 'vue'
 import download from '@/utils/download'
+const message = useMessage() // 消息弹窗
 
 /** 设备台账 列表 */
 defineOptions({ name: 'IotDevicePms' })
@@ -387,9 +388,32 @@ const getTableColumns = () => {
 }
 
 // 导出功能
+// const handleExport = async () => {
+//   exportDialogVisible.value = true
+//   getTableColumns()
+// }
+
+let exportLoading = ref(false)
 const handleExport = async () => {
-  exportDialogVisible.value = true
-  getTableColumns()
+  try {
+    const exportParams = {
+      pageNo: queryParams.pageNo,
+      pageSize: queryParams.pageSize,
+      deviceCode: queryParams.deviceCode,
+      deviceName: queryParams.deviceName,
+
+      exportFields: selectedColumns.value.join(',')
+    }
+    // 导出的二次确认
+    await message.exportConfirm()
+    // 发起导出
+    exportLoading.value = true
+    const data = await IotDeviceApi.exportDeviceReport(exportParams)
+    download.excel(data, '设备报表.xls')
+  } catch {
+  } finally {
+    exportLoading.value = false
+  }
 }
 
 // 确认导出
@@ -413,7 +437,7 @@ const confirmExport = async () => {
     // 调用后端导出接口
     const res = await IotDeviceApi.exportDeviceReport(exportParams)
 
-    download.excel(res, '设备报表.xls')
+    download.excel(res, '设备报表.xlsx')
     exportDialogVisible.value = false
   } catch (error) {
     console.error('导出失败:', error)

+ 26 - 0
src/views/report-statistics/fault_report/index.vue

@@ -145,6 +145,12 @@
                 >
               </el-form-item>
 
+              <el-form-item>
+                <el-button type="success" plain @click="handleExport" :loading="exportLoading"
+                  ><Icon icon="ep:download" class="mr-3px" /> 导出</el-button
+                >
+              </el-form-item>
+
               <el-form-item>
                 <el-button @click="resetQuery"
                   ><Icon icon="ep:refresh" class="mr-3px" />
@@ -274,6 +280,10 @@ import { DICT_TYPE } from '@/utils/dict'
 import DeptTree from '@/views/system/user/DeptTree.vue'
 import { dateFormatter } from '@/utils/formatTime'
 
+const message = useMessage() // 消息弹窗
+import { watch } from 'vue'
+import download from '@/utils/download'
+
 const { params } = useRoute()
 /** 巡检工单 列表 */
 defineOptions({ name: 'IotInspectOrder' })
@@ -314,6 +324,22 @@ const statusList = ref({
   close: false
 })
 
+let exportLoading = ref(false)
+const handleExport = async () => {
+  try {
+    // 导出的二次确认
+    await message.exportConfirm()
+    // 发起导出
+    exportLoading.value = true
+
+    const data = await IotInspectOrderApi.exportFaultReport(queryParams)
+    download.excel(data, '故障上报报表.xlsx')
+  } catch {
+  } finally {
+    exportLoading.value = false
+  }
+}
+
 /** 查询列表 */
 const getList = async (status: string = '', shouldResetStatus = true) => {
   if (shouldResetStatus) {

+ 37 - 3
src/views/report-statistics/inspection_order/index.vue

@@ -145,13 +145,18 @@
       </el-row>
 
       <el-row :gutter="20">
-        <el-col :span="24">
+        <el-col :span="22">
           <el-radio-group v-model="dateType" size="default" fill="#409eff">
             <el-radio-button label="年" value="year" />
             <el-radio-button label="月" value="month" />
             <el-radio-button label="日" value="day" />
           </el-radio-group>
         </el-col>
+        <el-col :span="2">
+          <el-button type="success" plain @click="handleExport" :loading="exportLoading"
+            ><Icon icon="ep:download" class="mr-3px" /> 导出</el-button
+          >
+        </el-col>
       </el-row>
 
       <!-- 列表 -->
@@ -328,8 +333,9 @@ import { IotInspectOrderApi, IotInspectOrderVO } from '@/api/pms/inspect/order'
 import { DICT_TYPE } from '@/utils/dict'
 import DeptTree from '@/views/system/user/DeptTree.vue'
 import { IotInspectItemVO, IotInspectOrderDetailApi } from '@/api/pms/inspect/order/detail'
-
+const message = useMessage() // 消息弹窗
 import { watch } from 'vue'
+import download from '@/utils/download'
 
 const { push } = useRouter()
 const { params } = useRoute()
@@ -419,6 +425,32 @@ const handleRowClick = (row, column: any, event: Event) => {
   }
 }
 
+let exportLoading = ref(false)
+const handleExport = async () => {
+  try {
+    // 导出的二次确认
+    await message.exportConfirm()
+    // 发起导出
+    exportLoading.value = true
+
+    if (isException.value) {
+      const data = await IotInspectOrderApi.exportExceptionDeviceInspectReport(queryParams)
+      download.excel(data, '异常设备报表.xlsx')
+    } else if (isExceptionPoint.value) {
+      const data = await IotInspectOrderApi.exportExceptionPointInspectReport(queryParams)
+      download.excel(data, '异常点报表.xlsx')
+    } else {
+      const data = await IotInspectOrderApi.exportInspectReport(queryParams)
+      download.excel(data, '巡检报表.xlsx')
+    }
+    // const data = await IotInspectOrderApi.exportInspectReport(queryParams)
+    // download.excel(data, '巡检报表.xlsx')
+  } catch {
+  } finally {
+    exportLoading.value = false
+  }
+}
+
 let isExceptionPoint = ref(false)
 const list2 = ref<IotInspectItemVO[]>([]) // 列表的数据
 
@@ -595,12 +627,14 @@ const detailQueryParams = ref({
   pageNo: 1,
   pageSize: 20,
   status: '异常',
-  deviceCode: ''
+  deviceCode: '',
+  createTime: []
 })
 const goDetail = async (code: string) => {
   // 抽屉打开设备详情
   drawerVisible.value = true
   detailQueryParams.value.deviceCode = code
+  detailQueryParams.value.createTime = queryParams.createTime
 
   const data = await IotInspectOrderDetailApi.getIotInspectItemStatusPage(detailQueryParams.value)
   deviceDetail.value = data.list

+ 21 - 0
src/views/report-statistics/rd-daily-report.vue

@@ -5,6 +5,7 @@ import { useDebounceFn } from '@vueuse/core'
 import dayjs from 'dayjs'
 import { DICT_TYPE, getDictOptions } from '@/utils/dict'
 import { useUserStore } from '@/store/modules/user'
+import download from '@/utils/download'
 
 defineOptions({ name: 'DailyReport' })
 
@@ -366,6 +367,23 @@ watch(
 const expandRowKeys = computed(() => {
   return list.value.filter((item) => item.lastGroupIdFlag).map((item) => item.id.toString())
 })
+
+const exportLoading = ref(false)
+
+const handleExport = () => {
+  exportLoading.value = true
+  if (tab.value === '井') {
+    IotRdDailyReportApi.exportIotRdDailyReportWell(query.value).then((data) => {
+      download.excel(data, '瑞都井日报统计.xls')
+      exportLoading.value = false
+    })
+  } else {
+    IotRdDailyReportApi.exportIotRdDailyReportTeam(query.value).then((data) => {
+      download.excel(data, '瑞都队伍日报统计.xls')
+      exportLoading.value = false
+    })
+  }
+}
 </script>
 
 <template>
@@ -416,6 +434,9 @@ const expandRowKeys = computed(() => {
           <Icon icon="ep:search" class="mr-5px" /> 搜索
         </el-button>
         <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" />重置</el-button>
+        <el-button plain type="success" @click="handleExport" :loading="exportLoading">
+          <Icon icon="ep:download" class="mr-5px" /> 导出
+        </el-button>
       </el-form-item>
     </el-form>
 

+ 27 - 23
src/views/report-statistics/ry-daily-report.vue

@@ -6,6 +6,7 @@ import dayjs from 'dayjs'
 import { DICT_TYPE, getDictOptions } from '@/utils/dict'
 import { TableColumnCtx } from 'element-plus/es/components/table/src/table-column/defaults'
 import { useUserStore } from '@/store/modules/user'
+import download from '@/utils/download'
 
 defineOptions({ name: 'DailyReport' })
 
@@ -116,26 +117,6 @@ const columns = ref<Column[]>([
     dictType: DICT_TYPE.PMS_PROJECT_TASK_RY_SCHEDULE,
     fixed: 'left'
   },
-  // {
-  //   label: '审批状态',
-  //   prop: 'auditStatus',
-  //   'min-width': '120px',
-  //   isTag: true,
-  //   formatter: (row: List) => {
-  //     switch (row.auditStatus) {
-  //       case 0:
-  //         return '待提交'
-  //       case 10:
-  //         return '待审批'
-  //       case 20:
-  //         return '审批通过'
-  //       case 30:
-  //         return '审批拒绝'
-  //       default:
-  //         return ''
-  //     }
-  //   }
-  // },
   {
     label: '设备型号',
     prop: 'equipmentType',
@@ -408,7 +389,7 @@ function cellStyle(data: {
   if (column.property === 'dailyFuel') {
     const originalValue = row.dailyFuel ?? 0
 
-    if (originalValue > 15)
+    if (originalValue > 9000)
       return {
         color: 'red',
         fontWeight: 'bold'
@@ -460,6 +441,7 @@ interface Query {
   taskName?: string
   wellName?: string
   createTime: string[]
+  projectClassification: number
 }
 
 const query = ref<Query>({
@@ -468,7 +450,8 @@ const query = ref<Query>({
   deptId: id,
   createTime: [
     ...rangeShortcuts[2].value().map((item) => dayjs(item).format('YYYY-MM-DD HH:mm:ss'))
-  ]
+  ],
+  projectClassification: 1
 })
 
 function handleSizeChange(val: number) {
@@ -527,7 +510,8 @@ function resetQuery() {
     taskName: '',
     createTime: [
       ...rangeShortcuts[2].value().map((item) => dayjs(item).format('YYYY-MM-DD HH:mm:ss'))
-    ]
+    ],
+    projectClassification: 1
   }
   handleQuery()
 }
@@ -549,6 +533,23 @@ watch(
 const expandRowKeys = computed(() => {
   return list.value.filter((item) => item.lastGroupIdFlag).map((item) => item.id.toString())
 })
+
+const exportLoading = ref(false)
+
+const handleExport = () => {
+  exportLoading.value = true
+  if (tab.value === '井') {
+    IotRyDailyReportApi.exportIotRyDailyReportWell(query.value).then((data) => {
+      download.excel(data, '瑞鹰钻井井日报统计.xls')
+      exportLoading.value = false
+    })
+  } else {
+    IotRyDailyReportApi.exportIotRyDailyReportTeam(query.value).then((data) => {
+      download.excel(data, '瑞鹰钻井队伍日报统计.xls')
+      exportLoading.value = false
+    })
+  }
+}
 </script>
 
 <template>
@@ -599,6 +600,9 @@ const expandRowKeys = computed(() => {
           <Icon icon="ep:search" class="mr-5px" /> 搜索
         </el-button>
         <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" />重置</el-button>
+        <el-button plain type="success" @click="handleExport" :loading="exportLoading">
+          <Icon icon="ep:download" class="mr-5px" /> 导出
+        </el-button>
       </el-form-item>
     </el-form>
 

+ 661 - 0
src/views/report-statistics/ry-xj-daily-report.vue

@@ -0,0 +1,661 @@
+<script lang="ts" setup>
+import { IotRyDailyReportApi } from '@/api/pms/iotrydailyreport'
+import { rangeShortcuts } from '@/utils/formatTime'
+import { useDebounceFn } from '@vueuse/core'
+import dayjs from 'dayjs'
+import { DICT_TYPE, getDictOptions } from '@/utils/dict'
+import { TableColumnCtx } from 'element-plus/es/components/table/src/table-column/defaults'
+import { useUserStore } from '@/store/modules/user'
+import download from '@/utils/download'
+
+defineOptions({ name: 'DailyReport' })
+
+const tab = ref('井')
+const tabOptions = ['井', '队伍']
+
+interface List {
+  id: number
+  deptId: number
+  projectId: number
+  taskId: number
+  projectClassification: string
+  relocationDays: number
+  latestWellDoneTime: number
+  currentDepth: number
+  dailyFootage: number
+  monthlyFootage: number
+  annualFootage: number
+  dailyPowerUsage: number
+  monthlyPowerUsage: number
+  dailyFuel: number
+  monthlyFuel: number
+  dailyOilVolume: number
+  remainDieselVolume: number
+  productionTime: number
+  nonProductionTime: number
+  ryNptReason: string
+  drillingWorkingTime: number
+  otherProductionTime: number
+  accidentTime: number
+  repairTime: number
+  selfStopTime: number
+  complexityTime: number
+  relocationTime: number
+  rectificationTime: number
+  waitingStopTime: number
+  winterBreakTime: number
+  constructionStartDate: number
+  constructionEndDate: number
+  productionStatus: string
+  currentOperation: string
+  nextPlan: string
+  rigStatus: string
+  repairStatus: string
+  personnel: string
+  totalStaffNum: number
+  leaveStaffNum: number
+  mudDensity: number
+  mudViscosity: number
+  lateralLength: number
+  wellInclination: number
+  azimuth: number
+  remark: string
+  status: number
+  processInstanceId: string
+  auditStatus: number
+  opinion: string
+  createTime: number
+  deptName: string
+  contractName: string
+  taskName: string
+  designWellDepth: number
+  designWellStruct: number
+  totalConstructionWells: number
+  completedWells: number
+  equipmentType: string
+  transitTime: number
+  lastCurrentDepth: number
+  lastGroupIdFlag: boolean
+  technique: string
+  wellCategory: string
+  casingPipeSize: number
+  wellControlLevel: string
+  ratedProductionTime: number
+  onDutyStaffNum: number
+  offDutyStaffNum: number
+}
+
+interface Column {
+  prop?: keyof List
+  label: string
+  'min-width'?: string
+  isTag?: boolean
+  fixed?: 'left' | 'right'
+  formatter?: (row: List) => any
+  children?: Column[]
+  dictType?: string
+}
+
+const { t } = useI18n()
+
+const columns = ref<Column[]>([
+  {
+    label: '日期',
+    prop: 'createTime',
+    'min-width': '120px',
+    formatter: (row: List) => dayjs(row.createTime).format('YYYY-MM-DD'),
+    fixed: 'left'
+  },
+  {
+    label: '施工队伍',
+    prop: 'deptName',
+    'min-width': '120px',
+    fixed: 'left'
+  },
+  {
+    label: '任务',
+    prop: 'taskName',
+    'min-width': '120px',
+    fixed: 'left'
+  },
+  {
+    label: '施工状态',
+    prop: 'rigStatus',
+    'min-width': '120px',
+    isTag: true,
+    dictType: DICT_TYPE.PMS_PROJECT_TASK_RY_SCHEDULE,
+    fixed: 'left'
+  },
+  {
+    label: '总施工井数',
+    prop: 'totalConstructionWells',
+    'min-width': '120px'
+  },
+  {
+    label: '完工井数',
+    prop: 'completedWells',
+    'min-width': '120px'
+  },
+  {
+    label: t('project.technology'),
+    prop: 'technique',
+    'min-width': '120px',
+    isTag: true,
+    dictType: DICT_TYPE.PMS_PROJECT_RY_TECHNOLOGY
+  },
+  {
+    label: '井别',
+    prop: 'wellCategory',
+    'min-width': '120px'
+  },
+  {
+    label: '井深(m)',
+    prop: 'designWellDepth',
+    'min-width': '120px'
+  },
+  {
+    label: '套生段产管尺寸(mm)',
+    prop: 'casingPipeSize',
+    'min-width': '120px'
+  },
+  {
+    label: '井控级别',
+    prop: 'wellControlLevel',
+    'min-width': '120px'
+  },
+  {
+    label: '当日',
+    children: [
+      {
+        label: '用电量(MWh)',
+        prop: 'dailyPowerUsage',
+        'min-width': '120px'
+      },
+      {
+        label: '油耗(升)',
+        prop: 'dailyFuel',
+        'min-width': '120px'
+      }
+    ]
+  },
+  {
+    label: t('project.currentOperation'),
+    prop: 'currentOperation',
+    'min-width': '120px'
+  },
+  {
+    label: t('project.nextPlan'),
+    prop: 'nextPlan',
+    'min-width': '120px'
+  },
+  {
+    label: t('project.transitTime'),
+    prop: 'transitTime',
+    'min-width': '120px',
+    formatter: (row: List) => (row.transitTime ? (row.transitTime * 100).toFixed(2) + '%' : '-')
+  },
+  {
+    label: '额定生产时间(H)',
+    prop: 'ratedProductionTime',
+    'min-width': '120px'
+  },
+  {
+    label: '生产时间(H)',
+    prop: 'productionTime',
+    'min-width': '120px'
+  },
+  {
+    label: '非生产时间(H)',
+    prop: 'nonProductionTime',
+    'min-width': '120px'
+  },
+  {
+    label: '非生产时间原因',
+    prop: 'ryNptReason',
+    'min-width': '120px',
+    isTag: true,
+    dictType: DICT_TYPE.PMS_PROJECT_RY_NPT_REASON
+  },
+  {
+    label: '生产动态',
+    prop: 'productionStatus',
+    'min-width': '120px'
+  },
+  {
+    label: '项目',
+    prop: 'contractName',
+    'min-width': '120px'
+  },
+  {
+    label: '全员数量',
+    prop: 'totalStaffNum',
+    'min-width': '120px'
+  },
+  {
+    label: '在岗人数',
+    prop: 'onDutyStaffNum',
+    'min-width': '120px',
+    formatter: (row: List) => (Number(row.totalStaffNum) || 0) - (Number(row.offDutyStaffNum) || 0)
+  },
+  {
+    label: '休假人员数量',
+    prop: 'leaveStaffNum',
+    'min-width': '120px'
+  }
+])
+
+const getTextWidth = (text: string, fontSize = 12) => {
+  const span = document.createElement('span')
+  span.style.visibility = 'hidden'
+  span.style.position = 'absolute'
+  span.style.whiteSpace = 'nowrap'
+  span.style.fontSize = `${fontSize}px`
+  span.style.fontFamily = 'PingFang SC'
+  span.innerText = text
+
+  document.body.appendChild(span)
+  const width = span.offsetWidth
+  document.body.removeChild(span)
+
+  return width
+}
+
+const calculateColumnWidths = (colums: Column[]) => {
+  for (const col of colums) {
+    let { formatter, prop, label, 'min-width': minWidth, isTag, children } = col
+
+    if (children && children.length > 0) {
+      calculateColumnWidths(children)
+      continue
+    }
+
+    minWidth =
+      Math.min(
+        ...[
+          Math.max(
+            ...[
+              getTextWidth(label),
+              ...list.value.map((v) => {
+                let tagLabel = ''
+                if (col.dictType) {
+                  const option = getDictOptions(col.dictType).find(
+                    (item) => item.value === v[prop!]
+                  )
+                  if (option) tagLabel = option.label
+                }
+                return getTextWidth(
+                  formatter ? formatter(v) : Boolean(tagLabel) ? tagLabel : v[prop!]
+                )
+              })
+            ]
+          ) + (isTag ? 40 : 20),
+          ...(isTag ? [] : [200])
+        ]
+      ) + 'px'
+
+    col['min-width'] = minWidth
+  }
+}
+
+function checkTimeSumEquals24(row: List) {
+  // 获取三个字段的值,转换为数字,如果为空则视为0
+  const gasTime = row.drillingWorkingTime || 0
+  const waterTime = row.otherProductionTime || 0
+  const nonProdTime =
+    row.accidentTime ||
+    0 + row.repairTime ||
+    0 + row.selfStopTime ||
+    0 + row.complexityTime ||
+    0 + row.relocationTime ||
+    0 + row.rectificationTime ||
+    0 + row.waitingStopTime ||
+    0 + row.winterBreakTime ||
+    0
+
+  // 计算总和
+  const sum = gasTime + waterTime + nonProdTime
+
+  // 返回是否等于24(允许一定的浮点数误差)
+  return Math.abs(sum - 24) < 0.01 // 使用0.01作为误差范围
+}
+
+function cellStyle(data: {
+  row: List
+  column: TableColumnCtx<List>
+  rowIndex: number
+  columnIndex: number
+}) {
+  const { row, column } = data
+
+  if (column.property === 'dailyFuel') {
+    const originalValue = row.dailyFuel ?? 0
+
+    if (originalValue > 9000)
+      return {
+        color: 'red',
+        fontWeight: 'bold'
+      }
+  }
+
+  const timeFields = [
+    'drillingWorkingTime',
+    'otherProductionTime',
+    'accidentTime',
+    'repairTime',
+    'selfStopTime',
+    'complexityTime',
+    'relocationTime',
+    'rectificationTime',
+    'waitingStopTime',
+    'winterBreakTime'
+  ]
+  if (timeFields.includes(column.property)) {
+    if (!checkTimeSumEquals24(row)) {
+      return {
+        color: 'orange',
+        fontWeight: 'bold'
+      }
+    }
+  }
+
+  // 默认返回空对象,不应用特殊样式
+  return {}
+}
+
+function rowClassName(data: { row: List; rowIndex: number }) {
+  const { row } = data
+  if (!row.lastGroupIdFlag) {
+    return 'hide-expand-icon'
+  }
+  return ''
+}
+
+const id = useUserStore().getUser.deptId
+
+const deptId = id
+
+interface Query {
+  pageNo: number
+  pageSize: number
+  deptId: number
+  contractName?: string
+  taskName?: string
+  wellName?: string
+  createTime: string[]
+  projectClassification: number
+}
+
+const query = ref<Query>({
+  pageNo: 1,
+  pageSize: 10,
+  deptId: id,
+  createTime: [
+    ...rangeShortcuts[2].value().map((item) => dayjs(item).format('YYYY-MM-DD HH:mm:ss'))
+  ],
+  projectClassification: 2
+})
+
+function handleSizeChange(val: number) {
+  query.value.pageSize = val
+  handleQuery()
+}
+
+function handleCurrentChange(val: number) {
+  query.value.pageNo = val
+  loadList()
+}
+
+const loading = ref(false)
+
+const list = ref<List[]>([])
+const total = ref(0)
+
+const loadList = useDebounceFn(async function () {
+  loading.value = true
+  try {
+    if (tab.value === '井') {
+      const { deptId, taskName, wellName, ...other } = query.value
+      const data = await IotRyDailyReportApi.getIotRyDailyReportTeamPage({
+        ...other,
+        taskName: wellName
+      })
+      list.value = data.list
+      total.value = data.total
+    } else {
+      const data = await IotRyDailyReportApi.getIotRyDailyReportWellPage(query.value)
+      list.value = data.list
+      total.value = data.total
+    }
+
+    nextTick(() => {
+      calculateColumnWidths(columns.value)
+    })
+  } finally {
+    loading.value = false
+  }
+}, 500)
+
+function handleQuery(setPage = true) {
+  if (setPage) {
+    query.value.pageNo = 1
+  }
+  loadList()
+}
+
+function resetQuery() {
+  query.value = {
+    pageNo: 1,
+    pageSize: 10,
+    deptId: 157,
+    contractName: '',
+    taskName: '',
+    createTime: [
+      ...rangeShortcuts[2].value().map((item) => dayjs(item).format('YYYY-MM-DD HH:mm:ss'))
+    ],
+    projectClassification: 2
+  }
+  handleQuery()
+}
+
+watch(
+  [
+    () => query.value.createTime,
+    () => query.value.deptId,
+    () => query.value.taskName,
+    () => query.value.contractName,
+    () => query.value.wellName
+  ],
+  () => {
+    handleQuery()
+  },
+  { immediate: true }
+)
+
+const expandRowKeys = computed(() => {
+  return list.value.filter((item) => item.lastGroupIdFlag).map((item) => item.id.toString())
+})
+
+const exportLoading = ref(false)
+
+const handleExport = () => {
+  exportLoading.value = true
+  if (tab.value === '井') {
+    IotRyDailyReportApi.exportIotRyDailyReportWell(query.value).then((data) => {
+      download.excel(data, '瑞鹰修井井日报统计.xls')
+      exportLoading.value = false
+    })
+  } else {
+    IotRyDailyReportApi.exportIotRyDailyReportTeam(query.value).then((data) => {
+      download.excel(data, '瑞鹰修井队伍日报统计.xls')
+      exportLoading.value = false
+    })
+  }
+}
+</script>
+
+<template>
+  <div
+    class="grid grid-cols-[15%_1fr] grid-rows-[62px_1fr] gap-4 h-[calc(100vh-20px-var(--top-tool-height)-var(--tags-view-height)-var(--app-footer-height))]"
+  >
+    <el-form
+      size="default"
+      class="col-span-2 bg-white dark:bg-[#1d1e1f] rounded-lg shadow px-8 gap-8 flex items-center"
+    >
+      <div class="flex items-center gap-8">
+        <el-segmented class="h-12" v-model="tab" :options="tabOptions" @change="handleQuery()" />
+      </div>
+      <div class="flex items-center gap-8">
+        <el-form-item label="项目">
+          <el-input
+            v-model="query.contractName"
+            placeholder="请输入项目"
+            clearable
+            @keyup.enter="handleQuery()"
+            class="!w-240px"
+          />
+        </el-form-item>
+        <el-form-item v-show="tab !== '井'" label="任务">
+          <el-input
+            v-model="query.taskName"
+            placeholder="请输入任务"
+            clearable
+            @keyup.enter="handleQuery()"
+            class="!w-240px"
+          />
+        </el-form-item>
+        <el-form-item label="创建时间">
+          <el-date-picker
+            v-model="query.createTime"
+            value-format="YYYY-MM-DD HH:mm:ss"
+            type="daterange"
+            start-placeholder="开始日期"
+            end-placeholder="结束日期"
+            :shortcuts="rangeShortcuts"
+            :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
+            class="!w-220px"
+          />
+        </el-form-item>
+      </div>
+      <el-form-item class="ml-auto">
+        <el-button type="primary" @click="handleQuery()">
+          <Icon icon="ep:search" class="mr-5px" /> 搜索
+        </el-button>
+        <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" />重置</el-button>
+        <el-button plain type="success" @click="handleExport" :loading="exportLoading">
+          <Icon icon="ep:download" class="mr-5px" /> 导出
+        </el-button>
+      </el-form-item>
+    </el-form>
+
+    <!-- 第二行左侧:自动落入第 2 行第 1 列 -->
+    <div class="p-4 bg-white dark:bg-[#1d1e1f] shadow rounded-lg h-full">
+      <DeptTreeSelect
+        v-show="tab === '队伍'"
+        :top-id="158"
+        :deptId="deptId"
+        v-model="query.deptId"
+        title="队伍"
+      />
+      <WellSelect
+        v-show="tab === '井'"
+        :deptId="158"
+        v-model="query.wellName"
+        v-model:contract-name="query.contractName"
+      />
+    </div>
+
+    <!-- 第二行右侧:自动落入第 2 行第 2 列 -->
+    <div class="bg-white dark:bg-[#1d1e1f] shadow rounded-lg p-4 flex flex-col">
+      <div class="flex-1 relative">
+        <el-auto-resizer class="absolute">
+          <template #default="{ width, height }">
+            <el-table
+              :data="list"
+              v-loading="loading"
+              stripe
+              class="absolute"
+              :max-height="height"
+              :height="height"
+              show-overflow-tooltip
+              :width="width"
+              :cell-style="cellStyle"
+              :row-class-name="rowClassName"
+              :tree-props="{ hasChildren: 'lastGroupIdFlag' }"
+              row-key="id"
+              scrollbar-always-on
+              :expand-row-keys="expandRowKeys"
+              border
+            >
+              <el-table-column type="expand" fixed="left">
+                <template #default="{ row }">
+                  <div
+                    class="flex items-center gap-8 h-10 px-14 sticky left-0 w-fit box-border bg-[var(--el-bg-color)]"
+                  >
+                    <el-tag>累计进尺: {{ row.groupIdFootage }} </el-tag>
+                    <el-tag>累计进尺工作时间: {{ row.groupIdFootageTime }} </el-tag>
+                    <el-tag>累计其它生产时间H(钻井) : {{ row.groupIdOtherProductTime }} </el-tag>
+                    <el-tag>累计非生产时间H(钻井) : {{ row.groupIdZjNoProductTime }} </el-tag>
+                    <el-tag>累计生产时间(修井) : {{ row.groupIdProductTime }} </el-tag>
+                    <el-tag>累计非生产时间H(修井) : {{ row.groupIdXjNoProductTime }} </el-tag>
+                    <el-tag>累计用电量(MWh) : {{ row.groupIdPower }} </el-tag>
+                    <el-tag>累计油耗(升) : {{ row.groupIdFuel }} </el-tag>
+                    <el-tag>平均运行时效 : {{ row.groupIdTransitTime }} </el-tag>
+                    <el-tag>累计施工井数 : {{ row.groupConstructionWells }} </el-tag>
+                    <el-tag>累计完工井数 : {{ row.groupCompletedWells }} </el-tag>
+                  </div>
+                </template>
+              </el-table-column>
+              <DailyTableColumn :columns="columns" />
+              <!-- <el-table-column label="操作" width="120px" align="center" fixed="right">
+                <template #default="{ row }">
+                  <el-button link type="success" v-hasPermi="['pms:iot-rh-daily-report:query']">
+                    查看
+                  </el-button>
+                  <el-button
+                    v-show="row.status === 0"
+                    link
+                    type="primary"
+                    v-hasPermi="['pms:iot-rh-daily-report:create']"
+                  >
+                    编辑
+                  </el-button>
+                </template>
+              </el-table-column> -->
+            </el-table>
+          </template>
+        </el-auto-resizer>
+      </div>
+      <div class="h-10 mt-4 flex items-center justify-end">
+        <el-pagination
+          size="default"
+          v-show="total > 0"
+          v-model:current-page="query.pageNo"
+          v-model:page-size="query.pageSize"
+          :background="true"
+          :page-sizes="[10, 20, 30, 50, 100]"
+          :total="total"
+          layout="total, sizes, prev, pager, next, jumper"
+          @size-change="handleSizeChange"
+          @current-change="handleCurrentChange"
+        />
+      </div>
+    </div>
+  </div>
+</template>
+
+<style scoped>
+:deep(.el-form-item) {
+  margin-bottom: 0;
+}
+
+.el-segmented {
+  --el-border-radius-base: 8px;
+  --el-segmented-padding: 8px;
+}
+
+:deep(.hide-expand-icon) {
+  .el-table__expand-icon {
+    display: none;
+  }
+}
+</style>

+ 31 - 2
src/views/report-statistics/work-order-completion.vue

@@ -5,6 +5,7 @@ import { useDebounceFn } from '@vueuse/core'
 import MiniBarChart from '@/components/WorkOrderCompletionBar/index.vue'
 import CountTo from '@/components/count-to1.vue'
 import { IotReportApi } from '@/api/pms/report'
+import { rangeShortcuts } from '@/utils/formatTime'
 
 // 定义时间类型
 type TimeType = 'year' | 'month' | 'day'
@@ -40,7 +41,7 @@ const timeOptions: { label: string; value: TimeType }[] = [
   { label: '日', value: 'day' }
 ]
 
-const activeTimeType = ref<TimeType>('year')
+const activeTimeType = ref<TimeType | undefined>('year')
 const query = ref<Query>({
   pageNo: 1,
   pageSize: 10
@@ -247,6 +248,14 @@ function handleReset() {
   handleTimeChange('year')
   selectType(undefined)
 }
+
+const handleClear = () => {
+  handleTimeChange('year')
+}
+
+const handleChange = () => {
+  activeTimeType.value = undefined
+}
 </script>
 
 <template>
@@ -346,6 +355,20 @@ function handleReset() {
       </div>
       <div class="flex justify-between">
         <div class="flex gap-4">
+          <el-date-picker
+            size="default"
+            v-model="query.createTime"
+            value-format="YYYY-MM-DD HH:mm:ss"
+            type="daterange"
+            start-placeholder="开始日期"
+            end-placeholder="结束日期"
+            :shortcuts="rangeShortcuts"
+            class="!w-220px"
+            @clear="handleClear"
+            @change="handleChange"
+            :clearable="false"
+            :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
+          />
           <el-button-group size="default">
             <el-button
               v-for="item in timeOptions"
@@ -356,9 +379,15 @@ function handleReset() {
               {{ item.label }}
             </el-button>
           </el-button-group>
+        </div>
+        <div class="flex gap-2">
           <el-button size="default" @click="handleReset">重置</el-button>
+          <!-- @click="handleExport"
+            :loading="exportLoading" -->
+          <el-button size="default" plain type="success">
+            <Icon icon="ep:download" class="mr-5px" /> 导出
+          </el-button>
         </div>
-        <el-button size="default" type="primary">导出</el-button>
       </div>
     </div>
     <div class="bg-white dark:bg-[#1d1e1f] shadow rounded-lg p-4 flex flex-col">

Některé soubory nejsou zobrazeny, neboť je v těchto rozdílových datech změněno mnoho souborů