Kaynağa Gözat

看板添加修改名称功能

Zimo 4 gün önce
ebeveyn
işleme
1db9988eb8

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

@@ -143,6 +143,9 @@ export const IotDeviceApi = {
   createIotDevice: async (data: IotDeviceVO) => {
     return await request.post({ url: `/rq/iot-device/create`, data })
   },
+  createLyDevice: async (data: any) => {
+    return await request.post({ url: `/rq/ly-device/create`, data })
+  },
 
   // 保存 设备-状态 的关联关系
   saveDeviceStatuses: async (data: any) => {

+ 262 - 7
src/views/oli-connection/monitoring-board/chart.vue

@@ -1,6 +1,7 @@
 <script lang="ts" setup>
 import { IotDeviceApi } from '@/api/pms/device'
 import { cancelAllRequests, IotStatApi } from '@/api/pms/stat'
+import { useTableComponents } from '@/components/ZmTable/useTableComponents'
 import { useMqtt } from '@/utils/useMqtt'
 import {
   Dimensions,
@@ -8,9 +9,11 @@ import {
   IotTdItem,
   mapToDimension,
   normalizeRangeValue,
+  RangeSettingDraft,
   withDisplayStyle
 } from '@/utils/useSocketBus'
 import { useDebounceFn } from '@vueuse/core'
+import { Setting } from '@element-plus/icons-vue'
 import dayjs from 'dayjs'
 import * as echarts from 'echarts'
 
@@ -27,6 +30,10 @@ const props = defineProps({
     type: String,
     required: true
   },
+  otherName: {
+    type: String,
+    default: ''
+  },
   mqttUrl: {
     type: String,
     required: true
@@ -65,8 +72,24 @@ const props = defineProps({
   }
 })
 
+const emit = defineEmits<{
+  (event: 'other-name-updated', payload: { id: number; otherName: string }): void
+}>()
+
 const dimensions = ref<Dimensions[]>([])
 const selectedDimension = ref<Record<string, boolean>>({})
+const message = useMessage()
+
+const currentOtherName = ref(props.otherName)
+
+const displayDeviceName = computed(() => currentOtherName.value || props.deviceName)
+
+watch(
+  () => props.otherName,
+  (value) => {
+    currentOtherName.value = value
+  }
+)
 
 const { connect, destroy, isConnected, subscribe } = useMqtt()
 
@@ -696,7 +719,7 @@ function handleDetailClick() {
       ifInline: props.ifInline,
       carOnline: props.carOnline,
       time: props.lastInlineTime,
-      name: props.deviceName,
+      name: displayDeviceName.value,
       code: props.deviceCode,
       dept: props.deptName,
       vehicle: props.vehicleName,
@@ -704,19 +727,148 @@ function handleDetailClick() {
     }
   })
 }
+
+const { ZmTable, ZmTableColumn } = useTableComponents<RangeSettingDraft>()
+const settingsDialogVisible = ref(false)
+const settingsSaving = ref(false)
+const deviceNameDraft = ref('')
+const rangeSettingDrafts = ref<RangeSettingDraft[]>([])
+
+function buildRangeSettingDrafts() {
+  rangeSettingDrafts.value = dimensions.value.map((item) => ({
+    name: item.name,
+    identifier: item.identifier,
+    color: item.color,
+    minValue: normalizeRangeValue(item.minValue),
+    maxValue: normalizeRangeValue(item.maxValue)
+  }))
+}
+
+function openSettingsDialog() {
+  deviceNameDraft.value = currentOtherName.value || props.deviceName
+  buildRangeSettingDrafts()
+  settingsDialogVisible.value = true
+}
+
+async function saveDeviceName() {
+  const nextName = deviceNameDraft.value.trim()
+
+  if (!nextName) {
+    throw new Error('设备名称不能为空')
+  }
+
+  if (nextName === (currentOtherName.value || props.deviceName)) return
+
+  await IotDeviceApi.createLyDevice({
+    deviceId: props.id,
+    deviceCode: props.deviceCode,
+    deviceName: props.deviceName,
+    otherName: nextName
+  })
+
+  currentOtherName.value = nextName
+  emit('other-name-updated', {
+    id: props.id,
+    otherName: nextName
+  })
+}
+
+async function saveRangeSetting(item: Dimensions, minValue?: number, maxValue?: number) {
+  const min = normalizeRangeValue(minValue)
+  const max = normalizeRangeValue(maxValue)
+
+  if (min === undefined && max === undefined) {
+    if (item.id) {
+      await IotDeviceApi.deleteMaxMin({ id: item.id })
+    }
+
+    item.minValue = undefined
+    item.maxValue = undefined
+    item.id = undefined
+    return
+  }
+
+  if (min === undefined || max === undefined) {
+    throw new Error(`${item.name} 的最大值和最小值需要同时填写`)
+  }
+
+  if (min > max) {
+    throw new Error(`${item.name} 的最小值不能大于最大值`)
+  }
+
+  if (min === max) {
+    throw new Error(`${item.name} 的最大值和最小值不能相等`)
+  }
+
+  const body = {
+    minValue: min,
+    maxValue: max,
+    deviceId: props.id,
+    propertyCode: item.identifier,
+    alarmProperty: item.name,
+    deviceName: props.deviceName,
+    id: item.id
+  }
+
+  const res = await IotDeviceApi.saveMaxMin(body)
+
+  if (res.id) item.id = res.id
+  item.minValue = min
+  item.maxValue = max
+}
+
+async function handleSettingsSave() {
+  settingsSaving.value = true
+  try {
+    await saveDeviceName()
+
+    for (const draft of rangeSettingDrafts.value) {
+      const item = dimensions.value.find((dimension) => dimension.identifier === draft.identifier)
+      if (!item) continue
+
+      await saveRangeSetting(item, draft.minValue, draft.maxValue)
+    }
+
+    updateSeriesByNames(dimensions.value.map((item) => item.name))
+    settingsDialogVisible.value = false
+    message.success('设置成功')
+  } catch (error: any) {
+    message.warning(error?.message || '设置失败')
+  } finally {
+    settingsSaving.value = false
+  }
+}
+
+function handleRangeDialogReset() {
+  rangeSettingDrafts.value = rangeSettingDrafts.value.map((item) => ({
+    ...item,
+    minValue: undefined,
+    maxValue: undefined
+  }))
+}
+
+function clearRangeDraft(row: RangeSettingDraft) {
+  row.minValue = undefined
+  row.maxValue = undefined
+}
 </script>
 <template>
   <div class="rounded-lg chart-container flex flex-col">
     <header class="chart-header justify-between">
       <div class="flex items-center">
         <div class="title-icon"></div>
-        <div>{{ `${props.deviceCode}-${props.deviceName}` }}</div>
+        <div>{{ `${props.deviceCode}-${displayDeviceName}` }}</div>
+      </div>
+      <div class="chart-header__actions">
+        <el-button link type="primary" :icon="Setting" @click.stop="openSettingsDialog">
+          设置
+        </el-button>
+        <el-button link type="primary" class="group" @click.stop="handleDetailClick">
+          详情
+          <div
+            class="i-material-symbols:arrow-right-alt-rounded size-4 transition-transform group-hover:translate-x-1"></div>
+        </el-button>
       </div>
-      <el-button link type="primary" class="group" @click="handleDetailClick">
-        详情
-        <div
-          class="i-material-symbols:arrow-right-alt-rounded size-4 transition-transform group-hover:translate-x-1"></div>
-      </el-button>
     </header>
     <main class="chart-main" v-loading="chartLoading" element-loading-background="transparent">
       <div class="chart-legend">
@@ -744,6 +896,87 @@ function handleDetailClick() {
         <div ref="chartRef" class="chart-canvas"></div>
       </div>
     </main>
+
+    <el-dialog
+      v-model="settingsDialogVisible"
+      title="设备设置"
+      width="900px"
+      class="board-settings-dialog"
+      modal-class="board-settings-overlay"
+      append-to-body
+      destroy-on-close>
+      <el-form label-width="90px" class="board-settings-form">
+        <el-form-item label="设备名称" required>
+          <el-input
+            v-model="deviceNameDraft"
+            maxlength="50"
+            show-word-limit
+            placeholder="请输入设备名称" />
+        </el-form-item>
+      </el-form>
+
+      <div class="board-settings-section">
+        <div class="board-settings-section__title">曲线量程设置</div>
+        <ZmTable :data="rangeSettingDrafts" :loading="false" :max-height="420" :show-border="true">
+          <ZmTableColumn label="曲线" min-width="180">
+            <template #default="{ row }">
+              <div class="flex items-center gap-2 min-w-0">
+                <span
+                  class="inline-block size-2.5 rounded-full shrink-0"
+                  :style="{ backgroundColor: row.color }"></span>
+                <span class="board-settings-line-name">{{ row.name }}</span>
+              </div>
+            </template>
+          </ZmTableColumn>
+          <ZmTableColumn label="最小值" width="180">
+            <template #default="{ row }">
+              <el-input-number
+                v-model="row.minValue"
+                class="!w-full"
+                :controls="false"
+                :placeholder="formatChartValue(getSeriesRange(row.name).labelMin)" />
+            </template>
+          </ZmTableColumn>
+          <ZmTableColumn label="最大值" width="180">
+            <template #default="{ row }">
+              <el-input-number
+                v-model="row.maxValue"
+                class="!w-full"
+                :controls="false"
+                :placeholder="formatChartValue(getSeriesRange(row.name).labelMax)" />
+            </template>
+          </ZmTableColumn>
+          <ZmTableColumn label="当前使用范围" width="170">
+            <template #default="{ row }">
+              <span class="board-settings-range-text">
+                {{ formatChartValue(getSeriesRange(row.name).labelMin) }}
+                -
+                {{ formatChartValue(getSeriesRange(row.name).labelMax) }}
+              </span>
+            </template>
+          </ZmTableColumn>
+          <ZmTableColumn label="操作" width="90">
+            <template #default="{ row }">
+              <el-button size="small" link type="primary" @click="clearRangeDraft(row)">
+                清空
+              </el-button>
+            </template>
+          </ZmTableColumn>
+        </ZmTable>
+      </div>
+
+      <template #footer>
+        <div class="flex justify-between">
+          <el-button @click="handleRangeDialogReset">全部清空</el-button>
+          <div class="flex gap-2">
+            <el-button @click="settingsDialogVisible = false">取消</el-button>
+            <el-button type="primary" :loading="settingsSaving" @click="handleSettingsSave">
+              保存
+            </el-button>
+          </div>
+        </div>
+      </template>
+    </el-dialog>
   </div>
 </template>
 <style scoped>
@@ -791,6 +1024,13 @@ function handleDetailClick() {
   align-items: center;
 }
 
+.chart-header__actions {
+  display: flex;
+  flex-shrink: 0;
+  gap: 10px;
+  align-items: center;
+}
+
 .title-icon {
   width: 4px;
   height: 16px;
@@ -799,6 +1039,21 @@ function handleDetailClick() {
   box-shadow: 0 0 8px #22d3ee;
 }
 
+.board-settings-form {
+  padding-right: 16px;
+}
+
+.board-settings-section {
+  margin-top: 14px;
+}
+
+.board-settings-section__title {
+  margin-bottom: 10px;
+  font-size: 14px;
+  font-weight: 700;
+  color: #334155;
+}
+
 .chart-main {
   display: grid;
   grid-template-columns: auto minmax(0, 1fr);

+ 37 - 7
src/views/oli-connection/monitoring-board/index.vue

@@ -20,6 +20,7 @@ interface DeviceData {
   lastInlineTime: string
   deviceCode: string
   deviceName: string
+  otherName: string
   deptName: string
   vehicleName: string
   carOnline: string
@@ -95,6 +96,14 @@ async function loadDeptOptions() {
 const deviceLoading = ref(false)
 const deviceOptions = ref<any[]>([])
 
+function getDeviceDisplayName(device: { deviceName?: string; otherName?: string }) {
+  return device.otherName || device.deviceName || ''
+}
+
+function getDeviceLabel(device: { deviceCode?: string; deviceName?: string; otherName?: string }) {
+  return `${device.deviceCode || ''}-${getDeviceDisplayName(device)}`
+}
+
 async function loadDeviceOptions() {
   if (!deviceQuery.value.deptId) return
 
@@ -111,7 +120,7 @@ async function loadDeviceOptions() {
     //   pageSize: 100
     // })
     deviceOptions.value = data.list.map((item: any) => ({
-      label: item.deviceCode + '-' + item.deviceName,
+      label: getDeviceLabel(item),
       value: item.id,
       raw: item
     }))
@@ -211,6 +220,7 @@ async function handleDeviceChange(selectedIds: number[]) {
         carOnline: option.raw.carOnline ?? '',
         deviceCode: option.raw.deviceCode,
         deviceName: option.raw.deviceName,
+        otherName: option.raw.otherName ?? '',
         mqttUrl: option.raw.mqttUrl
       })
     }
@@ -269,6 +279,19 @@ function handleTimeChange() {
   isRealTime.value = false
 }
 
+function handleOtherNameUpdated(payload: { id: number; otherName: string }) {
+  const device = deviceList.value.find((item) => item.id === payload.id)
+  if (device) {
+    device.otherName = payload.otherName
+  }
+
+  const option = deviceOptions.value.find((item) => item.value === payload.id)
+  if (option) {
+    option.raw.otherName = payload.otherName
+    option.label = getDeviceLabel(option.raw)
+  }
+}
+
 const token = ref('')
 const showSearchDialog = ref(false)
 
@@ -340,7 +363,9 @@ onMounted(() => {
       <div class="monitor-board-toolbar">
         <div class="monitor-board-status">
           <span>当前第 {{ pageIndex + 1 }} 组 / 共 {{ totalPages }} 组</span>
-          <span v-if="mainCard">主看板:{{ mainCard.deviceCode }}-{{ mainCard.deviceName }}</span>
+          <span v-if="mainCard">
+            主看板:{{ mainCard.deviceCode }}-{{ getDeviceDisplayName(mainCard) }}
+          </span>
         </div>
         <div class="flex items-center gap-3">
           <el-button class="custom-btn reset-btn" :disabled="pageIndex === 0" @click="goPrevGroup">
@@ -366,7 +391,8 @@ onMounted(() => {
               v-bind="sideCards[0]"
               :date="query.time"
               :is-real-time="isRealTime"
-              :token="token" />
+              :token="token"
+              @other-name-updated="handleOtherNameUpdated" />
           </div>
 
           <div
@@ -378,7 +404,8 @@ onMounted(() => {
               v-bind="sideCards[1]"
               :date="query.time"
               :is-real-time="isRealTime"
-              :token="token" />
+              :token="token"
+              @other-name-updated="handleOtherNameUpdated" />
           </div>
 
           <div class="monitor-card-shell monitor-card-main" @click="setMainCard(mainCard)">
@@ -387,7 +414,8 @@ onMounted(() => {
               v-bind="mainCard"
               :date="query.time"
               :is-real-time="isRealTime"
-              :token="token" />
+              :token="token"
+              @other-name-updated="handleOtherNameUpdated" />
           </div>
 
           <div
@@ -399,7 +427,8 @@ onMounted(() => {
               v-bind="sideCards[2]"
               :date="query.time"
               :is-real-time="isRealTime"
-              :token="token" />
+              :token="token"
+              @other-name-updated="handleOtherNameUpdated" />
           </div>
 
           <div
@@ -411,7 +440,8 @@ onMounted(() => {
               v-bind="sideCards[3]"
               :date="query.time"
               :is-real-time="isRealTime"
-              :token="token" />
+              :token="token"
+              @other-name-updated="handleOtherNameUpdated" />
           </div>
         </div>
       </div>

+ 0 - 778
src/views/oli-connection/monitoring/detail copy.vue

@@ -1,778 +0,0 @@
-<script setup lang="ts">
-import { Odometer, CircleCheckFilled, CircleCloseFilled } from '@element-plus/icons-vue'
-import { IotDeviceApi } from '@/api/pms/device'
-import dayjs from 'dayjs'
-import { rangeShortcuts } from '@/utils/formatTime'
-import { IotStatApi, cancelAllRequests } from '@/api/pms/stat'
-
-import * as echarts from 'echarts'
-import { colors } from '@/utils/td-color'
-import { useSocketBus } from '@/utils/useSocketBus'
-
-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 || ''
-})
-
-const { open: connect, onAny, close } = useSocketBus(data.value.deviceCode as string)
-
-onAny((msg) => {
-  if (!Array.isArray(msg) || msg.length === 0) return
-
-  const valueMap = new Map<string, number>()
-
-  for (const item of msg) {
-    const { identity, modelName, readTime, logValue } = item
-
-    const value = logValue ? Number(logValue) : 0
-
-    if (identity) {
-      valueMap.set(identity, value)
-    }
-
-    if (modelName && chartData.value[modelName]) {
-      chartData.value[modelName].push({
-        ts: dayjs(readTime).valueOf(),
-        value
-      })
-
-      updateSingleSeries(modelName)
-    }
-  }
-
-  const updateDimensions = (list) => {
-    list.forEach((item) => {
-      const v = valueMap.get(item.identifier)
-      if (v !== undefined) {
-        item.value = v
-      }
-    })
-  }
-
-  updateDimensions(dimensions.value)
-  updateDimensions(gatewayDimensions.value)
-  updateDimensions(carDimensions.value)
-
-  // 3️⃣ 统一一次调用
-  genderIntervalArr()
-})
-
-interface Dimensions {
-  identifier: string
-  name: string
-  value: string
-  color?: string
-  response?: boolean
-}
-
-const dimensions = ref<Dimensions[]>([])
-const gatewayDimensions = ref<Dimensions[]>([])
-const carDimensions = ref<Dimensions[]>([])
-
-const disabledDimensions = ref<string[]>(['online', 'vehicle_name'])
-
-interface SelectedDimension {
-  [key: Dimensions['name']]: boolean
-}
-
-const selectedDimension = ref<SelectedDimension>({})
-
-const dimensionLoading = ref(false)
-
-const disabledDimension = computed(() => (identifier: string) => {
-  const response = dimensions.value.find((item) => item.identifier === identifier)?.response
-
-  return { disabled: disabledDimensions.value.includes(identifier) || response, loading: response }
-})
-
-async function loadDimensions() {
-  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((item) => ({
-      identifier: item.identifier,
-      name: item.modelName,
-      value: item.value
-    }))
-  const car = (((await IotDeviceApi.getIotDeviceZHBDTds(Number(query.id))) as any[]) ?? [])
-    .sort((a, b) => b.modelOrder - a.modelOrder)
-    .map((item) => ({
-      identifier: item.identifier,
-      name: item.modelName,
-      value: item.value
-    }))
-
-  dimensions.value = [...gateway, ...car]
-    .filter((item) => !disabledDimensions.value.includes(item.identifier))
-    .map((item, index) => ({
-      ...item,
-      color: colors[index]
-    }))
-
-  gatewayDimensions.value = gateway
-  carDimensions.value = car
-
-  selectedDimension.value = Object.fromEntries(dimensions.value.map((item) => [item.name, false]))
-
-  selectedDimension.value[dimensions.value[0].name] = true
-
-  dimensionLoading.value = false
-}
-
-// async function updateDimensionValues() {
-//   if (!query.id) return
-
-//   try {
-//     // 1. 并行获取最新数据
-//     const [gatewayRes, carRes] = await Promise.all([
-//       IotDeviceApi.getIotDeviceTds(Number(query.id)),
-//       IotDeviceApi.getIotDeviceZHBDTds(Number(query.id))
-//     ])
-
-//     // 2. 创建一个 Map 用于快速查找 (Identifier -> Value)
-//     // 这样可以将复杂度从 O(N*M) 降低到 O(N)
-//     const newValueMap = new Map<string, any>()
-
-//     const addToMap = (data: any[]) => {
-//       if (!data) return
-//       data.forEach((item) => {
-//         if (item.identifier) {
-//           newValueMap.set(item.identifier, item.value)
-//         }
-//       })
-//     }
-
-//     addToMap(gatewayRes as any[])
-//     addToMap(carRes as any[])
-
-//     // 3. 更新 dimensions.value (保留了之前的 color 和其他属性)
-//     dimensions.value.forEach((item) => {
-//       if (newValueMap.has(item.identifier)) {
-//         item.value = newValueMap.get(item.identifier)
-//       }
-//     })
-
-//     // 4. 如果还需要同步更新 gatewayDimensions 和 carDimensions
-//     // (假设这些是引用类型,如果它们引用的是同一个对象,上面更新 dimensions 时可能已经同步了。
-//     // 如果它们是独立的对象数组,则需要显式更新)
-
-//     // 更新 Gateway 原始列表
-//     gatewayDimensions.value.forEach((item) => {
-//       if (newValueMap.has(item.identifier)) {
-//         item.value = newValueMap.get(item.identifier)
-//       }
-//     })
-
-//     // 更新 Car 原始列表
-//     carDimensions.value.forEach((item) => {
-//       if (newValueMap.has(item.identifier)) {
-//         item.value = newValueMap.get(item.identifier)
-//       }
-//     })
-//   } catch (error) {
-//     console.error('Failed to update dimension values:', error)
-//   }
-// }
-
-const selectedDate = ref<string[]>([
-  dayjs().subtract(5, 'minute').format('YYYY-MM-DD HH:mm:ss'),
-  dayjs().format('YYYY-MM-DD HH:mm:ss')
-])
-
-interface ChartData {
-  [key: Dimensions['name']]: { ts: number; value: number }[]
-}
-
-const chartData = ref<ChartData>({})
-
-let intervalArr = ref<number[]>([])
-let maxInterval = ref(0)
-let minInterval = ref(0)
-
-const chartRef = ref<HTMLDivElement | null>(null)
-let chart: echarts.ECharts | null = null
-
-// const genderIntervalArrDebounce = useDebounceFn(
-//   (init: boolean = false) => genderIntervalArr(init),
-//   300
-// )
-
-function genderIntervalArr(init: boolean = false) {
-  const values: number[] = []
-
-  for (const [key, value] of Object.entries(selectedDimension.value)) {
-    if (value) {
-      values.push(...(chartData.value[key]?.map((item) => item.value) ?? []))
-    }
-  }
-
-  const maxVal = values.length === 0 ? 10000 : Math.max(...values)
-  const minVal = values.length === 0 ? 0 : Math.min(...values) > 0 ? 0 : Math.min(...values)
-
-  const maxDigits = (Math.floor(maxVal) + '').length
-  const minDigits = minVal === 0 ? 0 : (Math.floor(Math.abs(minVal)) + '').length
-
-  const interval = Math.max(maxDigits, minDigits)
-
-  maxInterval.value = interval
-  minInterval.value = minDigits
-
-  intervalArr.value = [0]
-  for (let i = 1; i <= interval; i++) {
-    intervalArr.value.push(Math.pow(10, i))
-  }
-
-  if (!init) {
-    chart?.setOption({
-      yAxis: {
-        min: -minInterval.value,
-        max: maxInterval.value
-      }
-    })
-  }
-}
-
-function chartInit() {
-  if (!chart) return
-
-  chart.on('legendselectchanged', (params: any) => {
-    selectedDimension.value = params.selected
-  })
-
-  window.addEventListener('resize', () => {
-    if (chart) chart.resize()
-  })
-}
-
-function render() {
-  if (!chartRef.value) return
-
-  if (!chart) chart = echarts.init(chartRef.value, undefined, { renderer: 'canvas' })
-
-  chartInit()
-
-  genderIntervalArr(true)
-
-  chart.setOption({
-    animation: true,
-    animationDuration: 200,
-    animationEasing: 'linear',
-    animationDurationUpdate: 200,
-    animationEasingUpdate: 'linear',
-    grid: {
-      left: '6%',
-      top: '5%',
-      right: '6%',
-      bottom: '12%'
-    },
-    tooltip: {
-      trigger: 'axis',
-      axisPointer: {
-        type: 'line'
-      },
-      formatter: (params) => {
-        let d = `${params[0].axisValueLabel}<br>`
-        const exist: string[] = []
-        params = params.filter((el) => {
-          if (exist.includes(el.seriesName)) return false
-          exist.push(el.seriesName)
-          return true
-        })
-        let item = params.map(
-          (el) => `<div class="flex items-center justify-between mt-1">
-            <span>${el.marker} ${el.seriesName}</span>
-            <span>${el.value[2]?.toFixed(2)}</span>
-          </div>`
-        )
-
-        return d + item.join('')
-      }
-    },
-    xAxis: {
-      type: 'time',
-      axisLabel: {
-        formatter: (v) => dayjs(v).format('YYYY-MM-DD\nHH:mm:ss'),
-        rotate: 0,
-        align: 'left'
-      }
-    },
-    dataZoom: [
-      { type: 'inside', xAxisIndex: 0 },
-      { type: 'slider', xAxisIndex: 0 }
-    ],
-    yAxis: {
-      type: 'value',
-      min: -minInterval.value,
-      max: maxInterval.value,
-      interval: 1,
-      axisLabel: {
-        formatter: (v) => {
-          const num = v === 0 ? 0 : v > 0 ? Math.pow(10, v) : -Math.pow(10, -v)
-
-          return num.toLocaleString()
-        }
-      },
-      show: false
-    },
-    legend: {
-      data: dimensions.value.map((item) => item.name),
-      selected: selectedDimension.value,
-      show: false
-    },
-    // series: dimensions.value.map((item) => ({
-    //   name: item.name,
-    //   type: 'line',
-    //   smooth: true,
-    //   showSymbol: false,
-    //   color: item.color,
-    //   data: [] // 占位数组
-    // }))
-    series: dimensions.value.map((item) => ({
-      name: item.name,
-      type: 'line',
-
-      smooth: 0.2,
-
-      showSymbol: false,
-
-      endLabel: {
-        show: true,
-        formatter: (params) => params.value[2]?.toFixed(2),
-        offset: [6, 0],
-        color: item.color,
-        fontSize: 12
-      },
-
-      emphasis: {
-        focus: 'series'
-      },
-
-      lineStyle: {
-        width: 2
-      },
-
-      color: item.color,
-      data: [] // 占位数组
-    }))
-  })
-}
-
-function mapData({ value, ts }) {
-  if (!value) return [ts, 0, 0]
-
-  const isPositive = value > 0
-  const absItem = Math.abs(value)
-
-  const min_value = Math.max(...intervalArr.value.filter((v) => v <= absItem))
-  const min_index = intervalArr.value.findIndex((v) => v === min_value)
-
-  const new_value =
-    (absItem - min_value) / (intervalArr.value[min_index + 1] - intervalArr.value[min_index]) +
-    min_index
-
-  return [ts, isPositive ? new_value : -new_value, value]
-}
-
-function updateSingleSeries(name: string) {
-  if (!chart) render()
-  if (!chart) return
-
-  const idx = dimensions.value.findIndex((item) => item.name === name)
-  if (idx === -1) return
-
-  const data = chartData.value[name].map((v) => mapData(v))
-
-  chart.setOption({
-    series: [{ name, data }]
-  })
-}
-
-const lastTsMap = ref<Record<Dimensions['name'], number>>({})
-
-// async function fetchIncrementData() {
-//   for (const item of dimensions.value) {
-//     const { identifier, name } = item
-
-//     const lastTs = lastTsMap.value[name]
-//     if (!lastTs) continue
-
-//     item.response = true
-
-//     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)
-//           .map((item) => ({ ts: item.ts, value: item.value }))
-//         // push 到本地
-//         chartData.value[name].push(...sorted)
-//         // 更新 lastTs
-//         lastTsMap.value[identifier] = sorted.at(-1).ts
-
-//         // 更新图表
-//         updateSingleSeries(name)
-//       })
-//       .finally(() => {
-//         item.response = false
-//       })
-//   }
-// }
-
-// const timer = ref<NodeJS.Timeout | null>(null)
-
-// function startAutoFetch() {
-//   timer.value = setInterval(() => {
-//     updateDimensionValues()
-//     fetchIncrementData()
-//   }, 10000)
-// }
-
-// function stopAutoFetch() {
-//   cancelAllRequests()
-//   if (timer.value) clearInterval(timer.value)
-//   timer.value = null
-// }
-
-const chartLoading = ref(false)
-
-async function initLoadChartData(real_time: boolean = true) {
-  if (!dimensions.value.length) return
-
-  chartData.value = Object.fromEntries(dimensions.value.map((item) => [item.name, []]))
-
-  chartLoading.value = true
-
-  dimensions.value = dimensions.value.map((item) => {
-    item.response = true
-    return item
-  })
-
-  for (const item of dimensions.value) {
-    const { identifier, name } = item
-    try {
-      const res = await IotStatApi.getDeviceInfoChart(
-        data.value.deviceCode,
-        identifier,
-        selectedDate.value[0],
-        selectedDate.value[1]
-      )
-
-      const sorted = res
-        .sort((a, b) => a.ts - b.ts)
-        .map((item) => ({ ts: item.ts, value: item.value }))
-
-      chartData.value[name] = sorted
-
-      lastTsMap.value[name] = sorted.at(-1)?.ts ?? 0
-
-      updateSingleSeries(name)
-
-      chartLoading.value = false
-
-      if (selectedDimension.value[name]) {
-        genderIntervalArr()
-      }
-    } finally {
-      item.response = false
-    }
-  }
-
-  if (real_time) {
-    // startAutoFetch()
-    connect()
-  }
-}
-
-async function initfn(load: boolean = true, real_time: boolean = true) {
-  if (load) await loadDimensions()
-  render()
-  initLoadChartData(real_time)
-}
-
-onMounted(() => {
-  initfn()
-})
-
-function reset() {
-  cancelAllRequests().then(() => {
-    selectedDate.value = [
-      dayjs().subtract(5, 'minute').format('YYYY-MM-DD HH:mm:ss'),
-      dayjs().format('YYYY-MM-DD HH:mm:ss')
-    ]
-
-    close()
-    // stopAutoFetch()
-    if (chart) chart.clear()
-    initfn(false)
-  })
-}
-
-function handleDateChange() {
-  cancelAllRequests().then(() => {
-    close()
-    // stopAutoFetch()
-    if (chart) chart.clear()
-    initfn(false, false)
-  })
-}
-
-function handleClickSpec(modelName: string) {
-  selectedDimension.value[modelName] = !selectedDimension.value[modelName]
-  chart?.setOption({
-    legend: {
-      selected: selectedDimension.value
-    }
-  })
-  chart?.resize()
-  genderIntervalArr()
-}
-
-const exportChart = () => {
-  if (!chart) return
-  let img = new Image()
-  img.src = chart.getDataURL({
-    type: 'png',
-    pixelRatio: 1,
-    backgroundColor: '#fff'
-  })
-
-  img.onload = function () {
-    let canvas = document.createElement('canvas')
-    canvas.width = img.width
-    canvas.height = img.height
-    let ctx = canvas.getContext('2d')
-    ctx?.drawImage(img, 0, 0)
-    let dataURL = canvas.toDataURL('image/png')
-
-    let a = document.createElement('a')
-
-    let event = new MouseEvent('click')
-
-    a.href = dataURL
-    a.download = `${data.value.deviceName}-设备监控-${dayjs().format('YYYY-MM-DD HH:mm:ss')}.png`
-    a.dispatchEvent(event)
-  }
-}
-
-const maxmin = computed(() => {
-  if (!dimensions.value.length) return []
-  return dimensions.value
-    .filter((v) => selectedDimension.value[v.name])
-    .map((v) => ({
-      name: v.name,
-      color: v.color,
-      max: Math.max(...(chartData.value[v.name]?.map((v) => v.value) ?? [])).toFixed(2),
-      min: Math.min(...(chartData.value[v.name]?.map((v) => v.value) ?? [])).toFixed(2)
-    }))
-})
-
-onUnmounted(() => {
-  // stopAutoFetch()
-  close()
-
-  window.removeEventListener('resize', () => {
-    if (chart) chart.resize()
-  })
-})
-</script>
-
-<template>
-  <div
-    class="w-full bg-gradient-to-r from-blue-100 to-white rounded-lg p-4 shadow"
-    id="td-device-info"
-  >
-    <h2 class="flex items-center gap-2">
-      <el-icon class="text-sky!"><Odometer /></el-icon> 设备基础信息
-    </h2>
-    <el-form size="default" label-position="top" class="mt-4 grid grid-cols-4 gap-2">
-      <el-form-item label="资产编码"> {{ data.deviceCode }} </el-form-item>
-      <el-form-item label="设备类别"> {{ data.deviceName }} </el-form-item>
-      <el-form-item label="所在部门"> {{ data.dept }} </el-form-item>
-      <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-5 gap-2 min-h-30"
-      id="dimension"
-    >
-      <button
-        v-for="item in gatewayDimensions"
-        :key="item.identifier"
-        class="border-none h-12 bg-white dark:bg-[#1d1e1f] rounded-lg px-8 shadow flex items-center hover:scale-103 transition-all cursor-pointer"
-        :class="{ 'bg-blue-200': selectedDimension[item.name] }"
-        :disabled="disabledDimension(item.identifier).disabled"
-        @click="handleClickSpec(item.name)"
-      >
-        <span class="text-sm text-[var(--el-text-color-regular)] flex items-center gap-2 relative">
-          <!-- <i
-            v-show="disabledDimension(item.identifier).loading"
-            class="i-line-md:loading-loop size-5 absolute -left-6"
-          ></i> -->
-          {{ item.name }}
-        </span>
-        <!-- <span class="text-lg font-medium ms-a">{{ item.value }}</span> -->
-        <animated-count-to :value="item.value" will-change class="text-lg font-medium ms-a" />
-      </button>
-    </div>
-  </div>
-  <div
-    v-if="carDimensions.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-5 gap-2 min-h-30" id="dimension">
-      <button
-        v-for="item in carDimensions"
-        :key="item.identifier"
-        class="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': selectedDimension[item.name] }"
-        :disabled="disabledDimension(item.identifier).disabled"
-        @click="handleClickSpec(item.name)"
-      >
-        <span class="text-sm text-[var(--el-text-color-regular)] flex items-center gap-2">
-          <!-- <i
-            v-show="disabledDimension(item.identifier).loading"
-            class="i-line-md:loading-loop size-5"
-          ></i> -->
-          {{ 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="selectedDate"
-          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 class="flex h-160 mt-4">
-      <div class="flex gap-1">
-        <button
-          v-for="item of maxmin"
-          :key="item.name"
-          class="w-8 h-full flex flex-col items-center justify-between py-2 gap-1 rounded bg-transparent border-none"
-          @click="handleClickSpec(item.name)"
-        >
-          <span class="[writing-mode:sideways-lr]">{{ item.max }}</span>
-          <div class="flex-1 w-1" :style="{ backgroundColor: item.color }"></div>
-          <span class="[writing-mode:sideways-lr]">{{ item.name }}</span>
-          <div class="flex-1 w-1" :style="{ backgroundColor: item.color }"></div>
-          <span class="[writing-mode:sideways-lr]">{{ item.min }}</span>
-        </button>
-      </div>
-      <div class="flex flex-1">
-        <div
-          v-loading="chartLoading"
-          element-loading-background="transparent"
-          ref="chartRef"
-          class="flex-1 h-full"
-        >
-        </div>
-      </div>
-    </div>
-  </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>