Ver Fonte

实时曲线页面和接口开发

cuiHe há 2 semanas atrás
pai
commit
4b92613065

+ 14 - 0
frontend/src/views/web/detail/realtimeAlarm.vue

@@ -135,4 +135,18 @@ onUnmounted(() => {
 .pilot-lamp-bg-orange {
   color: #f39902;
 }
+
+::-webkit-scrollbar {
+  width: 5px;
+  height: 5px;
+}
+
+::-webkit-scrollbar-track {
+  border-radius: 10px;
+}
+
+::-webkit-scrollbar-thumb {
+  border-radius: 7px;
+  background-color: #798DAE;
+}
 </style>

+ 499 - 296
frontend/src/views/web/detail/realtimeCurve.vue

@@ -1,315 +1,518 @@
 <template>
-    <div class="realtime-curve-container">
-      <!-- ECharts 容器 -->
-      <div ref="chartRef" class="chart-box"></div>
-    </div>
-  </template>
-  
-  <script setup lang="ts">
-  // 补全所有必要导入,并严格指定类型
-  import { ref, reactive, onMounted, onUnmounted, watch } from 'vue';
-  import * as echarts from 'echarts';
-  import type { ECharts, EChartsOption, YAxisOption, SeriesOption } from 'echarts';
-  import BizVarDictAPI, { BizVarDictTable, BizVarDictPageQuery } from '@/api/module_business/vardict';
-  import MqttUtil, { MqttMessageCallback } from '@/utils/mqttUtil';
-  
-  // ========== 严格 TS 类型定义 ==========
-  /** MQTT 消息 payload 数据项类型 */
-  interface MqttPayloadItem {
-    var_code: string;
-    value: number | string;
-    [key: string]: unknown; // 兼容其他字段
+  <div class="realtimeCurve-content">
+    <div v-loading="loading" class="echarts-content">
+      <div ref="chartRef" class="echarts-container" :style="{ height: height }"></div>
+    </div>    
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, reactive, onMounted, onUnmounted, nextTick } from 'vue';
+import * as echarts from 'echarts';
+import type { ECharts, EChartsOption } from 'echarts';
+import BizVarDictAPI, { BizVarDictTable, BizVarDictPageQuery } from '@/api/module_business/vardict';
+import MqttUtil, { MqttMessageCallback } from '@/utils/mqttUtil';
+
+// 起重机信息
+const craneInfo = JSON.parse(localStorage.getItem('craneInfo') || '{}');
+const loading = ref(true);
+const height = ref('0px');
+
+// 查询参数
+const queryFormVarDictData = reactive<BizVarDictPageQuery>({
+  page_no: 0,
+  page_size: 0,
+  crane_no: craneInfo.crane_no,
+  var_code: undefined,
+  var_name: undefined,
+  mec_type: undefined,
+  switch_type: undefined,
+  gateway_id: undefined,
+  var_group: undefined,
+  var_category: undefined,
+  is_top_show: undefined,
+  is_save: undefined,
+  is_overview_top_show: undefined,
+  is_home_page_show: undefined,
+  status: undefined,
+  created_time: undefined,
+  updated_time: undefined,
+  created_id: undefined,
+  updated_id: undefined,
+});
+
+// MQTT配置
+const mqttConfig = {
+  wsUrl: import.meta.env.VITE_APP_WS_ENDPOINT || 'ws://127.0.0.1:9001',
+  topics: ['cdc/' + craneInfo.crane_no + '/analog/batch/#'],
+};
+const mqttUtil = new MqttUtil(mqttConfig);
+
+// 变量数据存储
+const allData = ref<BizVarDictTable[]>([]);
+
+// ========== ECharts核心配置 ==========
+const chartRef = ref<HTMLDivElement | null>(null);
+let chartInstance: ECharts | null = null;
+const MAX_DATA_POINTS = 100; // 单条曲线最大数据点
+const gridHeight = 150; // 每组Grid高度(px)
+const gridGap = 50; // 组间间距,保持宽松不拥挤
+const gridTopBase = 80; // 增大顶部基础间距,避免图例遮挡第一组
+
+// 全局X轴时间范围(关键:统一所有Grid的X轴范围)
+const xAxisRange = ref<{ min: number; max: number }>({
+  min: Date.now(), // 【修改1】初始无数据时设为当前时间,避免5分钟空白
+  max: Date.now()
+});
+
+// 单条曲线数据结构:[时间戳, 值]
+interface SeriesDataItem {
+  name: string; // 变量名(var_name)
+  code: string; // 变量编码
+  data: [number, number][]; // 时间戳+值
+}
+const seriesData = ref<SeriesDataItem[]>([]);
+
+// ========== 数据加载 ==========
+const getData = async () => {
+  const response = await BizVarDictAPI.listBizVarDictAnalog(queryFormVarDictData);
+  allData.value = response.data.data || [];
+  // 初始化多grid+多曲线
+  initMultiGridSeries();
+};
+
+// ========== 多Grid+多曲线初始化 ==========
+const initMultiGridSeries = () => {
+  if (allData.value.length === 0) {
+    console.warn('无变量数据,无法生成曲线');
+    return;
   }
-  
-  /** MQTT 消息 payload 类型 */
-  interface MqttPayload {
-    data: MqttPayloadItem[];
-    [key: string]: unknown;
-  }
-  
-  /** 曲线数据结构类型 */
-  interface CurveDataItem {
-    xData: string[]; // X轴:时间戳字符串
-    yData: number[]; // Y轴:数值
-  }
-  
-  /** 曲线数据映射类型 */
-  type CurveDataMap = Record<string, CurveDataItem>;
-  
-  // ========== 业务配置 ==========
-  const craneInfo = JSON.parse(localStorage.getItem('craneInfo') || '{}') as Record<string, string>;
-  const loading = ref<boolean>(true);
-  // 严格指定查询参数类型,避免 undefined 隐式类型
-  const queryFormVarDictData = reactive<BizVarDictPageQuery>({
-    page_no: 0,
-    page_size: 0,
-    crane_no: craneInfo.crane_no || '', // 兜底空字符串,避免 TS 报错
-    var_code: undefined,
-    var_name: undefined,
-    mec_type: undefined,
-    switch_type: undefined,
-    gateway_id: undefined,
-    var_group: undefined,
-    var_category: undefined,
-    is_top_show: undefined,
-    is_save: undefined,
-    is_overview_top_show: undefined,
-    is_home_page_show: undefined,
-    status: undefined,
-    created_time: undefined,
-    updated_time: undefined,
-    created_id: undefined,
-    updated_id: undefined,
+
+  // 为每个变量初始化曲线数据(初始值+当前时间戳)
+  seriesData.value = allData.value.map(item => ({
+    name: item.var_name || '未知变量',
+    code: item.var_code || '',
+    data: [], // 初始数据点
+  }));
+  // 确保动态高度赋值完成,DOM渲染就绪后再初始化ECharts
+  nextTick(() => {
+    initEchartsWithMultiGrid();
   });
-  
-  // MQTT 配置(TS 类型约束)
-  const mqttConfig = {
-    wsUrl: import.meta.env.VITE_APP_WS_ENDPOINT || 'ws://127.0.0.1:9001',
-    topics: [`cdc/${craneInfo.crane_no || ''}/analog/batch/#`], // 兜底空字符串
-  };
-  const mqttUtil = new MqttUtil(mqttConfig);
-  
-  // ========== 核心数据(严格 TS 类型) ==========
-  const allData = ref<BizVarDictTable[]>([]);
-  const chartRef = ref<HTMLDivElement | null>(null);
-  let chartInstance: ECharts | null = null;
-  const curveData = ref<CurveDataMap>({});
-  const MAX_DATA_LEN = 30; // 最大数据点数量
-  
-  // ========== 解决 ECharts 报错核心:规范 Y 轴/系列配置 ==========
-  const initECharts = (): void => {
-    if (!chartRef.value) return;
-    
-    // 销毁旧实例,避免重复创建
-    if (chartInstance) {
-      chartInstance.dispose();
-    }
-  
-    // 初始化 ECharts 实例(指定 DOM 类型)
-    chartInstance = echarts.init(chartRef.value);
-    
-    // 基础配置:无标题、图例置顶(规避 getAxesOnZeroOf 报错的核心:Y轴先初始化单轴,后续动态更新)
-    const baseOption: EChartsOption = {
-      title: { show: false }, // 隐藏标题
-      legend: {
-        top: 0, // 图例置顶
-        left: 'center',
-        textStyle: { fontSize: 12 },
-        type: 'scroll', // 图例过多时滚动,避免溢出
-      },
-      tooltip: {
-        trigger: 'axis',
-        axisPointer: { type: 'line' }, // 改用 line 类型,避免 shadow 兼容性问题
-        formatter: (params: echarts.EChartOption.TooltipFormatterParams) => {
-          if (!Array.isArray(params) || params.length === 0) return '';
-          const timeStr = new Date(params[0].name).toLocaleTimeString();
-          return params.reduce((res, param) => {
-            const value = param.value as number;
-            return `${res}${param.seriesName}: ${value.toFixed(2)}<br/>`;
-          }, `${timeStr}<br/>`);
-        },
-      },
-      grid: {
-        left: '10%',
-        right: '10%',
-        bottom: '15%',
-        top: '15%',
-        containLabel: true,
-      },
-      xAxis: {
-        type: 'category',
-        data: [],
-        axisLabel: {
-          formatter: (value: string) => new Date(value).toLocaleTimeString(),
-        },
-        boundaryGap: false, // 取消边界间隙,曲线更连续
+};
+
+// ========== 多Grid布局的ECharts初始化 ==========
+const initEchartsWithMultiGrid = () => {
+  if (!chartRef.value || !seriesData.value.length) return;
+
+  // 创建ECharts实例
+  chartInstance = echarts.init(chartRef.value);
+
+  // 计算每个Grid的尺寸:核心优化确保任意数量数据完整显示
+  const gridCount = seriesData.value.length; // 取allData实际数量(十几条也支持)
+
+  // 动态计算高度:根据allData数量计算总高度,赋值给height ref
+  const totalChartHeight = gridTopBase + gridCount * (gridHeight + gridGap);
+  height.value = `${totalChartHeight}px`; // 绑定到容器style的height属性
+
+  // 1. 构建多Grid配置
+  const grids = seriesData.value.map((_, index) => ({
+    id: `grid_${index}`,
+    left: '3%', // 【修改2】Grid左间距设为0,消除视觉上的左侧空白
+    right: '3%', // 【修改2】Grid右间距设为0,消除视觉上的右侧空白
+    top: gridTopBase + index * (gridHeight + gridGap),
+    height: gridHeight,
+    width: '96%', // 【修改2】Grid宽度设为100%,填满容器
+    containLabel: true,
+    padding: [10, 20, 20, 20], // 微调内边距,避免标签溢出
+    show: true
+  }));
+
+  // 2. 构建多X轴配置(统一时间范围 + 保留白色大字体 + 仅保留横线(竖线关闭))
+  const xAxes: any[] = seriesData.value.map((_, index) => ({
+    gridIndex: index,
+    type: 'time',
+    show: true,
+    // 关键:所有X轴使用统一的时间范围
+    min: xAxisRange.value.min,
+    max: xAxisRange.value.max,
+    axisLabel: {
+      formatter: (value: number) => new Date(value).toLocaleTimeString(),
+      fontSize: 14,
+      color: '#506388',
+      margin: 10
+    },
+    // 关闭X轴分割线,避免视觉干扰
+    splitLine: { show: false },
+    // 强制X轴刻度对齐
+    alignTicks: true,
+    boundaryGap: false, // 取消边界间隙,确保时间轴完全对齐
+    axisLine: {
+      onZero: false // 【修改3】关闭轴线对齐0点,避免额外留白
+    },
+    scale: false // 【修改3】禁用自动缩放,严格按min/max显示
+  }));
+
+  // 3. 构建多Y轴配置(保留白色大字体 + 恢复灰色细横线(仅横线))
+  const yAxes: any[] = seriesData.value.map((item, index) => ({
+    gridIndex: index,
+    type: 'value',
+    show: true,
+    name: item.name,
+    nameTextStyle: {
+      fontSize: 14, // 字号稍大
+      color: '#fff' // y轴名称白色字体
+    },
+    nameGap: 20,
+    axisLabel: {
+      formatter: '{value}',
+      fontSize: 14, // 字号稍大
+      color: '#506388', // 刻度值字体
+      margin: 30
+    },
+    // 水平网格线
+    splitLine: {
+      show: true, // 开启水平网格线(横线)
+      lineStyle: {
+        color: '#3E5487', // 灰色(与深色背景协调)
+        width: 1 // 柔和不突兀,避免太扎眼
+      }
+    },
+    position: 'left' // 恢复y轴左侧布局
+  }));
+
+  // 4. 构建多Series配置(确保曲线颜色和图例颜色对照,支持任意数据量)
+  const series: any[] = seriesData.value.map((item, index) => {
+    const curveColor = getColor(index); // 调用优化后的颜色函数,支持任意index
+    return {
+      name: item.name,
+      type: 'line',
+      smooth: true,
+      symbol: 'none',
+      gridIndex: index,
+      xAxisIndex: index,
+      yAxisIndex: index,
+      data: item.data,
+      color: curveColor, // 明确设置series颜色(图例将自动取该颜色)
+      lineStyle: { color: curveColor, width: 2 }, // 曲线颜色与series颜色一致
+      itemStyle: { color: curveColor }, // 图例标记颜色与曲线一致
+      areaStyle: {
+        // 优化面积图颜色,增加渐变和透明度,适配动态颜色
+        color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
+          { offset: 0, color: curveColor + 'cc' }, // 顶部半透明
+          { offset: 1, color: curveColor + '11' }  // 底部几乎透明
+        ])
+      }
+    };
+  });
+
+  // ECharts最终配置
+  const option: EChartsOption = {
+    // 【核心配置1】全局开启刻度对齐
+    alignTicks: true,
+    // 图例配置(确保与曲线颜色对照,支持任意数据量)
+    legend: {
+      show: true,
+      top: 20,
+      left: 'center',
+      data: seriesData.value.map(item => item.name),
+      textStyle: {
+        fontSize: 14,
+        color: '#fff' // 图例文字白色字体
       },
-      // 初始单 Y 轴(解决 getAxesOnZeroOf 报错),后续按变量值域分组分配区间
-      yAxis: {
-        type: 'value',
-        axisLine: { show: true },
-        splitLine: { show: true },
+      itemWidth: 12,
+      itemHeight: 12,
+      padding: [0, 20],
+      formatter: (name) => {
+        return name.length > 10 ? name.substring(0, 10) + '...' : name;
+      }
+    },
+    // Tooltip配置
+    tooltip: {
+      trigger: 'axis',
+      triggerOn: 'mousemove',
+      formatter: function (params: any) {
+        if (!params || params.length === 0) return '';
+
+        const targetTime = params[0].data[0];
+        const timeStr = new Date(targetTime).toLocaleTimeString();
+        let tooltipHtml = `<div style="font-weight: bold; margin-bottom: 8px;">时间:${timeStr}</div>`;
+
+        seriesData.value.forEach((seriesItem, idx) => {
+          const closestData = seriesItem.data.find(d => d[0] === targetTime);
+          const value = closestData ? closestData[1] : '-';
+          tooltipHtml += `
+            <div style="display: flex; align-items: center; margin: 4px 0;">
+              <span style="display: inline-block; width: 8px; height: 8px; background: ${getColor(idx)}; margin-right: 8px; border-radius: 50%;"></span>
+              <span>${seriesItem.name}:</span>
+              <span style="margin-left: 8px; font-family: monospace;">${value}</span>
+            </div>
+          `;
+        });
+        return tooltipHtml;
       },
-      series: [], // 动态生成系列
-    };
-  
-    chartInstance.setOption(baseOption);
-    
-    // 监听窗口大小变化
-    window.addEventListener('resize', handleResize);
-  };
-  
-  /** 窗口大小变化处理 */
-  const handleResize = (): void => {
-    chartInstance?.resize();
-  };
-  
-  /** 初始化曲线数据 */
-  const initCurveData = (): void => {
-    if (!allData.value.length) return;
-  
-    // 初始化每个变量的曲线数据
-    curveData.value = allData.value.reduce((map, item) => {
-      map[item.var_code] = { xData: [], yData: [] };
-      return map;
-    }, {} as CurveDataMap);
-  
-    // 初始化系列配置(曲线不交叉核心:按变量值域分配不同区间)
-    updateEChartsSeries();
+      backgroundColor: 'rgba(255,255,255,0.95)',
+      borderColor: '#e6e6e6',
+      borderWidth: 1,
+      padding: 12,
+      textStyle: { fontSize: 12 },
+    },
+    height: totalChartHeight,
+    grid: grids,
+    xAxis: xAxes,
+    yAxis: yAxes,
+    series: series,
   };
-  
-  /** 更新 ECharts 系列配置(规避曲线交叉:值域分段) */
-  const updateEChartsSeries = (): void => {
-    if (!chartInstance || !allData.value.length) return;
-  
-    // 为每个变量分配独立值域区间(避免交叉)
-    const seriesList: SeriesOption[] = allData.value.map((item, index) => {
-      // 按索引分配值域区间(如第1个变量[0-100],第2个[100-200],以此类推)
-      const valueRange = index * 100;
-  
+
+  chartInstance.setOption(option);
+
+  // 【监听图例勾选变化事件】
+  chartInstance.off('legendselectchanged');
+  chartInstance.on('legendselectchanged', (params: any) => {
+    if (!params || !params.selected) {
+      console.warn('无有效勾选状态数据');
+      return;
+    }
+
+    const selectedLegend = params.selected;
+
+    // 【核心步骤1:筛选出「显示状态」的图例名称和对应的索引】
+    const showSeriesNames = Object.entries(selectedLegend)
+      .filter(([_, isSelected]) => isSelected)
+      .map(([name]) => name);
+    // 得到显示的索引数组(按原有顺序排列)
+    const showIndices = seriesData.value
+      .map((item, index) => ({ name: item.name, index }))
+      .filter((item) => showSeriesNames.includes(item.name))
+      .map((item) => item.index);
+
+    // 【核心步骤2:遍历所有grid,更新show属性 + 重新计算显示grid的top值】
+    const updatedGrids = grids.map((grid, index) => {
+      const seriesName = seriesData.value[index].name;
+      const isShow = selectedLegend[seriesName] || false;
+
+      if (!isShow) {
+        // 隐藏的grid:仅设置show: false,保持其他属性不变
+        return { ...grid, show: false };
+      } else {
+        // 显示的grid:找到它在「显示数组」中的排序,重新计算top值
+        const showSortIndex = showIndices.findIndex(showIndex => showIndex === index);
+        const newTop = gridTopBase + showSortIndex * (gridHeight + gridGap);
+        return { ...grid, show: true, top: newTop }; // 应用新的top值,实现紧凑排列
+      }
+    });
+
+    // 【原有逻辑:更新xAxis/yAxis的show属性 + 同步X轴范围】
+    const updatedXAxes = xAxes.map((xAxis, index) => {
+      const seriesName = seriesData.value[index].name;
       return {
-        name: item.var_name || item.var_code,
-        type: 'line',
-        data: curveData.value[item.var_code]?.yData || [],
-        symbol: 'none',
-        smooth: true,
-        lineStyle: { width: 2 },
-        // 值域映射:将原始值映射到独立区间,避免交叉
-        encode: { y: 0 },
-        // 自定义数值转换(核心:曲线不交叉)
-        transform: [
-          {
-            type: 'ecSimpleTransform:custom',
-            config: {
-              callback: (params: { value: number }) => {
-                // 原始值 + 区间偏移量,保证每条曲线值域不重叠
-                return { value: params.value + valueRange };
-              },
-            },
-          },
-        ],
+        ...xAxis,
+        show: selectedLegend[seriesName] || false,
+        min: xAxisRange.value.min, // 保持X轴范围统一
+        max: xAxisRange.value.max, // 保持X轴范围统一
+        boundaryGap: false, // 【修改4】图例切换时同步保留无留白配置
+        axisLine: { onZero: false },
+        scale: false
       };
     });
-  
-    // 更新系列配置(避免直接修改 Y 轴导致 getAxesOnZeroOf 报错)
-    chartInstance.setOption({
-      series: seriesList,
+
+    const updatedYAxes = yAxes.map((yAxis, index) => {
+      const seriesName = seriesData.value[index].name;
+      return { ...yAxis, show: selectedLegend[seriesName] || false };
     });
-  };
-  
-  /** 更新曲线数据(严格 TS 类型) */
-  const updateCurveData = (varCode: string, value: number): void => {
-    if (!curveData.value[varCode]) return;
-  
-    const now = new Date().toISOString();
-    const curveItem = curveData.value[varCode];
-  
-    // 添加新数据
-    curveItem.xData.push(now);
-    curveItem.yData.push(Number(value));
-  
-    // 限制数据长度
-    if (curveItem.xData.length > MAX_DATA_LEN) {
-      curveItem.xData.shift();
-      curveItem.yData.shift();
-    }
-  
-    // 更新 ECharts(仅更新数据,不修改轴配置,规避报错)
+
+    // 【原有逻辑:重新计算图表总高度】
+    const showGridCount = showIndices.length;
+    const newTotalChartHeight = gridTopBase + showGridCount * (gridHeight + gridGap);
+    height.value = `${newTotalChartHeight}px`;
+
+    // 【核心步骤3:更新ECharts配置,应用新的grid位置和尺寸】
+    chartInstance?.setOption({
+      grid: updatedGrids,
+      xAxis: updatedXAxes,
+      yAxis: updatedYAxes,
+      height: newTotalChartHeight
+    });
+
+    // 【原有逻辑:重新调整图表尺寸】
+    chartInstance?.resize({
+      width: 'auto', // 精准获取容器实际宽度
+      height: newTotalChartHeight
+    });
+  });
+
+  // 初始化后主动调用resize,强制适配容器尺寸,解决不显示问题
+  chartInstance.resize({
+    width: 'auto',
+    height: totalChartHeight,
+  });
+
+  // 窗口自适应 + 滚动容器适配
+  const resizeHandler = () => {
     if (chartInstance) {
-      const seriesOption = chartInstance.getOption().series as SeriesOption[];
-      const targetSeriesIndex = allData.value.findIndex(item => item.var_code === varCode);
-      
-      if (targetSeriesIndex > -1) {
-        chartInstance.setOption({
-          xAxis: { data: curveItem.xData },
-          series: [
-            {
-              index: targetSeriesIndex,
-              data: curveItem.yData,
-            },
-          ],
-        });
-      }
-    }
-  };
-  
-  // ========== 数据获取 & MQTT 处理(严格 TS 类型) ==========
-  const getData = async (): Promise<void> => {
-    try {
-      const response = await BizVarDictAPI.listBizVarDictAnalog(queryFormVarDictData);
-      allData.value = response.data.data || [];
-      initCurveData();
-    } catch (error) {
-      console.error('获取变量数据失败:', error);
-    } finally {
-      loading.value = false;
-    }
-  };
-  
-  /** MQTT 消息处理(严格 TS 类型,兼容 analog/alarm 后缀) */
-  const handleMqttMessage: MqttMessageCallback = (topic: string, payload: MqttPayload): void => {
-    const topicLevels = topic.split('/');
-    if (topicLevels.length < 4 || !payload.data) return;
-  
-    const suffix = topicLevels[2];
-    // 兼容 analog(实时数据)和 alarm(告警数据)
-    if (['analog', 'alarm'].includes(suffix)) {
-      payload.data.forEach((payloadItem) => {
-        const { var_code, value } = payloadItem;
-        if (!var_code || value === undefined) return;
-  
-        // 更新 allData 中的值
-        const targetItem = allData.value.find(item => item.var_code === var_code);
-        if (targetItem) {
-          targetItem.value = Number(value);
-        }
-  
-        // 更新曲线数据(仅处理数值类型)
-        const numValue = Number(value);
-        if (!isNaN(numValue)) {
-          updateCurveData(var_code, numValue);
-        }
+      chartInstance.resize({
+        width: 'auto',
+        height: 'auto',
       });
     }
   };
-  
-  // ========== 生命周期(严格 TS 规范) ==========
-  onMounted(async (): Promise<void> => {
-    initECharts();
-    await getData();
-    mqttUtil.initConnect(handleMqttMessage);
+  window.addEventListener('resize', resizeHandler);
+};
+
+// ========== 更新全局X轴时间范围(关键:保证所有曲线水平对齐) ==========
+const updateXAxisRange = () => {
+  // 收集所有曲线的时间戳
+  const allTimestamps: number[] = [];
+  seriesData.value.forEach(item => {
+    item.data.forEach(([timestamp]) => allTimestamps.push(timestamp));
   });
-  
-  onUnmounted((): void => {
-    mqttUtil.releaseResources();
-    window.removeEventListener('resize', handleResize);
-    // 销毁 ECharts 实例,避免内存泄漏
-    if (chartInstance) {
-      chartInstance.dispose();
-      chartInstance = null;
+
+  if (allTimestamps.length === 0) {
+    // 无数据时设为当前时间(无空白)
+    xAxisRange.value = {
+      min: Date.now(),
+      max: Date.now()
+    };
+  } else {
+    // 【修改5:移除±1000的缓冲,严格使用数据的最小/最大时间】
+    const minTime = Math.min(...allTimestamps);
+    const maxTime = Math.max(...allTimestamps);
+    xAxisRange.value = {
+      min: minTime,
+      max: maxTime
+    };
+  }
+};
+
+// ========== MQTT回调(实时更新多Grid曲线) ==========
+const handleMqttMessage: MqttMessageCallback = (topic, payload) => {
+  if (!payload?.data) return;
+
+  // 遍历MQTT数据,更新对应变量的曲线
+  payload.data.forEach((payloadItem: any) => {
+    if (!payloadItem.var_code) return;
+
+    // 找到对应变量的曲线数据
+    const targetSeries = seriesData.value.find(item => item.code === payloadItem.var_code);
+    if (targetSeries) {
+      // 添加新数据点(当前时间戳 + 最新值)
+      targetSeries.data.push([payload.timestamp, payloadItem.value]);
+      // 超出最大数据点时,删除最早的点
+      if (targetSeries.data.length > MAX_DATA_POINTS) {
+        targetSeries.data.shift();
+      }
     }
   });
-  
-  // 深度监听 allData 变化
-  watch(
-    allData,
-    () => {
-      if (!loading.value) {
-        initCurveData();
+
+  // 关键:更新全局X轴范围,确保所有曲线时间轴对齐
+  updateXAxisRange();
+  // 实时更新图表
+  updateEcharts();
+  loading.value = false;
+};
+
+// ========== 图表更新方法(同步X轴范围) ==========
+const updateEcharts = () => {
+  if (!chartInstance || !seriesData.value.length) return;
+
+  // 1. 更新所有X轴的时间范围(同步无留白配置)
+  const updatedXAxes = seriesData.value.map((_, index) => ({
+    gridIndex: index,
+    min: xAxisRange.value.min,
+    max: xAxisRange.value.max,
+    boundaryGap: false, // 【修改6】数据更新时同步保留无留白配置
+    axisLine: { onZero: false },
+    scale: false
+  }));
+
+  // 2. 更新series数据(避免重复渲染grid/x/y轴)
+  const updatedSeries = seriesData.value.map((item, index) => {
+    const curveColor = getColor(index);
+    return {
+      gridIndex: index,
+      xAxisIndex: index,
+      yAxisIndex: index,
+      data: item.data,
+      color: curveColor, // 保持更新时颜色一致
+      lineStyle: { color: curveColor, width: 2 },
+      areaStyle: {
+        color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
+          { offset: 0, color: curveColor + 'cc' },
+          { offset: 1, color: curveColor + '11' }
+        ])
       }
-    },
-    { deep: true }
-  );
-  </script>
-  
-  <style lang="less" scoped>
-  .realtime-curve-container {
-    width: 100%;
-    height: 100vh;
-  
-    .chart-box {
-      width: 100%;
-      height: 100%;
-    }
+    };
+  });
+
+  // 3. 应用更新(先更X轴范围,再更数据,确保对齐)
+  chartInstance.setOption({
+    xAxis: updatedXAxes,
+    series: updatedSeries
+  });
+};
+
+// ========== 优化:支持任意数量数据的颜色生成函数(不再写死) ==========
+/**
+ * 生成区分度高的颜色,支持任意数量的索引(十几条、几十条都支持)
+ */
+const getColor = (index: number) => {
+  const extendedColors = [
+    '#FF6B6B', '#4ECDC4', '#45B7D1', '#96CEB4', '#FECA57',
+    '#FF9FF3', '#54A0FF', '#8E44AD', '#E74C3C', '#3498DB',
+    '#2ECC71', '#F1C40F', '#9B59B6', '#1ABC9C', '#E67E22',
+    '#34495E', '#FF7675', '#74B9FF', '#00B894', '#FDCB6E'
+  ];
+  return extendedColors[index % extendedColors.length];
+};
+
+onMounted(async () => {
+  await getData();
+  mqttUtil.initConnect(handleMqttMessage);
+});
+
+onUnmounted(() => {
+  // 销毁ECharts实例
+  if (chartInstance) {
+    chartInstance.dispose();
+    chartInstance = null;
+  }
+  // 释放MQTT资源
+  mqttUtil.releaseResources();
+  // 移除resize监听
+  window.removeEventListener('resize', () => chartInstance?.resize());
+});
+</script>
+
+<style lang="less" scoped>
+.realtimeCurve-content {
+  :deep(.el-loading-parent--relative) {
+    --el-mask-color: rgba(255, 255, 255, 0.1);
   }
-  </style>
+}
+
+.echarts-content {
+  width: 100%;
+  height: 100%;
+  overflow-x: hidden;
+  background: linear-gradient(to bottom, rgba(20, 66, 140, 0.8) 0%, rgba(18, 31, 52, 1) 15%);
+}
+
+// ECharts容器:适配动态高度
+.echarts-container {
+  width: 100%;
+  // 高度由动态计算的height ref决定,无需额外设置
+}
+
+::-webkit-scrollbar {
+  width: 5px;
+  height: 5px;
+}
+
+::-webkit-scrollbar-track {
+  border-radius: 10px;
+}
+
+::-webkit-scrollbar-thumb {
+  border-radius: 7px;
+  background-color: #798DAE;
+}
+</style>