Quellcode durchsuchen

✨ feat(tddeviceInfo): 运行监控调试

Zimo vor 1 Woche
Ursprung
Commit
f356dca79f
5 geänderte Dateien mit 499 neuen und 307 gelöschten Zeilen
  1. 3 3
      .vscode/settings.json
  2. 29 12
      src/api/pms/device/index.ts
  3. 94 0
      src/utils/formatTime.ts
  4. 370 290
      src/views/pms/device/monitor/TdDeviceInfo.vue
  5. 3 2
      uno.config.ts

+ 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 = () => {
+  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 })
-  },
+  }
 }

+ 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: '今天',

+ 370 - 290
src/views/pms/device/monitor/TdDeviceInfo.vue

@@ -1,332 +1,412 @@
-<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 } 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 || ''
 })
-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 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 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()
+  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()
+  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
+
+// 映射区间相关
+let intervalArr: number[] = []
+let maxInterval = 0
+let minInterval = 0
+
+// 1. 加载 specs
+const loadSpecs = async () => {
+  if (!params.id) return
+  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)
+
+  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)
+      return acc
+    },
+    {} as Record<string, boolean>
+  )
+
+  chartMap.value = specs.value
+    .filter((spec) => !['online', 'vehicle_name'].includes(spec.identifier))
+    .reduce(
+      (acc, spec) => {
+        acc[spec.identifier] = { name: spec.modelName, value: [] }
+        return acc
+      },
+      {} as Record<string, { name: string; value: any[] }>
+    )
+}
+
+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
+  })
+
+  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)
+  }
+
+  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)
+
+  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
 
-  chartInstance = markRaw(echarts.init(chartContainer.value))
+  intervalArr = [0]
+  for (let i = 1; i <= interval; i++) {
+    intervalArr.push(Math.pow(10, i))
+  }
 
-  const option = {
-    title: {
-      text: topicName.value + '数据趋势',
-      left: 'center',
+  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('')
+      }
     },
-    tooltip: { trigger: 'axis' },
+    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 },
-      inverse: true,
+      type: 'time',
+      axisLabel: {
+        formatter: (v) => dayjs(v).format('YYYY-MM-DD HH:mm:ss'),
+        rotate: 20
+      },
+      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]
 
-  chartInstance.setOption(option)
+  const isPositive = value > 0
+  const absItem = Math.abs(value)
 
-  // 窗口自适应
-  window.addEventListener('resize', () => chartInstance.resize())
+  const min_value = Math.max(...intervalArr.filter((v) => v <= absItem))
+  const min_index = intervalArr.findIndex((v) => v === min_value)
+
+  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 getChart(dateRange.value)
-  await renderChart()
+  await loadSpecs()
+  render()
+  initLoad()
+})
+
+onUnmounted(() => {
+  stopAutoFetch()
 })
 </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 type="success" size="default" class="flex items-center">
+          <el-icon class="text-emerald!"><CircleCheckFilled /></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">
+    <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 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="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">
+    <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="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>
 
-input[type="datetime-local"] {
-  padding: 8px 12px;
-  border: 1px solid #dcdfe6;
-  border-radius: 4px;
-  transition: border-color 0.2s;
-}
+      <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 ref="chartRef" class="w-full h-158 mt-4 mb-4"></div>
+  </div>
+</template>
 
-input[type="datetime-local"]:focus {
-  border-color: #409eff;
-  outline: none;
-}
+<style lang="scss" scoped>
+:deep(.el-form-item) {
+  margin-bottom: 0;
 
-.separator {
-  color: #606266;
-}
+  .el-form-item__label {
+    margin-bottom: 0;
+  }
 
-.query-btn {
-  padding: 8px 20px;
-  background: #409eff;
-  color: white;
-  border: none;
-  border-radius: 4px;
-  cursor: pointer;
-  transition: opacity 0.2s;
-}
+  .el-form-item__content {
+    font-size: 1rem;
+    font-weight: 500;
+  }
 
-.query-btn:hover {
-  opacity: 0.8;
-}
+  &.online {
+    .el-form-item__content {
+      height: 2.5rem;
 
-//.chart {
-//  width: 100%;
-//  height: 500px;
-//  margin-top: 20px;
-//}
-.custom-label{
-  font-size: 17px;
-  font-weight: bold;
+      .el-tag__content {
+        display: flex;
+        align-items: center;
+        gap: 2px;
+      }
+    }
+  }
 }
 </style>

+ 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'