Browse Source

Merge remote-tracking branch 'origin/feature-monitor'

Zimo 1 week ago
parent
commit
dc3904facb

+ 3 - 3
.vscode/settings.json

@@ -62,7 +62,7 @@
     "editor.defaultFormatter": "esbenp.prettier-vscode"
   },
   "[typescript]": {
-    "editor.defaultFormatter": "rvest.vs-code-prettier-eslint"
+    "editor.defaultFormatter": "esbenp.prettier-vscode"
   },
   "[typescriptreact]": {
     "editor.defaultFormatter": "rvest.vs-code-prettier-eslint"
@@ -83,8 +83,8 @@
     "editor.defaultFormatter": "esbenp.prettier-vscode"
   },
   "editor.codeActionsOnSave": {
-    "source.fixAll.eslint": "explicit",
-    "source.fixAll.stylelint": "explicit"
+    "source.fixAll.eslint": "explicit"
+    // "source.fixAll.stylelint": "explicit"
   },
   "[vue]": {
     "editor.defaultFormatter": "esbenp.prettier-vscode"

+ 29 - 12
src/api/pms/device/index.ts

@@ -7,8 +7,8 @@ export interface IotDeviceVO {
   deviceName: string // 设备名称
   brand: number // 品牌
   model: number // 规格型号
-  deptId: number    // 所在部门id
-  deptName: string  // 所在部门名称
+  deptId: number // 所在部门id
+  deptName: string // 所在部门名称
   deviceStatus: string // 设备状态
   deviceStatusName: string // 设备状态名称
   assetProperty: string // 资产性质
@@ -44,19 +44,26 @@ export interface IotDeviceVO {
   runningWorkOrder: boolean // 当前设备是否已经有待执行的保养工单
 }
 
+let globalController = new AbortController()
+
+export const cancelAllRequests = async () => {
+  globalController.abort()
+  globalController = new AbortController()
+}
+
 // 设备台账 API
 export const IotDeviceApi = {
-  getCompany: async (params:any) => {
-    return await request.get({ url: `/rq/iot-device/company?id=`+params})
+  getCompany: async (params: any) => {
+    return await request.get({ url: `/rq/iot-device/company?id=` + params })
   },
-  getMaxCode: async (params:any) => {
-    return await request.get({ url: `/rq/iot-device/max?yfCode=`+params})
+  getMaxCode: async (params: any) => {
+    return await request.get({ url: `/rq/iot-device/max?yfCode=` + params })
   },
-  getMapDevice: async (params:any) => {
-    return await request.get({ url: `/rq/iot-device/map`, params})
+  getMapDevice: async (params: any) => {
+    return await request.get({ url: `/rq/iot-device/map`, params })
   },
-  getAllDeviceParams: async (params:any) => {
-    return await request.get({ url: `/rq/iot-device/all/params`, params})
+  getAllDeviceParams: async (params: any) => {
+    return await request.get({ url: `/rq/iot-device/all/params`, params })
   },
   // 查询设备台账分页
   getIotDevicePage: async (params: any) => {
@@ -114,7 +121,17 @@ export const IotDeviceApi = {
     return await request.get({ url: `/rq/iot-device/get?id=` + id })
   },
   getIotDeviceTds: async (id: number) => {
-    return await request.get({ url: `/rq/iot-device/get/td?id=` + id })
+    return await request.get({
+      url: `/rq/iot-device/get/gateway/td?id=` + id,
+      signal: globalController.signal
+    })
+  },
+
+  getIotDeviceZHBDTds: async (id: number) => {
+    return await request.get({
+      url: `/rq/iot-device/get/zhbd/td?id=` + id,
+      signal: globalController.signal
+    })
   },
   // 新增设备台账
   createIotDevice: async (data: IotDeviceVO) => {
@@ -147,5 +164,5 @@ export const IotDeviceApi = {
   },
   getIotDeviceTdPage: async (params: any) => {
     return await request.get({ url: `/rq/iot-device/td/page`, params })
-  },
+  }
 }

+ 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

+ 94 - 0
src/utils/formatTime.ts

@@ -4,6 +4,100 @@ import type { TableColumnCtx } from 'element-plus'
 /**
  * 日期快捷选项适用于 el-date-picker
  */
+
+export  const rangeShortcuts = [
+  {
+    text: '今天',
+    value: () => {
+      const today = dayjs()
+      return [today.startOf('day').toDate(), today.endOf('day').toDate()]
+    }
+  },
+  {
+    text: '昨天',
+    value: () => {
+      const yesterday = dayjs().subtract(1, 'day')
+      return [yesterday.startOf('day').toDate(), yesterday.endOf('day').toDate()]
+    }
+  },
+  {
+    text: '本周',
+    value: () => {
+      return [dayjs().startOf('week').toDate(), dayjs().endOf('week').toDate()]
+    }
+  },
+  {
+    text: '上周',
+    value: () => {
+      const lastWeek = dayjs().subtract(1, 'week')
+      return [lastWeek.startOf('week').toDate(), lastWeek.endOf('week').toDate()]
+    }
+  },
+  {
+    text: '本月',
+    value: () => {
+      return [dayjs().startOf('month').toDate(), dayjs().endOf('month').toDate()]
+    }
+  },
+  {
+    text: '上月',
+    value: () => {
+      const lastMonth = dayjs().subtract(1, 'month')
+      return [lastMonth.startOf('month').toDate(), lastMonth.endOf('month').toDate()]
+    }
+  },
+  {
+    text: '本季度',
+    value: () => {
+      return [dayjs().startOf('quarter').toDate(), dayjs().endOf('quarter').toDate()]
+    }
+  },
+  {
+    text: '上季度',
+    value: () => {
+      const lastQuarter = dayjs().subtract(1, 'quarter')
+      return [lastQuarter.startOf('quarter').toDate(), lastQuarter.endOf('quarter').toDate()]
+    }
+  },
+  {
+    text: '今年',
+    value: () => {
+      return [dayjs().startOf('year').toDate(), dayjs().endOf('year').toDate()]
+    }
+  },
+  {
+    text: '去年',
+    value: () => {
+      const lastYear = dayjs().subtract(1, 'year')
+      return [lastYear.startOf('year').toDate(), lastYear.endOf('year').toDate()]
+    }
+  },
+  {
+    text: '最近7天',
+    value: () => {
+      return [dayjs().subtract(6, 'day').toDate(), dayjs().toDate()]
+    }
+  },
+  {
+    text: '最近30天',
+    value: () => {
+      return [dayjs().subtract(29, 'day').toDate(), dayjs().toDate()]
+    }
+  },
+  {
+    text: '最近90天',
+    value: () => {
+      return [dayjs().subtract(89, 'day').toDate(), dayjs().toDate()]
+    }
+  },
+  {
+    text: '最近一年',
+    value: () => {
+      return [dayjs().subtract(1, 'year').toDate(), dayjs().toDate()]
+    }
+  }
+]
+
 export const defaultShortcuts = [
   {
     text: '今天',

+ 330 - 0
src/views/pms/device/monitor/TdDeviceInfo copy.vue

@@ -0,0 +1,330 @@
+<template>
+  <ContentWrap v-loading="formLoading">
+    <ContentWrap>
+      <el-form style="height: 89px; margin-left: 20px">
+        <el-row style="display: flex; flex-direction: row">
+          <el-col :span="8">
+            <el-form-item prop="deviceCode">
+              <template #label>
+                <span class="custom-label">资产编码:</span>
+              </template>
+              <span class="custom-label">{{ formData.deviceCode }}</span>
+            </el-form-item>
+          </el-col>
+          <el-col :span="8">
+            <el-form-item prop="deviceName">
+              <template #label>
+                <span class="custom-label">设备类别:</span>
+              </template>
+              <span class="custom-label">{{ formData.deviceName }}</span>
+            </el-form-item>
+          </el-col>
+          <el-col :span="8">
+            <el-form-item prop="dept">
+              <template #label>
+                <span class="custom-label">所在部门:</span>
+              </template>
+              <span class="custom-label">{{ formData.dept }}</span>
+            </el-form-item>
+          </el-col>
+          <el-col :span="8">
+            <el-form-item prop="ifInline">
+              <template #label>
+                <span class="custom-label">是否在线:</span>
+              </template>
+              <template #default>
+                <dict-tag :type="DICT_TYPE.IOT_DEVICE_STATUS" :value="formData.ifInline" />
+              </template>
+            </el-form-item>
+          </el-col>
+          <el-col :span="8">
+            <el-form-item prop="lastInlineTime">
+              <template #label>
+                <span class="custom-label">最后数据时间:</span>
+              </template>
+              <span class="custom-label">{{ formData.lastInlineTime }}</span>
+            </el-form-item>
+          </el-col>
+          <el-col :span="8">
+            <el-form-item v-if="formData.vehicle" prop="vehicle">
+              <template #label>
+                <span class="custom-label">车牌号码:</span>
+              </template>
+              <span class="custom-label">{{ formData.vehicle }}</span>
+            </el-form-item>
+          </el-col>
+        </el-row>
+      </el-form>
+    </ContentWrap>
+    <ContentWrap>
+      <el-row>
+        <el-col :span="24">
+          <TdDeviceLabel :tags="specs" @select="labelSelect" tag-width="24%" />
+        </el-col>
+      </el-row>
+    </ContentWrap>
+    <ContentWrap>
+      <div class="chart-container">
+        <!-- 图表容器 -->
+        <el-date-picker
+          v-model="dateRange"
+          type="datetimerange"
+          :default-time="[new Date(2000, 1, 1, 0, 0, 0), new Date(2000, 1, 1, 23, 59, 59)]"
+          start-placeholder="起始日期时间"
+          end-placeholder="结束日期时间"
+          format="YYYY-MM-DD HH:mm:ss"
+          value-format="YYYY-MM-DD HH:mm:ss"
+          @change="handleDateChange"
+        />
+        <div v-loading="loading" style="height: 100%" ref="chartContainer"></div>
+      </div>
+    </ContentWrap>
+  </ContentWrap>
+</template>
+
+<script setup lang="ts">
+import { DICT_TYPE } from '@/utils/dict'
+import TdDeviceLabel from '@/views/pms/device/monitor/TdDeviceLabel.vue'
+import { IotDeviceApi } from '@/api/pms/device'
+import * as echarts from 'echarts'
+import dayjs from 'dayjs'
+import { IotStatApi } from '@/api/pms/stat'
+import { IotAlarmSettingApi } from '@/api/pms/alarm'
+
+const { params, name } = useRoute() // 查询参数
+const info = ref({})
+const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
+const id = params.id
+defineOptions({ name: 'TdDeviceDetail' })
+const formData = ref({
+  deviceCode: '',
+  deviceName: '',
+  ifInline: undefined,
+  lastInlineTime: '',
+  dept: '',
+  vehicle: ''
+})
+const specs = ref([])
+
+// 响应式数据
+const startTime = ref('')
+const endTime = ref('')
+const topicName = ref([])
+const loading = ref(false)
+const topic = ref('')
+// 设置固定阈值
+
+const handleDateChange = async (val) => {
+  if (val && val.length === 2) {
+    await getChart(val)
+    await renderChart()
+  }
+}
+
+const defaultEnd = dayjs()
+const defaultStart = defaultEnd.subtract(1, 'day')
+const dateRange = ref([
+  defaultStart.format('YYYY-MM-DD HH:mm:ss'),
+  defaultEnd.format('YYYY-MM-DD HH:mm:ss')
+])
+const labelSelect = async (row) => {
+  topic.value = row.identifier
+  topicName.value = row.modelName
+  await getChart(dateRange.value)
+  await renderChart()
+}
+
+const chartContainer = ref(null)
+let chartInstance = null
+
+// 时间格式化(HH:mm)
+const formatTime = (timestamp) => {
+  return new Date(timestamp)
+    .toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit', second: '2-digit' })
+    .slice(0, 5)
+}
+const result = ref([])
+const getChart = async (range) => {
+  loading.value = true
+  await IotStatApi.getDeviceInfoChart(params.code, topic.value, range[0], range[1]).then((res) => {
+    result.value = res
+    loading.value = false
+  })
+}
+// 初始化图表
+const renderChart = async () => {
+  if (!chartContainer.value) return
+  let upperLimit
+  let lowerLimit
+  await IotAlarmSettingApi.getDeviceRange(params.code, topic.value).then((res) => {
+    if (res) {
+      if (res.maxValue) {
+        upperLimit = res.maxValue
+      }
+      if (res.minValue) {
+        lowerLimit = res.minValue
+      }
+    }
+  })
+  // 销毁旧实例
+  if (chartInstance) chartInstance.dispose()
+
+  chartInstance = markRaw(echarts.init(chartContainer.value))
+
+  const option = {
+    title: {
+      text: topicName.value + '数据趋势',
+      left: 'center'
+    },
+    tooltip: { trigger: 'axis' },
+    xAxis: {
+      type: 'category',
+      data: result.value.map((d) => dayjs(d.timestamp).format('YYYY-MM-DD HH:mm:ss')),
+      axisLabel: { rotate: 45 },
+      inverse: true
+    },
+    yAxis: {
+      type: 'value'
+      // 根据固定阈值和实际数据调整Y轴范围,使阈值线更清晰
+      // min: Math.min(lowerLimit * 0.9, ...result.value.map(d => d.value || 0)),
+      // max: Math.max(upperLimit * 1.03, ...result.value.map(d => d.value || 0))
+    },
+    dataZoom: [
+      {
+        type: 'slider',
+        xAxisIndex: 0,
+        start: 0,
+        end: 100
+      }
+    ],
+    series: [
+      // 原始数据曲线
+      {
+        data: result.value.map((d) => d.value),
+        type: 'line',
+        smooth: true,
+        name: '实时数据',
+        lineStyle: { color: '#409eff' }
+      },
+      // 上限阈值线(固定100)
+      {
+        data: result.value.map(() => upperLimit),
+        type: 'line',
+        name: '上限阈值',
+        lineStyle: {
+          color: '#f56c6c', // 红色虚线
+          type: 'dashed'
+        },
+        symbol: 'none', // 不显示数据点
+        emphasis: { disabled: true } // 禁用悬停高亮
+      },
+      // 下限阈值线(固定95)
+      {
+        data: result.value.map(() => lowerLimit),
+        type: 'line',
+        name: '下限阈值',
+        lineStyle: {
+          color: '#e6a23c', // 橙色虚线
+          type: 'dashed'
+        },
+        symbol: 'none',
+        emphasis: { disabled: true }
+      }
+    ],
+    // 添加图例显示各线条含义
+    legend: {
+      data: ['实时数据', '上限阈值', '下限阈值'],
+      top: 30
+    }
+  }
+
+  chartInstance.setOption(option)
+
+  // 窗口自适应
+  window.addEventListener('resize', () => chartInstance.resize())
+}
+onMounted(async () => {
+  formLoading.value = true
+  formData.value.deviceCode = params.code
+  formData.value.deviceName = params.name
+  formData.value.lastInlineTime = params.time
+  formData.value.ifInline = params.ifInline
+  formData.value.dept = params.dept
+  formData.value.vehicle = params.vehicle
+  await IotDeviceApi.getIotDeviceTds(id).then((res) => {
+    specs.value = res
+    specs.value = specs.value.sort((a, b) => {
+      return b.modelOrder - a.modelOrder
+    })
+    formLoading.value = false
+    topic.value = specs.value[0].identifier
+    topicName.value = specs.value[0].modelName
+  })
+  await getChart(dateRange.value)
+  await renderChart()
+})
+</script>
+<style scoped lang="scss">
+.container {
+  width: 100%;
+  margin: 20px auto;
+  padding: 24px;
+  //background: #f8f9fa;
+  border-radius: 12px;
+}
+.chart-container {
+  width: 100%;
+  height: 600px;
+  padding: 20px;
+  background: #fff;
+  border-radius: 8px;
+  box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
+}
+
+.date-controls {
+  display: flex;
+  align-items: center;
+  gap: 15px;
+  margin-bottom: 20px;
+}
+
+input[type='datetime-local'] {
+  padding: 8px 12px;
+  border: 1px solid #dcdfe6;
+  border-radius: 4px;
+  transition: border-color 0.2s;
+}
+
+input[type='datetime-local']:focus {
+  border-color: #409eff;
+  outline: none;
+}
+
+.separator {
+  color: #606266;
+}
+
+.query-btn {
+  padding: 8px 20px;
+  background: #409eff;
+  color: white;
+  border: none;
+  border-radius: 4px;
+  cursor: pointer;
+  transition: opacity 0.2s;
+}
+
+.query-btn:hover {
+  opacity: 0.8;
+}
+
+//.chart {
+//  width: 100%;
+//  height: 500px;
+//  margin-top: 20px;
+//}
+.custom-label {
+  font-size: 17px;
+  font-weight: bold;
+}
+</style>

+ 448 - 294
src/views/pms/device/monitor/TdDeviceInfo.vue

@@ -1,339 +1,493 @@
-<template>
-  <ContentWrap v-loading="formLoading">
-    <ContentWrap>
-      <el-form style="height:89px;margin-left: 20px;">
-        <el-row style="display: flex;flex-direction: row; ">
-          <el-col :span="8">
-            <el-form-item prop="deviceCode">
-              <template #label>
-                <span class="custom-label">资产编码:</span>
-              </template>
-              <span class="custom-label">{{ formData.deviceCode }}</span>
-            </el-form-item>
-          </el-col>
-          <el-col :span="8">
-            <el-form-item prop="deviceName">
-              <template #label>
-                <span class="custom-label">设备类别:</span>
-              </template>
-              <span class="custom-label">{{ formData.deviceName }}</span>
-            </el-form-item>
-          </el-col>
-          <el-col :span="8">
-            <el-form-item prop="dept">
-              <template #label>
-                <span class="custom-label">所在部门:</span>
-              </template>
-              <span class="custom-label">{{ formData.dept }}</span>
-            </el-form-item>
-          </el-col>
-          <el-col :span="8">
-            <el-form-item  prop="ifInline">
-              <template #label>
-                <span class="custom-label">是否在线:</span>
-              </template>
-                <template #default>
-                  <dict-tag :type="DICT_TYPE.IOT_DEVICE_STATUS" :value="formData.ifInline" />
-                </template>
-            </el-form-item>
-          </el-col>
-          <el-col :span="8">
-            <el-form-item prop="lastInlineTime">
-              <template #label>
-                <span class="custom-label">最后数据时间:</span>
-              </template>
-              <span class="custom-label">{{ formData.lastInlineTime }}</span>
-            </el-form-item>
-          </el-col>
-          <el-col :span="8">
-            <el-form-item v-if="formData.vehicle" prop="vehicle">
-              <template #label>
-                <span class="custom-label">车牌号码:</span>
-              </template>
-              <span class="custom-label">{{ formData.vehicle }}</span>
-            </el-form-item>
-          </el-col>
-        </el-row>
-      </el-form>
-    </ContentWrap>
-    <ContentWrap>
-      <el-row>
-        <el-col :span="24">
-          <TdDeviceLabel :tags="specs" @select="labelSelect" tag-width="24%" />
-        </el-col>
-      </el-row>
-    </ContentWrap>
-    <ContentWrap>
-      <div class="chart-container">
-        <!-- 图表容器 -->
-        <el-date-picker
-          v-model="dateRange"
-          type="datetimerange"
-          :default-time="[
-        new Date(2000, 1, 1, 0, 0, 0),
-        new Date(2000, 1, 1, 23, 59, 59)
-      ]"
-          start-placeholder="起始日期时间"
-          end-placeholder="结束日期时间"
-          format="YYYY-MM-DD HH:mm:ss"
-          value-format="YYYY-MM-DD HH:mm:ss"
-          @change="handleDateChange"
-        />
-        <div v-loading="loading" style="height: 100%" ref="chartContainer"></div>
-      </div>
-    </ContentWrap>
-  </ContentWrap>
-</template>
-
 <script setup lang="ts">
-import { DICT_TYPE, getDictLabel } from '@/utils/dict'
-import TdDeviceLabel from '@/views/pms/device/monitor/TdDeviceLabel.vue'
-import {IotDeviceApi} from "@/api/pms/device";
 import * as echarts from 'echarts'
+import { Odometer, CircleCheckFilled, CircleCloseFilled } from '@element-plus/icons-vue'
+import { rangeShortcuts } from '@/utils/formatTime'
+
 import dayjs from 'dayjs'
-import {IotStatApi} from "@/api/pms/stat";
-import {IotAlarmSettingApi} from "@/api/pms/alarm";
+import quarterOfYear from 'dayjs/plugin/quarterOfYear'
+import { IotDeviceApi, cancelAllRequests } from '@/api/pms/device'
+import { IotStatApi } from '@/api/pms/stat'
 
-const { params, name } = useRoute() // 查询参数
-const info = ref({})
-const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
-const id = params.id
 defineOptions({ name: 'TdDeviceDetail' })
-const formData = ref({
-  deviceCode: '',
-  deviceName: '',
-  ifInline: undefined,
-  lastInlineTime: '',
-  dept:'',
-  vehicle:''
+
+dayjs.extend(quarterOfYear)
+
+const { params } = useRoute()
+
+const data = ref({
+  deviceCode: params.code || '',
+  deviceName: params.name || '',
+  lastInlineTime: params.time || '',
+  ifInline: params.ifInline || '',
+  dept: params.dept || '',
+  vehicle: params.vehicle || '',
+  carOnline: params.carOnline || ''
 })
-const specs = ref([])
-
-// 响应式数据
-const startTime = ref('')
-const endTime = ref('')
-const topicName = ref([])
-const loading = ref(false)
-const topic = ref('')
-// 设置固定阈值
-
-const handleDateChange = async (val) => {
-  if (val && val.length === 2) {
-    await getChart(val)
-    await renderChart()
-  }
-}
 
+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>>({})
 
-const defaultEnd = dayjs()
-const defaultStart = defaultEnd.subtract(1, 'day')
-const dateRange = ref([
-  defaultStart.format('YYYY-MM-DD HH:mm:ss'),
-  defaultEnd.format('YYYY-MM-DD HH:mm:ss')
+// 每 10s 刷新定时器
+const timer = ref<NodeJS.Timeout | null>(null)
+
+const defaultDate = rangeShortcuts[0].value()
+const date = ref([
+  dayjs(defaultDate[0]).format('YYYY-MM-DD HH:mm:ss'),
+  dayjs(defaultDate[1]).format('YYYY-MM-DD HH:mm:ss')
 ])
-const labelSelect =async (row) =>{
-  topic.value = row.identifier
-  topicName.value = row.modelName
-  await getChart(dateRange.value)
-  await renderChart()
-}
 
-const chartContainer = ref(null)
-let chartInstance = null
+const reset = () => {
+  cancelAllRequests().then(() => {
+    const def = rangeShortcuts[0].value()
 
-// 时间格式化(HH:mm)
-const formatTime = timestamp => {
-  return new Date(timestamp)
-    .toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit',second:'2-digit' })
-    .slice(0, 5)
+    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 result = ref([])
-const getChart = async (range) =>{
-  loading.value = true
-  await IotStatApi.getDeviceInfoChart(params.code, topic.value, range[0], range[1]).then(res=>{
-    result.value = res
-    loading.value = false
+
+const handleDateChange = () => {
+  cancelAllRequests().then(() => {
+    stopAutoFetch()
+    if (chart) chart.clear()
+    render()
+    initLoad(false)
   })
 }
-// 初始化图表
-const renderChart = async () => {
-  if (!chartContainer.value) return
-  let upperLimit;
-  let lowerLimit;
-  await IotAlarmSettingApi.getDeviceRange(params.code, topic.value).then(res=>{
-    if (res){
-      if (res.maxValue){
-        upperLimit = res.maxValue
-      }
-      if (res.minValue){
-        lowerLimit = res.minValue
-      }
+
+const handleClickSpec = (modelName: string) => {
+  selectSpec.value[modelName] = !selectSpec.value[modelName]
+  chart?.setOption({
+    legend: {
+      selected: selectSpec.value
     }
   })
-  // 销毁旧实例
-  if (chartInstance) chartInstance.dispose()
+}
+
+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
+let minInterval = 0
 
-  chartInstance = markRaw(echarts.init(chartContainer.value))
+// 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))
 
-  const option = {
-    title: {
-      text: topicName.value + '数据趋势',
-      left: 'center',
+  zhbdspecs.value = zhbdres.sort((a, b) => b.modelOrder - a.modelOrder)
+  gatewayspecs.value = res.sort((a, b) => b.modelOrder - a.modelOrder)
+
+  specs.value = [
+    ...JSON.parse(JSON.stringify(gatewayspecs.value)),
+    ...JSON.parse(JSON.stringify(zhbdspecs.value))
+  ]
+
+  selectSpec.value = specs.value.reduce(
+    (acc, spec) => {
+      acc[spec.modelName] =
+        gatewayspecs.value.some((item) => item.modelName === spec.modelName) &&
+        !disabledIdentifier.value.includes(spec.identifier)
+      return acc
     },
-    tooltip: { trigger: 'axis' },
+    {} as Record<string, boolean>
+  )
+
+  specsLoading.value = false
+
+  chartMap.value = specs.value
+    .filter((spec) => !disabledIdentifier.value.includes(spec.identifier))
+    .reduce(
+      (acc, spec) => {
+        acc[spec.identifier] = { name: spec.modelName, value: [] }
+        return acc
+      },
+      {} as Record<string, { name: string; value: any[] }>
+    )
+}
+
+const chartLoading = ref(false)
+
+const initLoad = async (real_time: boolean = true) => {
+  if (!specs.value.length) return
+
+  Object.keys(chartMap.value).forEach((identifier) => {
+    chartMap.value[identifier].value = []
+    lastTsMap.value[identifier] = 0
+  })
+
+  chartLoading.value = true
+
+  for (const identifier of Object.keys(chartMap.value)) {
+    const res = await IotStatApi.getDeviceInfoChart(
+      data.value.deviceCode,
+      identifier,
+      date.value[0],
+      date.value[1]
+    )
+
+    const sorted = res.sort((a, b) => a.ts - b.ts)
+    chartMap.value[identifier].value = sorted
+    lastTsMap.value[identifier] = sorted.at(-1)?.ts ?? 0
+
+    updateSingleSeries(identifier)
+
+    chartLoading.value = false
+  }
+
+  if (real_time) startAutoFetch()
+}
+
+const startAutoFetch = () => {
+  timer.value = setInterval(fetchIncrementData, 10000)
+}
+
+const stopAutoFetch = () => {
+  if (timer.value) clearInterval(timer.value)
+  timer.value = null
+}
+
+const fetchIncrementData = () => {
+  for (const identifier of Object.keys(chartMap.value)) {
+    const lastTs = lastTsMap.value[identifier]
+    if (!lastTs) continue
+
+    IotStatApi.getDeviceInfoChart(
+      data.value.deviceCode,
+      identifier,
+      dayjs(lastTs).format('YYYY-MM-DD HH:mm:ss'),
+      dayjs().format('YYYY-MM-DD HH:mm:ss')
+    ).then((res) => {
+      if (!res.length) return
+
+      const sorted = res.sort((a, b) => a.ts - b.ts)
+
+      // push 到本地
+      chartMap.value[identifier].value.push(...sorted)
+      // 更新 lastTs
+      lastTsMap.value[identifier] = sorted.at(-1).ts
+
+      appendToSeries(identifier, chartMap.value[identifier].value)
+    })
+  }
+}
+
+const render = () => {
+  if (!chartRef.value) return
+
+  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)
+  const minVal = Math.min(...values, -100)
+
+  const maxDigits = (Math.floor(maxVal) + '').length
+  const minDigits = (Math.floor(Math.abs(minVal)) + '').length
+  const interval = Math.max(maxDigits, minDigits)
+
+  maxInterval = interval
+  minInterval = minDigits
+
+  intervalArr = [0]
+  for (let i = 1; i <= interval; i++) {
+    intervalArr.push(Math.pow(10, i))
+  }
+
+  chart.setOption({
+    tooltip: {
+      trigger: 'axis',
+      formatter: (params) => {
+        let d = `${params[0].axisValueLabel}<br>`
+        let item = params.map((el) => `${el.marker} ${el.seriesName}: ${el.value[2]}<br>`)
+        return d + item.join('')
+      }
+    },
+    dataZoom: [
+      { type: 'inside', xAxisIndex: 0 },
+      { type: 'slider', xAxisIndex: 0 }
+    ],
     xAxis: {
-      type: 'category',
-      data: result.value.map(d => dayjs(d.timestamp).format('YYYY-MM-DD HH:mm:ss')),
-      axisLabel: { rotate: 45,
-        interval: function(index, value) {
-          // 将时间字符串转换为分钟数,每60分钟显示一个刻度(1小时)
-          const minutes = dayjs(value).minute();
-          // 只显示整点的刻度(分钟为0)
-          return minutes === 0;
-        }
+      type: 'time',
+      axisLabel: {
+        formatter: (v) => dayjs(v).format('YYYY-MM-DD HH:mm:ss'),
+        rotate: 20
       },
-      inverse: true,
+      inverse: true
     },
     yAxis: {
       type: 'value',
-      // 根据固定阈值和实际数据调整Y轴范围,使阈值线更清晰
-      // min: Math.min(lowerLimit * 0.9, ...result.value.map(d => d.value || 0)),
-      // max: Math.max(upperLimit * 1.03, ...result.value.map(d => d.value || 0))
+      min: -minInterval,
+      max: maxInterval,
+      interval: 1,
+      axisLabel: {
+        formatter: (v) => (v === 0 ? '0' : v > 0 ? Math.pow(10, v) : -Math.pow(10, -v))
+      }
+    },
+    legend: {
+      data: Object.values(chartMap.value).map((i) => i.name),
+      selected: selectSpec.value
     },
-    dataZoom: [{
-      type: 'slider',
-      xAxisIndex: 0,
-      start: 0,
-      end: 100
-    }],
+    series: Object.keys(chartMap.value).map((identifier) => ({
+      name: chartMap.value[identifier].name,
+      type: 'line',
+      smooth: true,
+      showSymbol: false,
+      data: [] // 占位数组
+    }))
+  })
+}
+
+const updateSingleSeries = (identifier: string) => {
+  if (!chart) render()
+  if (!chart) return
+
+  const idx = Object.keys(chartMap.value).indexOf(identifier)
+  if (idx === -1) return
+
+  const data = chartMap.value[identifier].value.map((v) => mapData(v))
+
+  chart.setOption({
     series: [
-      // 原始数据曲线
-      {
-        data: result.value.map(d => d.value),
-        type: 'line',
-        smooth: true,
-        name: '实时数据',
-        lineStyle: { color: '#409eff' }
-      },
-      // 上限阈值线(固定100)
       {
-        data: result.value.map(() => upperLimit),
-        type: 'line',
-        name: '上限阈值',
-        lineStyle: {
-          color: '#f56c6c',  // 红色虚线
-          type: 'dashed'
-        },
-        symbol: 'none',  // 不显示数据点
-        emphasis: { disabled: true }  // 禁用悬停高亮
-      },
-      // 下限阈值线(固定95)
+        name: chartMap.value[identifier].name,
+        data
+      }
+    ]
+  })
+}
+
+const appendToSeries = (identifier, list) => {
+  if (!chart) return
+
+  const idx = Object.keys(chartMap.value).indexOf(identifier)
+  if (idx === -1) return
+
+  const data = list.map(mapData)
+
+  chart.setOption({
+    series: [
       {
-        data: result.value.map(() => lowerLimit),
-        type: 'line',
-        name: '下限阈值',
-        lineStyle: {
-          color: '#e6a23c',  // 橙色虚线
-          type: 'dashed'
-        },
-        symbol: 'none',
-        emphasis: { disabled: true }
+        name: chartMap.value[identifier].name,
+        data
       }
-    ],
-    // 添加图例显示各线条含义
-    legend: {
-      data: ['实时数据', '上限阈值', '下限阈值'],
-      top: 30
-    }
-  }
+    ]
+  })
+}
+
+const mapData = ({ value, ts }) => {
+  if (value === 0) return [ts, 0, 0]
+
+  const isPositive = value > 0
+  const absItem = Math.abs(value)
 
-  chartInstance.setOption(option)
+  const min_value = Math.max(...intervalArr.filter((v) => v <= absItem))
+  const min_index = intervalArr.findIndex((v) => v === min_value)
 
-  // 窗口自适应
-  window.addEventListener('resize', () => chartInstance.resize())
+  const new_value =
+    (absItem - min_value) / (intervalArr[min_index + 1] - intervalArr[min_index]) + min_index
+
+  return [ts, isPositive ? new_value : -new_value, value]
 }
+
 onMounted(async () => {
-  formLoading.value = true
-  formData.value.deviceCode = params.code
-  formData.value.deviceName = params.name
-  formData.value.lastInlineTime = params.time
-  formData.value.ifInline = params.ifInline
-  formData.value.dept = params.dept
-  formData.value.vehicle = params.vehicle
-  await IotDeviceApi.getIotDeviceTds(id).then(res => {
-    specs.value = res
-    specs.value = specs.value.sort((a, b) => {
-      return b.modelOrder - a.modelOrder
-    })
-    formLoading.value = false
-    topic.value = specs.value[0].identifier
-    topicName.value = specs.value[0].modelName
+  await loadSpecs()
+  render()
+  initLoad()
+})
+
+onUnmounted(() => {
+  stopAutoFetch()
+
+  window.removeEventListener('resize', () => {
+    if (chart) chart.resize()
   })
-  await getChart(dateRange.value)
-  await renderChart()
 })
 </script>
-<style scoped lang="scss">
-.container {
-  width: 100%;
-  margin: 20px auto;
-  padding: 24px;
-  //background: #f8f9fa;
-  border-radius: 12px;
-}
-.chart-container {
-  width: 100%;
-  height: 600px;
-  padding: 20px;
-  background: #fff;
-  border-radius: 8px;
-  box-shadow: 0 2px 12px rgba(0,0,0,0.1);
-}
 
-.date-controls {
-  display: flex;
-  align-items: center;
-  gap: 15px;
-  margin-bottom: 20px;
-}
+<template>
+  <div
+    class="w-full bg-gradient-to-r from-blue-100 to-white rounded-lg p-6 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-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
+          v-if="data.ifInline === '3'"
+          type="success"
+          size="default"
+          class="flex items-center"
+        >
+          <el-icon class="text-emerald!"><CircleCheckFilled /></el-icon>
+          在线
+        </el-tag>
 
-input[type="datetime-local"] {
-  padding: 8px 12px;
-  border: 1px solid #dcdfe6;
-  border-radius: 4px;
-  transition: border-color 0.2s;
-}
+        <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="中航北斗" 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>
 
-input[type="datetime-local"]:focus {
-  border-color: #409eff;
-  outline: none;
-}
+        <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 min-h-50">
+    <header class="font-medium text-center w-full">网关数采</header>
+    <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"
+        class="h-12 bg-white dark:bg-[#1d1e1f] rounded-lg px-6 shadow flex items-center hover:scale-103 transition-all cursor-pointer"
+        :class="{
+          'bg-blue-200': selectSpec[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
+    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
+        v-for="item in zhbdspecs"
+        :key="item.productId"
+        class="h-12 bg-white dark:bg-[#1d1e1f] rounded-lg px-6 shadow flex items-center hover:scale-103 transition-all cursor-pointer"
+        :class="{
+          'bg-blue-200': selectSpec[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="w-full mt-4 bg-gradient-to-r from-blue-100 to-white min-h-175 rounded-lg p-6 shadow">
+    <header class="flex items-center justify-between">
+      <h3 class="flex items-center gap-2">
+        <div class="i-material-symbols:area-chart-outline-rounded text-sky size-6" text-sky></div>
+        数据趋势
+      </h3>
 
-.separator {
-  color: #606266;
-}
+      <div class="flex gap-4">
+        <el-button type="primary" size="default" @click="reset">重置</el-button>
+        <el-date-picker
+          v-model="date"
+          value-format="YYYY-MM-DD HH:mm:ss"
+          type="datetimerange"
+          unlink-panels
+          start-placeholder="开始日期"
+          end-placeholder="结束日期"
+          :shortcuts="rangeShortcuts"
+          size="default"
+          class="w-100!"
+          placement="bottom-end"
+          @change="handleDateChange"
+        />
+      </div>
+    </header>
+    <div
+      v-loading="specsLoading"
+      element-loading-background="transparent"
+      ref="chartRef"
+      class="w-full h-158 mt-4 mb-4"
+    ></div>
+  </div>
+</template>
 
-.query-btn {
-  padding: 8px 20px;
-  background: #409eff;
-  color: white;
-  border: none;
-  border-radius: 4px;
-  cursor: pointer;
-  transition: opacity 0.2s;
-}
+<style lang="scss" scoped>
+:deep(.el-form-item) {
+  margin-bottom: 0;
 
-.query-btn:hover {
-  opacity: 0.8;
-}
+  .el-form-item__label {
+    margin-bottom: 0;
+  }
+
+  .el-form-item__content {
+    font-size: 1rem;
+    font-weight: 500;
+  }
 
-//.chart {
-//  width: 100%;
-//  height: 500px;
-//  margin-top: 20px;
-//}
-.custom-label{
-  font-size: 17px;
-  font-weight: bold;
+  &.online {
+    .el-form-item__content {
+      height: 2.5rem;
+
+      .el-tag__content {
+        display: flex;
+        align-items: center;
+        gap: 2px;
+      }
+    }
+  }
 }
 </style>

+ 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); /* 原始大小 */
 

+ 3 - 2
uno.config.ts

@@ -1,7 +1,8 @@
-import { defineConfig, toEscapedSelector as e, presetUno } from 'unocss'
+import { defineConfig, toEscapedSelector as e, presetUno, presetIcons } from 'unocss'
 // import transformerVariantGroup from '@unocss/transformer-variant-group'
 
 export default defineConfig({
+  content: { pipeline: { include: [/\.(vue|svelte|[jt]sx|mdx?|astro|elm|php|phtml|html)($|\?)/, 'src/**/*.{js,ts}'] } },
   // ...UnoCSS options
   rules: [
     [
@@ -100,7 +101,7 @@ ${selector}:after {
       }
     ]
   ],
-  presets: [presetUno({ dark: 'class', attributify: false })],
+  presets: [presetUno({ dark: 'class', attributify: false }), presetIcons()],
   // transformers: [transformerVariantGroup()],
   shortcuts: {
     'wh-full': 'w-full h-full'