Przeglądaj źródła

调整看板查询设备调用接口

Zimo 5 dni temu
rodzic
commit
f476309812

+ 0 - 724
src/views/oli-connection/monitoring-board/chart1.vue

@@ -1,724 +0,0 @@
-<script lang="ts" setup>
-import { IotDeviceApi } from '@/api/pms/device'
-import { cancelAllRequests, IotStatApi } from '@/api/pms/stat'
-import { Dimensions, formatIotValue, useSocketBus } from '@/utils/useSocketBus'
-import dayjs from 'dayjs'
-import * as echarts from 'echarts'
-import { neonColors } from '@/utils/td-color'
-
-const props = defineProps({
-  id: {
-    type: Number,
-    required: true
-  },
-  deviceCode: {
-    type: String,
-    required: true
-  },
-  deviceName: {
-    type: String,
-    required: true
-  },
-  mqttUrl: {
-    type: String,
-    required: true
-  },
-  ifInline: {
-    type: String,
-    required: true
-  },
-  lastInlineTime: {
-    type: String,
-    required: true
-  },
-  deptName: {
-    type: String,
-    required: true
-  },
-  vehicleName: {
-    type: String,
-    required: true
-  },
-  carOnline: {
-    type: String,
-    required: true
-  },
-  // isRealTime: {
-  //   type: Boolean,
-  //   default: true
-  // },
-  date: {
-    type: Array as PropType<Array<string>>,
-    required: true
-  }
-})
-
-const dimensions = ref<Omit<Dimensions, 'color' | 'bgHover' | 'bgActive'>[]>([])
-const selectedDimension = ref<Record<string, boolean>>({})
-
-const { open: connect, onAny, close } = useSocketBus(props.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, logValue, remark } = item
-
-    const value = logValue ? Number(logValue) : 0
-
-    if (identity) {
-      valueMap.set(identity, value)
-    }
-
-    const modelName = dimensions.value.find((item) => item.identifier === identity)?.name
-
-    if (modelName && chartData.value[modelName]) {
-      chartData.value[modelName].push({
-        ts: dayjs.unix(remark).valueOf(),
-        value
-      })
-
-      updateSingleSeries(modelName)
-    }
-  }
-})
-
-async function loadDimensions() {
-  if (!props.id) return
-  try {
-    dimensions.value = (((await IotDeviceApi.getIotDeviceTds(Number(props.id))) as any[]) ?? [])
-      .sort((a, b) => b.modelOrder - a.modelOrder)
-      .map((item) => {
-        const { value, suffix, isText } = formatIotValue(item.value)
-        return {
-          identifier: item.identifier,
-          name: item.modelName,
-          value: value,
-          suffix: suffix,
-          isText: isText,
-          response: false
-        }
-      })
-      .filter((item) => item.isText === false)
-
-    selectedDimension.value = Object.fromEntries(dimensions.value.map((item) => [item.name, true]))
-  } catch (error) {
-    console.error(error)
-  }
-}
-
-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
-
-function genderIntervalArr(init: boolean = false) {
-  // 1. 使用正负无穷大初始化,避免先把所有数存入数组
-  let maxVal = -Infinity
-  let minVal = Infinity
-  let hasData = false
-
-  // 2. 直接遍历数据查找最值 (不使用 spread ...)
-  for (const [key, value] of Object.entries(selectedDimension.value)) {
-    if (value) {
-      const dataset = chartData.value[key]
-      if (dataset && dataset.length > 0) {
-        hasData = true
-        // 使用循环代替 ...spread
-        for (const item of dataset) {
-          const val = item.value
-          if (val > maxVal) maxVal = val
-          if (val < minVal) minVal = val
-        }
-      }
-    }
-  }
-
-  // 3. 处理无数据的默认情况
-  if (!hasData) {
-    maxVal = 10000
-    minVal = 0
-  } else {
-    // 保持你原有的逻辑:如果最小值大于0,则归零
-    minVal = minVal > 0 ? 0 : minVal
-  }
-
-  // 4. 计算位数逻辑 (保持不变)
-  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 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) => {
-    // 1. 同步选中状态
-    selectedDimension.value = params.selected
-    const clickedModelName = params.name
-    const isSelected = params.selected[clickedModelName]
-
-    const oldMax = maxInterval.value
-    const oldMin = minInterval.value
-
-    genderIntervalArr()
-
-    const isScaleChanged = oldMax !== maxInterval.value || oldMin !== minInterval.value
-
-    if (isScaleChanged) {
-      Object.keys(selectedDimension.value).forEach((name) => {
-        if (selectedDimension.value[name]) {
-          updateSingleSeries(name)
-        }
-      })
-    } else {
-      if (isSelected) {
-        updateSingleSeries(clickedModelName)
-      }
-    }
-  })
-
-  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({
-    color: neonColors,
-    animation: true,
-    animationDuration: 200,
-    animationEasing: 'linear',
-    animationDurationUpdate: 200,
-    animationEasingUpdate: 'linear',
-    grid: {
-      left: '3%',
-      top: '60px',
-      right: '6%',
-      bottom: '10%',
-      containLabel: true,
-      show: false
-    },
-    tooltip: {
-      trigger: 'axis',
-      confine: true,
-      enterable: true,
-      className: 'echarts-tooltip-scroll',
-      extraCssText:
-        'max-height: 300px; overflow-y: auto; pointer-events: auto; border-radius: 4px;',
-      backgroundColor: 'rgba(11, 17, 33, 0.95)',
-      borderColor: '#22d3ee',
-      borderWidth: 1,
-      textStyle: {
-        color: '#e2e8f0'
-      },
-      axisPointer: {
-        type: 'cross',
-        label: { backgroundColor: '#22d3ee', color: '#000' },
-        lineStyle: { color: 'rgba(255,255,255,0.3)', type: 'dashed' }
-      },
-      formatter: (params: any) => {
-        let d = `<div style="font-weight:bold; border-bottom:1px solid rgba(255,255,255,0.1); padding-bottom:5px; margin-bottom:5px;">${params[0].axisValueLabel}</div>`
-        const exist: string[] = []
-        params = params.filter((el: any) => {
-          if (exist.includes(el.seriesName)) return false
-          exist.push(el.seriesName)
-          return true
-        })
-
-        // 优化列表显示,圆点使用原本的颜色
-        let item = params.map(
-          (
-            el: any
-          ) => `<div class="flex items-center justify-between mt-1" style="font-size:12px; min-width: 180px;">
-            <span style="display:flex; align-items:center;">
-                <span style="display:inline-block;width:8px;height:8px;border-radius:50%;background-color:${el.color};margin-right:6px;"></span>
-                <span style="color:#cbd5e1">${el.seriesName}</span>
-            </span>
-            <span style="color:#fff; font-weight:bold; margin-left:10px;">${el.value[2]?.toFixed(2)}</span>
-          </div>`
-        )
-
-        return d + item.join('')
-      }
-    },
-    xAxis: {
-      type: 'time',
-      boundaryGap: ['0%', '25%'],
-      axisLabel: {
-        formatter: (v) => dayjs(v).format('YYYY-MM-DD\nHH:mm:ss'),
-        rotate: 0,
-        align: 'center',
-        color: '#94a3b8',
-        fontSize: 11
-      },
-      splitLine: {
-        show: false,
-        lineStyle: { color: 'rgba(255,255,255,0.5)', type: 'dashed' }
-      }
-    },
-    dataZoom: [
-      { type: 'inside', xAxisIndex: 0 },
-      {
-        type: 'slider',
-        xAxisIndex: 0,
-        height: 20,
-        bottom: 10,
-        borderColor: 'transparent',
-        backgroundColor: 'rgba(255,255,255,0.05)',
-        fillerColor: 'rgba(34,211,238,0.2)',
-        handleStyle: {
-          color: '#22d3ee',
-          borderColor: '#22d3ee'
-        },
-        labelFormatter: (value: any) => {
-          return dayjs(value).format('YYYY-MM-DD\nHH:mm:ss')
-        },
-        textStyle: {
-          color: '#94a3b8',
-          fontSize: 10,
-          lineHeight: 12
-        }
-      }
-    ],
-    yAxis: {
-      type: 'value',
-      min: -minInterval.value,
-      max: maxInterval.value,
-      interval: 1,
-      axisLabel: {
-        color: '#94a3b8',
-        formatter: (v) => {
-          const num = v === 0 ? 0 : v > 0 ? Math.pow(10, v) : -Math.pow(10, -v)
-          if (Math.abs(num) >= 10000) return (num / 10000).toFixed(0) + 'w'
-          if (Math.abs(num) >= 1000) return (num / 1000).toFixed(0) + 'k'
-          return num.toLocaleString()
-        }
-      },
-      show: true,
-      splitLine: {
-        show: true,
-        lineStyle: {
-          color: 'rgba(255,255,255,0.05)',
-          type: 'dashed'
-        }
-      },
-      axisPointer: {
-        show: true,
-        snap: false, // 必须设为 false,才能平滑显示小数部分的真实值
-        label: {
-          show: true,
-          backgroundColor: '#22d3ee', // 青色背景
-          color: '#000', // 黑色文字
-          fontWeight: 'bold',
-          precision: 2, // 保证精度
-
-          // --- 具体的实现逻辑 ---
-          formatter: (params: any) => {
-            const val = params.value // 这里拿到的是索引值,比如 4.21
-            if (val === 0) return '0.00'
-
-            // A. 处理正负号
-            const sign = val >= 0 ? 1 : -1
-            const absVal = Math.abs(val)
-
-            // B. 分离 整数部分(区间下标) 和 小数部分(区间内百分比)
-            const idx = Math.floor(absVal)
-
-            const percent = absVal - idx
-
-            // C. 安全检查:如果 intervalArr 还没生成,直接返回指数值
-            if (!intervalArr.value || intervalArr.value.length === 0) {
-              return (sign * Math.pow(10, absVal)).toFixed(2)
-            }
-
-            // D. 获取该区间的真实数值范围
-            // 例如 idx=2, 对应 intervalArr[2]=100, intervalArr[3]=1000
-            const min = intervalArr.value[idx]
-            // 如果到了最后一个区间,或者越界,就默认下一级是当前的10倍(防止报错)
-            const max =
-              intervalArr.value[idx + 1] !== undefined ? intervalArr.value[idx + 1] : min * 10
-
-            // E. 反向线性插值公式
-            // 真实值 = 下界 + (上下界之差 * 百分比)
-            const realVal = min + (max - min) * percent
-
-            // F. 加上符号并格式化
-            return (realVal * sign).toFixed(2)
-          }
-        }
-      }
-    },
-    legend: {
-      type: 'scroll', // 开启滚动,防止遮挡
-      top: 10,
-
-      left: 'center',
-      width: '90%',
-
-      textStyle: {
-        color: '#e2e8f0', // 亮白色
-        fontSize: 12
-      },
-      pageIconColor: '#22d3ee',
-      pageIconInactiveColor: '#475569',
-      pageTextStyle: { color: '#fff' },
-      data: dimensions.value.map((item) => item.name),
-      selected: selectedDimension.value,
-      show: true
-    },
-    // legend: {
-    //   data: dimensions.value.map((item) => item.name),
-    //   selected: selectedDimension.value,
-    //   show: true
-    // },
-    series: dimensions.value.map((item) => ({
-      name: item.name,
-      type: 'line',
-      smooth: 0.3,
-      showSymbol: false,
-
-      endLabel: {
-        show: true,
-        formatter: (params) => params.value[2]?.toFixed(2),
-        offset: [4, 0],
-        color: '#fff',
-        backgroundColor: 'auto',
-        padding: [2, 6],
-        borderRadius: 4,
-        fontSize: 11,
-        fontWeight: 'bold'
-      },
-
-      emphasis: {
-        focus: 'series',
-        lineStyle: { width: 4 }
-      },
-
-      lineStyle: {
-        width: 3,
-        shadowColor: 'rgba(0, 0, 0, 0.5)',
-        shadowBlur: 10,
-        shadowOffsetY: 5
-      },
-
-      data: [] // 占位数组
-    }))
-  })
-}
-
-function mapData({ value, ts }) {
-  if (value === null || value === undefined || value === 0) return [ts, 0, 0]
-
-  const isPositive = value > 0
-  const absItem = Math.abs(value)
-
-  if (!intervalArr.value.length) return [ts, 0, value]
-
-  const min_value = Math.max(...intervalArr.value.filter((v) => v <= absItem))
-  const min_index = intervalArr.value.findIndex((v) => v === min_value)
-
-  let denominator = 1
-  if (min_index < intervalArr.value.length - 1) {
-    denominator = intervalArr.value[min_index + 1] - intervalArr.value[min_index]
-  } else {
-    denominator = intervalArr.value[min_index] || 1
-  }
-
-  const new_value = (absItem - min_value) / denominator + 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>>({})
-
-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(
-        props.deviceCode,
-        identifier,
-        props.date[0],
-        props.date[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
-
-      genderIntervalArr()
-
-      updateSingleSeries(name)
-
-      chartLoading.value = false
-    } finally {
-      item.response = false
-    }
-  }
-
-  if (real_time) {
-    connect()
-  }
-}
-
-async function initfn(load: boolean = true, real_time: boolean = true) {
-  if (load) await loadDimensions()
-  render()
-  initLoadChartData(real_time)
-}
-
-onMounted(() => {
-  initfn()
-})
-
-watch(
-  () => props.date,
-  async (newDate, oldDate) => {
-    if (!newDate || newDate.length !== 2) return
-
-    if (oldDate && newDate[0] === oldDate[0] && newDate[1] === oldDate[1]) return
-
-    await cancelAllRequests()
-
-    close()
-
-    const endTime = dayjs(newDate[1])
-    const now = dayjs()
-    const isRealTime = endTime.isAfter(now.subtract(1, 'minute'))
-
-    if (chart) chart.clear()
-
-    if (isRealTime) initfn(false)
-    else initfn(false, false)
-  }
-)
-
-onUnmounted(() => {
-  close()
-
-  window.removeEventListener('resize', () => {
-    if (chart) chart.resize()
-  })
-})
-
-const router = useRouter()
-
-function handleDetailClick() {
-  router.push({
-    name: 'MonitoringDetail',
-    query: {
-      id: props.id,
-      ifInline: props.ifInline,
-      carOnline: props.carOnline,
-      time: props.lastInlineTime,
-      name: props.deviceName,
-      code: props.deviceCode,
-      dept: props.deptName,
-      vehicle: props.vehicleName
-    }
-  })
-}
-</script>
-<template>
-  <div class="h-100 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>
-      <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="flex-1 chart-main"
-      ref="chartRef"
-      v-loading="chartLoading"
-      element-loading-background="transparent"
-    ></main>
-  </div>
-</template>
-<style scoped>
-.chart-container {
-  position: relative;
-  overflow: hidden;
-  background-color: rgb(11 17 33 / 90%);
-  border: 2px solid rgb(34 211 238 / 30%);
-  box-shadow:
-    0 0 20px rgb(0 0 0 / 80%),
-    inset 0 0 15px rgb(34 211 238 / 10%);
-  transition:
-    border-color 0.3s ease,
-    transform 0.3s ease;
-}
-
-.chart-container::before {
-  position: absolute;
-  pointer-events: none;
-  background: radial-gradient(
-    400px circle at var(--mouse-x, 50%) var(--mouse-y, 50%),
-    rgb(34 211 238 / 5%),
-    transparent 40%
-  );
-  content: '';
-  inset: 0;
-}
-
-.chart-container:hover {
-  border-color: rgb(34 211 238 / 60%);
-  transform: scale(1.005);
-}
-
-.chart-header {
-  display: flex;
-  padding: 12px 16px;
-  font-size: 18px;
-  font-weight: 600;
-  letter-spacing: 1px;
-  color: #e2e8f0;
-  background: rgb(255 255 255 / 3%);
-  border-bottom: 1px solid transparent;
-  border-image: linear-gradient(to right, rgb(34 211 238 / 50%), transparent) 1;
-  align-items: center;
-}
-
-.title-icon {
-  width: 4px;
-  height: 16px;
-  margin-right: 10px;
-  background: #22d3ee;
-  box-shadow: 0 0 8px #22d3ee;
-}
-
-.chart-main {
-  padding-top: 12px;
-  background-image: radial-gradient(circle at 50% 50%, rgb(34 211 238 / 10%) 0%, transparent 80%),
-    linear-gradient(to right, rgb(34 211 238 / 15%) 1px, transparent 1px),
-    linear-gradient(to bottom, rgb(34 211 238 / 15%) 1px, transparent 1px),
-    linear-gradient(135deg, rgb(11 17 33 / 90%) 0%, rgb(6 9 18 / 95%) 100%);
-  background-size:
-    100% 100%,
-    40px 40px,
-    40px 40px,
-    100% 100%;
-}
-
-/* 针对 ECharts tooltip 的滚动条美化 */
-.echarts-tooltip-scroll::-webkit-scrollbar {
-  width: 6px;
-}
-
-.echarts-tooltip-scroll::-webkit-scrollbar-thumb {
-  background: #22d3ee; /* 青色滑块 */
-  border-radius: 3px;
-}
-
-.echarts-tooltip-scroll::-webkit-scrollbar-track {
-  background: rgb(255 255 255 / 10%); /* 深色轨道 */
-}
-</style>

+ 0 - 716
src/views/oli-connection/monitoring-board/chart2.vue

@@ -1,716 +0,0 @@
-<script lang="ts" setup>
-import { IotDeviceApi } from '@/api/pms/device'
-import { cancelAllRequests, IotStatApi } from '@/api/pms/stat'
-import { useMqtt } from '@/utils/useMqtt'
-import { Dimensions, formatIotValue } from '@/utils/useSocketBus'
-import dayjs from 'dayjs'
-import * as echarts from 'echarts'
-import { neonColors } from '@/utils/td-color'
-
-const props = defineProps({
-  id: {
-    type: Number,
-    required: true
-  },
-  deviceCode: {
-    type: String,
-    required: true
-  },
-  deviceName: {
-    type: String,
-    required: true
-  },
-  mqttUrl: {
-    type: String,
-    required: true
-  },
-  ifInline: {
-    type: String,
-    required: true
-  },
-  lastInlineTime: {
-    type: String,
-    required: true
-  },
-  deptName: {
-    type: String,
-    required: true
-  },
-  vehicleName: {
-    type: String,
-    required: true
-  },
-  carOnline: {
-    type: String,
-    required: true
-  },
-  // isRealTime: {
-  //   type: Boolean,
-  //   default: true
-  // },
-  date: {
-    type: Array as PropType<Array<string>>,
-    required: true
-  },
-  token: {
-    type: String,
-    required: true
-  }
-})
-
-const dimensions = ref<Omit<Dimensions, 'color' | 'bgHover' | 'bgActive'>[]>([])
-const selectedDimension = ref<Record<string, boolean>>({})
-
-const { connect, destroy, isConnected, subscribe } = useMqtt()
-
-const handleMessageUpdate = (_topic: string, data: any) => {
-  const valueMap = new Map<string, number>()
-
-  for (const item of data) {
-    const { id: identity, value: logValue, remark } = item
-
-    const value = logValue ? Number(logValue) : 0
-
-    if (identity) {
-      valueMap.set(identity, value)
-    }
-
-    const modelName = dimensions.value.find((item) => item.identifier === identity)?.name
-
-    if (modelName && chartData.value[modelName]) {
-      chartData.value[modelName].push({
-        ts: dayjs.unix(remark).valueOf(),
-        value
-      })
-
-      updateSingleSeries(modelName)
-    }
-  }
-}
-
-watch(isConnected, (newVal) => {
-  if (newVal) {
-    // subscribe(`/636/${props.deviceCode}/property/post`)
-
-    // switch (props.deviceCode) {
-    //   case 'YF1539':
-    //     subscribe(`/656/${props.deviceCode}/property/post`)
-    //   case 'YF325':
-    //   case 'YF288':
-    //   case 'YF671':
-    //   case 'YF459':
-    //     subscribe(`/635/${props.deviceCode}/property/post`)
-    //   case 'YF649':
-    //     subscribe(`/636/${props.deviceCode}/property/post`)
-    //   default:
-    //     subscribe(`/636/${props.deviceCode}/property/post`)
-    // }
-
-    subscribe(props.mqttUrl)
-    // subscribe('/636/YF649/property/post')
-  }
-})
-
-async function loadDimensions() {
-  if (!props.id) return
-  try {
-    dimensions.value = (((await IotDeviceApi.getIotDeviceTds(Number(props.id))) as any[]) ?? [])
-      .sort((a, b) => b.modelOrder - a.modelOrder)
-      .map((item) => {
-        const { value, suffix, isText } = formatIotValue(item.value)
-        return {
-          identifier: item.identifier,
-          name: item.modelName,
-          value: value,
-          suffix: suffix,
-          isText: isText,
-          response: false
-        }
-      })
-      .filter((item) => item.isText === false)
-
-    selectedDimension.value = Object.fromEntries(dimensions.value.map((item) => [item.name, true]))
-  } catch (error) {
-    console.error(error)
-  }
-}
-
-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
-
-function genderIntervalArr(init: boolean = false) {
-  // 1. 使用正负无穷大初始化,避免先把所有数存入数组
-  let maxVal = -Infinity
-  let minVal = Infinity
-  let hasData = false
-
-  // 2. 直接遍历数据查找最值 (不使用 spread ...)
-  for (const [key, value] of Object.entries(selectedDimension.value)) {
-    if (value) {
-      const dataset = chartData.value[key]
-      if (dataset && dataset.length > 0) {
-        hasData = true
-        // 使用循环代替 ...spread
-        for (const item of dataset) {
-          const val = item.value
-          if (val > maxVal) maxVal = val
-          if (val < minVal) minVal = val
-        }
-      }
-    }
-  }
-
-  // 3. 处理无数据的默认情况
-  if (!hasData) {
-    maxVal = 10000
-    minVal = 0
-  } else {
-    // 保持你原有的逻辑:如果最小值大于0,则归零
-    minVal = minVal > 0 ? 0 : minVal
-  }
-
-  // 4. 计算位数逻辑 (保持不变)
-  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) => {
-    // 1. 同步选中状态
-    selectedDimension.value = params.selected
-    const clickedModelName = params.name
-    const isSelected = params.selected[clickedModelName]
-
-    const oldMax = maxInterval.value
-    const oldMin = minInterval.value
-
-    genderIntervalArr()
-
-    const isScaleChanged = oldMax !== maxInterval.value || oldMin !== minInterval.value
-
-    if (isScaleChanged) {
-      Object.keys(selectedDimension.value).forEach((name) => {
-        if (selectedDimension.value[name]) {
-          updateSingleSeries(name)
-        }
-      })
-    } else {
-      if (isSelected) {
-        updateSingleSeries(clickedModelName)
-      }
-    }
-  })
-
-  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({
-    color: neonColors,
-    animation: true,
-    animationDuration: 200,
-    animationEasing: 'linear',
-    animationDurationUpdate: 200,
-    animationEasingUpdate: 'linear',
-    grid: {
-      left: '3%',
-      top: '60px',
-      right: '6%',
-      bottom: '10%',
-      containLabel: true,
-      show: false
-    },
-    tooltip: {
-      trigger: 'axis',
-      confine: true,
-      enterable: true,
-      className: 'echarts-tooltip-scroll',
-      extraCssText:
-        'max-height: 300px; overflow-y: auto; pointer-events: auto; border-radius: 4px;',
-      backgroundColor: 'rgba(11, 17, 33, 0.95)',
-      borderColor: '#22d3ee',
-      borderWidth: 1,
-      textStyle: {
-        color: '#e2e8f0'
-      },
-      axisPointer: {
-        type: 'cross',
-        label: { backgroundColor: '#22d3ee', color: '#000' },
-        lineStyle: { color: 'rgba(255,255,255,0.3)', type: 'dashed' }
-      },
-      formatter: (params: any) => {
-        let d = `<div style="font-weight:bold; border-bottom:1px solid rgba(255,255,255,0.1); padding-bottom:5px; margin-bottom:5px;">${params[0].axisValueLabel}</div>`
-        const exist: string[] = []
-        params = params.filter((el: any) => {
-          if (exist.includes(el.seriesName)) return false
-          exist.push(el.seriesName)
-          return true
-        })
-
-        // 优化列表显示,圆点使用原本的颜色
-        let item = params.map(
-          (
-            el: any
-          ) => `<div class="flex items-center justify-between mt-1" style="font-size:12px; min-width: 180px;">
-            <span style="display:flex; align-items:center;">
-                <span style="display:inline-block;width:8px;height:8px;border-radius:50%;background-color:${el.color};margin-right:6px;"></span>
-                <span style="color:#cbd5e1">${el.seriesName}</span>
-            </span>
-            <span style="color:#fff; font-weight:bold; margin-left:10px;">${el.value[2]?.toFixed(2)}</span>
-          </div>`
-        )
-
-        return d + item.join('')
-      }
-    },
-    xAxis: {
-      type: 'time',
-      boundaryGap: ['0%', '25%'],
-      axisLabel: {
-        formatter: (v) => dayjs(v).format('YYYY-MM-DD\nHH:mm:ss'),
-        rotate: 0,
-        align: 'center',
-        color: '#94a3b8',
-        fontSize: 11
-      },
-      splitLine: {
-        show: false,
-        lineStyle: { color: 'rgba(255,255,255,0.5)', type: 'dashed' }
-      }
-    },
-    dataZoom: [
-      { type: 'inside', xAxisIndex: 0 },
-      {
-        type: 'slider',
-        xAxisIndex: 0,
-        height: 20,
-        bottom: 10,
-        borderColor: 'transparent',
-        backgroundColor: 'rgba(255,255,255,0.05)',
-        fillerColor: 'rgba(34,211,238,0.2)',
-        handleStyle: {
-          color: '#22d3ee',
-          borderColor: '#22d3ee'
-        },
-        labelFormatter: (value: any) => {
-          return dayjs(value).format('YYYY-MM-DD\nHH:mm:ss')
-        },
-        textStyle: {
-          color: '#94a3b8',
-          fontSize: 10,
-          lineHeight: 12
-        }
-      }
-    ],
-    yAxis: {
-      type: 'value',
-      min: -minInterval.value,
-      max: maxInterval.value,
-      interval: 1,
-      axisLabel: {
-        color: '#94a3b8',
-        formatter: (v) => {
-          const num = v === 0 ? 0 : v > 0 ? Math.pow(10, v) : -Math.pow(10, -v)
-          if (Math.abs(num) >= 10000) return (num / 10000).toFixed(0) + 'w'
-          if (Math.abs(num) >= 1000) return (num / 1000).toFixed(0) + 'k'
-          return num.toLocaleString()
-        }
-      },
-      show: true,
-      splitLine: {
-        show: true,
-        lineStyle: {
-          color: 'rgba(255,255,255,0.05)',
-          type: 'dashed'
-        }
-      },
-      axisPointer: {
-        show: true,
-        snap: false, // 必须设为 false,才能平滑显示小数部分的真实值
-        label: {
-          show: true,
-          backgroundColor: '#22d3ee', // 青色背景
-          color: '#000', // 黑色文字
-          fontWeight: 'bold',
-          precision: 2, // 保证精度
-
-          // --- 具体的实现逻辑 ---
-          formatter: (params: any) => {
-            const val = params.value // 这里拿到的是索引值,比如 4.21
-            if (val === 0) return '0.00'
-
-            // A. 处理正负号
-            const sign = val >= 0 ? 1 : -1
-            const absVal = Math.abs(val)
-
-            // B. 分离 整数部分(区间下标) 和 小数部分(区间内百分比)
-            const idx = Math.floor(absVal)
-
-            const percent = absVal - idx
-
-            // C. 安全检查:如果 intervalArr 还没生成,直接返回指数值
-            if (!intervalArr.value || intervalArr.value.length === 0) {
-              return (sign * Math.pow(10, absVal)).toFixed(2)
-            }
-
-            // D. 获取该区间的真实数值范围
-            // 例如 idx=2, 对应 intervalArr[2]=100, intervalArr[3]=1000
-            const min = intervalArr.value[idx]
-            // 如果到了最后一个区间,或者越界,就默认下一级是当前的10倍(防止报错)
-            const max =
-              intervalArr.value[idx + 1] !== undefined ? intervalArr.value[idx + 1] : min * 10
-
-            // E. 反向线性插值公式
-            // 真实值 = 下界 + (上下界之差 * 百分比)
-            const realVal = min + (max - min) * percent
-
-            // F. 加上符号并格式化
-            return (realVal * sign).toFixed(2)
-          }
-        }
-      }
-    },
-    legend: {
-      type: 'scroll', // 开启滚动,防止遮挡
-      top: 10,
-
-      left: 'center',
-      width: '90%',
-
-      textStyle: {
-        color: '#e2e8f0', // 亮白色
-        fontSize: 12
-      },
-      pageIconColor: '#22d3ee',
-      pageIconInactiveColor: '#475569',
-      pageTextStyle: { color: '#fff' },
-      data: dimensions.value.map((item) => item.name),
-      selected: selectedDimension.value,
-      show: true
-    },
-    // legend: {
-    //   data: dimensions.value.map((item) => item.name),
-    //   selected: selectedDimension.value,
-    //   show: true
-    // },
-    series: dimensions.value.map((item) => ({
-      name: item.name,
-      type: 'line',
-      smooth: 0.3,
-      showSymbol: false,
-
-      endLabel: {
-        show: true,
-        formatter: (params) => params.value[2]?.toFixed(2),
-        offset: [4, 0],
-        color: '#fff',
-        backgroundColor: 'auto',
-        padding: [2, 6],
-        borderRadius: 4,
-        fontSize: 11,
-        fontWeight: 'bold'
-      },
-
-      emphasis: {
-        focus: 'series',
-        lineStyle: { width: 4 }
-      },
-
-      lineStyle: {
-        width: 3,
-        shadowColor: 'rgba(0, 0, 0, 0.5)',
-        shadowBlur: 10,
-        shadowOffsetY: 5
-      },
-
-      data: [] // 占位数组
-    }))
-  })
-}
-
-function mapData({ value, ts }) {
-  if (value === null || value === undefined || value === 0) return [ts, 0, 0]
-
-  const isPositive = value > 0
-  const absItem = Math.abs(value)
-
-  if (!intervalArr.value.length) return [ts, 0, value]
-
-  const min_value = Math.max(...intervalArr.value.filter((v) => v <= absItem))
-  const min_index = intervalArr.value.findIndex((v) => v === min_value)
-
-  let denominator = 1
-  if (min_index < intervalArr.value.length - 1) {
-    denominator = intervalArr.value[min_index + 1] - intervalArr.value[min_index]
-  } else {
-    denominator = intervalArr.value[min_index] || 1
-  }
-
-  const new_value = (absItem - min_value) / denominator + 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>>({})
-
-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(
-        props.deviceCode,
-        identifier,
-        props.date[0],
-        props.date[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
-
-      genderIntervalArr()
-
-      updateSingleSeries(name)
-
-      chartLoading.value = false
-    } finally {
-      item.response = false
-    }
-  }
-
-  if (real_time) {
-    connect(`wss://aims.deepoil.cc/mqtt`, { password: props.token }, handleMessageUpdate)
-  }
-}
-
-async function initfn(load: boolean = true, real_time: boolean = true) {
-  if (load) await loadDimensions()
-  render()
-  initLoadChartData(real_time)
-}
-
-onMounted(() => {
-  initfn()
-})
-
-watch(
-  () => props.date,
-  async (newDate, oldDate) => {
-    if (!newDate || newDate.length !== 2) return
-
-    if (oldDate && newDate[0] === oldDate[0] && newDate[1] === oldDate[1]) return
-
-    await cancelAllRequests()
-
-    destroy()
-
-    const endTime = dayjs(newDate[1])
-    const now = dayjs()
-    const isRealTime = endTime.isAfter(now.subtract(1, 'minute'))
-
-    if (chart) chart.clear()
-
-    if (isRealTime) initfn(false)
-    else initfn(false, false)
-  }
-)
-
-onUnmounted(() => {
-  destroy()
-
-  window.removeEventListener('resize', () => {
-    if (chart) chart.resize()
-  })
-})
-
-const router = useRouter()
-
-function handleDetailClick() {
-  router.push({
-    name: 'MonitoringDetail',
-    query: {
-      id: props.id,
-      ifInline: props.ifInline,
-      carOnline: props.carOnline,
-      time: props.lastInlineTime,
-      name: props.deviceName,
-      code: props.deviceCode,
-      dept: props.deptName,
-      vehicle: props.vehicleName
-    }
-  })
-}
-</script>
-<template>
-  <div class="h-100 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>
-      <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="flex-1 chart-main"
-      ref="chartRef"
-      v-loading="chartLoading"
-      element-loading-background="transparent"
-    ></main>
-  </div>
-</template>
-<style scoped>
-.chart-container {
-  position: relative;
-  overflow: hidden;
-  background-color: rgb(11 17 33 / 90%);
-  border: 2px solid rgb(34 211 238 / 30%);
-  box-shadow:
-    0 0 20px rgb(0 0 0 / 80%),
-    inset 0 0 15px rgb(34 211 238 / 10%);
-  transition:
-    border-color 0.3s ease,
-    transform 0.3s ease;
-}
-
-.chart-container::before {
-  position: absolute;
-  pointer-events: none;
-  background: radial-gradient(
-    400px circle at var(--mouse-x, 50%) var(--mouse-y, 50%),
-    rgb(34 211 238 / 5%),
-    transparent 40%
-  );
-  content: '';
-  inset: 0;
-}
-
-.chart-container:hover {
-  border-color: rgb(34 211 238 / 60%);
-  transform: scale(1.005);
-}
-
-.chart-header {
-  display: flex;
-  padding: 12px 16px;
-  font-size: 18px;
-  font-weight: 600;
-  letter-spacing: 1px;
-  color: #e2e8f0;
-  background: rgb(255 255 255 / 3%);
-  border-bottom: 1px solid transparent;
-  border-image: linear-gradient(to right, rgb(34 211 238 / 50%), transparent) 1;
-  align-items: center;
-}
-
-.title-icon {
-  width: 4px;
-  height: 16px;
-  margin-right: 10px;
-  background: #22d3ee;
-  box-shadow: 0 0 8px #22d3ee;
-}
-
-.chart-main {
-  padding-top: 12px;
-  background-image: radial-gradient(circle at 50% 50%, rgb(34 211 238 / 10%) 0%, transparent 80%),
-    linear-gradient(to right, rgb(34 211 238 / 15%) 1px, transparent 1px),
-    linear-gradient(to bottom, rgb(34 211 238 / 15%) 1px, transparent 1px),
-    linear-gradient(135deg, rgb(11 17 33 / 90%) 0%, rgb(6 9 18 / 95%) 100%);
-  background-size:
-    100% 100%,
-    40px 40px,
-    40px 40px,
-    100% 100%;
-}
-
-/* 针对 ECharts tooltip 的滚动条美化 */
-.echarts-tooltip-scroll::-webkit-scrollbar {
-  width: 6px;
-}
-
-.echarts-tooltip-scroll::-webkit-scrollbar-thumb {
-  background: #22d3ee; /* 青色滑块 */
-  border-radius: 3px;
-}
-
-.echarts-tooltip-scroll::-webkit-scrollbar-track {
-  background: rgb(255 255 255 / 10%); /* 深色轨道 */
-}
-</style>

+ 0 - 717
src/views/oli-connection/monitoring-board/chart4.vue

@@ -1,717 +0,0 @@
-<script lang="ts" setup>
-import { IotDeviceApi } from '@/api/pms/device'
-import { cancelAllRequests, IotStatApi } from '@/api/pms/stat'
-import { useMqtt } from '@/utils/useMqtt'
-import { Dimensions, formatIotValue } from '@/utils/useSocketBus'
-import dayjs from 'dayjs'
-import * as echarts from 'echarts'
-import { neonColors } from '@/utils/td-color'
-
-const props = defineProps({
-  id: {
-    type: Number,
-    required: true
-  },
-  deviceCode: {
-    type: String,
-    required: true
-  },
-  deviceName: {
-    type: String,
-    required: true
-  },
-  mqttUrl: {
-    type: String,
-    required: true
-  },
-  ifInline: {
-    type: String,
-    required: true
-  },
-  lastInlineTime: {
-    type: String,
-    required: true
-  },
-  deptName: {
-    type: String,
-    required: true
-  },
-  vehicleName: {
-    type: String,
-    required: true
-  },
-  carOnline: {
-    type: String,
-    required: true
-  },
-  // isRealTime: {
-  //   type: Boolean,
-  //   default: true
-  // },
-  date: {
-    type: Array as PropType<Array<string>>,
-    required: true
-  },
-  token: {
-    type: String,
-    required: true
-  }
-})
-
-const dimensions = ref<Omit<Dimensions, 'color' | 'bgHover' | 'bgActive'>[]>([])
-const selectedDimension = ref<Record<string, boolean>>({})
-
-const { connect, destroy, isConnected, subscribe } = useMqtt()
-
-const handleMessageUpdate = (_topic: string, data: any) => {
-  const valueMap = new Map<string, number>()
-
-  for (const item of data) {
-    const { id: identity, value: logValue, remark } = item
-
-    const value = logValue ? Number(logValue) : 0
-
-    if (identity) {
-      valueMap.set(identity, value)
-    }
-
-    const modelName = dimensions.value.find((item) => item.identifier === identity)?.name
-
-    if (modelName && chartData.value[modelName]) {
-      chartData.value[modelName].push({
-        ts: dayjs.unix(remark).valueOf(),
-        value
-      })
-
-      updateSingleSeries(modelName)
-    }
-  }
-}
-
-watch(isConnected, (newVal) => {
-  if (newVal) {
-    // subscribe(`/636/${props.deviceCode}/property/post`)
-
-    // switch (props.deviceCode) {
-    //   case 'YF1539':
-    //     subscribe(`/656/${props.deviceCode}/property/post`)
-    //   case 'YF325':
-    //   case 'YF288':
-    //   case 'YF671':
-    //   case 'YF459':
-    //     subscribe(`/635/${props.deviceCode}/property/post`)
-    //   case 'YF649':
-    //     subscribe(`/636/${props.deviceCode}/property/post`)
-    //   default:
-    //     subscribe(`/636/${props.deviceCode}/property/post`)
-    // }
-
-    subscribe(props.mqttUrl)
-    // subscribe('/636/YF649/property/post')
-  }
-})
-
-async function loadDimensions() {
-  if (!props.id) return
-  try {
-    dimensions.value = (((await IotDeviceApi.getIotDeviceTds(Number(props.id))) as any[]) ?? [])
-      .sort((a, b) => b.modelOrder - a.modelOrder)
-      .map((item) => {
-        const { value, suffix, isText } = formatIotValue(item.value)
-        return {
-          identifier: item.identifier,
-          name: item.modelName,
-          value: value,
-          suffix: suffix,
-          isText: isText,
-          response: false
-        }
-      })
-      .filter((item) => item.isText === false)
-
-    selectedDimension.value = Object.fromEntries(dimensions.value.map((item) => [item.name, true]))
-  } catch (error) {
-    console.error(error)
-  }
-}
-
-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
-
-function genderIntervalArr(init: boolean = false) {
-  // 1. 使用正负无穷大初始化,避免先把所有数存入数组
-  let maxVal = -Infinity
-  let minVal = Infinity
-  let hasData = false
-
-  // 2. 直接遍历数据查找最值 (不使用 spread ...)
-  for (const [key, value] of Object.entries(selectedDimension.value)) {
-    if (value) {
-      const dataset = chartData.value[key]
-      if (dataset && dataset.length > 0) {
-        hasData = true
-        // 使用循环代替 ...spread
-        for (const item of dataset) {
-          const val = item.value
-          if (val > maxVal) maxVal = val
-          if (val < minVal) minVal = val
-        }
-      }
-    }
-  }
-
-  // 3. 处理无数据的默认情况
-  if (!hasData) {
-    maxVal = 10000
-    minVal = 0
-  } else {
-    // 保持你原有的逻辑:如果最小值大于0,则归零
-    minVal = minVal > 0 ? 0 : minVal
-  }
-
-  // 4. 计算位数逻辑 (保持不变)
-  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) => {
-    // 1. 同步选中状态
-    selectedDimension.value = params.selected
-    const clickedModelName = params.name
-    const isSelected = params.selected[clickedModelName]
-
-    const oldMax = maxInterval.value
-    const oldMin = minInterval.value
-
-    genderIntervalArr()
-
-    const isScaleChanged = oldMax !== maxInterval.value || oldMin !== minInterval.value
-
-    if (isScaleChanged) {
-      Object.keys(selectedDimension.value).forEach((name) => {
-        if (selectedDimension.value[name]) {
-          updateSingleSeries(name)
-        }
-      })
-    } else {
-      if (isSelected) {
-        updateSingleSeries(clickedModelName)
-      }
-    }
-  })
-
-  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({
-    color: neonColors,
-    animation: true,
-    animationDuration: 200,
-    animationEasing: 'linear',
-    animationDurationUpdate: 200,
-    animationEasingUpdate: 'linear',
-    grid: {
-      left: '3%',
-      top: '60px',
-      right: '6%',
-      bottom: '10%',
-      containLabel: true,
-      show: false
-    },
-    tooltip: {
-      trigger: 'axis',
-      confine: true,
-      enterable: true,
-      className: 'echarts-tooltip-scroll',
-      extraCssText:
-        'max-height: 300px; overflow-y: auto; pointer-events: auto; border-radius: 4px;',
-      backgroundColor: 'rgba(11, 17, 33, 0.95)',
-      borderColor: '#22d3ee',
-      borderWidth: 1,
-      textStyle: {
-        color: '#e2e8f0'
-      },
-      axisPointer: {
-        type: 'cross',
-        label: { backgroundColor: '#22d3ee', color: '#000' },
-        lineStyle: { color: 'rgba(255,255,255,0.3)', type: 'dashed' }
-      },
-      formatter: (params: any) => {
-        let d = `<div style="font-weight:bold; border-bottom:1px solid rgba(255,255,255,0.1); padding-bottom:5px; margin-bottom:5px;">${params[0].axisValueLabel}</div>`
-        const exist: string[] = []
-        params = params.filter((el: any) => {
-          if (exist.includes(el.seriesName)) return false
-          exist.push(el.seriesName)
-          return true
-        })
-
-        // 优化列表显示,圆点使用原本的颜色
-        let item = params.map(
-          (
-            el: any
-          ) => `<div class="flex items-center justify-between mt-1" style="font-size:12px; min-width: 180px;">
-            <span style="display:flex; align-items:center;">
-                <span style="display:inline-block;width:8px;height:8px;border-radius:50%;background-color:${el.color};margin-right:6px;"></span>
-                <span style="color:#cbd5e1">${el.seriesName}</span>
-            </span>
-            <span style="color:#fff; font-weight:bold; margin-left:10px;">${el.value[2]?.toFixed(2)}</span>
-          </div>`
-        )
-
-        return d + item.join('')
-      }
-    },
-    xAxis: {
-      type: 'time',
-      boundaryGap: ['0%', '25%'],
-      axisLabel: {
-        formatter: (v) => dayjs(v).format('YYYY-MM-DD\nHH:mm:ss'),
-        rotate: 0,
-        align: 'center',
-        color: '#94a3b8',
-        fontSize: 11
-      },
-      splitLine: {
-        show: false,
-        lineStyle: { color: 'rgba(255,255,255,0.5)', type: 'dashed' }
-      }
-    },
-    dataZoom: [
-      { type: 'inside', xAxisIndex: 0 },
-      {
-        type: 'slider',
-        xAxisIndex: 0,
-        height: 20,
-        bottom: 10,
-        borderColor: 'transparent',
-        backgroundColor: 'rgba(255,255,255,0.05)',
-        fillerColor: 'rgba(34,211,238,0.2)',
-        handleStyle: {
-          color: '#22d3ee',
-          borderColor: '#22d3ee'
-        },
-        labelFormatter: (value: any) => {
-          return dayjs(value).format('YYYY-MM-DD\nHH:mm:ss')
-        },
-        textStyle: {
-          color: '#94a3b8',
-          fontSize: 10,
-          lineHeight: 12
-        }
-      }
-    ],
-    yAxis: {
-      type: 'value',
-      min: -minInterval.value,
-      max: maxInterval.value,
-      interval: 1,
-      axisLabel: {
-        color: '#94a3b8',
-        formatter: (v) => {
-          const num = v === 0 ? 0 : v > 0 ? Math.pow(10, v) : -Math.pow(10, -v)
-          if (Math.abs(num) >= 10000) return (num / 10000).toFixed(0) + 'w'
-          if (Math.abs(num) >= 1000) return (num / 1000).toFixed(0) + 'k'
-          return num.toLocaleString()
-        }
-      },
-      show: true,
-      splitLine: {
-        show: true,
-        lineStyle: {
-          color: 'rgba(255,255,255,0.05)',
-          type: 'dashed'
-        }
-      },
-      axisPointer: {
-        show: true,
-        snap: false, // 必须设为 false,才能平滑显示小数部分的真实值
-        label: {
-          show: true,
-          backgroundColor: '#22d3ee', // 青色背景
-          color: '#000', // 黑色文字
-          fontWeight: 'bold',
-          precision: 2, // 保证精度
-
-          // --- 具体的实现逻辑 ---
-          formatter: (params: any) => {
-            const val = params.value // 这里拿到的是索引值,比如 4.21
-            if (val === 0) return '0.00'
-
-            // A. 处理正负号
-            const sign = val >= 0 ? 1 : -1
-            const absVal = Math.abs(val)
-
-            // B. 分离 整数部分(区间下标) 和 小数部分(区间内百分比)
-            const idx = Math.floor(absVal)
-
-            const percent = absVal - idx
-
-            // C. 安全检查:如果 intervalArr 还没生成,直接返回指数值
-            if (!intervalArr.value || intervalArr.value.length === 0) {
-              return (sign * Math.pow(10, absVal)).toFixed(2)
-            }
-
-            // D. 获取该区间的真实数值范围
-            // 例如 idx=2, 对应 intervalArr[2]=100, intervalArr[3]=1000
-            const min = intervalArr.value[idx]
-            // 如果到了最后一个区间,或者越界,就默认下一级是当前的10倍(防止报错)
-            const max =
-              intervalArr.value[idx + 1] !== undefined ? intervalArr.value[idx + 1] : min * 10
-
-            // E. 反向线性插值公式
-            // 真实值 = 下界 + (上下界之差 * 百分比)
-            const realVal = min + (max - min) * percent
-
-            // F. 加上符号并格式化
-            return (realVal * sign).toFixed(2)
-          }
-        }
-      }
-    },
-    legend: {
-      type: 'scroll', // 开启滚动,防止遮挡
-      top: 10,
-
-      left: 'center',
-      width: '90%',
-
-      textStyle: {
-        color: '#e2e8f0', // 亮白色
-        fontSize: 12
-      },
-      pageIconColor: '#22d3ee',
-      pageIconInactiveColor: '#475569',
-      pageTextStyle: { color: '#fff' },
-      data: dimensions.value.map((item) => item.name),
-      selected: selectedDimension.value,
-      show: true
-    },
-    // legend: {
-    //   data: dimensions.value.map((item) => item.name),
-    //   selected: selectedDimension.value,
-    //   show: true
-    // },
-    series: dimensions.value.map((item) => ({
-      name: item.name,
-      type: 'line',
-      smooth: 0.3,
-      showSymbol: false,
-
-      endLabel: {
-        show: true,
-        formatter: (params) => params.value[2]?.toFixed(2),
-        offset: [4, 0],
-        color: '#fff',
-        backgroundColor: 'auto',
-        padding: [2, 6],
-        borderRadius: 4,
-        fontSize: 11,
-        fontWeight: 'bold'
-      },
-
-      emphasis: {
-        focus: 'series',
-        lineStyle: { width: 4 }
-      },
-
-      lineStyle: {
-        width: 3,
-        shadowColor: 'rgba(0, 0, 0, 0.5)',
-        shadowBlur: 10,
-        shadowOffsetY: 5
-      },
-
-      data: [] // 占位数组
-    }))
-  })
-}
-
-function mapData({ value, ts }) {
-  if (value === null || value === undefined || value === 0) return [ts, 0, 0]
-
-  const isPositive = value > 0
-  const absItem = Math.abs(value)
-
-  if (!intervalArr.value.length) return [ts, 0, value]
-
-  const min_value = Math.max(...intervalArr.value.filter((v) => v <= absItem))
-  const min_index = intervalArr.value.findIndex((v) => v === min_value)
-
-  let denominator = 1
-  if (min_index < intervalArr.value.length - 1) {
-    denominator = intervalArr.value[min_index + 1] - intervalArr.value[min_index]
-  } else {
-    denominator = intervalArr.value[min_index] || 1
-  }
-
-  const new_value = (absItem - min_value) / denominator + 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>>({})
-
-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(
-        props.deviceCode,
-        identifier,
-        props.date[0],
-        props.date[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
-
-      genderIntervalArr()
-
-      updateSingleSeries(name)
-
-      chartLoading.value = false
-    } finally {
-      item.response = false
-    }
-  }
-
-  if (real_time) {
-    connect(`wss://aims.deepoil.cc/mqtt`, { password: props.token }, handleMessageUpdate)
-  }
-}
-
-async function initfn(load: boolean = true, real_time: boolean = true) {
-  if (load) await loadDimensions()
-  render()
-  initLoadChartData(real_time)
-}
-
-onMounted(() => {
-  initfn()
-})
-
-watch(
-  () => props.date,
-  async (newDate, oldDate) => {
-    if (!newDate || newDate.length !== 2) return
-
-    if (oldDate && newDate[0] === oldDate[0] && newDate[1] === oldDate[1]) return
-
-    await cancelAllRequests()
-
-    destroy()
-
-    const endTime = dayjs(newDate[1])
-    const now = dayjs()
-    const isRealTime = endTime.isAfter(now.subtract(1, 'minute'))
-
-    if (chart) chart.clear()
-
-    if (isRealTime) initfn(false)
-    else initfn(false, false)
-  }
-)
-
-onUnmounted(() => {
-  destroy()
-
-  window.removeEventListener('resize', () => {
-    if (chart) chart.resize()
-  })
-})
-
-const router = useRouter()
-
-function handleDetailClick() {
-  router.push({
-    name: 'MonitoringDetail',
-    query: {
-      id: props.id,
-      ifInline: props.ifInline,
-      carOnline: props.carOnline,
-      time: props.lastInlineTime,
-      name: props.deviceName,
-      code: props.deviceCode,
-      dept: props.deptName,
-      vehicle: props.vehicleName,
-      mqttUrl: props.mqttUrl
-    }
-  })
-}
-</script>
-<template>
-  <div class="h-100 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>
-      <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="flex-1 chart-main"
-      ref="chartRef"
-      v-loading="chartLoading"
-      element-loading-background="transparent"
-    ></main>
-  </div>
-</template>
-<style scoped>
-.chart-container {
-  position: relative;
-  overflow: hidden;
-  background-color: rgb(11 17 33 / 90%);
-  border: 2px solid rgb(34 211 238 / 30%);
-  box-shadow:
-    0 0 20px rgb(0 0 0 / 80%),
-    inset 0 0 15px rgb(34 211 238 / 10%);
-  transition:
-    border-color 0.3s ease,
-    transform 0.3s ease;
-}
-
-.chart-container::before {
-  position: absolute;
-  pointer-events: none;
-  background: radial-gradient(
-    400px circle at var(--mouse-x, 50%) var(--mouse-y, 50%),
-    rgb(34 211 238 / 5%),
-    transparent 40%
-  );
-  content: '';
-  inset: 0;
-}
-
-.chart-container:hover {
-  border-color: rgb(34 211 238 / 60%);
-  transform: scale(1.005);
-}
-
-.chart-header {
-  display: flex;
-  padding: 12px 16px;
-  font-size: 18px;
-  font-weight: 600;
-  letter-spacing: 1px;
-  color: #e2e8f0;
-  background: rgb(255 255 255 / 3%);
-  border-bottom: 1px solid transparent;
-  border-image: linear-gradient(to right, rgb(34 211 238 / 50%), transparent) 1;
-  align-items: center;
-}
-
-.title-icon {
-  width: 4px;
-  height: 16px;
-  margin-right: 10px;
-  background: #22d3ee;
-  box-shadow: 0 0 8px #22d3ee;
-}
-
-.chart-main {
-  padding-top: 12px;
-  background-image: radial-gradient(circle at 50% 50%, rgb(34 211 238 / 10%) 0%, transparent 80%),
-    linear-gradient(to right, rgb(34 211 238 / 15%) 1px, transparent 1px),
-    linear-gradient(to bottom, rgb(34 211 238 / 15%) 1px, transparent 1px),
-    linear-gradient(135deg, rgb(11 17 33 / 90%) 0%, rgb(6 9 18 / 95%) 100%);
-  background-size:
-    100% 100%,
-    40px 40px,
-    40px 40px,
-    100% 100%;
-}
-
-/* 针对 ECharts tooltip 的滚动条美化 */
-.echarts-tooltip-scroll::-webkit-scrollbar {
-  width: 6px;
-}
-
-.echarts-tooltip-scroll::-webkit-scrollbar-thumb {
-  background: #22d3ee; /* 青色滑块 */
-  border-radius: 3px;
-}
-
-.echarts-tooltip-scroll::-webkit-scrollbar-track {
-  background: rgb(255 255 255 / 10%); /* 深色轨道 */
-}
-</style>

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

@@ -94,16 +94,16 @@ async function loadDeviceOptions() {
 
   deviceLoading.value = true
   try {
-    // const data = await IotDeviceApi.getBoardDevice({
-    //   deptId: [deviceQuery.value.deptId].flat().at(-1),
-    //   ifInline: deviceQuery.value.ifInline,
-    //   pageSize: 100
-    // })
-    const data = await IotDeviceApi.getIotDeviceTdPage({
+    const data = await IotDeviceApi.getBoardDevice({
       deptId: [deviceQuery.value.deptId].flat().at(-1),
       ifInline: deviceQuery.value.ifInline,
       pageSize: 100
     })
+    // const data = await IotDeviceApi.getIotDeviceTdPage({
+    //   deptId: [deviceQuery.value.deptId].flat().at(-1),
+    //   ifInline: deviceQuery.value.ifInline,
+    //   pageSize: 100
+    // })
     deviceOptions.value = data.list.map((item: any) => ({
       label: item.deviceCode + '-' + item.deviceName,
       value: item.id,