Bläddra i källkod

修改振动页面吉林需求

liujiejie 1 vecka sedan
förälder
incheckning
36077dab11

+ 2 - 3
.env.dev

@@ -1,8 +1,8 @@
 ###
  # @Author: your name
  # @Date: 2025-07-17 14:14:27
- # @LastEditTime: 2026-03-19 10:46:18
- # @LastEditors: bogon
+ # @LastEditTime: 2026-03-26 16:56:07
+ # @LastEditors: MacBookPro
  # @Description: In User Settings Edit
  # @FilePath: /performance-test/.env.dev
 #这里需要在router/index.js 文件中进行判断cockpitComponent;
@@ -11,7 +11,6 @@ VUE_APP_THEM="green"
 VUE_APP_ISSHOWHD='default'
 VUE_APP_Helath='dev'
 VUE_APP_TITLE='机组功率曲线异常检测数据分析系统'
-
 #外网
 VUE_APP_MAPVIEW= "http://106.120.102.238:18000/tiles/{z}/{x}/{y}.png"
 VUE_APP_UPLOAD="http://106.120.102.238:16700/energy-manage-service/api/check/upload"

BIN
src/assets/analyse/3d.png


+ 398 - 0
src/views/health/components/data/failureFrequency.json

@@ -0,0 +1,398 @@
+[
+  {
+    "type": "Speed meas. point",
+    "name": "Speed",
+    "ratio": 1,
+    "id": 1,
+    "brand": null,
+    "typeno": null,
+    "rotatingrace": 0,
+    "speedPointId": 10,
+    "speedPointSrc": "/points/10",
+    "faultFrequencies": []
+  },
+  {
+    "type": "Bearing",
+    "name": "GEN NDE",
+    "ratio": 1,
+    "id": 2,
+    "brand": "SKF",
+    "typeno": "6324",
+    "rotatingrace": 0,
+    "speedPointId": 10,
+    "speedPointSrc": "/points/10",
+    "faultFrequencies": [
+      {
+        "name": "GEN NDE BPFO",
+        "multiple": 3.13
+      },
+      {
+        "name": "GEN NDE BPFI",
+        "multiple": 4.87
+      },
+      {
+        "name": "GEN NDE BSF",
+        "multiple": 2.19
+      },
+      {
+        "name": "GEN NDE FTF",
+        "multiple": 0.39
+      }
+    ]
+  },
+  {
+    "type": "Stator",
+    "name": "Stator",
+    "ratio": 1,
+    "id": 3,
+    "brand": null,
+    "typeno": null,
+    "rotatingrace": 0,
+    "speedPointId": 10,
+    "speedPointSrc": "/points/10",
+    "faultFrequencies": [
+      {
+        "name": "Stator, Pole pass freq.",
+        "multiple": 4
+      },
+      {
+        "name": "Stator, Grid freq.",
+        "multiple": 0
+      }
+    ]
+  },
+  {
+    "type": "Rotor",
+    "name": "Rotor",
+    "ratio": 1,
+    "id": 4,
+    "brand": null,
+    "typeno": null,
+    "rotatingrace": 0,
+    "speedPointId": 10,
+    "speedPointSrc": "/points/10",
+    "faultFrequencies": [
+      {
+        "name": "Rotor, Rotor bars",
+        "multiple": 0
+      },
+      {
+        "name": "Rotor, Grid freq.",
+        "multiple": 0
+      }
+    ]
+  },
+  {
+    "type": "Bearing",
+    "name": "GEN-DE",
+    "ratio": 1,
+    "id": 5,
+    "brand": "SKF",
+    "typeno": "6326 M/C3VL2071",
+    "rotatingrace": 0,
+    "speedPointId": 10,
+    "speedPointSrc": "/points/10",
+    "faultFrequencies": [
+      {
+        "name": "GEN-DE BPFO",
+        "multiple": 3.13
+      },
+      {
+        "name": "GEN-DE BPFI",
+        "multiple": 4.87
+      },
+      {
+        "name": "GEN-DE BSF",
+        "multiple": 2.2
+      },
+      {
+        "name": "GEN-DE FTF",
+        "multiple": 0.39
+      }
+    ]
+  },
+  {
+    "type": "Gear wheel",
+    "name": "GearL",
+    "ratio": 1,
+    "id": 6,
+    "brand": null,
+    "typeno": null,
+    "rotatingrace": 0,
+    "speedPointId": 10,
+    "speedPointSrc": "/points/10",
+    "faultFrequencies": [
+      {
+        "name": "GearL",
+        "multiple": 29
+      }
+    ]
+  },
+  {
+    "type": "Gear wheel",
+    "name": "GearH",
+    "ratio": 0.295918375,
+    "id": 7,
+    "brand": null,
+    "typeno": null,
+    "rotatingrace": 0,
+    "speedPointId": 10,
+    "speedPointSrc": "/points/10",
+    "faultFrequencies": [
+      {
+        "name": "GearH",
+        "multiple": 29
+      }
+    ]
+  },
+  {
+    "type": "Planet gear",
+    "name": "PG",
+    "ratio": 0.06445747,
+    "id": 9,
+    "brand": null,
+    "typeno": null,
+    "rotatingrace": 0,
+    "speedPointId": 10,
+    "speedPointSrc": "/points/10",
+    "faultFrequencies": [
+      {
+        "name": "PG",
+        "multiple": 2.320469
+      },
+      {
+        "name": "PG Planet shaft",
+        "multiple": 0.8086482
+      },
+      {
+        "name": "PG BPFO",
+        "multiple": 15.8171587
+      },
+      {
+        "name": "PG BPFI",
+        "multiple": 18.1460667
+      },
+      {
+        "name": "PG BSF",
+        "multiple": 5.62819147
+      },
+      {
+        "name": "PG FTF",
+        "multiple": 0.380064666
+      }
+    ]
+  },
+  {
+    "type": "Bearing",
+    "name": "MB02",
+    "ratio": 0.105475858,
+    "id": 10,
+    "brand": "SKF",
+    "typeno": "241/630 ECAK30/W33",
+    "rotatingrace": 0,
+    "speedPointId": 10,
+    "speedPointSrc": "/points/10",
+    "faultFrequencies": [
+      {
+        "name": "MB02 BPFO",
+        "multiple": 0.9714326
+      },
+      {
+        "name": "MB02 BPFI",
+        "multiple": 1.24356031
+      },
+      {
+        "name": "MB02 BSF",
+        "multiple": 0.4103011
+      },
+      {
+        "name": "MB02 FTF",
+        "multiple": 0.046409376
+      }
+    ]
+  },
+  {
+    "type": "Shaft",
+    "name": "MainShaft",
+    "ratio": 0.105475858,
+    "id": 11,
+    "brand": null,
+    "typeno": null,
+    "rotatingrace": 0,
+    "speedPointId": 10,
+    "speedPointSrc": "/points/10",
+    "faultFrequencies": [
+      {
+        "name": "MainShaft",
+        "multiple": 0.105475858
+      }
+    ]
+  },
+  {
+    "type": "Bearing",
+    "name": "MB01",
+    "ratio": 0.105475858,
+    "id": 12,
+    "brand": "SKF",
+    "typeno": "241/600 ECAK30/W33",
+    "rotatingrace": 0,
+    "speedPointId": 10,
+    "speedPointSrc": "/points/10",
+    "faultFrequencies": [
+      {
+        "name": "MB01 BPFO",
+        "multiple": 0.970377862
+      },
+      {
+        "name": "MB01 BPFI",
+        "multiple": 1.2446152
+      },
+      {
+        "name": "MB01 BSF",
+        "multiple": 0.4071368
+      },
+      {
+        "name": "MB01 FTF",
+        "multiple": 0.046409376
+      }
+    ]
+  },
+  {
+    "type": "Impeller",
+    "name": "Blade",
+    "ratio": 0.105475858,
+    "id": 13,
+    "brand": null,
+    "typeno": null,
+    "rotatingrace": 0,
+    "speedPointId": 10,
+    "speedPointSrc": "/points/10",
+    "faultFrequencies": [
+      {
+        "name": "Blade",
+        "multiple": 0.3164276
+      }
+    ]
+  },
+  {
+    "type": "Gear wheel",
+    "name": "PG02",
+    "ratio": 0.06445747,
+    "id": 14,
+    "brand": null,
+    "typeno": null,
+    "rotatingrace": 0,
+    "speedPointId": 10,
+    "speedPointSrc": "/points/10",
+    "faultFrequencies": [
+      {
+        "name": "PG02",
+        "multiple": 6.51020432
+      }
+    ]
+  },
+  {
+    "type": "Gear wheel",
+    "name": "PG01",
+    "ratio": 0.295918375,
+    "id": 15,
+    "brand": null,
+    "typeno": null,
+    "rotatingrace": 0,
+    "speedPointId": 10,
+    "speedPointSrc": "/points/10",
+    "faultFrequencies": [
+      {
+        "name": "PG01",
+        "multiple": 6.51020432
+      }
+    ]
+  },
+  {
+    "type": "Bearing",
+    "name": "GEAR-NU2228",
+    "ratio": 0.295918375,
+    "id": 16,
+    "brand": "SKF",
+    "typeno": "NU2228",
+    "rotatingrace": 0,
+    "speedPointId": 10,
+    "speedPointSrc": "/points/10",
+    "faultFrequencies": [
+      {
+        "name": "GEAR-NU2228 BPFO",
+        "multiple": 2.1779592
+      },
+      {
+        "name": "GEAR-NU2228 BPFI",
+        "multiple": 2.849694
+      },
+      {
+        "name": "GEAR-NU2228 BSF",
+        "multiple": 1.0889796
+      },
+      {
+        "name": "GEAR-NU2228 FTF",
+        "multiple": 0.1272449
+      }
+    ]
+  },
+  {
+    "type": "Bearing",
+    "name": "GEAR-QJ328",
+    "ratio": 0.295918375,
+    "id": 17,
+    "brand": "SKF",
+    "typeno": "QJ328",
+    "rotatingrace": 0,
+    "speedPointId": 10,
+    "speedPointSrc": "/points/10",
+    "faultFrequencies": [
+      {
+        "name": "GEAR-QJ328 BPFO",
+        "multiple": 1.45887756
+      },
+      {
+        "name": "GEAR-QJ328 BPFI",
+        "multiple": 2.08918381
+      },
+      {
+        "name": "GEAR-QJ328 BSF",
+        "multiple": 0.659898
+      },
+      {
+        "name": "GEAR-QJ328 FTF",
+        "multiple": 0.121326536
+      }
+    ]
+  },
+  {
+    "type": "Bearing",
+    "name": "GEAR-7318",
+    "ratio": 1,
+    "id": 18,
+    "brand": "SKF",
+    "typeno": "7318 BECBM",
+    "rotatingrace": 0,
+    "speedPointId": 10,
+    "speedPointSrc": "/points/10",
+    "faultFrequencies": [
+      {
+        "name": "GEAR-7318 BPFO",
+        "multiple": 4.96
+      },
+      {
+        "name": "GEAR-7318 BPFI",
+        "multiple": 7.04
+      },
+      {
+        "name": "GEAR-7318 BSF",
+        "multiple": 2.14
+      },
+      {
+        "name": "GEAR-7318 FTF",
+        "multiple": 0.41
+      }
+    ]
+  }
+]

+ 7 - 5
src/views/health/components/envelopecharts/Bdgb.js

@@ -109,9 +109,9 @@ export default {
         },
       };
 
-      // 更新图表(保留原有系列)
-      const otherSeries = option.series.filter(
-        (s) => s.id !== "SIDEBAND_CURSOR"
+      // 更新图表(保留原有系列;series 中可能有 null 占位
+      const otherSeries = (option.series || []).filter(
+        (s) => s && s.id !== "SIDEBAND_CURSOR"
       );
       this.chartInstance.setOption(
         {
@@ -128,8 +128,10 @@ export default {
       // 获取当前选项
       const option = this.chartInstance.getOption();
 
-      // 过滤掉边带光标系列
-      const series = option.series.filter((s) => s.id !== "SIDEBAND_CURSOR");
+      // 过滤掉边带光标系列(series 中可能有 null 占位)
+      const series = (option.series || []).filter(
+        (s) => s && s.id !== "SIDEBAND_CURSOR"
+      );
 
       // 更新图表
       this.chartInstance.setOption(

+ 57 - 47
src/views/health/components/envelopecharts/Tjgb.js

@@ -1,8 +1,8 @@
 export default {
   data() {
     return {
-    // 添加光标
-    cursorPoints: [], // 存储参考点数据
+      // 添加光标
+      cursorPoints: [], // 存储参考点数据
     };
   },
   mounted() {},
@@ -11,37 +11,39 @@ export default {
   methods: {
     handleDoubleClick(event) {
       if (!this.checkedGB.includes("添加光标")) return;
-      
-      // 获取鼠标位置对应的X值
+      if (!this.chartInstance) return;
+
+      const xData = this.spectrumList?.x || this.timeList?.x;
+      const yData = this.spectrumList?.y || this.timeList?.y;
+
+      if (!xData || !yData) return;
+
       const pointInGrid = this.chartInstance.convertFromPixel(
         { seriesIndex: 0 },
-        [event.offsetX, event.offsetY]
+        [event.offsetX, event.offsetY],
       );
-      const xValue = pointInGrid[0];
-    
-      // 找到最接近的X轴数据点
-      const closestIndex = this.findClosestIndex(xValue);
-      const yValue = this.envelopeList.y[closestIndex];
-    
-      // 存储参考点
+
+      const xValue = pointInGrid?.[0];
+      if (xValue == null) return;
+
+      const closestIndex = this.findClosestIndex(xValue, xData);
+      const yValue = yData[closestIndex];
+
       this.cursorPoints.push({
-        xAxis: this.envelopeList.x[closestIndex], // 使用实际数据点的X坐标
-        yAxis: yValue.toFixed(2), // 使用实际数据点的Y坐标
-        val: yValue.toFixed(2), // 显示实际Y值
+        xAxis: xData[closestIndex],
+        yAxis: Number(yValue).toFixed(2),
+        val: Number(yValue).toFixed(2),
       });
-    
-      console.log("cursorPoints:", this.cursorPoints);
-    
-      // 强制更新图表
+
       this.updateCursorElements();
     },
-        // 找到最接近的X轴索引
-        findClosestIndex(xValue) {
+    // 找到最接近的X轴索引
+    findClosestIndex(xValue) {
       if (!this.envelopeList.x) return 0;
-      
+
       let minDiff = Infinity;
       let closestIndex = 0;
-      
+
       this.envelopeList.x.forEach((x, index) => {
         const diff = Math.abs(x - xValue);
         if (diff < minDiff) {
@@ -49,27 +51,27 @@ export default {
           closestIndex = index;
         }
       });
-      
+
       return closestIndex;
     },
-    
-        updateCursorElements() {
+
+    updateCursorElements() {
       if (!this.chartInstance) return;
-      
+
       // 获取当前图表选项
       const currentOption = this.chartInstance.getOption();
-      
+
       // 主数据系列
       const mainSeries = currentOption.series[0];
-      
+
       // 特征值系列(过滤掉已有的光标系列)
       const featureSeries = currentOption.series.filter(
-        s => s.markLine && !s.id?.startsWith('CURSOR_')
+        (s) => s.markLine && !s.id?.startsWith("CURSOR_"),
       );
-    
+
       // 参考线配置(垂直线)
       const cursorLineSeries = {
-        id: 'CURSOR_LINE_SERIES',
+        id: "CURSOR_LINE_SERIES",
         type: "line",
         markLine: {
           silent: true,
@@ -79,20 +81,20 @@ export default {
             width: 1,
           },
           symbol: ["none", "none"],
-          label: { 
+          label: {
             show: true,
-            formatter: (params) => params.data.val || ''
+            formatter: (params) => params.data.val || "",
           },
           data: this.cursorPoints.map((point) => ({
             xAxis: point.xAxis,
-            val: point.val
+            val: point.val,
           })),
         },
       };
-    
+
       // 参考点配置(实际数据点位置)
       const cursorPointSeries = {
-        id: 'CURSOR_POINT_SERIES',
+        id: "CURSOR_POINT_SERIES",
         type: "scatter",
         symbol: "circle",
         symbolSize: 8,
@@ -112,17 +114,25 @@ export default {
           borderRadius: 3,
         },
       };
-    
+
       // 设置新选项
-      this.chartInstance.setOption({
-        series: [
-          mainSeries,
-          ...featureSeries,
-          cursorLineSeries,
-          cursorPointSeries
-        ]
-      }, { replaceMerge: ['series'] });
+      this.chartInstance.setOption(
+        {
+          series: [
+            mainSeries,
+            ...featureSeries,
+            {
+              id: "CURSOR_LINE_SERIES",
+              ...cursorLineSeries,
+            },
+            {
+              id: "CURSOR_POINT_SERIES",
+              ...cursorPointSeries,
+            },
+          ],
+        },
+        { replaceMerge: ["series"] },
+      );
     },
-
   },
 };

+ 5 - 3
src/views/health/components/envelopecharts/Xdgb.js

@@ -83,8 +83,8 @@ export default {
 
       // 更新图表
       const option = this.chartInstance.getOption();
-      const otherSeries = option.series.filter(
-        (s) => s.id !== "HARMONIC_CURSOR"
+      const otherSeries = (option.series || []).filter(
+        (s) => s && s.id !== "HARMONIC_CURSOR"
       );
 
       this.chartInstance.setOption(
@@ -101,7 +101,9 @@ export default {
     // 移除谐波光标
     removeHarmonicCursor() {
       const option = this.chartInstance.getOption();
-      const series = option.series.filter((s) => s.id !== "HARMONIC_CURSOR");
+      const series = (option.series || []).filter(
+        (s) => s && s.id !== "HARMONIC_CURSOR"
+      );
       this.chartInstance.setOption({ series }, { replaceMerge: ["series"] });
       this.harmonicCursorPoints = [];
     },

+ 60 - 28
src/views/health/components/loadTree.vue

@@ -1,7 +1,7 @@
 <!--
  * @Author: your name
  * @Date: 2026-03-20 15:02:08
- * @LastEditTime: 2026-03-23 11:09:44
+ * @LastEditTime: 2026-03-31 15:18:54
  * @LastEditors: MacBookPro
  * @Description: In User Settings Edit
  * @FilePath: /performance-test/src/views/health/components/tree.vue
@@ -44,7 +44,10 @@
       >
         <span class="custom-tree-node" slot-scope="{ node, data }">
           <el-tooltip effect="dark" :content="data.itemValue" placement="top">
-            <span class="node-label">{{ data.itemValue }}</span>
+            <span class="node-label"
+              >{{ data.itemValue }}
+              {{ data.otherData && data.otherData.Hz + `Hz` }}</span
+            >
           </el-tooltip>
         </span>
       </el-tree>
@@ -81,23 +84,24 @@ export default {
           return !data.hasNext;
         },
       },
-
       // 🌟 是否使用本地 nextData(你现在接口是嵌套的)
       useLocalData: true,
     };
   },
   created() {
-    this.handleSearch();
+    this.handleSearch("init");
   },
   methods: {
     // ===========================
     // 🌟 搜索(统一入口)
     // ===========================
-    async handleSearch() {
+    async handleSearch(state) {
       // 1️⃣ 获取数据
       const rootData = await this.fetchRootData();
       this.rootDataCache = rootData;
-
+      if (state && state === "init") {
+        this.$emit("initTreeData", rootData[0]);
+      }
       // 2️⃣ 默认展开第一个风场
       if (rootData.length > 0) {
         this.defaultExpandedKeys = [rootData[0].itemKey];
@@ -105,32 +109,42 @@ export default {
 
       // 3️⃣ 强制刷新
       this.treeKey++;
-
-      // 4️⃣ 等渲染后 → 自动展开二级
       this.$nextTick(() => {
-        const tree = this.$refs.tree;
-        const first = tree.store.nodesMap[rootData?.[0]?.itemKey];
-
-        if (!first) return;
+        const tryExpand = () => {
+          const tree = this.$refs.tree;
 
-        // 一级
-        first.expand();
+          if (!tree || !tree.store) {
+            setTimeout(tryExpand, 50);
+            return;
+          }
 
-        // 二级(更稳写法)
-        const expandSecond = () => {
-          const child = first.childNodes[0];
-          if (child) {
-            child.expand();
-          } else {
-            setTimeout(expandSecond, 100);
+          const first = tree.store.nodesMap[rootData?.[0]?.itemKey];
+          if (!first) {
+            setTimeout(tryExpand, 50);
+            return;
           }
+
+          // 一级
+          first.expand();
+
+          // 二级
+          const expandSecond = () => {
+            const child = first.childNodes[0];
+            if (child) {
+              child.expand();
+            } else {
+              setTimeout(expandSecond, 100);
+            }
+          };
+
+          expandSecond();
         };
 
-        expandSecond();
+        tryExpand();
       });
     },
     // ===========================
-    // 🌟 获取根数据
+    //  获取根数据
     // ===========================
     async fetchRootData() {
       const res = await axios.post(
@@ -151,7 +165,7 @@ export default {
 
       let children = [];
 
-      // 🌟 第一个风场(有 nextData)
+      //  第一个风场(有 nextData)
       if (data.nextData && data.nextData.length > 0) {
         children = data.nextData;
       }
@@ -172,7 +186,7 @@ export default {
     },
 
     handleNodeClick(data, node) {
-      // 🌟 非叶子节点
+      //  非叶子节点
       if (!node.isLeaf) {
         if (!node.loaded) {
           if (node.loading) return;
@@ -183,9 +197,27 @@ export default {
         return;
       }
 
-      // 🌟 叶子节点
+      //  叶子节点
       const path = this.getNodePath(node);
-      this.loadChartData && this.loadChartData(data, path);
+      //获取所有父级节点
+      const pathNodes = this.getNodeFullPath(node);
+
+      console.log(data, pathNodes, "data, pathNodes");
+      // 将叶子节点选择结果抛给父组件(由父组件决定如何联动查询/选中行/加载图表)
+      this.$emit("node-select", data, path, pathNodes);
+    },
+    getNodeFullPath(node) {
+      const path = [];
+      let current = node;
+
+      while (current) {
+        if (current.data) {
+          path.unshift(current.data); // 🔥 放完整 data
+        }
+        current = current.parent;
+      }
+
+      return path;
     },
     getNodePath(node) {
       const path = [];
@@ -201,7 +233,7 @@ export default {
       return path;
     },
     // ===========================
-    // 🌟 子节点请求
+    //  子节点请求
     // ===========================
     async getChildList(nodeData) {
       // 🌟 加缓存

+ 10 - 7
src/views/health/components/spectrogramcharts.vue

@@ -211,27 +211,30 @@ export default {
         const currentOption = this.chartInstance.getOption();
 
         // 获取现有的光标系列
+        const sa = currentOption.series || [];
         const cursorLineSeries =
-          currentOption.series.find((s) => s.id === "CURSOR_LINE_SERIES") || {};
+          sa.find((s) => s && s.id === "CURSOR_LINE_SERIES") || {};
         const cursorPointSeries =
-          currentOption.series.find((s) => s.id === "CURSOR_POINT_SERIES") ||
-          {};
+          sa.find((s) => s && s.id === "CURSOR_POINT_SERIES") || {};
         const cursorHighLineSeries =
-          currentOption.series.find((s) => s.id === "PEAK_REFERENCE_LINE") ||
-          {};
+          sa.find((s) => s && s.id === "PEAK_REFERENCE_LINE") || {};
 
         // 生成新的特征值系列
         const featureSeries = this.generateSeries(newFeatureLines);
 
+        const mainSeries =
+          sa.find((s) => s && s.name === "数据系列" && s.type === "line") ||
+          sa.find((s) => s);
+
         this.chartInstance.setOption(
           {
             series: [
-              currentOption.series[0], // 主数据系列
+              mainSeries,
               ...featureSeries.slice(1), // 新的特征值系列
               cursorLineSeries, // 保留光标线系列
               cursorPointSeries, // 保留光标点系列
               cursorHighLineSeries, // 保留峰值参考线
-            ],
+            ].filter((s) => s && s.type),
           },
           { replaceMerge: ["series"] }
         );

+ 57 - 53
src/views/health/components/spectrogramcharts/Bdgb.js

@@ -14,41 +14,72 @@ export default {
     // 边带光标
     enableSidebandCursor() {
       this.sidebandCursorVisible = true;
-      // 添加鼠标移动事件监听
-      this.chartInstance.getZr().on("mousemove", this.handleSidebandCursorMove);
+      if (!this.chartInstance) return;
+      // 边带设置点击会和单指针点击冲突:边带启用时暂停单指针点击(保留单指针线不动)
+      this.unbindSinglePointerClick?.();
+      // 先解绑,避免重复绑定
+      this.chartInstance
+        .getZr()
+        .off("click", this.handleSidebandCursorClick);
+      this.chartInstance.getZr().on("click", this.handleSidebandCursorClick);
     },
 
     disableSidebandCursor() {
       this.sidebandCursorVisible = false;
-      // 移除鼠标移动事件监听
-      this.chartInstance
-        .getZr()
-        .off("mousemove", this.handleSidebandCursorMove);
+      if (this.chartInstance) {
+        this.chartInstance
+          .getZr()
+          .off("click", this.handleSidebandCursorClick);
+      }
       // 移除边带光标
       this.removeSidebandCursor();
+      // 恢复单指针点击(如果单指针仍处于选中状态)
+      if (this.checkedGB?.includes?.("单指针")) {
+        this.bindSinglePointerClick?.();
+      }
     },
 
-    handleSidebandCursorMove(event) {
+    // 点击设置边带:以单指针为中心,根据点击与单指针的频率差作为间隔,
+    // 生成 ±1~±5 倍间隔的边带
+    handleSidebandCursorClick(event) {
       if (!this.sidebandCursorVisible) return;
+      if (!this.chartInstance) return;
+
+      // 必须先有单指针作为中心
+      if (!this.singlePointerPoint || this.singlePointerPoint.xAxis == null) {
+        return;
+      }
 
       // 获取鼠标位置对应的X值
       const pointInGrid = this.chartInstance.convertFromPixel(
         { seriesIndex: 0 },
-        [event.offsetX, event.offsetY]
+        [event.offsetX, event.offsetY],
       );
-      const xValue = pointInGrid[0];
+      const clickX = pointInGrid?.[0];
+      if (clickX == null || Number.isNaN(clickX)) return;
+
+      const centerX = Number(this.singlePointerPoint.xAxis);
+      const delta = Math.abs(clickX - centerX);
+      if (!delta) return;
+
+      const maxOrder = 5; // 默认 ±5 倍
+      const points = [];
 
-      // 生成3个光标点(中心、左、右)
-      this.sidebandCursorPoints = [
-        { xAxis: xValue - 5, val: (xValue - 5).toFixed(2) }, // 左边带
-        { xAxis: xValue, val: xValue.toFixed(2) }, // 中心
-        { xAxis: xValue + 5, val: (xValue + 5).toFixed(2) }, // 右边带
-      ];
+      for (let n = -maxOrder; n <= maxOrder; n++) {
+        if (n === 0) continue;
+        const x = centerX + n * delta;
+        points.push({
+          xAxis: x,
+          val: `${n > 0 ? "+" : ""}${n}`,
+        });
+      }
+
+      this.sidebandCursorPoints = points;
 
       this.updateSidebandCursor();
     },
     updateSidebandCursor() {
-      if (this.sidebandCursorPoints.length < 3) return;
+      if (!this.sidebandCursorPoints.length || !this.chartInstance) return;
 
       // 获取当前图表选项
       const option = this.chartInstance.getOption();
@@ -77,41 +108,11 @@ export default {
             val: point.val,
           })),
         },
-        markArea: {
-          silent: true,
-          itemStyle: {
-            color: "rgba(255, 0, 0, 0.2)", // 红色半透明背景
-          },
-          data: [
-            // 左侧区域(左线到中线)
-            [
-              {
-                xAxis: this.sidebandCursorPoints[0].xAxis,
-                yAxis: "min", // 从Y轴最小值开始
-              },
-              {
-                xAxis: this.sidebandCursorPoints[1].xAxis,
-                yAxis: "max", // 到Y轴最大值结束
-              },
-            ],
-            // 右侧区域(中线到右线)
-            [
-              {
-                xAxis: this.sidebandCursorPoints[1].xAxis,
-                yAxis: "min",
-              },
-              {
-                xAxis: this.sidebandCursorPoints[2].xAxis,
-                yAxis: "max",
-              },
-            ],
-          ],
-        },
       };
 
-      // 更新图表(保留原有系列)
-      const otherSeries = option.series.filter(
-        (s) => s.id !== "SIDEBAND_CURSOR"
+      // 更新图表(保留原有系列;series 中可能有 null 占位,勿直接读 s.id)
+      const otherSeries = (option.series || []).filter(
+        (s) => s && s.id !== "SIDEBAND_CURSOR",
       );
       this.chartInstance.setOption(
         {
@@ -120,23 +121,26 @@ export default {
         {
           replaceMerge: ["series"], // 关键配置:只替换series
           notMerge: false,
-        }
+        },
       );
     },
 
     removeSidebandCursor() {
       // 获取当前选项
+      if (!this.chartInstance) return;
       const option = this.chartInstance.getOption();
 
-      // 过滤掉边带光标系列
-      const series = option.series.filter((s) => s.id !== "SIDEBAND_CURSOR");
+      // 过滤掉边带光标系列(series 中可能有 null 占位)
+      const series = (option.series || []).filter(
+        (s) => s && s.id !== "SIDEBAND_CURSOR",
+      );
 
       // 更新图表
       this.chartInstance.setOption(
         {
           series: series,
         },
-        { replaceMerge: "series" }
+        { replaceMerge: ["series"] },
       );
 
       this.sidebandCursorPoints = [];

+ 74 - 71
src/views/health/components/spectrogramcharts/Tjgb.js

@@ -1,8 +1,8 @@
 export default {
   data() {
     return {
-    // 添加光标
-    cursorPoints: [], // 存储参考点数据
+      // 添加光标
+      cursorPoints: [], // 存储参考点数据
     };
   },
   mounted() {},
@@ -14,7 +14,7 @@ export default {
       // 获取鼠标位置对应的X值
       const pointInGrid = this.chartInstance.convertFromPixel(
         { seriesIndex: 0 },
-        [event.offsetX, event.offsetY]
+        [event.offsetX, event.offsetY],
       );
       const xValue = pointInGrid[0];
 
@@ -40,84 +40,87 @@ export default {
       });
     },
     // 找到最接近的X轴索引
-    findClosestIndex(xValue) {
-      let minDiff = Infinity;
+    findClosestIndex(target, xData) {
+      if (!xData || !xData.length) return 0;
+
       let closestIndex = 0;
-      this.spectrumList.x.forEach((x, index) => {
-        const diff = Math.abs(x - xValue);
+      let minDiff = Math.abs(xData[0] - target);
+
+      for (let i = 1; i < xData.length; i++) {
+        const diff = Math.abs(xData[i] - target);
         if (diff < minDiff) {
           minDiff = diff;
-          closestIndex = index;
+          closestIndex = i;
         }
-      });
+      }
+
       return closestIndex;
     },
 
     updateCursorElements() {
-  // 获取当前图表选项
-  const currentOption = this.chartInstance.getOption();
-  
-  // 找到现有的特征值系列(它们有 markLine 但没有特定标识)
-  const featureSeries = currentOption.series.filter(
-    s => s.markLine && !s.id?.startsWith('CURSOR_')
-  );
-  
-  // 参考线配置(垂直线)
-  const cursorLineSeries = {
-    id: 'CURSOR_LINE_SERIES',
-    type: "line",
-    markLine: {
-      silent: true,
-      lineStyle: {
-        color: "#FF0000",
-        type: "dashed",
-        width: 1,
-      },
-      symbol: ["none", "none"],
-      label: { show: false },
-      data: this.cursorPoints.map((point) => ({
-        xAxis: point.xAxis,
-      })),
-    },
-  };
+      // 获取当前图表选项
+      const currentOption = this.chartInstance.getOption();
 
-  // 参考点配置(实际数据点位置)
-  const cursorPointSeries = {
-    id: 'CURSOR_POINT_SERIES',
-    type: "scatter",
-    symbol: "circle",
-    symbolSize: 8,
-    itemStyle: { color: "#FF0000" },
-    data: this.cursorPoints.map((point) => ({
-      value: [point.xAxis, point.yAxis],
-      name: point.val,
-    })),
-    label: {
-      show: true,
-      formatter: "{@[1]}",
-      position: "top",
-      color: "#FF0000",
-      fontSize: 12,
-      backgroundColor: "rgba(255,255,255,0.7)",
-      padding: [2, 4],
-      borderRadius: 3,
-    },
-  };
+      // 找到现有的特征值系列(它们有 markLine 但没有特定标识)
+      const featureSeries = (currentOption.series || []).filter(
+        (s) => s && s.markLine && !s.id?.startsWith("CURSOR_"),
+      );
 
-  if (this.chartInstance) {
-    this.chartInstance.setOption(
-      {
-        series: [
-          currentOption.series[0], // 主数据系列
-          ...featureSeries,      // 保留所有特征值系列
-          cursorLineSeries,       // 光标线系列
-          cursorPointSeries       // 光标点系列
-        ],
-      },
-      { replaceMerge: ["series"] }
-    );
-  }
-},
+      // 参考线配置(垂直线)
+      const cursorLineSeries = {
+        id: "CURSOR_LINE_SERIES",
+        type: "line",
+        markLine: {
+          silent: true,
+          lineStyle: {
+            color: "#FF0000",
+            type: "dashed",
+            width: 1,
+          },
+          symbol: ["none", "none"],
+          label: { show: false },
+          data: this.cursorPoints.map((point) => ({
+            xAxis: point.xAxis,
+          })),
+        },
+      };
 
+      // 参考点配置(实际数据点位置)
+      const cursorPointSeries = {
+        id: "CURSOR_POINT_SERIES",
+        type: "scatter",
+        symbol: "circle",
+        symbolSize: 8,
+        itemStyle: { color: "#FF0000" },
+        data: this.cursorPoints.map((point) => ({
+          value: [point.xAxis, point.yAxis],
+          name: point.val,
+        })),
+        label: {
+          show: true,
+          formatter: "{@[1]}",
+          position: "top",
+          color: "#FF0000",
+          fontSize: 12,
+          backgroundColor: "rgba(255,255,255,0.7)",
+          padding: [2, 4],
+          borderRadius: 3,
+        },
+      };
+
+      if (this.chartInstance) {
+        this.chartInstance.setOption(
+          {
+            series: [
+              currentOption.series[0], // 主数据系列
+              ...featureSeries, // 保留所有特征值系列
+              cursorLineSeries, // 光标线系列
+              cursorPointSeries, // 光标点系列
+            ],
+          },
+          { replaceMerge: ["series"] },
+        );
+      }
+    },
   },
 };

+ 37 - 15
src/views/health/components/spectrogramcharts/Xdgb.js

@@ -1,10 +1,10 @@
 export default {
   data() {
     return {
-    // 谐波光标
-    harmonicCursorVisible: false,
-    harmonicCursorPoints: [],
-    harmonicCursorSeries: null,
+      // 谐波光标
+      harmonicCursorVisible: false,
+      harmonicCursorPoints: [],
+      harmonicCursorSeries: null,
     };
   },
   mounted() {},
@@ -26,22 +26,36 @@ export default {
     handleHarmonicCursorClick(event) {
       if (!this.harmonicCursorVisible) return;
 
-      // 获取点击位置对应的X值
+      if (!this.chartInstance) return;
+
+      // ✅ 统一数据源(关键)
+      const xData = this.spectrumList?.x || this.timeList?.x;
+      const yData = this.spectrumList?.y || this.timeList?.y;
+
+      if (!xData || !yData) {
+        console.warn("数据未准备好");
+        return;
+      }
+
       const pointInGrid = this.chartInstance.convertFromPixel(
         { seriesIndex: 0 },
-        [event.offsetX, event.offsetY]
+        [event.offsetX, event.offsetY],
       );
-      const baseX = pointInGrid[0];
 
-      // 生成1-6倍频的X坐标
+      const baseX = pointInGrid?.[0];
+      if (baseX == null) return;
+
       this.harmonicCursorPoints = [];
+
       for (let i = 1; i <= 6; i++) {
         const xValue = baseX * i;
-        const closestIndex = this.findClosestIndex(xValue);
+
+        const closestIndex = this.findClosestIndex(xValue, xData);
+
         this.harmonicCursorPoints.push({
           xAxis: xValue,
           multiple: i,
-          yValue: this.spectrumList.y[closestIndex]?.toFixed(2) || "0",
+          yValue: yData?.[closestIndex]?.toFixed?.(2) || "0",
         });
       }
 
@@ -83,25 +97,33 @@ export default {
 
       // 更新图表
       const option = this.chartInstance.getOption();
-      const otherSeries = option.series.filter(
-        (s) => s.id !== "HARMONIC_CURSOR"
+      const otherSeries = (option.series || []).filter(
+        (s) => s && s.id !== "HARMONIC_CURSOR",
       );
 
       this.chartInstance.setOption(
         {
-          series: [...otherSeries, this.harmonicCursorSeries],
+          series: [
+            ...otherSeries,
+            {
+              id: "HARMONIC_CURSOR",
+              ...this.harmonicCursorSeries,
+            },
+          ],
         },
         {
           replaceMerge: ["series"],
           notMerge: false,
-        }
+        },
       );
     },
 
     // 移除谐波光标
     removeHarmonicCursor() {
       const option = this.chartInstance.getOption();
-      const series = option.series.filter((s) => s.id !== "HARMONIC_CURSOR");
+      const series = (option.series || []).filter(
+        (s) => s && s.id !== "HARMONIC_CURSOR",
+      );
       this.chartInstance.setOption({ series }, { replaceMerge: ["series"] });
       this.harmonicCursorPoints = [];
     },

+ 203 - 186
src/views/health/components/spectrogramcharts/cursorReferenceMixin.js

@@ -1,295 +1,312 @@
 // cursorReferenceMixin.js
-// import * as echarts from "echarts";
 
 export default {
   data() {
     return {
-      // 参考线相关数据
-      cursorHightPoints: [], // 存储所有参考线
-      currentCursorIndex: -1, // 当前参考线索引
-      peakPoints: [], // 存储所有峰值点
-      isHandlingCursor: false, // 防止重复处理
-      chartInstance: null, // ECharts实例
+      cursorHightPoints: [],
+      currentCursorIndex: -1,
+      peakPoints: [],
+      isHandlingCursor: false,
+      /** 单指针:{ xAxis, val },val 为展示用 Y 值字符串 */
+      singlePointerPoint: null,
     };
   },
 
   mounted() {
-    // 监听键盘事件
     window.addEventListener("keydown", this.handleKeyDown);
   },
 
   beforeDestroy() {
-    // 移除事件监听
     window.removeEventListener("keydown", this.handleKeyDown);
   },
 
+  computed: {
+    // ✅ 统一数据源(核心)
+    chartDataSource() {
+      return {
+        x: this.spectrumList?.x || this.timeList?.x || [],
+        y: this.spectrumList?.y || this.timeList?.y || [],
+      };
+    },
+  },
+
   methods: {
+    bindSinglePointerClick() {
+      if (!this.chartInstance) return;
+      this.chartInstance
+        .getZr()
+        .off("click", this.handleSinglePointerZrClick);
+      this.chartInstance
+        .getZr()
+        .on("click", this.handleSinglePointerZrClick);
+    },
+
+    unbindSinglePointerClick() {
+      if (!this.chartInstance) return;
+      this.chartInstance
+        .getZr()
+        .off("click", this.handleSinglePointerZrClick);
+    },
+
     /**
-     * 处理按钮点击生成参考线
+     * 生成峰值光标
      */
     handleMoveCursor() {
-      console.log("handleMoveCursor", "222");
-      if (this.isHandlingCursor || !this.spectrumList) return;
+      if (this.isHandlingCursor) return;
+
+      const { x, y } = this.chartDataSource;
+      if (!x.length || !y.length) return;
+
       this.isHandlingCursor = true;
 
-      // 1. 找到所有峰值点
       this.findPeakPoints();
 
-      // 2. 如果没有峰值点则返回
-      if (this.peakPoints.length === 0) {
+      if (!this.peakPoints.length) {
         this.isHandlingCursor = false;
         return;
       }
-      console.log(this.peakPoints);
 
-      // 3. 找到Y轴最大值点
-      const maxPeak = this.peakPoints.reduce((prev, current) =>
-        prev.y > current.y ? prev : current
+      const maxPeak = this.peakPoints.reduce((prev, cur) =>
+        prev.y > cur.y ? prev : cur,
       );
 
-      // 4. 创建参考线数据
-      const referenceLine = {
-        xAxis: maxPeak.x,
-        val: maxPeak.y.toFixed(9),
-        index: maxPeak.index,
-      };
+      this.cursorHightPoints = [
+        {
+          xAxis: maxPeak.x,
+          val: maxPeak.y.toFixed(6),
+          index: maxPeak.index,
+        },
+      ];
 
-      // 5. 更新参考线
-      this.cursorHightPoints = [referenceLine];
       this.currentCursorIndex = this.peakPoints.findIndex(
-        (p) => p.index === maxPeak.index
+        (p) => p.index === maxPeak.index,
       );
 
-      // 6. 更新图表
       this.updateCursorElements2();
 
       this.isHandlingCursor = false;
     },
 
     /**
-     * 查找所有峰值
+     * 查找峰值
      */
     findPeakPoints() {
+      const { x, y } = this.chartDataSource;
+
       this.peakPoints = [];
-      const yValues = this.spectrumList.y;
-      const xValues = this.spectrumList.x;
-
-      // 1. 找到所有局部峰值点(比相邻点都高的点)
-      const allPeaks = [];
-      for (let i = 1; i < yValues.length - 1; i++) {
-        if (yValues[i] > yValues[i - 1] && yValues[i] > yValues[i + 1]) {
-          allPeaks.push({
-            x: xValues[i],
-            y: yValues[i],
+
+      for (let i = 1; i < y.length - 1; i++) {
+        if (y[i] > y[i - 1] && y[i] > y[i + 1]) {
+          this.peakPoints.push({
+            x: x[i],
+            y: y[i],
             index: i,
           });
         }
       }
 
-      // 2. 按y值从大到小排序
-      allPeaks.sort((a, b) => b.y - a.y);
-
-      this.peakPoints = allPeaks;
+      this.peakPoints.sort((a, b) => b.y - a.y);
     },
 
     /**
-     * 处理键盘事件
+     * 键盘控制
      */
-    handleKeyDown(event) {
-      if (this.cursorHightPoints.length === 0) return;
-
-      switch (event.keyCode) {
-        case 37: // 左箭头
-          this.moveCursorToLeft();
-          break;
-        case 39: // 右箭头
-          this.moveCursorToRight();
-          break;
-        default:
-          return;
-      }
+    handleKeyDown(e) {
+      if (!this.cursorHightPoints.length) return;
 
-      // 阻止默认行为
-      event.preventDefault();
+      if (e.keyCode === 37) this.moveCursorToLeft();
+      if (e.keyCode === 39) this.moveCursorToRight();
     },
 
-    /**
-     * 向左移动参考线并保持居中
-     */
     moveCursorToLeft() {
       this.findPeakPoints();
       if (this.currentCursorIndex <= 0) return;
 
       const newIndex = this.currentCursorIndex - 1;
-      const newPeak = this.peakPoints[newIndex];
-
-      this.updateCursorPosition(newPeak, newIndex);
-      this.centerViewOnPeak(newPeak);
+      this.updateCursorPosition(this.peakPoints[newIndex], newIndex);
     },
 
-    /**
-     * 向右移动参考线并保持居中
-     */
     moveCursorToRight() {
       this.findPeakPoints();
       if (this.currentCursorIndex >= this.peakPoints.length - 1) return;
 
       const newIndex = this.currentCursorIndex + 1;
-      const newPeak = this.peakPoints[newIndex];
-
-      this.updateCursorPosition(newPeak, newIndex);
-      // this.centerViewOnPeak(newPeak);
+      this.updateCursorPosition(this.peakPoints[newIndex], newIndex);
     },
 
-    /**
-     * 将视图中心对准峰值点
-     */
-    centerViewOnPeak(peak) {
-      if (!this.chartInstance || !peak) return;
-
-      // 获取当前x轴配置和数据范围
-      const option = this.chartInstance.getOption();
-      const xAxis = option.xAxis[0];
-
-      // 计算当前可视范围
-      const axisModel = this.chartInstance.getModel().getComponent("xAxis");
-      const currentRange = axisModel.axis.scale.getExtent();
-      const viewWidth = currentRange[1] - currentRange[0];
-
-      // 计算新的居中范围(确保不超出数据边界)
-      const newMin = Math.max(xAxis.min, peak.x - viewWidth / 2);
-      const newMax = Math.min(xAxis.max, peak.x + viewWidth / 2);
-
-      // 应用新的视图范围
-      this.chartInstance.dispatchAction({
-        type: "dataZoom",
-        xAxisIndex: 0,
-        start: ((newMin - xAxis.min) / (xAxis.max - xAxis.min)) * 100,
-        end: ((newMax - xAxis.min) / (xAxis.max - xAxis.min)) * 100,
-        animation: {
-          duration: 300, // 添加平滑过渡效果
-        },
-      });
-    },
-
-    /**
-     * 更新参考线位置
-     */
     updateCursorPosition(peak, index) {
       this.cursorHightPoints = [
         {
           xAxis: peak.x,
-          val: peak.y.toFixed(9),
+          val: peak.y.toFixed(6),
           index: peak.index,
         },
       ];
-      console.log(this.cursorHightPoints, "updateCursorPosition");
 
       this.currentCursorIndex = index;
+
       this.$nextTick(() => {
         this.updateCursorElements2();
       });
     },
 
     /**
-     * 更新参考线元素(不覆盖已有参考线)
+     * ✅ 核心修复:只更新自己,不重建 series
      */
     updateCursorElements2() {
       if (!this.chartInstance) return;
 
-      // 1. 获取当前图表配置中的已有series
-      const currentOption = this.chartInstance.getOption();
-      const existingSeries = currentOption.series || [];
+      this.chartInstance.setOption({
+        series: [
+          {
+            id: "PEAK_REFERENCE_LINE",
+            type: "line",
+            markLine: {
+              silent: true,
+              symbol: ["none", "none"],
+              lineStyle: {
+                color: "#ff0000",
+                width: 1,
+              },
+              label: {
+                show: true,
+                formatter: (params) => "峰值: " + params.data.val,
+              },
+              data: this.cursorHightPoints.map((p) => ({
+                xAxis: p.xAxis,
+                val: p.val,
+              })),
+            },
+          },
+        ],
+      });
+    },
 
-      // 2. 过滤掉旧的峰值参考线(通过id或特定标记识别)
-      const filteredSeries = existingSeries.filter(
-        (series) => !series.id || !series.id.includes("PEAK_REFERENCE_LINE")
-      );
-      console.log(filteredSeries);
+    /**
+     * 移除光标(不会影响其他 series)
+     */
+    removeCursor() {
+      if (!this.chartInstance) return;
 
-      // 3. 准备新的峰值参考线配置
-      const cursorHighLineSeries = {
-        id: "PEAK_REFERENCE_LINE",
-        type: "line",
-        markLine: {
-          data: this.cursorHightPoints.map((point) => ({
-            xAxis: point.xAxis,
-            label: {
-              formatter: "峰值: " + point.val,
-            },
-            lineStyle: {
-              color: "#FF0000",
+      this.chartInstance.setOption({
+        series: [
+          {
+            id: "PEAK_REFERENCE_LINE",
+            markLine: {
+              data: [],
             },
-          })),
-        },
-      };
-
-      // 4. 合并所有series配置
-      const allSeries = [
-        ...filteredSeries, // 保留已有series
-        cursorHighLineSeries, // 添加新的峰值参考线
-      ];
-      console.log(allSeries);
+          },
+        ],
+      });
 
-      // 5. 更新图表配置
-      this.chartInstance.setOption(
-        {
-          series: allSeries,
-        },
-        {
-          replaceMerge: ["series"],
-          notMerge: false,
-        }
-      );
+      this.cursorHightPoints = [];
+      this.currentCursorIndex = -1;
     },
+
     /**
-     * 获取参考线markLine配置
+     * 单指针:启用后点击图表,在最近数据点处画红色虚线;再次点击则移动
      */
-    getCursorMarkLineConfig() {
-      return {
-        data: this.cursorHightPoints.map((point) => ({
-          xAxis: point.xAxis,
-          label: {
-            formatter: "峰值: " + point.val,
-          },
-          lineStyle: {
-            color: "#FF0000",
-          },
-        })),
-        symbol: ["none", "none"],
-        silent: true,
+    enableSinglePointer() {
+      if (!this.chartInstance) return;
+      this.bindSinglePointerClick();
+    },
+
+    disableSinglePointer() {
+      if (this.chartInstance) {
+        this.unbindSinglePointerClick();
+        const option = this.chartInstance.getOption();
+        const series = (option.series || []).filter(
+          (s) => s && s.id !== "SINGLE_POINTER_LINE",
+        );
+        this.chartInstance.setOption(
+          { series },
+          { replaceMerge: ["series"] },
+        );
+      }
+      this.singlePointerPoint = null;
+    },
+
+    handleSinglePointerZrClick(event) {
+      if (!this.checkedGB?.includes?.("单指针")) return;
+
+      const { x: xData, y: yData } = this.chartDataSource;
+      if (!xData?.length || !yData?.length || !this.chartInstance) return;
+
+      const pointInGrid = this.chartInstance.convertFromPixel(
+        { seriesIndex: 0 },
+        [event.offsetX, event.offsetY],
+      );
+      const xClick = pointInGrid?.[0];
+      if (xClick == null || Number.isNaN(xClick)) return;
+
+      const idx = this.findClosestXIndex(xClick, xData);
+      const xAxis = xData[idx];
+      const yVal = yData[idx];
+      if (xAxis == null || yVal == null) return;
+
+      this.singlePointerPoint = {
+        xAxis,
+        val: Number(yVal).toFixed(6),
       };
+
+      this.applySinglePointerSeries();
     },
 
+    findClosestXIndex(target, xData) {
+      let closestIndex = 0;
+      let minDiff = Math.abs(Number(xData[0]) - target);
+      for (let i = 1; i < xData.length; i++) {
+        const diff = Math.abs(Number(xData[i]) - target);
+        if (diff < minDiff) {
+          minDiff = diff;
+          closestIndex = i;
+        }
+      }
+      return closestIndex;
+    },
 
+    applySinglePointerSeries() {
+      if (!this.chartInstance || !this.singlePointerPoint) return;
 
+      const seriesConfig = {
+        id: "SINGLE_POINTER_LINE",
+        type: "line",
+        data: [],
+        silent: true,
+        z: 20,
+        markLine: {
+          silent: false,
+          symbol: ["none", "none"],
+          lineStyle: {
+            color: "#ff0000",
+            type: "dashed",
+            width: 2,
+          },
+          label: {
+            show: true,
+            position: "end",
+            formatter: (params) =>
+              params.data?.val ?? this.singlePointerPoint.val,
+          },
+          data: [
+            {
+              xAxis: this.singlePointerPoint.xAxis,
+              val: this.singlePointerPoint.val,
+            },
+          ],
+        },
+      };
 
-    removeCursor() {
-      if (!this.chartInstance) return;
-  
-      // 获取当前图表配置中的所有series
-      const currentOption = this.chartInstance.getOption();
-      const existingSeries = currentOption.series || [];
-  
-      // 过滤掉峰值参考线(通过id或特定标记识别)
-      const filteredSeries = existingSeries.filter(
-        (series) => !series.id || !series.id.includes("PEAK_REFERENCE_LINE")
+      const option = this.chartInstance.getOption();
+      const otherSeries = (option.series || []).filter(
+        (s) => s && s.id !== "SINGLE_POINTER_LINE",
       );
-  
-      // 更新图表配置,移除所有峰值参考线
       this.chartInstance.setOption(
         {
-          series: filteredSeries,
+          series: [...otherSeries, seriesConfig],
         },
-        {
-          replaceMerge: ["series"],
-          notMerge: false,
-        }
+        { replaceMerge: ["series"] },
       );
-  
-      // 清空当前光标数据
-      this.cursorHightPoints = [];
-      this.currentCursorIndex = -1;
     },
   },
 };

Filskillnaden har hållts tillbaka eftersom den är för stor
+ 517 - 297
src/views/health/components/spectrogramchartsNew.vue


+ 282 - 0
src/views/health/components/tendencychartsNew.vue

@@ -0,0 +1,282 @@
+<template>
+  <div>
+    <div class="line-chart" ref="chart"></div>
+
+    <div class="linkageCharts">
+      <spectrogramcharts
+        :isshow="false"
+        :loading="loading"
+        :spectrumListTwo="spectrumListTwo"
+        :currentRow="currentRow"
+        :windCode="windCode"
+        :activeIndex="currentIndex"
+        :ids="ids"
+        @update:currentIndex="handleCurrentIndexUpdate"
+      />
+
+      <timedomaincharts
+        :loading="loading"
+        :timeListTwo="timeList"
+        :currentRow="currentRow"
+        :activeIndex="currentIndex"
+        :ids="ids"
+        :windCode="windCode"
+        @update:currentIndex="handleCurrentIndexUpdate"
+      />
+    </div>
+  </div>
+</template>
+
+<script>
+import * as echarts from "echarts";
+import axios from "axios";
+
+import timedomaincharts from "./timedomainchartsNew.vue";
+import spectrogramcharts from "./spectrogramchartsNew.vue";
+
+export default {
+  name: "TimedomainCharts",
+  components: { timedomaincharts, spectrogramcharts },
+
+  props: {
+    qsList: { type: Object, default: () => ({}) },
+    currentRow: { type: Object, default: () => ({}) },
+    ids: { type: Array, default: () => [] },
+    windCode: { type: String, default: "" },
+    chartId: { type: Number, required: true },
+    measurementPointData: { type: Array, default: () => [] },
+  },
+
+  data() {
+    return {
+      chartInstance: null,
+      spectrumListTwo: {},
+      timeList: {},
+      loading: false,
+      currentIndex: 0,
+      cacheMap: {}, // ✅ 缓存(性能优化)
+    };
+  },
+
+  watch: {
+    qsList: {
+      deep: true,
+      immediate: true,
+      handler() {
+        this.$nextTick(() => {
+          this.initDefaultData(); // ✅ 初始化数据
+          this.updateChart();
+        });
+      },
+    },
+  },
+
+  mounted() {
+    this.initChart();
+    window.addEventListener("resize", this.handleResize);
+  },
+
+  beforeDestroy() {
+    window.removeEventListener("resize", this.handleResize);
+    this.chartInstance?.off("click");
+  },
+
+  methods: {
+    /* ---------------- 初始化 ---------------- */
+    initChart() {
+      this.chartInstance = echarts.init(this.$refs.chart);
+      this.chartInstance.on("click", this.handleChartClick);
+    },
+
+    initDefaultData() {
+      if (!this.measurementPointData?.length || !this.ids?.length) return;
+
+      const first = this.measurementPointData[0];
+      console.log(first, "first");
+      if (!first) return;
+
+      this.fetchAllData(first.itemValue, [Number(first.itemKey)]);
+    },
+
+    /* ---------------- 点击事件 ---------------- */
+    async handleChartClick(params) {
+      if (this.loading) return;
+
+      const { time, ids } = this.getParams(params);
+      if (!time || !ids.length) return;
+
+      this.fetchAllData(time, ids);
+    },
+
+    /* ---------------- 参数处理 ---------------- */
+    getParams(params) {
+      let time = null;
+      let ids = [];
+      console.log(this.ids, this.windCode, "ids");
+      if (params?.name) {
+        time = params.name.slice(0, 16);
+        console.log(params, "ids", this.measurementPointData);
+        const match = (this.measurementPointData || []).find(
+          (item, index) =>
+            item.itemValue === time.slice(0, 16) && params.dataIndex === index,
+        );
+        console.log(match, "match");
+        if (match?.itemKey) ids = [Number(match.itemKey)];
+      } else {
+        ids = this.ids;
+        time = this.windCode;
+      }
+
+      return { time, ids };
+    },
+
+    /* ---------------- 核心请求 ---------------- */
+    async fetchAllData(time, ids) {
+      const cacheKey = `${time}_${ids.join(",")}`;
+      console.log(cacheKey, this.cacheMap, "this.cacheMap");
+      // ✅ 缓存命中
+      if (this.cacheMap[cacheKey]) {
+        const cache = this.cacheMap[cacheKey];
+        this.timeList = cache.time;
+        this.spectrumListTwo = cache.freq;
+        return;
+      }
+      console.log(ids, time, "ids, time");
+      try {
+        this.loading = true;
+
+        const [timeRes, freqRes] = await Promise.all([
+          this.fetch("time", ids, time),
+          this.fetch("frequency", ids, time),
+        ]);
+
+        const timeData = this.safeParse(timeRes?.data);
+        const freqData = this.safeParse(freqRes?.data);
+        console.log(timeData, "timeData");
+        console.log(freqData, "spectrumListTwo");
+        this.timeList = timeData;
+        this.spectrumListTwo = freqData;
+
+        // ✅ 写缓存
+        // this.cacheMap[cacheKey] = {
+        //   time: timeData,
+        //   freq: freqData,
+        // };
+        this.cacheMap[cacheKey] = {
+          time: JSON.parse(JSON.stringify(timeData)),
+          freq: JSON.parse(JSON.stringify(freqData)),
+        };
+      } catch (err) {
+        console.error("请求失败:", err);
+      } finally {
+        this.loading = false;
+      }
+    },
+
+    fetch(type, ids, time) {
+      return axios.post(`/AnalysisMulti/analysis/${type}`, {
+        ids,
+        windCode: this.windCode,
+        analysisType: type,
+        time,
+      });
+    },
+
+    safeParse(data) {
+      if (!data) return {};
+      if (typeof data === "string") {
+        try {
+          return JSON.parse(data);
+        } catch {
+          return {};
+        }
+      }
+      return { ...data };
+    },
+
+    /* ---------------- 图表 ---------------- */
+    updateChart() {
+      const data = this.qsList?.data || [];
+      if (!data.length) return;
+      const fields = [
+        "Ce",
+        "Cf",
+        "Cq",
+        "Cw",
+        "If",
+        "Max",
+        "Mean",
+        "Min",
+        "Sf",
+        "Xp",
+        "Xrms",
+        "fs",
+      ];
+      const map = {
+        Ce: "裕度指标",
+        Cf: "峰值指标",
+        Cq: "峭度指标",
+        Cw: "偏度指标",
+        If: "脉冲指标",
+        Max: "最大值",
+        Mean: "平均值",
+        Min: "最小值",
+        Sf: "波形指标",
+        Xp: "峰值",
+        Xrms: "有效值",
+        fs: "采样频率",
+      };
+
+      const option = {
+        grid: { left: 80, right: 80, bottom: 70, top: 30 },
+        // title: { text: "趋势图", left: "center" },
+        tooltip: { trigger: "axis" },
+        legend: { data: fields.map((f) => map[f]), bottom: 20 },
+        xAxis: {
+          type: "category",
+          data: data.map((i) => i.time_stamp || "N/A"),
+        },
+        yAxis: { type: "value" },
+        series: fields.map((f) => ({
+          name: map[f],
+          type: "line",
+          symbol: "circle",
+          symbolSize: 6,
+          data: data.map((i) => i[f] || 0),
+        })),
+      };
+
+      this.chartInstance.setOption(option, true);
+    },
+
+    /* ---------------- 其他 ---------------- */
+    handleResize() {
+      this.chartInstance?.resize();
+    },
+
+    handleCurrentIndexUpdate(i) {
+      this.currentIndex = i;
+    },
+
+    handleXrmsUpdate(v) {
+      this.XrmsValue = v;
+    },
+  },
+};
+</script>
+
+<style scoped>
+.line-chart {
+  width: 100%;
+  height: 300px;
+}
+
+.linkageCharts {
+  display: flex;
+  width: 100%;
+}
+.linkageCharts > * {
+  flex: 1; /* 每个占 50% */
+  min-width: 0; /* 🔥 防止图表撑爆(很关键) */
+}
+</style>

+ 489 - 237
src/views/health/components/timedomainchartsNew.vue

@@ -1,21 +1,7 @@
 <template>
-  <div>
-    <!-- <div class="FD">
-      <div v-if="TZshow" class="eigenvalue">
-        <h5>特征值</h5>
-        <p>有效值:{{ this.timeList.Xrms }}</p>
-        <p>平均值:{{ this.timeList.mean_value }}</p>
-        <p>最大值:{{ this.timeList.max_value }}</p>
-        <p>最小值:{{ this.timeList.min_value }}</p>
-        <p>峰值:{{ this.timeList.Xp }}</p>
-        <p>峰峰值:{{ this.timeList.Xpp }}</p>
-        <p>波形因子:{{ this.timeList.Sf }}</p>
-        <p>脉冲指标:{{ this.timeList.If }}</p>
-        <p>裕度指标:{{ this.timeList.Ce }}</p>
-        <p>偏度指标:{{ this.timeList.Cw }}</p>
-        <p>峭度指标:{{ this.timeList.Cq }}</p>
-      </div>
-    </div> -->
+  <div v-loading="loading">
+    <!-- ECharts 图表容器 -->
+    <div class="line-chart" ref="chart"></div>
     <div class="control-panel">
       <!-- 光标 -->
       <div class="panel-block">
@@ -31,33 +17,34 @@
           </span>
         </div>
       </div>
-
-      <!-- 特征值 -->
-      <div class="panel-block">
-        <span class="label">特征值</span>
-        <div class="btn-group">
-          <span
-            v-for="item in TZFeatureList"
-            :key="item.key"
-            :class="['btn', activeFeatures.includes(item.key) ? 'active' : '']"
-            @click="toggleTZFeature(item.key)"
-          >
-            {{ item.label }}
-          </span>
-        </div>
+      <!-- 特征值(多选) -->
+      <div class="panel-block full-width">
+        <span class="label">特征 </span>
+        <el-cascader
+          v-model="selectedFeatures"
+          :options="cascaderOptions"
+          :props="cascaderProps"
+          collapse-tags
+          clearable
+          placeholder="请选择特征"
+          size="small"
+          @change="handleCascaderChange"
+        />
       </div>
     </div>
-    <!-- ECharts 图表容器 -->
-    <div class="line-chart" ref="chart" v-loading="loadingTime"></div>
   </div>
 </template>
 
 <script>
 import axios from "axios";
 import * as echarts from "echarts"; // 导入 echarts 库
-
+import cursorReferenceMixin from "./spectrogramcharts/cursorReferenceMixin";
+import Bdgb from "./spectrogramcharts/Bdgb";
+import Xdgb from "./spectrogramcharts/Xdgb";
+import Tjgb from "./spectrogramcharts/Tjgb";
 export default {
   name: "TimedomainCharts", // 组件名称
+  mixins: [cursorReferenceMixin, Bdgb, Xdgb, Tjgb],
   props: {
     currentIndex: {
       type: Number,
@@ -67,7 +54,7 @@ export default {
       type: Number,
       default: 0,
     },
-    loadingTime: {
+    loading: {
       type: Boolean,
       default: false,
     },
@@ -95,13 +82,44 @@ export default {
       TZshow: false,
       timeList: {},
       manualMarks: [],
+      // 时域:多单指针
+      singlePointerPoints: [],
+      activeSinglePointer: null,
+      // 时域:边带
+      sidebandCursorVisible: false,
+      sidebandCursorPoints: [],
       // 光标
       GBcheckList: [
         { val: "单指针" },
-        { val: "频带光标" },
-        { val: "边带光标" },
+        // { val: "谐波光标" },
+        // { val: "边带光标" },
       ],
       checkedGB: [],
+      featureGroups: [
+        {
+          label: "时域特征",
+          type: "time",
+          children: [
+            { key: "Xrms", label: "有效值" },
+            { key: "mean_value", label: "平均值" },
+            { key: "max_value", label: "最大值" },
+            { key: "min_value", label: "最小值" },
+            { key: "Xp", label: "峰值" },
+            { key: "Xpp", label: "峰峰值" },
+          ],
+        },
+      ],
+      selectedFeatures: [],
+      cascaderProps: {
+        // multiple: true, // 多选
+        // checkStrictly: false, // 不能选父节点(只选叶子)
+        // emitPath: true, // 返回路径(重要)
+        multiple: true,
+        checkStrictly: false,
+        emitPath: true, // 保留(你需要路径)
+        value: "value",
+        label: "label",
+      },
       // 特征值按钮
       TZFeatureList: [
         { key: "Xrms", label: "有效值" },
@@ -114,6 +132,18 @@ export default {
       activeFeatures: [],
     };
   },
+  computed: {
+    cascaderOptions() {
+      return this.featureGroups.map((group) => ({
+        label: group.label,
+        value: group.type, // ✅ 改这里
+        children: group.children.map((item) => ({
+          label: item.label,
+          value: item.key,
+        })),
+      }));
+    },
+  },
   watch: {
     // 监听 chartData 和 chartLabels 的变化,重新绘制图表
     chartData(newData) {
@@ -140,239 +170,359 @@ export default {
   },
   mounted() {
     this.$nextTick(() => {
-      setTimeout(() => {
-        this.initializeChart(); // 延迟2秒后调用
-        this.getTime();
-      }, 500); // 2000毫秒,即2秒
+      this.initializeChart();
     });
   },
-
+  beforeDestroy() {
+    if (this.chartInstance) {
+      this.chartInstance.getZr().off("click", this.handleSinglePointerClick);
+      this.chartInstance.getZr().off("click", this.handleSidebandCursorClick);
+      this.chartInstance.dispose();
+      this.chartInstance = null;
+    }
+  },
   methods: {
-    initializeChart() {
-      const chartDom = this.$refs.chart;
-      if (chartDom && !this.chartInstance) {
-        this.chartInstance = echarts.init(chartDom);
-      }
-      if (this.timeList.y && this.timeList.x) {
-        this.updateChart(this.timeList.y, this.timeList.x);
+    getChartXYData() {
+      return {
+        x: this.timeList?.x || [],
+        y: this.timeList?.y || [],
+      };
+    },
+    findClosestIndex(target, xData) {
+      if (!Array.isArray(xData) || !xData.length) return -1;
+      let closestIndex = 0;
+      let minDiff = Math.abs(Number(xData[0]) - target);
+      for (let i = 1; i < xData.length; i++) {
+        const diff = Math.abs(Number(xData[i]) - target);
+        if (diff < minDiff) {
+          minDiff = diff;
+          closestIndex = i;
+        }
       }
+      return closestIndex;
+    },
+    bindSinglePointerClick() {
+      if (!this.chartInstance) return;
+      this.chartInstance.getZr().off("click", this.handleSinglePointerClick);
+      this.chartInstance.getZr().on("click", this.handleSinglePointerClick);
+    },
+    unbindSinglePointerClick() {
+      if (!this.chartInstance) return;
+      this.chartInstance.getZr().off("click", this.handleSinglePointerClick);
+    },
+    enableSinglePointer() {
+      this.bindSinglePointerClick();
     },
+    disableSinglePointer() {
+      this.unbindSinglePointerClick();
+      this.singlePointerPoints = [];
+      this.activeSinglePointer = null;
+      this.removeSinglePointerSeries();
+    },
+    handleSinglePointerClick(event) {
+      if (!this.checkedGB.includes("单指针")) return;
+      if (this.checkedGB.includes("边带光标")) return; // 边带模式冻结单指针
+      if (!this.chartInstance) return;
+
+      const { x, y } = this.getChartXYData();
+      if (!x.length || !y.length) return;
+
+      const pointInGrid = this.chartInstance.convertFromPixel(
+        { seriesIndex: 0 },
+        [event.offsetX, event.offsetY],
+      );
+      const xClick = pointInGrid?.[0];
+      if (xClick == null || Number.isNaN(xClick)) return;
+
+      const idx = this.findClosestIndex(xClick, x);
+      if (idx < 0) return;
+
+      const xAxis = Number(x[idx]);
+      const yValue = Number(y[idx]);
+      if (Number.isNaN(xAxis) || Number.isNaN(yValue)) return;
+
+      const existed = this.singlePointerPoints.some(
+        (p) => Number(p.xAxis) === xAxis,
+      );
+      if (!existed) {
+        this.singlePointerPoints.push({
+          xAxis,
+          val: yValue.toFixed(6),
+        });
+      }
 
-    renderManualMarks(marks) {
+      // 默认最后一次点击的单指针作为边带中心(0线)
+      this.activeSinglePointer = {
+        xAxis,
+        val: yValue.toFixed(6),
+      };
+
+      this.applySinglePointerSeries();
+    },
+    applySinglePointerSeries() {
+      if (!this.chartInstance) return;
       const option = this.chartInstance.getOption();
+      const otherSeries = (option.series || []).filter(
+        (s) => s && s.id !== "SINGLE_POINTER_LINE",
+      );
+
+      if (!this.singlePointerPoints.length) {
+        this.chartInstance.setOption(
+          { series: otherSeries },
+          { replaceMerge: ["series"] },
+        );
+        return;
+      }
 
-      const manualSeries = {
-        id: "MANUAL_MARK",
+      const seriesConfig = {
+        id: "SINGLE_POINTER_LINE",
         type: "line",
+        data: [],
+        silent: true,
+        z: 20,
         markLine: {
+          silent: false,
           symbol: ["none", "none"],
           lineStyle: {
             color: "#ff0000",
             type: "dashed",
+            width: 2,
           },
           label: {
             show: true,
+            position: "end",
+            formatter: (params) => params.data?.val || "",
           },
-          data: marks,
+          data: this.singlePointerPoints.map((p) => ({
+            xAxis: p.xAxis,
+            val: p.val,
+          })),
         },
       };
 
-      this.chartInstance.setOption({
-        series: [
-          ...option.series.filter((s) => s.id !== "MANUAL_MARK"),
-          manualSeries,
-        ],
-      });
+      this.chartInstance.setOption(
+        { series: [...otherSeries, seriesConfig] },
+        { replaceMerge: ["series"] },
+      );
     },
-    selectCursor(val) {
-      if (this.checkedGB.includes(val)) {
-        this.checkedGB = [];
-      } else {
-        this.checkedGB = [val];
-      }
-      this.chartInstance.off("click");
-      this.applyCursor(); // 👈 核心入口
+    removeSinglePointerSeries() {
+      if (!this.chartInstance) return;
+      const option = this.chartInstance.getOption();
+      const series = (option.series || []).filter(
+        (s) => s && s.id !== "SINGLE_POINTER_LINE",
+      );
+      this.chartInstance.setOption({ series }, { replaceMerge: ["series"] });
     },
-    toggleTZFeature(key) {
-      const i = this.activeFeatures.indexOf(key);
-
-      if (i > -1) {
-        this.activeFeatures.splice(i, 1);
-      } else {
-        this.activeFeatures.push(key);
+    enableSidebandCursor() {
+      this.sidebandCursorVisible = true;
+      if (!this.chartInstance) return;
+      // 边带模式:只响应边带点击,冻结单指针
+      this.unbindSinglePointerClick();
+      this.chartInstance.getZr().off("click", this.handleSidebandCursorClick);
+      this.chartInstance.getZr().on("click", this.handleSidebandCursorClick);
+    },
+    disableSidebandCursor() {
+      this.sidebandCursorVisible = false;
+      if (this.chartInstance) {
+        this.chartInstance.getZr().off("click", this.handleSidebandCursorClick);
+      }
+      this.removeSidebandCursor();
+      if (this.checkedGB.includes("单指针")) {
+        this.bindSinglePointerClick();
       }
-
-      this.renderTZFeatures();
     },
-    renderTZFeatures() {
-      const marks = this.activeFeatures.map((key) => ({
-        yAxis: this.timeList[key],
-        label: {
-          formatter: key,
-        },
-      }));
+    handleSidebandCursorClick(event) {
+      if (!this.sidebandCursorVisible || !this.chartInstance) return;
+      if (!this.singlePointerPoints.length) return;
+
+      const pointInGrid = this.chartInstance.convertFromPixel(
+        { seriesIndex: 0 },
+        [event.offsetX, event.offsetY],
+      );
+      const clickX = pointInGrid?.[0];
+      if (clickX == null || Number.isNaN(clickX)) return;
+
+      // 以离点击最近的单指针来定义边带间隔 delta
+      const nearestCenter = this.singlePointerPoints.reduce((prev, cur) => {
+        const prevDiff = Math.abs(clickX - Number(prev.xAxis));
+        const curDiff = Math.abs(clickX - Number(cur.xAxis));
+        return curDiff < prevDiff ? cur : prev;
+      });
+      const referenceCenterX = Number(nearestCenter.xAxis);
+      const delta = Math.abs(clickX - referenceCenterX);
+      if (!delta) return;
+
+      const points = [];
+      this.singlePointerPoints.forEach((center) => {
+        const centerX = Number(center.xAxis);
+        for (let n = -5; n <= 5; n++) {
+          if (n === 0) continue;
+          points.push({
+            xAxis: centerX + n * delta,
+            val: `${n > 0 ? "+" : ""}${n}`,
+          });
+        }
+      });
+      this.sidebandCursorPoints = points;
+      this.applySidebandSeries();
+    },
+    applySidebandSeries() {
+      if (!this.chartInstance) return;
+      const option = this.chartInstance.getOption();
+      const otherSeries = (option.series || []).filter(
+        (s) => s && s.id !== "SIDEBAND_CURSOR",
+      );
+
+      if (!this.sidebandCursorPoints.length) {
+        this.chartInstance.setOption(
+          { series: otherSeries },
+          { replaceMerge: ["series"] },
+        );
+        return;
+      }
 
-      const featureSeries = {
-        id: "TZ_FEATURE",
+      const sidebandSeries = {
+        id: "SIDEBAND_CURSOR",
         type: "line",
+        data: [],
+        silent: true,
+        z: 21,
         markLine: {
+          silent: true,
+          symbol: ["none", "none"],
           lineStyle: {
-            color: "#00aaff",
-            type: "solid",
+            color: "#ff0000",
+            type: "dashed",
+            width: 1,
+          },
+          label: {
+            show: true,
+            position: "start",
+            formatter: (params) => params.data?.val || "",
+            color: "#ff0000",
           },
-          data: marks,
+          data: this.sidebandCursorPoints.map((p) => ({
+            xAxis: p.xAxis,
+            val: p.val,
+          })),
         },
       };
 
-      const option = this.chartInstance.getOption();
-
-      this.chartInstance.setOption({
-        series: [
-          ...option.series.filter((s) => s.id !== "TZ_FEATURE"),
-          featureSeries,
-        ],
-      });
+      this.chartInstance.setOption(
+        { series: [...otherSeries, sidebandSeries] },
+        { replaceMerge: ["series"] },
+      );
     },
-    applyCursor() {
+    removeSidebandCursor() {
+      this.sidebandCursorPoints = [];
       if (!this.chartInstance) return;
-
       const option = this.chartInstance.getOption();
-
-      this.chartInstance.setOption({
-        series: option.series.filter(
-          (s) =>
-            !["CURSOR_SINGLE", "CURSOR_BAND", "CURSOR_SIDEBAND"].includes(s.id),
-        ),
-      });
-
-      const type = this.checkedGB[0];
-
-      if (type === "单指针") {
-        this.enableSingleCursor();
-      } else if (type === "频带光标") {
-        this.enableBandCursor();
-      } else if (type === "边带光标") {
-        this.enableSidebandCursor();
+      const series = (option.series || []).filter(
+        (s) => s && s.id !== "SIDEBAND_CURSOR",
+      );
+      this.chartInstance.setOption({ series }, { replaceMerge: ["series"] });
+    },
+    initializeChart() {
+      const chartDom = this.$refs.chart;
+      if (chartDom && !this.chartInstance) {
+        this.chartInstance = echarts.init(chartDom);
+      }
+      if (this.timeList.y && this.timeList.x) {
+        this.updateChart(this.timeList.y, this.timeList.x);
       }
     },
-    enableSingleCursor() {
-      this.chartInstance.off("click");
-      console.log("单指针");
-      this.chartInstance.on("click", (params) => {
-        const x = params.value[0];
+    handleCascaderChange(val) {
+      if (!val || val.length === 0) {
+        this.activeFeatures = [];
+        this.renderTZFeatures();
+        return;
+      }
 
-        const option = this.chartInstance.getOption();
+      // val = [["时域特征", "Xrms"], ["时域特征", "max_value"]]
 
-        const newSeries = {
-          id: "CURSOR_SINGLE",
-          type: "line",
-          markLine: {
-            symbol: ["none", "none"],
-            lineStyle: { color: "#ff0000", width: 1 },
-            label: {
-              formatter: `X: ${x.toFixed(2)}`,
-            },
-            data: [{ xAxis: x }],
-          },
-        };
+      this.activeFeatures = val.map((item) => item[item.length - 1]);
 
-        this.chartInstance.setOption({
-          series: [
-            ...option.series.filter((s) => s.id !== "CURSOR_SINGLE"),
-            newSeries,
-          ],
-        });
-      });
+      this.renderTZFeatures(); // ✅ 直接用你已有方法
     },
-    enableBandCursor() {
-      console.log("频带");
-      let points = [];
-      this.chartInstance.off("click");
-
-      this.chartInstance.on("click", (params) => {
-        const x = params.value[0];
-        points.push(x);
-
-        if (points.length === 2) {
-          const [x1, x2] = points.sort((a, b) => a - b);
+    renderTZFeatures() {
+      if (!this.chartInstance) return;
 
-          const option = this.chartInstance.getOption();
+      const marks = this.activeFeatures
+        .filter((key) => this.timeList[key] !== undefined)
+        .map((key) => ({
+          yAxis: this.timeList[key],
+          label: {
+            // formatter: key,
+            formatter: this.timeList[key],
+          },
+        }));
 
-          const bandSeries = {
-            id: "CURSOR_BAND",
-            type: "line",
-            markArea: {
-              itemStyle: {
-                color: "rgba(0, 128, 255, 0.2)",
+      this.chartInstance.setOption({
+        series: [
+          {
+            id: "MAIN_SERIES",
+            markLine: {
+              silent: true,
+              symbol: ["none", "none"],
+              lineStyle: {
+                color: "#00aaff",
+                type: "dashed",
               },
-              data: [[{ xAxis: x1 }, { xAxis: x2 }]],
+              data: marks,
             },
-          };
-
-          this.chartInstance.setOption({
-            series: [
-              ...option.series.filter((s) => s.id !== "CURSOR_BAND"),
-              bandSeries,
-            ],
-          });
-
-          points = [];
-        }
+          },
+        ],
       });
     },
-    enableSidebandCursor() {
-      console.log("边带");
-      this.chartInstance.off("click");
-
-      this.chartInstance.on("click", (params) => {
-        const center = params.value[0];
-
-        // ✅ 正确转频(核心!!!)
-        const rpm = this.timeList.rpm_Gen || 0;
-        const spacing = rpm / 60 || 10;
-
-        const count = 5;
-
-        const lines = [];
-
-        for (let i = -count; i <= count; i++) {
-          const x = center + i * spacing;
-
-          lines.push({
-            xAxis: x,
-            label: {
-              formatter: `${i}x`,
-            },
-          });
+    selectCursor(val) {
+      const isAlreadyChecked = this.checkedGB.includes(val);
+      const isSinglePointerVal = val === "单指针";
+      const isSidebandVal = val === "边带光标";
+      const isHarmonicVal = val === "谐波光标";
+
+      if (isAlreadyChecked) {
+        if (isSinglePointerVal) {
+          // 单指针取消时,级联取消边带
+          this.checkedGB = this.checkedGB.filter(
+            (v) => v !== "单指针" && v !== "边带光标",
+          );
+        } else {
+          this.checkedGB = this.checkedGB.filter((v) => v !== val);
         }
+      } else if (isSinglePointerVal || isSidebandVal) {
+        const next = new Set(this.checkedGB);
+        next.add(val);
+        next.delete("谐波光标");
+        this.checkedGB = Array.from(next);
+      } else if (isHarmonicVal) {
+        this.checkedGB = [val];
+      } else {
+        this.checkedGB = [val];
+      }
 
-        const option = this.chartInstance.getOption();
-
-        const sidebandSeries = {
-          id: "CURSOR_SIDEBAND",
-          type: "line",
-          markLine: {
-            symbol: ["none", "none"],
-            lineStyle: {
-              color: "#ffa500",
-              type: "dashed",
-            },
-            data: lines,
-          },
-        };
-
-        this.chartInstance.setOption({
-          series: [
-            ...option.series.filter((s) => s.id !== "CURSOR_SIDEBAND"),
-            sidebandSeries,
-          ],
-        });
-      });
+      const isSidebandChecked = this.checkedGB.includes("边带光标");
+      const isHarmonicChecked = this.checkedGB.includes("谐波光标");
+      const isSinglePointerChecked = this.checkedGB.includes("单指针");
+
+      isSidebandChecked
+        ? this.enableSidebandCursor()
+        : this.disableSidebandCursor();
+      isHarmonicChecked
+        ? this.enableHarmonicCursor()
+        : this.disableHarmonicCursor();
+      if (!isSinglePointerChecked) {
+        this.disableSinglePointer();
+      } else if (isSidebandChecked) {
+        // 保留单指针线,冻结单指针点击
+        this.unbindSinglePointerClick();
+      } else {
+        this.enableSinglePointer();
+      }
     },
     // 更新图表数据
     updateChart(data, labels) {
       if (!this.chartInstance) return; // Check if chartInstance is available
-
+      console.log(this.timeList, "updataChart  时域图");
       const option = {
         // title: {
         //   text: this.timeList.title,
@@ -382,11 +532,12 @@ export default {
           left: 60, // 原来是100,适当缩小左右边距
           right: 60,
           //   bottom: 90, // 给推拽条和坐标轴腾出空间
-          top: 60,
+          top: 30,
+          bottom: 50,
         },
         toolbox: {
           right: 10,
-          top: 35, // 👈 工具栏在最上面
+          // top: 55, // 👈 工具栏在最上面
           feature: {
             dataZoom: { yAxisIndex: "none" },
             restore: {},
@@ -404,7 +555,7 @@ export default {
           name: this.timeList.xaxis,
           nameLocation: "center",
           nameTextStyle: {
-            fontSize: 14,
+            fontSize: 12,
             color: "#333",
             padding: [15, 0, 0, 0], // 增加X轴标题和轴线的距离
           },
@@ -423,7 +574,7 @@ export default {
           type: "value",
           name: this.timeList.yaxis,
           nameTextStyle: {
-            fontSize: 14,
+            fontSize: 12,
             color: "#333",
             padding: [0, 10, 0, 0], // 根据需要微调
           },
@@ -452,24 +603,36 @@ export default {
         dataZoom: [
           {
             type: "inside",
-            start: 0,
-            end: 10,
-          },
-          {
-            type: "slider",
-            start: 0,
-            end: 10,
-            handleSize: "80%",
-            showDataShadow: false,
-            top: 0, // 👈 放到顶部
-            height: 20, // 👈 控制高度(可选)
+            xAxisIndex: 0,
+            filterMode: "none",
+            zoomOnMouseWheel: false,
+            moveOnMouseMove: false,
+            moveOnMouseWheel: false,
           },
         ],
+        // dataZoom: [
+        //   {
+        //     type: "inside",
+        //     start: 0,
+        //     end: 10,
+        //   },
+        //   {
+        //     type: "slider",
+        //     start: 0,
+        //     end: 10,
+        //     handleSize: "80%",
+        //     showDataShadow: false,
+        //     top: 30, // 👈 放到顶部
+        //     height: 20, // 👈 控制高度(可选)
+        //   },
+        // ],
         series: [
           {
+            id: "MAIN_SERIES", // ✅ 必须加
             name: "数据系列",
             type: "line",
             data: labels.map((item, index) => [item, data[index]]),
+            // data: (labels || []).map((item, index) => [item, data?.[index]])
             symbol: "none",
             symbolSize: 8,
             lineStyle: {
@@ -487,13 +650,99 @@ export default {
         ],
       };
 
-      this.chartInstance.setOption(option, {
-        notMerge: false, // 👈 关键
+      this.chartInstance.setOption(option, true);
+      this.$nextTick(() => {
+        this.renderTZFeatures(); // ✅ 防止被覆盖
+        this.applySinglePointerSeries();
+        this.applySidebandSeries();
+      });
+    },
+    generateSeries(featureLines) {
+      const createMarkLine = (dataSource, color) => ({
+        type: "line",
+        markLine: {
+          silent: false,
+          lineStyle: { color, type: "dashed", width: 1 },
+          symbol: ["none", "none"],
+          label: {
+            show: true,
+            position: "end",
+            formatter: ({ data }) => data.val,
+          },
+          emphasis: {
+            symbol: ["none", "none"],
+            lineStyle: { color: "#FF6A00", width: 2 },
+            label: {
+              show: true,
+              formatter: ({ value }) => `特征值: ${value}`,
+              color: "#000",
+            },
+          },
+          data: dataSource.map(({ Xaxis, val }) => ({ xAxis: Xaxis, val })),
+        },
       });
+
+      const markLines = [
+        { data: featureLines.Fr, color: "#A633FF" },
+        { data: featureLines.BPFI, color: "#23357e" },
+        { data: featureLines.BPFO, color: "#42a0ae" },
+        { data: featureLines.BSF, color: "#008080" },
+        { data: featureLines.FTF, color: "#af254f" },
+        { data: featureLines.B3P, color: "#FFD700" },
+      ].map(({ data, color }) => createMarkLine(data, color));
+
+      return [
+        {
+          name: "数据系列",
+          type: "line",
+          data: this.spectrumList.x.map((x, i) => [x, this.spectrumList.y[i]]),
+          symbol: "none",
+          lineStyle: { color: "#162961", width: 1 },
+          itemStyle: { color: "#162961", borderColor: "#fff", borderWidth: 1 },
+          large: true,
+        },
+        ...markLines,
+      ];
     },
+    renderFeatureSeries(featureLines) {
+      if (!this.chartInstance) return;
+
+      const currentOption = this.chartInstance.getOption();
 
+      // 保留光标
+      const cursorLineSeries =
+        currentOption.series.find((s) => s && s.id === "CURSOR_LINE_SERIES") ||
+        {};
+      const cursorPointSeries =
+        currentOption.series.find((s) => s && s.id === "CURSOR_POINT_SERIES") ||
+        {};
+      const cursorHighLineSeries =
+        currentOption.series.find((s) => s && s.id === "PEAK_REFERENCE_LINE") ||
+        {};
+
+      const featureSeries = this.generateSeries(featureLines);
+
+      this.chartInstance.setOption(
+        {
+          series: [
+            (currentOption.series || []).find(
+              (s) => s && (s.id === "MAIN_SERIES" || s.name === "数据系列"),
+            ) || (currentOption.series || []).find((s) => s),
+            ...featureSeries.slice(1),
+            cursorLineSeries,
+            cursorPointSeries,
+            cursorHighLineSeries,
+          ].filter((s) => s && s.type),
+        },
+        { replaceMerge: ["series"] },
+      );
+    },
     getTime() {
-      this.$emit("handleLoading", null, true, this.activeIndex);
+      this.$emit("handleLoading", {
+        id: this.chartId,
+        currentRow: this.currentRow,
+        loading: true,
+      });
       const params = {
         ids: this.ids,
         analysisType: "time",
@@ -508,10 +757,13 @@ export default {
         })
         .catch((error) => {})
         .finally(() => {
-          this.$emit("handleLoading", this.currentRow, false, this.activeIndex);
+          this.$emit("handleLoading", {
+            id: this.chartId,
+            currentRow: this.currentRow,
+            loading: false,
+          });
         });
     },
-
     Show() {
       this.TZshow = !this.TZshow;
     },
@@ -522,7 +774,7 @@ export default {
 <style lang="scss" scoped>
 .line-chart {
   width: 100%;
-  height: 320px;
+  height: 400px;
 }
 .FD {
   width: 100%;

+ 636 - 0
src/views/health/components/waterfallChart.vue

@@ -0,0 +1,636 @@
+<template>
+  <div>
+    <div ref="chart" class="waterfall-chart" id="waterfall-chart"></div>
+    <div class="control-panel">
+      <!-- 频率范围 -->
+      <div class="box full-row">
+        <div class="panel-block">
+          <span class="label1">频率</span>
+          <el-input v-model="freqMin" size="mini" placeholder="下限" />
+          <span>~</span>
+          <el-input v-model="freqMax" size="mini" placeholder="上限" />
+          <el-button size="mini" type="primary" @click="handleFreqRange">
+            应用
+          </el-button>
+        </div>
+      </div>
+    </div>
+    <div class="control-panel">
+      <!-- 手动标注 -->
+      <div class="panel-block">
+        <span class="label1">标注</span>
+        <el-input v-model="manualFreq" size="mini" placeholder="频率" />
+        <el-input v-model="multiple" size="mini" placeholder="倍频" />
+        <el-button size="mini" type="success" @click="handleMark"
+          >标注</el-button
+        >
+        <el-button size="mini" type="info" @click="removeMark">清除</el-button>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+import Plotly from "plotly.js-dist-min";
+
+export default {
+  name: "Waterfall3D",
+
+  props: {
+    // 数据格式:[freq, amp, time]
+    data: {
+      type: Array,
+      default: () => [],
+    },
+  },
+  data() {
+    return {
+      freqMin: "",
+      freqMax: "",
+      manualFreq: "",
+      multiple: 1,
+
+      freqRange: null, // 当前频率范围
+      markLines: [], // 标注线
+    };
+  },
+
+  watch: {
+    data: {
+      deep: true,
+      immediate: true,
+      handler() {
+        this.$nextTick(() => {
+          this.renderChart();
+        });
+      },
+    },
+  },
+
+  methods: {
+    /** ✅ 时间解析(核心修复) */
+    parseTime(time) {
+      if (typeof time === "number") {
+        return time < 1e12 ? time * 1000 : time;
+      }
+      if (typeof time === "string") {
+        return new Date(time.replace(/-/g, "/")).getTime();
+      }
+      return new Date(time).getTime();
+    },
+
+    /** ✅ 时间格式化 */
+    formatTime(t) {
+      const d = new Date(t);
+      const pad = (n) => String(n).padStart(2, "0");
+
+      return `${d.getFullYear()}/${pad(d.getMonth() + 1)}/${pad(
+        d.getDate(),
+      )} ${pad(d.getHours())}:${pad(d.getMinutes())}`;
+    },
+    renderChart() {
+      if (!this.data.length) return;
+
+      /** =========================
+   * 1. 分组(时间 + Hz)
+   ========================== */
+      const groupMap = new Map();
+
+      this.data.forEach(([freq, amp, time, Hz]) => {
+        const t = this.parseTime(time);
+        const key = `${t}_${Hz}`;
+
+        if (!groupMap.has(key)) {
+          groupMap.set(key, {
+            time: t,
+            Hz,
+            points: [],
+          });
+        }
+
+        groupMap.get(key).points.push([freq, amp]);
+      });
+
+      /** =========================
+   * 2. 转数组 + 排序
+   ========================== */
+      const groups = [...groupMap.values()];
+      //   .sort((a, b) => a.time - b.time);
+
+      /** =========================
+   * 3. 同时间分组(关键!!!)
+   ========================== */
+      const timeGroupMap = new Map();
+
+      groups.forEach((g) => {
+        if (!timeGroupMap.has(g.time)) {
+          timeGroupMap.set(g.time, []);
+        }
+        timeGroupMap.get(g.time).push(g);
+      });
+
+      /** =========================
+   * 4. traces
+   ========================== */
+      const traces = [];
+
+      groups.forEach((group, globalIndex) => {
+        const { time, Hz, points } = group;
+
+        const sameTimeGroups = timeGroupMap.get(time);
+        const index = sameTimeGroups.indexOf(group);
+        const total = sameTimeGroups.length;
+
+        const spread = 200; // 👈 控制展开宽度(可调)
+
+        // 👉 同时间“对称展开”
+        const offsetX = total > 1 ? (index - (total - 1) / 2) * spread : 0;
+
+        const x = [];
+        const y = [];
+        const z = [];
+        const text = [];
+
+        points.sort((a, b) => a[0] - b[0]);
+
+        points.forEach(([freq, amp]) => {
+          if (this.freqRange) {
+            if (freq < this.freqRange.min || freq > this.freqRange.max) return;
+          }
+
+          x.push(time + offsetX); // 👈 关键
+          y.push(freq);
+          z.push(amp + globalIndex * 0.02); // 防Z重叠
+
+          text.push(
+            `时间: ${this.formatTime(time)}<br>` +
+              `采样频率: ${Hz} Hz<br>` +
+              `频率: ${freq.toFixed(2)} Hz<br>` +
+              `幅值: ${amp.toFixed(3)}`,
+          );
+        });
+
+        traces.push({
+          x,
+          y,
+          z,
+          text,
+          type: "scatter3d",
+          mode: "lines",
+          opacity: 0.7,
+          line: {
+            width: 2,
+            color: "#162961",
+          },
+          showlegend: false,
+          hovertemplate: "%{text}<extra></extra>",
+        });
+      });
+
+      /** =========================
+   * 5. X轴刻度(时间 + Hz)
+   ========================== */
+      const tickvals = groups.map((g) => g.time);
+
+      const ticktext = groups.map((g) => {
+        return `${this.formatTime(g.time)}-${g.Hz}Hz`;
+      });
+
+      /** =========================
+   * 6. 标注线
+   ========================== */
+      if (this.markLines.length) {
+        const minTime = Math.min(...groups.map((g) => g.time));
+        const maxTime = Math.max(...groups.map((g) => g.time));
+
+        this.markLines.forEach((line) => {
+          traces.push({
+            x: [minTime, maxTime],
+            y: [line.freq, line.freq],
+            z: [0, 0],
+            type: "scatter3d",
+            mode: "lines+text",
+            line: {
+              color: "#ff0000",
+              width: 4,
+              dash: "dash",
+            },
+            text: ["", line.label],
+            textposition: "top right",
+            showlegend: false,
+            hovertemplate: `${line.label}<extra></extra>`,
+          });
+        });
+      }
+
+      /** =========================
+   * 7. layout
+   ========================== */
+      const layout = {
+        // title: "3D 瀑布频谱图",
+        paper_bgcolor: "#f5f7fa",
+        margin: { l: 0, r: 0, b: 0, t: 40 },
+
+        scene: {
+          xaxis: {
+            autorange: "reversed",
+            title: "时间",
+            tickvals,
+            ticktext,
+            tickangle: 30,
+            gridcolor: "#fff",
+            backgroundcolor: "#e0e7f1",
+            showbackground: true,
+          },
+          yaxis: {
+            title: "频率 (Hz)",
+            gridcolor: "#fff",
+            backgroundcolor: "#e0e7f1",
+            showbackground: true,
+          },
+          zaxis: {
+            title: "幅值",
+            gridcolor: "#fff",
+            backgroundcolor: "#e0e7f1",
+            showbackground: true,
+          },
+          aspectmode: "manual",
+          aspectratio: { x: 2.0, y: 2.8, z: 1.2 },
+          camera: {
+            up: {
+              x: -0.1644947035315824,
+              y: -0.07969781808287146,
+              z: 0.9831529638377166,
+            },
+            center: {
+              x: -0.052807476121180814,
+              y: 0.02451796399554085,
+              z: -0.022911006648570736,
+            },
+            eye: {
+              x: 2.980700714870927,
+              y: 1.6273671421077383,
+              z: 0.6145682420564063,
+            },
+            projection: {
+              type: "orthographic",
+            },
+          },
+        },
+      };
+
+      /** =========================
+   * 8. 渲染
+   ========================== */
+      Plotly.react(this.$refs.chart, traces, layout, {
+        responsive: true,
+        displayModeBar: false,
+      });
+    },
+    //     renderChart() {
+    //       if (!this.data.length) return;
+
+    //       /** =========================
+    //  * 1. 分组
+    //  ========================== */
+    //       //   console.log(this.data, "groupMap");
+    //       const groupMap = new Map();
+
+    //       this.data.forEach(([freq, amp, time, Hz]) => {
+    //         const t = this.parseTime(time);
+    //         const key = `${t}_${Hz}`; // 👈 用key区分
+
+    //         if (!groupMap.has(key)) {
+    //           groupMap.set(key, {
+    //             time: t,
+    //             Hz,
+    //             points: [],
+    //           });
+    //         }
+
+    //         groupMap.get(key).points.push([freq, amp]);
+    //       });
+
+    //       /** =========================
+    //  * 2. 排序
+    //  ========================== */
+    //       const times = [...groupMap.keys()].sort((a, b) => a - b);
+
+    //       /** =========================
+    //  * 3. traces
+    //  ========================== */
+    //       const traces = [];
+    //       times.forEach((time, index) => {
+    //         const points = groupMap.get(time);
+
+    //         points.sort((a, b) => a[0] - b[0]);
+
+    //         const x = [];
+    //         const y = [];
+    //         const z = [];
+    //         const text = [];
+
+    //         // const offset = index * 0.02;
+
+    //         points.forEach(([freq, amp]) => {
+    //           if (this.freqRange) {
+    //             if (freq < this.freqRange.min || freq > this.freqRange.max) return;
+    //           }
+    //           const offsetX = index * 50; // 👈 新增
+    //           const offsetZ = index * 0.02; // 👈 你已有
+    //           x.push(time + offsetX);
+    //           y.push(freq);
+    //           z.push(amp + offsetZ);
+
+    //           text.push(
+    //             `时间: ${this.formatTime(time)}<br>` +
+    //               `频率: ${freq.toFixed(2)} Hz<br>` +
+    //               `幅值: ${amp.toFixed(3)}`,
+    //           );
+    //         });
+
+    //         traces.push({
+    //           x,
+    //           y,
+    //           z,
+    //           text,
+    //           type: "scatter3d",
+    //           mode: "lines",
+    //           opacity: 0.7,
+    //           line: {
+    //             width: 2,
+    //             color: "#162961",
+    //           },
+    //           showlegend: false,
+    //           hovertemplate: "%{text}<extra></extra>",
+    //         });
+    //       });
+    //       /** =========================
+    //        * ✅ 标注线(修复重复问题)
+    //        ========================== */
+    //       if (this.markLines.length) {
+    //         const minTime = Math.min(...times);
+    //         const maxTime = Math.max(...times);
+
+    //         this.markLines.forEach((line) => {
+    //           traces.push({
+    //             x: [minTime, maxTime],
+    //             y: [line.freq, line.freq],
+    //             z: [0, 0],
+    //             type: "scatter3d",
+    //             mode: "lines+text",
+    //             line: {
+    //               color: "#ff0000",
+    //               width: 4,
+    //               dash: "dash",
+    //             },
+    //             text: ["", line.label],
+
+    //             textposition: "top right",
+    //             showlegend: false,
+    //             hovertemplate: `${line.label}<extra></extra>`,
+    //           });
+    //         });
+    //       }
+
+    //       // 👉 抽稀刻度(防止重叠)
+    //       const step = Math.ceil(times.length / 8);
+    //       const tickvals = times.filter((_, i) => i % step === 0);
+    //       const ticktext = tickvals.map(this.formatTime);
+
+    //       /** =========================
+    //          * 5. 布局(工业风)
+    //          ========================== */
+    //       const layout = {
+    //         title: "3D 瀑布频谱图",
+    //         paper_bgcolor: "#f5f7fa",
+    //         margin: { l: 0, r: 0, b: 0, t: 40 },
+
+    //         scene: {
+    //           xaxis: {
+    //             autorange: "reversed", //轴方向
+    //             title: "时间",
+    //             tickvals,
+    //             ticktext,
+    //             dtick: "D3",
+    //             // showbackground: true,
+    //             // backgroundcolor: "#ffffff",
+    //             // gridcolor: "#e0e7f1",
+    //             gridcolor: "#fff",
+    //             backgroundcolor: "#e0e7f1",
+    //             showbackground: true,
+    //             linecolor: "black",
+    //             ticks: "outside",
+    //             ticklen: 10,
+    //             tickcolor: "black",
+    //             zeroline: false,
+    //             tickangle: -10,
+    //             tickangle: 30, // 👈 倾斜
+    //             margin: {
+    //               l: 20,
+    //               r: 120, // 👉 关键!!给时间留空间
+    //               t: 40,
+    //               b: 40,
+    //             },
+    //           },
+    //           yaxis: {
+    //             title: "频率 (Hz)",
+    //             gridcolor: "#fff",
+    //             backgroundcolor: "#e0e7f1",
+    //             showbackground: true,
+    //             linecolor: "black",
+    //             ticks: "outside",
+    //             ticklen: 10,
+    //             tickcolor: "black",
+    //             zeroline: false,
+    //             tickangle: -10,
+    //             tickangle: 0,
+    //           },
+    //           zaxis: {
+    //             title: "幅值",
+    //             gridcolor: "#fff",
+    //             backgroundcolor: "#e0e7f1",
+    //             showbackground: true,
+    //             linecolor: "black",
+    //             ticks: "outside",
+    //             ticklen: 10,
+    //             tickcolor: "black",
+    //             zeroline: false,
+    //             tickangle: -10,
+    //             tickangle: 0,
+    //           },
+    //           /** ✅ 核心1:比例(决定“扁不扁”) */
+    //           //   bgcolor: "#e5ecf6",
+    //           //   gridcolor: "#fff",
+    //           aspectmode: "manual",
+    //           aspectratio: { x: 2.0, y: 2.8, z: 1.2 },
+    //           /** ✅ 核心2:相机(决定轴在哪边) */
+    //           camera: {
+    //             up: {
+    //               x: -0.1644947035315824,
+    //               y: -0.07969781808287146,
+    //               z: 0.9831529638377166,
+    //             },
+    //             center: {
+    //               x: -0.052807476121180814,
+    //               y: 0.02451796399554085,
+    //               z: -0.022911006648570736,
+    //             },
+    //             eye: {
+    //               x: 2.980700714870927,
+    //               y: 1.6273671421077383,
+    //               z: 0.6145682420564063,
+    //             },
+    //             projection: {
+    //               type: "orthographic",
+    //             },
+    //           },
+    //         },
+    //       };
+
+    //       /** =========================
+    //          * 6. 渲染
+    //          ========================== */
+    //       Plotly.react(this.$refs.chart, traces, layout, {
+    //         responsive: true,
+    //         displayModeBar: false,
+    //       });
+    //       // 监听图表的 relayout 事件,获取并输出相机视角
+    //       const plotElement = document.getElementById(`waterfall-chart`);
+    //       plotElement.on("plotly_relayout", function (eventData) {
+    //         // 在每次布局变更时,打印当前相机视角
+    //         if (eventData["scene.camera"]) {
+    //           console.log(
+    //             "当前相机视角:",
+    //             eventData["scene.camera"],
+    //             eventData["scene.aspectratio"],
+    //           );
+    //         }
+    //       });
+    //     },
+    removeMark() {
+      this.markLines = [];
+      this.manualFreq = "";
+      this.multiple = 1;
+      this.renderChart();
+    },
+    handleFreqRange() {
+      const min = Number(this.freqMin);
+      const max = Number(this.freqMax);
+
+      if (isNaN(min) || isNaN(max)) return;
+
+      this.freqRange = { min, max };
+
+      this.renderChart();
+    },
+    handleMark() {
+      const base = Number(this.manualFreq);
+      const multi = Number(this.multiple) || 1;
+
+      if (isNaN(base)) return;
+
+      const newLines = [];
+
+      for (let i = 1; i <= multi; i++) {
+        newLines.push({
+          freq: base * i,
+          label: `${i}x (${base * i}Hz)`,
+        });
+      }
+
+      this.markLines = [...this.markLines, ...newLines];
+
+      this.renderChart();
+    },
+  },
+};
+</script>
+
+<style scoped>
+.waterfall-chart {
+  width: 100%;
+  height: 520px;
+}
+.control-panel {
+  display: flex;
+  flex-wrap: wrap;
+  justify-content: space-between;
+  gap: 12px;
+  padding: 8px 12px;
+  background: #f5f7fa;
+  border: 1px solid #ddd;
+  border-radius: 6px;
+  margin-bottom: 10px;
+}
+/* 🌟 关键:独占一行 */
+.full-row {
+  width: 100%;
+  .panel-block {
+    width: 45%;
+  }
+}
+.panel-block {
+  display: flex;
+  align-items: center;
+  gap: 6px;
+}
+
+.label {
+  font-size: 12px;
+  color: #666;
+}
+.label1 {
+  font-size: 12px;
+  color: #666;
+  display: inline-block;
+  width: 75px;
+}
+
+.btn-group {
+  display: flex;
+  gap: 6px;
+}
+
+.btn {
+  padding: 2px 8px;
+  font-size: 12px;
+  border: 1px solid #ccc;
+  border-radius: 3px;
+  cursor: pointer;
+}
+
+.btn.active {
+  background: #409eff;
+  color: #fff;
+}
+.full-width {
+  width: 100%;
+}
+
+.el-cascader {
+  font-size: 12px;
+}
+
+.el-cascader__tags {
+  max-width: 240px;
+  overflow: hidden;
+}
+
+.feature-grid {
+  display: grid;
+  grid-template-columns: repeat(4, 1fr);
+  gap: 10px;
+  width: 100%;
+}
+
+.feature-item {
+  display: flex;
+  align-items: center;
+  gap: 6px;
+}
+
+.el-select {
+  flex: 1;
+}
+</style>

+ 9 - 11
src/views/health/malfunction.vue

@@ -213,7 +213,7 @@ export default {
         onPick: ({ minDate, maxDate }) => {
           if (minDate && !maxDate) {
             const maxTime = new Date(
-              minDate.getTime() + 30 * 24 * 60 * 60 * 1000
+              minDate.getTime() + 30 * 24 * 60 * 60 * 1000,
             );
             this.datePickerOptions.disabledDate = (time) => {
               return (
@@ -298,7 +298,7 @@ export default {
           this.$set(
             tab,
             "monitoringvalue",
-            "gearbox_first_stage_planet_large_ring_radial_vibration"
+            "gearbox_first_stage_planet_large_ring_radial_vibration",
           );
         } else {
           // 其他 tab:只保留主轴承测点
@@ -311,7 +311,7 @@ export default {
           this.$set(
             tab,
             "monitoringvalue",
-            "main_bearing_radial_horizontal_vibration"
+            "main_bearing_radial_horizontal_vibration",
           );
         }
 
@@ -350,7 +350,7 @@ export default {
         .then((res) => {
           condition.frequencyoptions = (res.data.datas || [])
             .map((item) => Number(item))
-            .filter((num) => num >= 12800)
+            .filter((num) => num >= 2000)
             .map((num) => num.toString());
         });
 
@@ -455,7 +455,7 @@ export default {
                   `温度诊断 trend 接口耗时: ${(
                     (trendEnd - trendStart) /
                     1000
-                  ).toFixed(2)}s`
+                  ).toFixed(2)}s`,
                 );
                 const echartsdata = res.data.data || {};
                 this.tabData[tab].echartsdata = echartsdata;
@@ -475,7 +475,7 @@ export default {
                 `温度诊断 threshold 接口耗时: ${(
                   (thresholdEnd - thresholdStart) /
                   1000
-                ).toFixed(2)}s`
+                ).toFixed(2)}s`,
               );
 
               const data = res.data.data.records || [];
@@ -516,7 +516,7 @@ export default {
             .then((res) => {
               const endTime = performance.now();
               console.log(
-                `其他 tab 接口请求耗时: ${(endTime - startTime).toFixed(2)}ms`
+                `其他 tab 接口请求耗时: ${(endTime - startTime).toFixed(2)}ms`,
               );
 
               const data = res.data || {};
@@ -531,9 +531,7 @@ export default {
 };
 </script>
 
-
-
-  <style lang="scss" scoped>
+<style lang="scss" scoped>
 .global-variable {
   padding: 10px;
 }
@@ -640,4 +638,4 @@ export default {
   color: #303133;
   letter-spacing: 0.5px;
 }
-</style>
+</style>

Filskillnaden har hållts tillbaka eftersom den är för stor
+ 573 - 716
src/views/health/vibration.vue


+ 2 - 0
src/views/performance/components/chartsCom/Time3DChart.vue

@@ -428,6 +428,8 @@ export default {
             type: "orthographic",
           },
         },
+
+     
         "scene.aspectratio": {
           x: 2.2,
           y: 1.7,

Vissa filer visades inte eftersom för många filer har ändrats