Browse Source

🐞 fix(设备监控): tooltip tofixed 保留两位

Zimo 6 days ago
parent
commit
321fdb916a

+ 11 - 2
src/views/pms/device/monitor/TdDeviceInfo.vue

@@ -182,7 +182,12 @@ const initLoad = async (real_time: boolean = true) => {
       date.value[1]
     )
 
-    const sorted = res.sort((a, b) => a.ts - b.ts)
+    const sorted = res
+      .sort((a, b) => a.ts - b.ts)
+      .map((item) => ({
+        ts: item.ts,
+        value: item.value
+      }))
     chartMap.value[identifier].value = sorted
     lastTsMap.value[identifier] = sorted.at(-1)?.ts ?? 0
 
@@ -195,6 +200,8 @@ const initLoad = async (real_time: boolean = true) => {
     chartLoading.value = false
   }
 
+  console.log('chartMap.value :>> ', JSON.stringify(Object.values(chartMap.value), null, 2))
+
   if (real_time) startAutoFetch()
 }
 
@@ -287,7 +294,9 @@ const render = () => {
           exist.push(el.seriesName)
           return true
         })
-        let item = params.map((el) => `${el.marker} ${el.seriesName}: ${el.value[2]}<br>`)
+        let item = params.map(
+          (el) => `${el.marker} ${el.seriesName}: ${el.value[2].toFixed(2)}<br>`
+        )
         return d + item.join('')
       }
     },

+ 275 - 0
src/views/pms/device/monitor/TdDeviceInfo2.vue

@@ -0,0 +1,275 @@
+<script setup lang="ts">
+import { Odometer, CircleCheckFilled, CircleCloseFilled } from '@element-plus/icons-vue'
+import { rangeShortcuts } from '@/utils/formatTime'
+import { IotDeviceApi } from '@/api/pms/device'
+import dayjs from 'dayjs'
+import { IotStatApi } from '@/api/pms/stat'
+
+import { use } from 'echarts/core'
+import { LineChart } from 'echarts/charts'
+import {
+  TitleComponent,
+  TooltipComponent,
+  LegendComponent,
+  GridComponent
+} from 'echarts/components'
+import { CanvasRenderer } from 'echarts/renderers'
+
+use([TitleComponent, TooltipComponent, LegendComponent, GridComponent, LineChart, CanvasRenderer])
+
+defineOptions({ name: 'TdDeviceDetail' })
+
+const { query } = useRoute()
+
+const data = ref({
+  deviceCode: query.code || '',
+  deviceName: query.name || '',
+  lastInlineTime: query.time || '',
+  ifInline: query.ifInline || '',
+  dept: query.dept || '',
+  vehicle: query.vehicle || '',
+  carOnline: query.carOnline || ''
+})
+
+// 首先加载 维度 分为两种一种 网关维度 一种中航北斗维度
+interface Dimension {
+  name: string
+  identifier: string
+  value: string
+}
+
+const dimensions = ref<Dimension[]>([])
+const gatewayDimensions = ref<Dimension[]>([])
+const carDimensions = ref<Dimension[]>([])
+
+// 一部分值不能展示在图表上
+const disabledDimensions = ['车牌号码', '是否在线']
+
+// 选中的维度
+const selectedDimensions = ref<{ [key: Dimension['name']]: boolean }>({})
+
+const dimensionLoading = ref(false)
+
+const loadDimensions = async () => {
+  if (!query.id) return
+  dimensionLoading.value = true
+
+  // 网关维度
+  const gateway = (((await IotDeviceApi.getIotDeviceTds(Number(query.id))) as any[]) ?? [])
+    .sort((a, b) => b.modelOrder - a.modelOrder)
+    .map((v) => ({ name: v.modelName, identifier: v.identifier, value: v.value }))
+
+  // 中航北斗维度
+  const car = (((await IotDeviceApi.getIotDeviceZHBDTds(Number(query.id))) as any[]) ?? [])
+    .sort((a, b) => b.modelOrder - a.modelOrder)
+    .map((v) => ({ name: v.modelName, identifier: v.identifier, value: v.value }))
+
+  // 合并维度 过滤不可展示维度
+  dimensions.value = [...gateway, ...car].filter((v) => !disabledDimensions.includes(v.name))
+  gatewayDimensions.value = gateway
+  carDimensions.value = car
+
+  // 生成 chart legend selected
+  selectedDimensions.value = Object.fromEntries(dimensions.value.map((v) => [v.name, false]))
+
+  // 默认选中第一个
+  selectedDimensions.value[dimensions.value[0].name] = true
+
+  dimensionLoading.value = false
+
+  chartData.value = Object.fromEntries(dimensions.value.map((v) => [v.name, []]))
+}
+
+// 时间范围
+const dateRange = ref<string[]>([
+  ...rangeShortcuts[3].value().map((v) => dayjs(v).format('YYYY-MM-DD HH:mm:ss'))
+])
+
+// 获取图表数据
+const chartData = ref<{ [key: Dimension['name']]: number[][] }>({})
+
+const loadChartData = async () => {
+  if (!dimensions.value.length) return
+
+  chartData.value = Object.fromEntries(dimensions.value.map((v) => [v.name, []]))
+
+  for (const { name, identifier } of dimensions.value) {
+    const res = await IotStatApi.getDeviceInfoChart(
+      data.value.deviceCode,
+      identifier,
+      dateRange.value[0],
+      dateRange.value[1]
+    )
+
+    const sorted = res.sort((a, b) => a.ts - b.ts)
+    const values = sorted.map((v) => v.value)
+
+    // --- 归一化参数 ---
+    const min = Math.min(...values)
+    const max = Math.max(...values)
+    const range = max - min || 1 // 防止除 0
+
+    const seriesData = sorted.map((v) => {
+      const norm = (v.value - min) / range
+      return [v.ts, norm, v.value] // ts, 归一化值, 原始值
+    })
+
+    chartData.value[name] = seriesData
+  }
+
+  console.log('chartData :>> ', Object.values(chartData.value))
+}
+
+onMounted(() => {
+  loadDimensions().then(() => {
+    loadChartData()
+  })
+})
+</script>
+
+<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>
+
+        <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 v-if="data.carOnline" 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>
+
+        <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="dimensionLoading"
+      element-loading-background="transparent"
+      class="w-full mt-4 grid grid-cols-4 gap-4 min-h-30"
+      id="dimension"
+    >
+      <button
+        v-for="item in gatewayDimensions"
+        :key="item.identifier"
+        class="outline-none border-none 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': selectedDimensions[item.name] }"
+        :disabled="disabledDimensions.includes(item.name)"
+      >
+        <span class="text-sm text-[var(--el-text-color-regular)]">{{ item.name }}</span>
+        <span class="text-lg font-medium ms-a">{{ item.value }}</span>
+      </button>
+    </div>
+  </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="dimensionLoading"
+      element-loading-background="transparent"
+      class="w-full mt-4 grid grid-cols-4 gap-4 min-h-30"
+      id="dimension"
+    >
+      <button
+        v-for="item in carDimensions"
+        :key="item.identifier"
+        class="outline-none border-none 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': selectedDimensions[item.name] }"
+        :disabled="disabledDimensions.includes(item.name)"
+      >
+        <span class="text-sm text-[var(--el-text-color-regular)]">{{ item.name }}</span>
+        <span class="text-lg font-medium ms-a">{{ item.value }}</span>
+      </button>
+    </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>
+
+      <div class="flex gap-4">
+        <!-- <el-button type="primary" size="default" @click="exportChart">导出为图片</el-button>
+        <el-button size="default" @click="reset">重置</el-button> -->
+        <el-date-picker
+          v-model="dateRange"
+          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"
+        />
+      </div>
+    </header>
+  </div>
+</template>
+
+<style lang="scss" scoped>
+:deep(.el-form-item) {
+  margin-bottom: 0;
+
+  .el-form-item__label {
+    margin-bottom: 0;
+  }
+
+  .el-form-item__content {
+    font-size: 1rem;
+    font-weight: 500;
+  }
+
+  &.online {
+    .el-form-item__content {
+      height: 2.5rem;
+
+      .el-tag__content {
+        display: flex;
+        align-items: center;
+        gap: 2px;
+      }
+    }
+  }
+}
+</style>