Преглед на файлове

🐛 fix(设备监控): 修正悬重悬轻合并与监控数据导出

- 修复 MQTT 拆包、缺失值导致的错误 0 数据
- 按秒归一化悬重/悬轻组合时间,避免同秒重复点
- 同步监控详情页与看板的曲线处理逻辑
- 调整监控数据导出为时间行、属性列
- 同步告警配置项和表格自适应列宽
Zimo преди 2 дни
родител
ревизия
fa899559dc
променени са 2 файла, в които са добавени 218 реда и са изтрити 179 реда
  1. 88 23
      src/views/oli-connection/monitoring-board/chart.vue
  2. 130 156
      src/views/oli-connection/monitoring/detail.vue

+ 88 - 23
src/views/oli-connection/monitoring-board/chart.vue

@@ -175,9 +175,7 @@ function getPointTimestamp(value: unknown) {
   return Number.isFinite(parsedValue) ? parsedValue : Date.now()
 }
 
-const handleMessageUpdate = (topic: string, data: any) => {
-  console.log('[监控看板] MQTT 返回数据:', { topic, data })
-
+const handleMessageUpdate = (_topic: string, data: any) => {
   const list = Array.isArray(data) ? data : Array.isArray(data?.data) ? data.data : []
   const valueMap = new Map<string, number>()
   const pointMap = new Map<string, ChartPoint>()
@@ -185,9 +183,20 @@ const handleMessageUpdate = (topic: string, data: any) => {
 
   for (const item of list) {
     const identity = String(item.id ?? item.identity ?? '')
-    const value = Number(item.value ?? item.logValue ?? 0)
+    // 【修复前】缺少 value/logValue 时会被转换成 0。
+    // 【修复后】缺少数值直接跳过,但真实上报的数值 0 仍然有效。
+    const rawValue = item.value ?? item.logValue
+    const value = Number(rawValue)
 
-    if (!identity || !Number.isFinite(value)) continue
+    if (
+      !identity ||
+      rawValue === null ||
+      rawValue === undefined ||
+      rawValue === '' ||
+      !Number.isFinite(value)
+    ) {
+      continue
+    }
 
     valueMap.set(identity, value)
     const point = {
@@ -213,15 +222,9 @@ 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)
-      }
+    const combinedPoint = getCombinedSuspensionPoint(pointMap)
 
+    if (combinedPoint) {
       valueMap.set(SUSPENSION_WEIGHT_IDENTIFIER, combinedPoint.value)
       pointMap.set(SUSPENSION_WEIGHT_IDENTIFIER, combinedPoint)
       pointMap.delete(SUSPENSION_LIGHT_IDENTIFIER)
@@ -234,6 +237,12 @@ const handleMessageUpdate = (topic: string, data: any) => {
         appendChartPoint(modelName, combinedPoint)
         updatedNames.add(modelName)
       }
+    } else {
+      // 组合尚未完整时,不让单侧原始值进入当前值和告警判断,保留上一条完整组合结果。
+      valueMap.delete(SUSPENSION_WEIGHT_IDENTIFIER)
+      valueMap.delete(SUSPENSION_LIGHT_IDENTIFIER)
+      pointMap.delete(SUSPENSION_WEIGHT_IDENTIFIER)
+      pointMap.delete(SUSPENSION_LIGHT_IDENTIFIER)
     }
   }
 
@@ -321,6 +330,41 @@ interface ChartData {
 
 const chartData = shallowRef<ChartData>({})
 
+// 【修复前】每条 MQTT 消息只在当前 payload 内查找 1001(悬重)和 1008(悬轻)。
+// 如果设备把两个测点拆成两条消息上报,当前消息缺少的那一侧会被当成 0,
+// 组合曲线就会出现错误的 0 点。
+//
+// 【修复后】保存两侧最近一次真实收到的有效点。只收到一侧时使用另一侧最近值,
+// 两侧都还没有数据时不生成组合点,避免把“暂无数据”伪造成数值 0。
+const latestSuspensionPoints = new Map<string, ChartPoint>()
+
+function normalizeSuspensionTimestamp(timestamp: number) {
+  // tooltip 只显示到秒,因此将组合曲线的毫秒归入对应的秒桶。
+  // 【修复前】14:26:26.000 和 14:26:26.500 会被当成两个不同点,
+  // 但页面都显示为 14:26:26,视觉上像同一秒出现两个值。
+  // 【修复后】同一秒只保留一个组合点,后到的结果覆盖先到的结果。
+  return Math.floor(timestamp / 1000) * 1000
+}
+
+function getCombinedSuspensionPoint(pointMap: Map<string, ChartPoint>) {
+  // MQTT 可能把悬重和悬轻拆成不同消息,因此先更新最近值缓存,再计算组合值。
+  for (const identifier of [SUSPENSION_WEIGHT_IDENTIFIER, SUSPENSION_LIGHT_IDENTIFIER]) {
+    const point = pointMap.get(identifier)
+    if (point) latestSuspensionPoints.set(identifier, point)
+  }
+
+  const weightPoint = latestSuspensionPoints.get(SUSPENSION_WEIGHT_IDENTIFIER)
+  const lightPoint = latestSuspensionPoints.get(SUSPENSION_LIGHT_IDENTIFIER)
+
+  // 任意一侧还没有真实数据时不计算,避免用默认 0 制造伪数据。
+  if (!weightPoint || !lightPoint) return null
+
+  return {
+    ts: normalizeSuspensionTimestamp(Math.max(weightPoint.ts, lightPoint.ts)),
+    value: weightPoint.value - lightPoint.value
+  }
+}
+
 const chartRef = ref<HTMLDivElement | null>(null)
 let chart: echarts.ECharts | null = null
 
@@ -912,16 +956,26 @@ function mergeSuspensionHistory(weightPoints: ChartPoint[], lightPoints: ChartPo
   const mergedPoints = new Map<number, { weight?: number; light?: number }>()
 
   weightPoints.forEach((point) => {
-    mergedPoints.set(point.ts, { ...mergedPoints.get(point.ts), weight: point.value })
+    const timestamp = normalizeSuspensionTimestamp(point.ts)
+    mergedPoints.set(timestamp, { ...mergedPoints.get(timestamp), weight: point.value })
   })
   lightPoints.forEach((point) => {
-    mergedPoints.set(point.ts, { ...mergedPoints.get(point.ts), light: point.value })
+    const timestamp = normalizeSuspensionTimestamp(point.ts)
+    mergedPoints.set(timestamp, { ...mergedPoints.get(timestamp), light: point.value })
   })
 
-  return Array.from(mergedPoints, ([ts, value]) => ({
-    ts,
-    value: (value.weight ?? 0) - (value.light ?? 0)
-  })).sort((a, b) => a.ts - b.ts)
+  return Array.from(mergedPoints, ([ts, value]) => {
+    // 【修复前】任意一侧缺失也会使用 ?? 0 计算,生成无法代表真实状态的曲线点。
+    // 【修复后】两侧不完整时跳过该点,避免看板也出现错误的 0 值。
+    if (value.weight === undefined || value.light === undefined) return null
+
+    return {
+      ts,
+      value: value.weight - value.light
+    }
+  })
+    .filter((point): point is ChartPoint => point !== null)
+    .sort((a, b) => a.ts - b.ts)
 }
 
 async function initLoadChartData(real_time: boolean = true) {
@@ -930,6 +984,7 @@ async function initLoadChartData(real_time: boolean = true) {
   const loadVersion = ++chartLoadVersion
   chartRealtimeMode.value = real_time
   chartData.value = Object.fromEntries(dimensions.value.map((item) => [item.name, []]))
+  latestSuspensionPoints.clear()
   chartLoading.value = true
 
   try {
@@ -955,10 +1010,19 @@ async function initLoadChartData(real_time: boolean = true) {
               props.date[1]
             )
 
-            chartData.value[item.name] = mergeSuspensionHistory(
-              normalizeHistoryPoints(res),
-              normalizeHistoryPoints(lightRes)
-            )
+            const weightPoints = normalizeHistoryPoints(res)
+            const lightPoints = normalizeHistoryPoints(lightRes)
+            const latestWeightPoint = weightPoints[weightPoints.length - 1]
+            const latestLightPoint = lightPoints[lightPoints.length - 1]
+
+            if (latestWeightPoint) {
+              latestSuspensionPoints.set(SUSPENSION_WEIGHT_IDENTIFIER, latestWeightPoint)
+            }
+            if (latestLightPoint) {
+              latestSuspensionPoints.set(SUSPENSION_LIGHT_IDENTIFIER, latestLightPoint)
+            }
+
+            chartData.value[item.name] = mergeSuspensionHistory(weightPoints, lightPoints)
           } else {
             chartData.value[item.name] = (((res as any[]) ?? []) as any[])
               .sort((a, b) => a.ts - b.ts)
@@ -1033,6 +1097,7 @@ watch(
 
 onUnmounted(() => {
   chartLoadVersion += 1
+  latestSuspensionPoints.clear()
   destroy()
   cancelRealtimeChartUpdate()
 

+ 130 - 156
src/views/oli-connection/monitoring/detail.vue

@@ -142,6 +142,14 @@ interface ChartData {
 }
 
 const chartData = shallowRef<ChartData>({})
+
+// 【修复前】每条 MQTT 消息只在当前 payload 内查找 1001(悬重)和 1008(悬轻)。
+// 如果设备把两个测点拆成两条消息上报,当前消息缺少的那一侧会被当成 0。
+// 例如:只收到悬轻 0 时,旧逻辑会计算成 0 - 0 = 0,形成错误的“掉零”点。
+//
+// 【修复后】保存两侧最近一次真实收到的有效点。后续只收到一侧时,使用另一侧最近值参与计算,
+// 在还没有拿到两侧数据之前则不生成组合点,避免用虚假的 0 代替“暂无数据”。
+const latestSuspensionPoints = new Map<string, ChartPoint>()
 const resizeChart = () => chart?.resize()
 
 watch(isConnected, (newVal) => {
@@ -309,6 +317,34 @@ function getPointTimestamp(value: unknown) {
   return Number.isFinite(parsedValue) ? parsedValue : Date.now()
 }
 
+function normalizeSuspensionTimestamp(timestamp: number) {
+  // 图表 tooltip 只展示到秒,因此将组合曲线的毫秒统一归入对应的秒桶。
+  // 【修复前】14:26:26.000 和 14:26:26.500 会被 appendChartPoint 视为两个不同点,
+  // 但 tooltip 都显示为 14:26:26,视觉上就像同一秒出现了两个值。
+  // 【修复后】同一秒使用同一个时间戳,后到的组合结果覆盖先到的结果。
+  return Math.floor(timestamp / 1000) * 1000
+}
+
+function getCombinedSuspensionPoint(pointMap: Map<string, ChartPoint>) {
+  // 当前 payload 可能只包含 1001 或 1008 的其中一个,因此先更新缓存,
+  // 再从缓存中读取两侧最近一次有效值,而不是只依赖当前 payload。
+  for (const identifier of [SUSPENSION_WEIGHT_IDENTIFIER, SUSPENSION_LIGHT_IDENTIFIER]) {
+    const point = pointMap.get(identifier)
+    if (point) latestSuspensionPoints.set(identifier, point)
+  }
+
+  const weightPoint = latestSuspensionPoints.get(SUSPENSION_WEIGHT_IDENTIFIER)
+  const lightPoint = latestSuspensionPoints.get(SUSPENSION_LIGHT_IDENTIFIER)
+
+  // 两侧都没有数据时不能计算;返回 null 后,本次不会追加组合曲线点。
+  if (!weightPoint || !lightPoint) return null
+
+  return {
+    ts: normalizeSuspensionTimestamp(Math.max(weightPoint.ts, lightPoint.ts)),
+    value: weightPoint.value - lightPoint.value
+  }
+}
+
 type SeriesRange = ReturnType<typeof getSeriesRange>
 
 function mapChartPoint({ ts, value }: ChartPoint, range: SeriesRange) {
@@ -660,19 +696,33 @@ 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) => {
     const identity = String(item.id ?? item.identity ?? '')
-    const numberValue = Number(item.value ?? item.logValue ?? 0)
+    // 【修复前】item.value ?? item.logValue ?? 0 会把缺少数值的 MQTT 项伪装成 0。
+    // 【修复后】缺少 value/logValue 时直接忽略该项;真正上报的数值 0 仍然会被保留。
+    const rawValue = item.value ?? item.logValue
+    const numberValue = Number(rawValue)
 
-    if (!identity || !Number.isFinite(numberValue)) return
+    if (
+      !identity ||
+      rawValue === null ||
+      rawValue === undefined ||
+      rawValue === '' ||
+      !Number.isFinite(numberValue)
+    ) {
+      return
+    }
 
     valueMap.set(identity, numberValue)
     const point = {
-      ts: getPointTimestamp(item.remark),
+      // 【修复前】详情页只读取 remark;如果后端返回 readTime/ts/time,就会走 Date.now()。
+      // 【修复后】按设备消息可能使用的字段依次取值,保证点的时间尽量来自设备实际上报时间。
+      ts: getPointTimestamp(item.remark || item.readTime || item.ts || item.time),
       value: numberValue
     }
     pointMap.set(identity, point)
@@ -692,15 +742,9 @@ function handleMessageUpdate(_topic: string, payload: 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)
-      }
+    const combinedPoint = getCombinedSuspensionPoint(pointMap)
 
+    if (combinedPoint) {
       valueMap.set(SUSPENSION_WEIGHT_IDENTIFIER, combinedPoint.value)
 
       const dimension = dimensions.value.find(
@@ -711,6 +755,10 @@ function handleMessageUpdate(_topic: string, payload: any) {
         appendChartPoint(dimension.name, combinedPoint)
         updatedNames.add(dimension.name)
       }
+    } else {
+      // 组合尚未完整时,不让当前 payload 中的单侧原始值覆盖页面上的上一条完整组合值。
+      valueMap.delete(SUSPENSION_WEIGHT_IDENTIFIER)
+      valueMap.delete(SUSPENSION_LIGHT_IDENTIFIER)
     }
   }
 
@@ -749,16 +797,28 @@ function mergeSuspensionHistory(weightPoints: ChartPoint[], lightPoints: ChartPo
   const mergedPoints = new Map<number, { weight?: number; light?: number }>()
 
   weightPoints.forEach((point) => {
-    mergedPoints.set(point.ts, { ...mergedPoints.get(point.ts), weight: point.value })
+    // 历史接口的两条曲线可能存在毫秒差,例如悬重 26.100、悬轻 26.800;
+    // 先按展示秒归桶,才能把同一采样秒的两侧数据配对起来。
+    const timestamp = normalizeSuspensionTimestamp(point.ts)
+    mergedPoints.set(timestamp, { ...mergedPoints.get(timestamp), weight: point.value })
   })
   lightPoints.forEach((point) => {
-    mergedPoints.set(point.ts, { ...mergedPoints.get(point.ts), light: point.value })
+    const timestamp = normalizeSuspensionTimestamp(point.ts)
+    mergedPoints.set(timestamp, { ...mergedPoints.get(timestamp), light: point.value })
   })
 
-  return Array.from(mergedPoints, ([ts, value]) => ({
-    ts,
-    value: (value.weight ?? 0) - (value.light ?? 0)
-  })).sort((a, b) => a.ts - b.ts)
+  return Array.from(mergedPoints, ([ts, value]) => {
+    // 【修复前】任意一侧缺失都会使用 ?? 0 继续计算,产生无法代表真实状态的组合值。
+    // 【修复后】缺少任意一侧就不生成历史点,宁可没有点,也不显示伪造的 0。
+    if (value.weight === undefined || value.light === undefined) return null
+
+    return {
+      ts,
+      value: value.weight - value.light
+    }
+  })
+    .filter((point): point is ChartPoint => point !== null)
+    .sort((a, b) => a.ts - b.ts)
 }
 
 async function initLoadChartData({
@@ -775,6 +835,10 @@ async function initLoadChartData({
   }
 
   chartData.value = Object.fromEntries(dimensions.value.map((item) => [item.name, []]))
+
+  // 切换时间范围、重置图表或重新加载历史数据时,必须清理上一段数据的最近值缓存,
+  // 否则新时间范围的第一条单侧 MQTT 消息可能会和旧时间范围的数据错误组合。
+  latestSuspensionPoints.clear()
   chartLoading.value = true
 
   try {
@@ -803,10 +867,19 @@ async function initLoadChartData({
               selectedDate.value[1]
             )
 
-            chartData.value[item.name] = mergeSuspensionHistory(
-              normalizeHistoryPoints(res),
-              normalizeHistoryPoints(lightRes)
-            )
+            const weightPoints = normalizeHistoryPoints(res)
+            const lightPoints = normalizeHistoryPoints(lightRes)
+            const latestWeightPoint = weightPoints[weightPoints.length - 1]
+            const latestLightPoint = lightPoints[lightPoints.length - 1]
+
+            if (latestWeightPoint) {
+              latestSuspensionPoints.set(SUSPENSION_WEIGHT_IDENTIFIER, latestWeightPoint)
+            }
+            if (latestLightPoint) {
+              latestSuspensionPoints.set(SUSPENSION_LIGHT_IDENTIFIER, latestLightPoint)
+            }
+
+            chartData.value[item.name] = mergeSuspensionHistory(weightPoints, lightPoints)
           } else {
             chartData.value[item.name] = (((res as any[]) ?? []) as any[])
               .sort((a, b) => a.ts - b.ts)
@@ -918,7 +991,7 @@ async function exportChart() {
   }
 }
 
-const MAX_XLSX_TIME_COLUMNS = 16_383
+const MAX_XLSX_DATA_COLUMNS = 16_383
 
 function getSelectedExportDimensions() {
   return dimensions.value.filter((item) => selectedDimension.value[item.name])
@@ -933,18 +1006,24 @@ function buildExportRows(selectedDimensions: Dimensions[]) {
     )
   ).sort((a, b) => a - b)
 
+  // 导出矩阵转置为“每行一个时间点、每列一个属性”:
+  // 第一列是时间,后续列是选中的属性,便于按时间顺序查看和继续处理数据。
+  const valuesByDimension = new Map(
+    selectedDimensions.map((dimension) => [
+      dimension.name,
+      new Map((chartData.value[dimension.name] || []).map((point) => [point.ts, point.value]))
+    ])
+  )
   const rows: Array<Array<string | number | Date | null>> = [
-    ['属性名称', ...timestamps.map((timestamp) => new Date(timestamp))]
+    ['时间', ...selectedDimensions.map((dimension) => dimension.name)]
   ]
 
-  selectedDimensions.forEach((dimension) => {
-    const valuesByTimestamp = new Map(
-      (chartData.value[dimension.name] || []).map((point) => [point.ts, point.value])
-    )
-
+  timestamps.forEach((timestamp) => {
     rows.push([
-      dimension.name,
-      ...timestamps.map((timestamp) => valuesByTimestamp.get(timestamp) ?? null)
+      new Date(timestamp),
+      ...selectedDimensions.map(
+        (dimension) => valuesByDimension.get(dimension.name)?.get(timestamp) ?? null
+      )
     ])
   })
 
@@ -966,18 +1045,20 @@ async function exportChartData() {
     return
   }
 
-  if (timestamps.length > MAX_XLSX_TIME_COLUMNS) {
-    message.warning(`时间点共 ${timestamps.length} 个,请缩短时间范围后再导出`)
+  if (selectedDimensions.length > MAX_XLSX_DATA_COLUMNS) {
+    message.warning(
+      `属性共 ${selectedDimensions.length} 个,超过 Excel 列数限制,请减少属性后再导出`
+    )
     return
   }
 
   exportDataLoading.value = true
   try {
     const worksheet = XLSX.utils.aoa_to_sheet(rows, { cellDates: true })
-    worksheet['!cols'] = [{ wch: 24 }, ...timestamps.map(() => ({ wch: 20 }))]
+    worksheet['!cols'] = [{ wch: 22 }, ...selectedDimensions.map(() => ({ wch: 20 }))]
 
     timestamps.forEach((_, index) => {
-      const cell = worksheet[XLSX.utils.encode_cell({ r: 0, c: index + 1 })]
+      const cell = worksheet[XLSX.utils.encode_cell({ r: index + 1, c: 0 })]
       if (cell) cell.z = 'yyyy-mm-dd hh:mm:ss'
     })
 
@@ -1029,8 +1110,6 @@ function openRangeDialog(targetName?: string) {
     name: item.name,
     identifier: item.identifier,
     color: item.color,
-    minValue: normalizeRangeValue(item.minValue),
-    maxValue: normalizeRangeValue(item.maxValue),
     periodSecond: normalizeRangeValue(item.periodSecond),
     changeValue: normalizeRangeValue(item.changeValue),
     axisMin: normalizeRangeValue(item.axisMin),
@@ -1041,23 +1120,17 @@ function openRangeDialog(targetName?: string) {
 
 async function saveRangeSetting(
   item: Dimensions,
-  minValue?: number,
-  maxValue?: number,
   periodSecondValue?: number,
   changeValueValue?: number,
   axisMinValue?: number,
   axisMaxValue?: number
 ) {
-  const min = normalizeRangeValue(minValue)
-  const max = normalizeRangeValue(maxValue)
   const periodSecond = normalizeRangeValue(periodSecondValue)
   const changeValue = normalizeRangeValue(changeValueValue)
   const axisMin = normalizeRangeValue(axisMinValue)
   const axisMax = normalizeRangeValue(axisMaxValue)
 
   if (
-    min === undefined &&
-    max === undefined &&
     periodSecond === undefined &&
     changeValue === undefined &&
     axisMin === undefined &&
@@ -1077,26 +1150,10 @@ async function saveRangeSetting(
     return
   }
 
-  if ((min === undefined) !== (max === undefined)) {
-    throw new Error(`${item.name} 的告警上限和告警下限需要同时填写`)
-  }
-
-  if (min !== undefined && max !== undefined && min > max) {
-    throw new Error(`${item.name} 的告警下限不能大于告警上限`)
-  }
-
-  if (min !== undefined && max !== undefined && min === max) {
-    throw new Error(`${item.name} 的告警上限和告警下限不能相等`)
-  }
-
   if ((periodSecond === undefined) !== (changeValue === undefined)) {
     throw new Error(`${item.name} 的检查时间和超限范围需要同时填写`)
   }
 
-  if ((periodSecond !== undefined || changeValue !== undefined) && min === undefined) {
-    throw new Error(`${item.name} 需要先设置告警上限和告警下限`)
-  }
-
   if (periodSecond !== undefined && (!Number.isInteger(periodSecond) || periodSecond <= 0)) {
     throw new Error(`${item.name} 的检查时间必须为大于 0 的整数秒`)
   }
@@ -1114,8 +1171,8 @@ async function saveRangeSetting(
   }
 
   const body = {
-    minValue: min ?? null,
-    maxValue: max ?? null,
+    minValue: null,
+    maxValue: null,
     periodSecond: periodSecond ?? null,
     changeValue: changeValue ?? null,
     axisMin: axisMin ?? null,
@@ -1130,8 +1187,8 @@ async function saveRangeSetting(
   const res = await IotDeviceApi.saveMaxMin(body)
 
   if (res.id) item.id = res.id
-  item.minValue = min
-  item.maxValue = max
+  item.minValue = undefined
+  item.maxValue = undefined
   item.periodSecond = periodSecond
   item.changeValue = changeValue
   item.axisMin = axisMin
@@ -1146,8 +1203,6 @@ async function handleRangeDialogSave() {
 
       await saveRangeSetting(
         item,
-        draft.minValue,
-        draft.maxValue,
         draft.periodSecond,
         draft.changeValue,
         draft.axisMin,
@@ -1166,8 +1221,6 @@ async function handleRangeDialogSave() {
 function handleRangeDialogReset() {
   rangeSettingDrafts.value = rangeSettingDrafts.value.map((item) => ({
     ...item,
-    minValue: undefined,
-    maxValue: undefined,
     periodSecond: undefined,
     changeValue: undefined,
     axisMin: undefined,
@@ -1176,8 +1229,6 @@ function handleRangeDialogReset() {
 }
 
 function clearRangeDraft(row: RangeSettingDraft) {
-  row.minValue = undefined
-  row.maxValue = undefined
   row.periodSecond = undefined
   row.changeValue = undefined
   row.axisMin = undefined
@@ -1190,6 +1241,7 @@ onMounted(() => {
 
 onUnmounted(() => {
   chartLoadVersion += 1
+  latestSuspensionPoints.clear()
   destroy()
   cancelRealtimeChartUpdate()
   window.removeEventListener('resize', resizeChart)
@@ -1279,22 +1331,6 @@ onUnmounted(() => {
                     <span v-else class="dimension-card__value">
                       {{ item.value }}
                     </span>
-
-                    <span
-                      v-if="item.minValue !== undefined || item.maxValue !== undefined"
-                      class="dimension-card__ranges">
-                      <span v-if="item.maxValue !== undefined" class="range-pill range-pill--max">
-                        <i class="i-material-symbols:arrow-upward-alt-rounded"></i>
-                        <span>告警上限</span>
-                        <strong>{{ formatChartValue(item.maxValue) }}</strong>
-                      </span>
-
-                      <span v-if="item.minValue !== undefined" class="range-pill range-pill--min">
-                        <i class="i-material-symbols:arrow-downward-alt-rounded"></i>
-                        <span>告警下限</span>
-                        <strong>{{ formatChartValue(item.minValue) }}</strong>
-                      </span>
-                    </span>
                   </span>
                 </button>
               </div>
@@ -1389,17 +1425,17 @@ onUnmounted(() => {
 
     <el-dialog
       v-model="rangeDialogVisible"
-      title="告警阈值、检查时间、超限范围与 Y 轴范围"
+      title="检查时间、超限范围与 Y 轴范围"
       width="min(1140px, calc(100vw - 32px))"
       append-to-body
       destroy-on-close>
       <ZmTable
         :data="rangeSettingDrafts"
         :loading="false"
-        :max-height="520"
+        :max-height="420"
         :show-border="true"
         :fit="false">
-        <ZmTableColumn label="曲线" width="210" fixed="left" align="left">
+        <ZmTableColumn label="曲线" fixed="left" align="left">
           <template #default="{ row }">
             <div class="flex items-center gap-2 min-w-0">
               <span
@@ -1411,25 +1447,7 @@ onUnmounted(() => {
             </div>
           </template>
         </ZmTableColumn>
-        <ZmTableColumn label="告警下限" width="115">
-          <template #default="{ row }">
-            <el-input-number
-              v-model="row.minValue"
-              class="!w-full"
-              :controls="false"
-              placeholder="未设置" />
-          </template>
-        </ZmTableColumn>
-        <ZmTableColumn label="告警上限" width="115">
-          <template #default="{ row }">
-            <el-input-number
-              v-model="row.maxValue"
-              class="!w-full"
-              :controls="false"
-              placeholder="未设置" />
-          </template>
-        </ZmTableColumn>
-        <ZmTableColumn label="检查时间(秒)" width="125">
+        <ZmTableColumn label="检查时间(秒)">
           <template #default="{ row }">
             <el-input-number
               v-model="row.periodSecond"
@@ -1437,10 +1455,10 @@ onUnmounted(() => {
               :controls="false"
               :min="1"
               :precision="0"
-              placeholder="仅当前值" />
+              placeholder="未设置" />
           </template>
         </ZmTableColumn>
-        <ZmTableColumn label="超限范围" width="115">
+        <ZmTableColumn label="超限范围">
           <template #default="{ row }">
             <el-input-number
               v-model="row.changeValue"
@@ -1450,7 +1468,7 @@ onUnmounted(() => {
               placeholder="0" />
           </template>
         </ZmTableColumn>
-        <ZmTableColumn label="Y 轴下限" width="110">
+        <ZmTableColumn label="Y 轴下限">
           <template #default="{ row }">
             <el-input-number
               v-model="row.axisMin"
@@ -1459,7 +1477,7 @@ onUnmounted(() => {
               :placeholder="formatChartValue(getSeriesRange(row.name).labelMin)" />
           </template>
         </ZmTableColumn>
-        <ZmTableColumn label="Y 轴上限" width="110">
+        <ZmTableColumn label="Y 轴上限">
           <template #default="{ row }">
             <el-input-number
               v-model="row.axisMax"
@@ -1468,7 +1486,7 @@ onUnmounted(() => {
               :placeholder="formatChartValue(getSeriesRange(row.name).labelMax)" />
           </template>
         </ZmTableColumn>
-        <ZmTableColumn label="当前 Y 轴范围" width="120">
+        <ZmTableColumn label="当前 Y 轴范围">
           <template #default="{ row }">
             <span class="text-xs text-slate-500">
               {{ formatChartValue(getSeriesRange(row.name).labelMin) }}
@@ -1477,7 +1495,7 @@ onUnmounted(() => {
             </span>
           </template>
         </ZmTableColumn>
-        <ZmTableColumn label="操作" width="66">
+        <ZmTableColumn label="操作">
           <template #default="{ row }">
             <el-button size="small" link type="primary" @click="clearRangeDraft(row)">
               清空
@@ -1683,50 +1701,6 @@ onUnmounted(() => {
   white-space: nowrap;
 }
 
-.dimension-card__ranges {
-  display: flex;
-  flex-direction: column;
-  gap: 4px;
-  align-items: flex-end;
-  justify-content: center;
-  max-width: 100%;
-}
-
-.range-pill {
-  display: inline-flex;
-  align-items: center;
-  gap: 3px;
-  height: 20px;
-  padding: 0 6px;
-  font-size: 10px;
-  font-weight: 800;
-  line-height: 1;
-  border: 1px solid;
-  border-radius: 999px;
-}
-
-.range-pill i {
-  width: 12px;
-  height: 12px;
-}
-
-.range-pill strong {
-  font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
-  font-size: 11px;
-}
-
-.range-pill--max {
-  color: #047857;
-  background: rgb(236 253 245 / 78%);
-  border-color: rgb(167 243 208 / 86%);
-}
-
-.range-pill--min {
-  color: #be123c;
-  background: rgb(255 241 242 / 78%);
-  border-color: rgb(254 205 211 / 88%);
-}
-
 .chart-panel {
   display: flex;
   flex-direction: column;