فهرست منبع

✨ feat(设备监控): 合并悬重与悬轻曲线展示

- 同时存在 1001 和 1008 属性时隐藏悬轻,统一展示为悬重\悬轻
- 合并历史数据、实时数据及当前值,悬重显示正值、悬轻显示负值
- 同步监控看板与设备详情页的图表、设置及数据导出逻辑
- 为组合曲线设置默认正负 Y 轴范围,并保留单独悬重的原有逻辑
Zimo 4 روز پیش
والد
کامیت
3691247d5a
2فایلهای تغییر یافته به همراه233 افزوده شده و 16 حذف شده
  1. 111 7
      src/views/oli-connection/monitoring-board/chart.vue
  2. 122 9
      src/views/oli-connection/monitoring/detail.vue

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

@@ -136,6 +136,10 @@ const { client, connect, destroy, isConnected, subscribe } = useMqtt()
 
 const REALTIME_WINDOW_MS = 10 * 60 * 1000
 const REALTIME_RIGHT_BLANK_MS = 60 * 1000
+const SUSPENSION_WEIGHT_IDENTIFIER = '1001'
+const SUSPENSION_LIGHT_IDENTIFIER = '1008'
+const SUSPENSION_COMBINED_NAME = '悬重\\悬轻'
+const suspensionWeightAndLightEnabled = ref(false)
 
 function getPointTimestamp(value: unknown) {
   if (value === null || value === undefined || value === '') {
@@ -153,7 +157,9 @@ function getPointTimestamp(value: unknown) {
   return Number.isFinite(parsedValue) ? parsedValue : Date.now()
 }
 
-const handleMessageUpdate = (_topic: string, data: any) => {
+const handleMessageUpdate = (topic: string, data: any) => {
+  console.log('[监控看板] MQTT 返回数据:', { topic, data })
+
   const list = Array.isArray(data) ? data : Array.isArray(data?.data) ? data.data : []
   const valueMap = new Map<string, number>()
   const pointMap = new Map<string, ChartPoint>()
@@ -172,6 +178,13 @@ const handleMessageUpdate = (_topic: string, data: any) => {
     }
     pointMap.set(identity, point)
 
+    if (
+      suspensionWeightAndLightEnabled.value &&
+      (identity === SUSPENSION_WEIGHT_IDENTIFIER || identity === SUSPENSION_LIGHT_IDENTIFIER)
+    ) {
+      continue
+    }
+
     const modelName = dimensions.value.find((item) => item.identifier === identity)?.name
 
     if (modelName && chartData.value[modelName]) {
@@ -181,6 +194,31 @@ const handleMessageUpdate = (_topic: string, data: any) => {
     }
   }
 
+  if (suspensionWeightAndLightEnabled.value) {
+    const weightPoint = pointMap.get(SUSPENSION_WEIGHT_IDENTIFIER)
+    const lightPoint = pointMap.get(SUSPENSION_LIGHT_IDENTIFIER)
+
+    if (weightPoint || lightPoint) {
+      const combinedPoint = {
+        ts: Math.max(weightPoint?.ts ?? 0, lightPoint?.ts ?? 0),
+        value: (weightPoint?.value ?? 0) - (lightPoint?.value ?? 0)
+      }
+
+      valueMap.set(SUSPENSION_WEIGHT_IDENTIFIER, combinedPoint.value)
+      pointMap.set(SUSPENSION_WEIGHT_IDENTIFIER, combinedPoint)
+      pointMap.delete(SUSPENSION_LIGHT_IDENTIFIER)
+
+      const modelName = dimensions.value.find(
+        (item) => String(item.identifier) === SUSPENSION_WEIGHT_IDENTIFIER
+      )?.name
+
+      if (modelName && chartData.value[modelName]) {
+        appendChartPoint(modelName, combinedPoint)
+        updatedNames.add(modelName)
+      }
+    }
+  }
+
   updateDimensionValues(valueMap)
   updateThresholdAlarm(pointMap)
 
@@ -222,12 +260,30 @@ watch(isConnected, (newVal) => {
 async function loadDimensions() {
   if (!props.id) return
   try {
-    dimensions.value = (
+    const dimensionList =
       ((await IotDeviceApi.getIotDeviceTds(Number(props.id))) as IotTdItem[]) ?? []
-    )
+    const identifiers = new Set(dimensionList.map((item) => String(item.identifier)))
+
+    suspensionWeightAndLightEnabled.value =
+      identifiers.has(SUSPENSION_WEIGHT_IDENTIFIER) && identifiers.has(SUSPENSION_LIGHT_IDENTIFIER)
+
+    if (suspensionWeightAndLightEnabled.value) {
+      const weightDimension = dimensionList.find(
+        (item) => String(item.identifier) === SUSPENSION_WEIGHT_IDENTIFIER
+      )
+
+      if (weightDimension) weightDimension.modelName = SUSPENSION_COMBINED_NAME
+    }
+
+    dimensions.value = dimensionList
       .sort((a, b) => (b.modelOrder ?? 0) - (a.modelOrder ?? 0))
       .map(mapToDimension)
       .filter((item) => item.isText === false)
+      .filter(
+        (item) =>
+          !suspensionWeightAndLightEnabled.value ||
+          String(item.identifier) !== SUSPENSION_LIGHT_IDENTIFIER
+      )
       .map(withDisplayStyle)
 
     selectedDimension.value = Object.fromEntries(dimensions.value.map((item) => [item.name, true]))
@@ -292,7 +348,10 @@ function getSeriesRange(name: string) {
   const configuredMax = normalizeRangeValue(dimension?.axisMax)
   const configured =
     configuredMin !== undefined && configuredMax !== undefined && configuredMin < configuredMax
-  const min = configured ? configuredMin : 0
+  const isSuspensionCurve =
+    suspensionWeightAndLightEnabled.value &&
+    String(dimension?.identifier) === SUSPENSION_WEIGHT_IDENTIFIER
+  const min = configured ? configuredMin : isSuspensionCurve ? -100 : 0
   const max = configured ? configuredMax : 100
 
   return {
@@ -780,6 +839,34 @@ function render() {
 
 const chartLoading = ref(false)
 
+function normalizeHistoryPoints(data: unknown): ChartPoint[] {
+  if (!Array.isArray(data)) return []
+
+  return data
+    .map((item) => ({
+      ts: Number(item.ts),
+      value: Number(item.value)
+    }))
+    .filter((item) => Number.isFinite(item.ts) && Number.isFinite(item.value))
+    .sort((a, b) => a.ts - b.ts)
+}
+
+function mergeSuspensionHistory(weightPoints: ChartPoint[], lightPoints: ChartPoint[]) {
+  const mergedPoints = new Map<number, { weight?: number; light?: number }>()
+
+  weightPoints.forEach((point) => {
+    mergedPoints.set(point.ts, { ...mergedPoints.get(point.ts), weight: point.value })
+  })
+  lightPoints.forEach((point) => {
+    mergedPoints.set(point.ts, { ...mergedPoints.get(point.ts), light: point.value })
+  })
+
+  return Array.from(mergedPoints, ([ts, value]) => ({
+    ts,
+    value: (value.weight ?? 0) - (value.light ?? 0)
+  })).sort((a, b) => a.ts - b.ts)
+}
+
 async function initLoadChartData(real_time: boolean = true) {
   if (!dimensions.value.length) return
 
@@ -800,9 +887,26 @@ async function initLoadChartData(real_time: boolean = true) {
             props.date[1]
           )
 
-          chartData.value[item.name] = (((res as any[]) ?? []) as any[])
-            .sort((a, b) => a.ts - b.ts)
-            .map((item) => ({ ts: item.ts, value: item.value }))
+          if (
+            suspensionWeightAndLightEnabled.value &&
+            String(item.identifier) === SUSPENSION_WEIGHT_IDENTIFIER
+          ) {
+            const lightRes = await IotStatApi.getDeviceInfoChartly(
+              props.deviceCode,
+              SUSPENSION_LIGHT_IDENTIFIER,
+              props.date[0],
+              props.date[1]
+            )
+
+            chartData.value[item.name] = mergeSuspensionHistory(
+              normalizeHistoryPoints(res),
+              normalizeHistoryPoints(lightRes)
+            )
+          } else {
+            chartData.value[item.name] = (((res as any[]) ?? []) as any[])
+              .sort((a, b) => a.ts - b.ts)
+              .map((item) => ({ ts: item.ts, value: item.value }))
+          }
         } catch (error) {
           console.error(error)
         } finally {

+ 122 - 9
src/views/oli-connection/monitoring/detail.vue

@@ -114,6 +114,10 @@ function isRangeGreaterThan120Minutes(range: string[]) {
 }
 
 const REALTIME_RIGHT_BLANK_MS = 60 * 1000
+const SUSPENSION_WEIGHT_IDENTIFIER = '1001'
+const SUSPENSION_LIGHT_IDENTIFIER = '1008'
+const SUSPENSION_COMBINED_NAME = '悬重\\悬轻'
+const suspensionWeightAndLightEnabled = ref(false)
 const selectedDate = ref<string[]>(getInitialDateRange())
 const chartRef = ref<HTMLDivElement | null>(null)
 const targetArea = ref<HTMLElement | null>(null)
@@ -150,11 +154,39 @@ async function loadDimensions() {
   if (!query.id) return
   dimensionLoading.value = true
   try {
-    const gateway = [
+    const gatewaySource = [
       ...(((await IotDeviceApi.getIotDeviceTds(Number(query.id))) as IotTdItem[]) ?? [])
     ]
       .sort((a, b) => (b.modelOrder ?? 0) - (a.modelOrder ?? 0))
       .map(mapToDimension)
+    const gatewayIdentifiers = new Set(gatewaySource.map((item) => String(item.identifier)))
+
+    suspensionWeightAndLightEnabled.value =
+      gatewayIdentifiers.has(SUSPENSION_WEIGHT_IDENTIFIER) &&
+      gatewayIdentifiers.has(SUSPENSION_LIGHT_IDENTIFIER)
+
+    if (suspensionWeightAndLightEnabled.value) {
+      const weightDimension = gatewaySource.find(
+        (item) => String(item.identifier) === SUSPENSION_WEIGHT_IDENTIFIER
+      )
+      const lightDimension = gatewaySource.find(
+        (item) => String(item.identifier) === SUSPENSION_LIGHT_IDENTIFIER
+      )
+      const weightValue = Number(weightDimension?.value ?? 0)
+      const lightValue = Number(lightDimension?.value ?? 0)
+
+      if (weightDimension && Number.isFinite(weightValue) && Number.isFinite(lightValue)) {
+        weightDimension.value = weightValue - lightValue
+      }
+
+      if (weightDimension) weightDimension.name = SUSPENSION_COMBINED_NAME
+    }
+
+    const gateway = gatewaySource.filter(
+      (item) =>
+        !suspensionWeightAndLightEnabled.value ||
+        String(item.identifier) !== SUSPENSION_LIGHT_IDENTIFIER
+    )
     // const car = [
     //   ...(((await IotDeviceApi.getIotDeviceZHBDTds(Number(query.id))) as IotTdItem[]) ?? [])
     // ]
@@ -203,7 +235,10 @@ function getSeriesRange(name: string) {
   const configuredMax = normalizeRangeValue(dimension?.axisMax)
   const configured =
     configuredMin !== undefined && configuredMax !== undefined && configuredMin < configuredMax
-  const min = configured ? configuredMin : 0
+  const isSuspensionCurve =
+    suspensionWeightAndLightEnabled.value &&
+    String(dimension?.identifier) === SUSPENSION_WEIGHT_IDENTIFIER
+  const min = configured ? configuredMin : isSuspensionCurve ? -100 : 0
   const max = configured ? configuredMax : 100
 
   return {
@@ -626,6 +661,7 @@ function updateDimensionValues(valueMap: Map<string, number>) {
 function handleMessageUpdate(_topic: string, payload: any) {
   const list = Array.isArray(payload) ? payload : Array.isArray(payload?.data) ? payload.data : []
   const valueMap = new Map<string, number>()
+  const pointMap = new Map<string, ChartPoint>()
   const updatedNames = new Set<string>()
 
   list.forEach((item) => {
@@ -635,17 +671,49 @@ function handleMessageUpdate(_topic: string, payload: any) {
     if (!identity || !Number.isFinite(numberValue)) return
 
     valueMap.set(identity, numberValue)
+    const point = {
+      ts: getPointTimestamp(item.remark),
+      value: numberValue
+    }
+    pointMap.set(identity, point)
+
+    if (
+      suspensionWeightAndLightEnabled.value &&
+      (identity === SUSPENSION_WEIGHT_IDENTIFIER || identity === SUSPENSION_LIGHT_IDENTIFIER)
+    ) {
+      return
+    }
 
     const dimension = dimensions.value.find((item) => item.identifier === identity)
     if (!dimension) return
 
-    appendChartPoint(dimension.name, {
-      ts: getPointTimestamp(item.remark),
-      value: numberValue
-    })
+    appendChartPoint(dimension.name, point)
     updatedNames.add(dimension.name)
   })
 
+  if (suspensionWeightAndLightEnabled.value) {
+    const weightPoint = pointMap.get(SUSPENSION_WEIGHT_IDENTIFIER)
+    const lightPoint = pointMap.get(SUSPENSION_LIGHT_IDENTIFIER)
+
+    if (weightPoint || lightPoint) {
+      const combinedPoint = {
+        ts: Math.max(weightPoint?.ts ?? 0, lightPoint?.ts ?? 0),
+        value: (weightPoint?.value ?? 0) - (lightPoint?.value ?? 0)
+      }
+
+      valueMap.set(SUSPENSION_WEIGHT_IDENTIFIER, combinedPoint.value)
+
+      const dimension = dimensions.value.find(
+        (item) => String(item.identifier) === SUSPENSION_WEIGHT_IDENTIFIER
+      )
+
+      if (dimension) {
+        appendChartPoint(dimension.name, combinedPoint)
+        updatedNames.add(dimension.name)
+      }
+    }
+  }
+
   if (!updatedNames.size) return
 
   updateDimensionValues(valueMap)
@@ -665,6 +733,34 @@ async function startRealtimeMqtt() {
   connect(`wss://aims.deepoil.cc/mqtt`, { password: token.value }, handleMessageUpdate)
 }
 
+function normalizeHistoryPoints(data: unknown): ChartPoint[] {
+  if (!Array.isArray(data)) return []
+
+  return data
+    .map((item) => ({
+      ts: Number(item.ts),
+      value: Number(item.value)
+    }))
+    .filter((item) => Number.isFinite(item.ts) && Number.isFinite(item.value))
+    .sort((a, b) => a.ts - b.ts)
+}
+
+function mergeSuspensionHistory(weightPoints: ChartPoint[], lightPoints: ChartPoint[]) {
+  const mergedPoints = new Map<number, { weight?: number; light?: number }>()
+
+  weightPoints.forEach((point) => {
+    mergedPoints.set(point.ts, { ...mergedPoints.get(point.ts), weight: point.value })
+  })
+  lightPoints.forEach((point) => {
+    mergedPoints.set(point.ts, { ...mergedPoints.get(point.ts), light: point.value })
+  })
+
+  return Array.from(mergedPoints, ([ts, value]) => ({
+    ts,
+    value: (value.weight ?? 0) - (value.light ?? 0)
+  })).sort((a, b) => a.ts - b.ts)
+}
+
 async function initLoadChartData({
   realtime = true,
   resetDate = realtime
@@ -696,9 +792,26 @@ async function initLoadChartData({
             selectedDate.value[1]
           )
 
-          chartData.value[item.name] = (((res as any[]) ?? []) as any[])
-            .sort((a, b) => a.ts - b.ts)
-            .map((item) => ({ ts: item.ts, value: item.value }))
+          if (
+            suspensionWeightAndLightEnabled.value &&
+            String(item.identifier) === SUSPENSION_WEIGHT_IDENTIFIER
+          ) {
+            const lightRes = await getChartData(
+              data.value.deviceCode,
+              SUSPENSION_LIGHT_IDENTIFIER,
+              selectedDate.value[0],
+              selectedDate.value[1]
+            )
+
+            chartData.value[item.name] = mergeSuspensionHistory(
+              normalizeHistoryPoints(res),
+              normalizeHistoryPoints(lightRes)
+            )
+          } else {
+            chartData.value[item.name] = (((res as any[]) ?? []) as any[])
+              .sort((a, b) => a.ts - b.ts)
+              .map((item) => ({ ts: item.ts, value: item.value }))
+          }
         } catch (error) {
           console.error(error)
         } finally {