Browse Source

✨ feat(设备监控): 添加中航北斗状态,部分完善

Zimo 1 week ago
parent
commit
29826e2df2

+ 1 - 1
.env.local

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

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

@@ -46,7 +46,7 @@ export interface IotDeviceVO {
 
 let globalController = new AbortController()
 
-export const cancelAllRequests = () => {
+export const cancelAllRequests = async () => {
   globalController.abort()
   globalController = new AbortController()
 }

+ 54 - 50
src/router/modules/remaining.ts

@@ -74,8 +74,8 @@ const remainingRouter: AppRouteRecordRaw[] = [
     path: '/dingding',
     name: 'dingtalk',
     component: () => import('@/views/pms/dingding.vue'),
-    meta:{
-      hidden: true,
+    meta: {
+      hidden: true
     }
   },
   {
@@ -137,7 +137,7 @@ const remainingRouter: AppRouteRecordRaw[] = [
           canTo: true,
           activeMenu: '/template/info'
         }
-      },
+      }
     ]
   },
 
@@ -340,8 +340,7 @@ const remainingRouter: AppRouteRecordRaw[] = [
     children: [
       {
         path: 'template/detail/:deptId/:isFill/:createTime*',
-        component: () => import('@/views/pms/iotopeationfill/StatisticsForm.vue' +
-        ''),
+        component: () => import('@/views/pms/iotopeationfill/StatisticsForm.vue' + ''),
         name: 'FillOrderInfo2',
         meta: {
           title: t('rem.FillInInformation'),
@@ -350,7 +349,7 @@ const remainingRouter: AppRouteRecordRaw[] = [
           canTo: true,
           activeMenu: '/template/info'
         }
-      },
+      }
     ]
   },
   {
@@ -364,7 +363,7 @@ const remainingRouter: AppRouteRecordRaw[] = [
     children: [
       {
         path: 'template/detail/:id',
-        component: () => import('@/views/pms/iotopeationfill/index1.vue'+''),
+        component: () => import('@/views/pms/iotopeationfill/index1.vue' + ''),
         name: 'FillOrderInfo',
         meta: {
           title: t('rem.FillInInformation'),
@@ -374,7 +373,7 @@ const remainingRouter: AppRouteRecordRaw[] = [
           keepAlive: true,
           activeMenu: '/template/info'
         }
-      },
+      }
     ]
   },
   {
@@ -397,7 +396,7 @@ const remainingRouter: AppRouteRecordRaw[] = [
           keepAlive: true,
           activeMenu: '/template/info'
         }
-      },
+      }
     ]
   },
   {
@@ -446,7 +445,7 @@ const remainingRouter: AppRouteRecordRaw[] = [
           title: t('rem.OperationPlanEdit'),
           activeMenu: '/iotopeationfill/plan/add'
         }
-      },
+      }
     ]
   },
   {
@@ -502,7 +501,7 @@ const remainingRouter: AppRouteRecordRaw[] = [
           hidden: true,
           canTo: true,
           icon: 'ep:add',
-          title:  t('rem.AddEquipment'),
+          title: t('rem.AddEquipment'),
           activeMenu: '/device/base'
         }
       },
@@ -518,7 +517,8 @@ const remainingRouter: AppRouteRecordRaw[] = [
           title: t('rem.EquipmentEditing'),
           activeMenu: '/device/base'
         }
-      },{
+      },
+      {
         path: 'device/detail/:id',
         component: () => import('@/views/pms/device/DeviceInfo.vue'),
         name: 'DeviceDetailInfo',
@@ -530,8 +530,9 @@ const remainingRouter: AppRouteRecordRaw[] = [
           title: t('rem.EquipmentDetails'),
           activeMenu: '/device/info'
         }
-      },{
-        path: 'tddevice/detail/:id/:ifInline/:time/:name/:code/:dept/:vehicle?',
+      },
+      {
+        path: 'tddevice/detail/:id/:ifInline/:carOnline/:time/:name/:code/:dept/:vehicle?',
         component: () => import('@/views/pms/device/monitor/TdDeviceInfo.vue'),
         name: 'TdDeviceDetail',
         meta: {
@@ -542,7 +543,8 @@ const remainingRouter: AppRouteRecordRaw[] = [
           title: t('rem.MonitoringDetails'),
           activeMenu: '/device/info'
         }
-      },{
+      },
+      {
         path: 'device/upload/:id',
         component: () => import('@/views/pms/device/DeviceUpload.vue'),
         name: 'DeviceUpload',
@@ -554,7 +556,8 @@ const remainingRouter: AppRouteRecordRaw[] = [
           title: t('rem.UploadFile'),
           activeMenu: '/device/upload'
         }
-      },{
+      },
+      {
         path: 'device/bom/:id',
         component: () => import('@/views/pms/device/bom/BomInfo.vue'),
         name: 'DeviceBom',
@@ -709,7 +712,7 @@ const remainingRouter: AppRouteRecordRaw[] = [
           title: t('rem.ConfigureSafetyStock'),
           activeMenu: '/sapstock/safe'
         }
-      },
+      }
     ]
   },
 
@@ -786,7 +789,7 @@ const remainingRouter: AppRouteRecordRaw[] = [
     name: 'PmsMainWorkOrderCenter',
     meta: {
       hidden: true,
-      keepAlive: true,
+      keepAlive: true
     },
     children: [
       {
@@ -945,7 +948,7 @@ const remainingRouter: AppRouteRecordRaw[] = [
           title: t('rem.CollectionFormDetails'),
           activeMenu: '/materialreq/detail'
         }
-      },
+      }
     ]
   },
 
@@ -1070,7 +1073,8 @@ const remainingRouter: AppRouteRecordRaw[] = [
           title: t('rem.RepairOrderEdit'),
           activeMenu: '/maintain/edit'
         }
-      },{
+      },
+      {
         path: 'maintain/detail/:id(\\d+)',
         component: () => import('@/views/pms/maintain/IotMaintainDetail.vue'),
         name: 'MaintainDetail',
@@ -1082,7 +1086,7 @@ const remainingRouter: AppRouteRecordRaw[] = [
           title: t('rem.RepairOrderDetail'),
           activeMenu: '/maintain/detail'
         }
-      },
+      }
     ]
   },
   {
@@ -1118,20 +1122,20 @@ const remainingRouter: AppRouteRecordRaw[] = [
           title: t('rem.InspectionRouteEdit'),
           activeMenu: '/route/edit'
         }
-      // }
+        // }
         // ,{
-      //   path: 'route/detail/:id(\\d+)',
-      //   component: () => import('@/views/pms/maintain/IotMaintainDetail.vue'),
-      //   name: 'InspectRouteDetail',
-      //   meta: {
-      //     noCache: false,
-      //     hidden: true,
-      //     canTo: true,
-      //     icon: 'ep:add',
-      //     title: '巡检路线详情',
-      //     activeMenu: '/route/detail'
-      //   }
-      },
+        //   path: 'route/detail/:id(\\d+)',
+        //   component: () => import('@/views/pms/maintain/IotMaintainDetail.vue'),
+        //   name: 'InspectRouteDetail',
+        //   meta: {
+        //     noCache: false,
+        //     hidden: true,
+        //     canTo: true,
+        //     icon: 'ep:add',
+        //     title: '巡检路线详情',
+        //     activeMenu: '/route/detail'
+        //   }
+      }
     ]
   },
 
@@ -1181,7 +1185,7 @@ const remainingRouter: AppRouteRecordRaw[] = [
         //     title: '巡检路线详情',
         //     activeMenu: '/route/detail'
         //   }
-      },
+      }
     ]
   },
   {
@@ -1256,20 +1260,21 @@ const remainingRouter: AppRouteRecordRaw[] = [
           title: t('rem.InspectPlanEdit'),
           activeMenu: '/inspect/order/edit'
         }
+      },
+      {
+        path: '/inspect/order/detail/:id(\\d+)',
+        component: () => import('@/views/pms/inspect/order/InspectOrderDetail.vue'),
+        name: 'InspectOrderDetail',
+        meta: {
+          noCache: false,
+          hidden: true,
+          canTo: true,
+          icon: 'ep:add',
+          title: t('rem.InspectOrderDetail'),
+          activeMenu: '/inspect/order/detail'
         }
-        ,{
-          path: '/inspect/order/detail/:id(\\d+)',
-          component: () => import('@/views/pms/inspect/order/InspectOrderDetail.vue'),
-          name: 'InspectOrderDetail',
-          meta: {
-            noCache: false,
-            hidden: true,
-            canTo: true,
-            icon: 'ep:add',
-            title: t('rem.InspectOrderDetail'),
-            activeMenu: '/inspect/order/detail'
-          }
-      },{
+      },
+      {
         path: '/inspect/order/write/:id(\\d+)',
         component: () => import('@/views/pms/inspect/order/WriteOrder.vue'),
         name: 'InspectOrderWrite',
@@ -2043,8 +2048,7 @@ const remainingRouter: AppRouteRecordRaw[] = [
         component: () => import('@/views/iot/plugin/detail/index.vue')
       }
     ]
-  },
-
+  }
 ]
 
 export default remainingRouter

+ 108 - 27
src/views/pms/device/monitor/TdDeviceInfo.vue

@@ -1,6 +1,6 @@
 <script setup lang="ts">
 import * as echarts from 'echarts'
-import { Odometer, CircleCheckFilled } from '@element-plus/icons-vue'
+import { Odometer, CircleCheckFilled, CircleCloseFilled } from '@element-plus/icons-vue'
 import { rangeShortcuts } from '@/utils/formatTime'
 
 import dayjs from 'dayjs'
@@ -20,15 +20,20 @@ const data = ref({
   lastInlineTime: params.time || '',
   ifInline: params.ifInline || '',
   dept: params.dept || '',
-  vehicle: params.vehicle || ''
+  vehicle: params.vehicle || '',
+  carOnline: params.carOnline || ''
 })
 
+const disabledIdentifier = ref<string[]>(['online', 'vehicle_name', 'touchScreenDataAccumulate'])
+
 const specs = ref<any[]>([])
 const gatewayspecs = ref<any[]>([])
 const zhbdspecs = ref<any[]>([])
 const selectSpec = ref<Record<string, boolean>>({})
 const chartMap = ref<Record<string, { name: string; value: any[] }>>({})
 
+const specsLoading = ref(false)
+
 const lastTsMap = ref<Record<string, number>>({})
 
 // 每 10s 刷新定时器
@@ -41,25 +46,27 @@ const date = ref([
 ])
 
 const reset = () => {
-  cancelAllRequests()
-  const def = rangeShortcuts[0].value()
+  cancelAllRequests().then(() => {
+    const def = rangeShortcuts[0].value()
 
-  date.value = [
-    dayjs(def[0]).format('YYYY-MM-DD HH:mm:ss'),
-    dayjs(def[1]).format('YYYY-MM-DD HH:mm:ss')
-  ]
-  stopAutoFetch()
-  if (chart) chart.clear()
-  render()
-  initLoad()
+    date.value = [
+      dayjs(def[0]).format('YYYY-MM-DD HH:mm:ss'),
+      dayjs(def[1]).format('YYYY-MM-DD HH:mm:ss')
+    ]
+    stopAutoFetch()
+    if (chart) chart.clear()
+    render()
+    initLoad()
+  })
 }
 
 const handleDateChange = () => {
-  cancelAllRequests()
-  stopAutoFetch()
-  if (chart) chart.clear()
-  render()
-  initLoad(false)
+  cancelAllRequests().then(() => {
+    stopAutoFetch()
+    if (chart) chart.clear()
+    render()
+    initLoad(false)
+  })
 }
 
 const handleClickSpec = (modelName: string) => {
@@ -74,6 +81,18 @@ const handleClickSpec = (modelName: string) => {
 const chartRef = ref<HTMLDivElement | null>(null)
 let chart: echarts.ECharts | null = null
 
+const chartInit = () => {
+  if (!chart) return
+
+  chart.on('legendselectchanged', (params: any) => {
+    selectSpec.value = params.selected
+  })
+
+  window.addEventListener('resize', () => {
+    if (chart) chart.resize()
+  })
+}
+
 // 映射区间相关
 let intervalArr: number[] = []
 let maxInterval = 0
@@ -82,8 +101,10 @@ let minInterval = 0
 // 1. 加载 specs
 const loadSpecs = async () => {
   if (!params.id) return
+  specsLoading.value = true
   const res = await IotDeviceApi.getIotDeviceTds(Number(params.id))
   const zhbdres = await IotDeviceApi.getIotDeviceZHBDTds(Number(params.id))
+
   zhbdspecs.value = zhbdres.sort((a, b) => b.modelOrder - a.modelOrder)
   gatewayspecs.value = res.sort((a, b) => b.modelOrder - a.modelOrder)
 
@@ -94,14 +115,18 @@ const loadSpecs = async () => {
 
   selectSpec.value = specs.value.reduce(
     (acc, spec) => {
-      acc[spec.modelName] = gatewayspecs.value.some((item) => item.modelName === spec.modelName)
+      acc[spec.modelName] =
+        gatewayspecs.value.some((item) => item.modelName === spec.modelName) &&
+        !disabledIdentifier.value.includes(spec.identifier)
       return acc
     },
     {} as Record<string, boolean>
   )
 
+  specsLoading.value = false
+
   chartMap.value = specs.value
-    .filter((spec) => !['online', 'vehicle_name'].includes(spec.identifier))
+    .filter((spec) => !disabledIdentifier.value.includes(spec.identifier))
     .reduce(
       (acc, spec) => {
         acc[spec.identifier] = { name: spec.modelName, value: [] }
@@ -111,6 +136,8 @@ const loadSpecs = async () => {
     )
 }
 
+const chartLoading = ref(false)
+
 const initLoad = async (real_time: boolean = true) => {
   if (!specs.value.length) return
 
@@ -119,6 +146,8 @@ const initLoad = async (real_time: boolean = true) => {
     lastTsMap.value[identifier] = 0
   })
 
+  chartLoading.value = true
+
   for (const identifier of Object.keys(chartMap.value)) {
     const res = await IotStatApi.getDeviceInfoChart(
       data.value.deviceCode,
@@ -132,6 +161,8 @@ const initLoad = async (real_time: boolean = true) => {
     lastTsMap.value[identifier] = sorted.at(-1)?.ts ?? 0
 
     updateSingleSeries(identifier)
+
+    chartLoading.value = false
   }
 
   if (real_time) startAutoFetch()
@@ -176,6 +207,8 @@ const render = () => {
 
   if (!chart) chart = echarts.init(chartRef.value)
 
+  chartInit()
+
   const values = Object.values(chartMap.value).flatMap((item) => item.value.map((d) => d.value))
 
   const maxVal = Math.max(...values)
@@ -297,6 +330,10 @@ onMounted(async () => {
 
 onUnmounted(() => {
   stopAutoFetch()
+
+  window.removeEventListener('resize', () => {
+    if (chart) chart.resize()
+  })
 })
 </script>
 
@@ -312,19 +349,55 @@ onUnmounted(() => {
       <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>
-      <el-form-item label="是否在线" class="online" type="plain">
-        <el-tag type="success" size="default" class="flex items-center">
+      <el-form-item label="网关状态" class="online" type="plain">
+        <el-tag
+          v-if="data.ifInline === '3'"
+          type="success"
+          size="default"
+          class="flex items-center"
+        >
+          <el-icon class="text-emerald!"><CircleCheckFilled /></el-icon>
+          在线
+        </el-tag>
+
+        <el-tag v-if="data.ifInline === '4'" type="danger" size="default" class="flex items-center">
+          <el-icon class="text-rose"><CircleCloseFilled /></el-icon>
+          离线
+        </el-tag>
+      </el-form-item>
+      <el-form-item label="中航北斗2" class="online" type="plain">
+        <el-tag
+          v-if="data.carOnline === 'true'"
+          type="success"
+          size="default"
+          class="flex items-center"
+        >
           <el-icon class="text-emerald!"><CircleCheckFilled /></el-icon>
           在线
         </el-tag>
+
+        <el-tag
+          v-if="data.carOnline === 'false'"
+          type="danger"
+          size="default"
+          class="flex items-center"
+        >
+          <el-icon class="text-rose"><CircleCloseFilled /></el-icon>
+          离线
+        </el-tag>
       </el-form-item>
       <el-form-item label="最后数据时间"> {{ data.lastInlineTime }} </el-form-item>
       <el-form-item v-if="data.vehicle" label="车牌号码"> {{ data.vehicle }} </el-form-item>
     </el-form>
   </div>
-  <div class="mt-4 w-full rounded-lg bg-gradient-to-r from-blue-100 to-white p-4 shadow">
+  <div class="mt-4 w-full rounded-lg bg-gradient-to-r from-blue-100 to-white p-4 shadow min-h-50">
     <header class="font-medium text-center w-full">网关数采</header>
-    <div class="w-full mt-4 grid grid-cols-4 gap-4" id="dimension">
+    <div
+      v-loading="specsLoading"
+      element-loading-background="transparent"
+      class="w-full mt-4 grid grid-cols-4 gap-4 min-h-30"
+      id="dimension"
+    >
       <div
         v-for="item in gatewayspecs"
         :key="item.productId"
@@ -332,14 +405,17 @@ onUnmounted(() => {
         :class="{
           'bg-blue-200': selectSpec[item.modelName]
         }"
-        @click="handleClickSpec(item.modelName)"
+        @click="!disabledIdentifier.includes(item.identifier) && handleClickSpec(item.modelName)"
       >
         <span class="text-sm text-[var(--el-text-color-regular)]">{{ item.modelName }}</span>
         <span class="text-lg font-medium ms-a">{{ item.value }}</span>
       </div>
     </div>
   </div>
-  <div class="mt-4 w-full rounded-lg bg-gradient-to-r from-blue-100 to-white p-4 shadow">
+  <div
+    v-if="zhbdspecs.length"
+    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
@@ -349,7 +425,7 @@ onUnmounted(() => {
         :class="{
           'bg-blue-200': selectSpec[item.modelName]
         }"
-        @click="handleClickSpec(item.modelName)"
+        @click="!disabledIdentifier.includes(item.identifier) && handleClickSpec(item.modelName)"
       >
         <span class="text-sm text-[var(--el-text-color-regular)]">{{ item.modelName }}</span>
         <span class="text-lg font-medium ms-a">{{ item.value }}</span>
@@ -380,7 +456,12 @@ onUnmounted(() => {
         />
       </div>
     </header>
-    <div ref="chartRef" class="w-full h-158 mt-4 mb-4"></div>
+    <div
+      v-loading="specsLoading"
+      element-loading-background="transparent"
+      ref="chartRef"
+      class="w-full h-158 mt-4 mb-4"
+    ></div>
   </div>
 </template>
 

+ 19 - 10
src/views/pms/device/monitor/index.vue

@@ -121,12 +121,12 @@
                   </div>
                   <div class="p-4 relative">
                     <!-- 标题区域 -->
-                    <div class="flex items-center mb-3" style="height: 38px;">
+                    <div class="flex items-center mb-3" style="height: 38px">
                       <div class="mr-2.5 flex items-center">
                         <img src="@/assets/svgs/iot/card-fill.svg" class="w-[18px] h-[18px]" />
                       </div>
                       <div class="text-[16px] font-600 flex-1">{{
-                        item.deviceCode +'/'+ item.deviceName
+                        item.deviceCode + '/' + item.deviceName
                       }}</div>
                       <!-- 添加设备状态标签 -->
                       <div class="inline-flex items-center">
@@ -173,9 +173,12 @@
                         </div>
                       </div>
                       <div class="w-[100px] h-[100px]">
-                        <img v-if="!item.carId" src="@/assets/imgs/iot/device.png" class="w-full h-full" />
-                        <img v-if="item.carId" src="@/assets/imgs/iot/car.png" class="mt-4 ml-4"  />
-
+                        <img
+                          v-if="!item.carId"
+                          src="@/assets/imgs/iot/device.png"
+                          class="w-full h-full"
+                        />
+                        <img v-if="item.carId" src="@/assets/imgs/iot/car.png" class="mt-4 ml-4" />
                       </div>
                     </div>
 
@@ -196,7 +199,8 @@
                             item.deviceName,
                             item.deviceCode,
                             item.deptName,
-                            item.vehicleName
+                            item.vehicleName,
+                            item.carOnline
                           )
                         "
                       >
@@ -262,7 +266,8 @@
                       scope.row.deviceName,
                       scope.row.deviceCode,
                       scope.row.deptName,
-                      scope.row.vehicleName
+                      scope.row.vehicleName,
+                      scope.row.carOnline
                     )
                   "
                 >
@@ -380,13 +385,17 @@ const openDetail = (
   name: string,
   code: string,
   dept: string,
-  vehicle:string
+  vehicle: string,
+  carOnline: string
 ) => {
   if (time === null || time === undefined) {
     message.warning('没有数采数据')
     return
   }
-  push({ name: 'TdDeviceDetail', params: { id, ifInline, time, name, code, dept,vehicle } })
+  push({
+    name: 'TdDeviceDetail',
+    params: { id, ifInline, carOnline, time, name, code, dept, vehicle }
+  })
 }
 
 /** 导出方法 */
@@ -438,7 +447,7 @@ const handleDeptNodeClick = async (row) => {
 }
 </script>
 <style scoped>
-.custom-card{
+.custom-card {
   box-shadow: 0 2px 12px rgba(0, 0, 0, 0.06);
   transform: scale(1); /* 原始大小 */