Forráskód Böngészése

修改故障诊断页面和振动分析页面关联

liujiejie 23 órája
szülő
commit
fe3ff90d83

+ 9 - 5
.env.dev

@@ -1,8 +1,8 @@
 ###
  # @Author: your name
  # @Date: 2025-07-17 14:14:27
- # @LastEditTime: 2026-03-26 16:56:07
- # @LastEditors: MacBookPro
+ # @LastEditTime: 2026-04-08 13:43:11
+ # @LastEditors: bogon
  # @Description: In User Settings Edit
  # @FilePath: /performance-test/.env.dev
 #这里需要在router/index.js 文件中进行判断cockpitComponent;
@@ -14,10 +14,13 @@ 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"
-VUE_APP_APIPROXY='http://106.120.102.238:16700'
+#VUE_APP_APIPROXY='http://106.120.102.238:16700' #生产外网
+VUE_APP_APIPROXY='http://106.120.102.238:16600' #开发外网
 VUE_APP_MAP=http://106.120.102.238:18080
-VUE_APP_WZLAPIPROXY='http://106.120.102.238:18080/WindTransDev'
-VUE_APP_ETLAPIPROXY='http://106.120.102.238:18080/WindTransDev'
+# VUE_APP_WZLAPIPROXY='http://106.120.102.238:18080/WindTransDev'
+# VUE_APP_ETLAPIPROXY='http://106.120.102.238:18080/WindTransDev'
+VUE_APP_WZLAPIPROXY='http://106.120.102.238:58880/transDataWeb'
+VUE_APP_ETLAPIPROXY='http://106.120.102.238:58880/transDataWeb'
 VUE_APP_AnalysisMultiAPIPROXY='http://106.120.102.238:28999/AnalysisMulti'
 #自定义算法文佳 目前无法使用,可能是服务未启动
 VUE_APP_sAlgorithmAPIPROXY='http://106.120.102.238:58880'
@@ -29,6 +32,7 @@ VUE_APP_downLoadChartAPIPROXY='http://0.0.0.0:3001'
 # VUE_APP_UPLOAD="http://192.168.50.235/energy-manage-service/api/check/upload"
 # VUE_APP_MAPVIEW=/tiles/{z}/{x}/{y}.png
 # VUE_APP_MAP=http://192.168.50.235
+#http://192.168.50.235:16200 开发内网地址
 # VUE_APP_APIPROXY='http://192.168.50.235:16300' # 生产数据库
 # VUE_APP_WZLAPIPROXY='http://192.168.50.241:9002'
 # # VUE_APP_ETLAPIPROXY='http://192.168.50.241:9002'

+ 5 - 5
.env.jl

@@ -22,14 +22,14 @@ VUE_APP_PROJECT='jl'
 # # VUE_APP_downLoadChartAPIPROXY='http://0.0.0.0:3001'
 
 #公司 生产配置
-# VUE_APP_MAPVIEW= "http://106.120.102.238:18000/tiles/{z}/{x}/{y}.png"
+VUE_APP_MAPVIEW= "http://106.120.102.238:18000/tiles/{z}/{x}/{y}.png"
 VUE_APP_MAPVIEW=/tiles/{z}/{x}/{y}.png
 VUE_APP_UPLOAD="http://106.120.102.238:16700/energy-manage-service/api/check/upload"
-#外网生产 26500  外网开发16700
-VUE_APP_APIPROXY='http://106.120.102.238:16600'
+#外网生产 26500  外网开发16700 
+VUE_APP_APIPROXY='http://106.120.102.238:16700'
 VUE_APP_MAP='http://106.120.102.238:18080'
-VUE_APP_WZLAPIPROXY='http://106.120.102.238:18000/transDataWeb'
-VUE_APP_ETLAPIPROXY='http://106.120.102.238:18000/transDataWeb'
+VUE_APP_WZLAPIPROXY='http://106.120.102.238:58880/transDataWeb'
+VUE_APP_ETLAPIPROXY='http://106.120.102.238:58880/transDataWeb'
 VUE_APP_AnalysisMultiAPIPROXY='http://106.120.102.238:18000/AnalysisMulti'
 #自定义算法文佳 目前无法使用,可能是服务未启动
 VUE_APP_sAlgorithmAPIPROXY='http://106.120.102.238:58880'

+ 4 - 4
src/styles/global.scss

@@ -72,10 +72,10 @@
   color: var(--primary-color) !important;
 }
 
-.el-tag {
-  background-color: var(--primary-color) !important;
-  border-color: var(--primary-color) !important;
-  color: var(--text-color) !important;
+.el-tag:not(.el-tag--warning):not(.el-tag--danger):not(.el-tag--info) {
+  background-color: #00808036 !important;
+  border-color: #00808042 !important;
+  color: var(--primary-color) !important;
 }
 .el-tag {
   &.selected-tag {

+ 196 - 7
src/views/health/components/loadTree.vue

@@ -1,7 +1,7 @@
 <!--
  * @Author: your name
  * @Date: 2026-03-20 15:02:08
- * @LastEditTime: 2026-03-31 15:18:54
+ * @LastEditTime: 2026-04-08 17:23:34
  * @LastEditors: MacBookPro
  * @Description: In User Settings Edit
  * @FilePath: /performance-test/src/views/health/components/tree.vue
@@ -41,6 +41,7 @@
         node-key="itemKey"
         highlight-current
         @node-click="handleNodeClick"
+        @node-contextmenu="handleNodeContextMenu"
       >
         <span class="custom-tree-node" slot-scope="{ node, data }">
           <el-tooltip effect="dark" :content="data.itemValue" placement="top">
@@ -185,6 +186,21 @@ export default {
       this.handleSearch();
     },
 
+    handleNodeContextMenu(event, data, node) {
+      if (!node.isLeaf) return;
+      event.preventDefault();
+      event.stopPropagation();
+      const path = this.getNodePath(node);
+      const pathNodes = this.getNodeFullPath(node);
+      this.$emit("node-contextmenu", {
+        data,
+        path,
+        pathNodes,
+        clientX: event.clientX,
+        clientY: event.clientY,
+      });
+    },
+
     handleNodeClick(data, node) {
       //  非叶子节点
       if (!node.isLeaf) {
@@ -198,13 +214,13 @@ export default {
       }
 
       //  叶子节点
-      const path = this.getNodePath(node);
-      //获取所有父级节点
-      const pathNodes = this.getNodeFullPath(node);
+      // const path = this.getNodePath(node);
+      // //获取所有父级节点
+      // const pathNodes = this.getNodeFullPath(node);
 
-      console.log(data, pathNodes, "data, pathNodes");
-      // 将叶子节点选择结果抛给父组件(由父组件决定如何联动查询/选中行/加载图表)
-      this.$emit("node-select", data, path, pathNodes);
+      // console.log(data, pathNodes, "data, pathNodes");
+      // // 将叶子节点选择结果抛给父组件(由父组件决定如何联动查询/选中行/加载图表)
+      // this.$emit("node-select", data, path, pathNodes);
     },
     getNodeFullPath(node) {
       const path = [];
@@ -251,6 +267,179 @@ export default {
 
       return list;
     },
+
+    /**
+     * 从轴承诊断等页带入 windCode / 风机 / 测点 id 或 itemKey,在懒加载树中展开并选中对应末级节点。
+     * @returns {Promise<{ data: object, pathNodes: object[] } | null>}
+     */
+    async applyDeepLink(payload) {
+      const { windTurbineNumber, itemKey, id, timeStamp, companyCode } =
+        payload || {};
+      const windCode = payload?.windCode || companyCode;
+      if (!windCode || windTurbineNumber === undefined || windTurbineNumber === "") {
+        return null;
+      }
+
+      if (timeStamp) {
+        const t = new Date(String(timeStamp).replace(/-/g, "/"));
+        if (!isNaN(t.getTime())) {
+          const halfDay = 12 * 60 * 60 * 1000;
+          const fmt = (d) => {
+            const pad = (n) => (n < 10 ? "0" + n : "" + n);
+            return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
+          };
+          this.fromObj.beginTime = fmt(new Date(t.getTime() - halfDay));
+          this.fromObj.endTime = fmt(new Date(t.getTime() + halfDay));
+          this.value1 = [this.fromObj.beginTime, this.fromObj.endTime];
+        }
+      }
+
+      await this.handleSearch();
+      await this.$nextTick();
+      await this._sleep(80);
+
+      const tree = this.$refs.tree;
+      if (!tree || !tree.store) return null;
+
+      const matchLeaf = (data) => {
+        if (!data) return false;
+        const ik = itemKey != null && itemKey !== "" ? String(itemKey) : "";
+        const rid = id != null && id !== "" ? String(id) : "";
+        if (ik && String(data.itemKey) === ik) return true;
+        /** 波形表主键 id 常与末级 itemKey 一致 */
+        if (rid && String(data.itemKey) === rid) return true;
+        if (rid && data.id != null && String(data.id) === rid) return true;
+        if (timeStamp && data.itemValue === timeStamp) return true;
+        return false;
+      };
+
+      const root = tree.store.root;
+      const windNode = (root.childNodes || []).find(
+        (n) => n.data && String(n.data.itemKey) === String(windCode),
+      );
+      if (!windNode) return null;
+
+      await this._expandNodeLazy(windNode);
+      const turbNode = (windNode.childNodes || []).find(
+        (n) =>
+          n.data &&
+          (String(n.data.itemKey) === String(windTurbineNumber) ||
+            String(n.data.itemValue) === String(windTurbineNumber)),
+      );
+      if (!turbNode) return null;
+
+      await this._expandNodeLazy(turbNode);
+
+      const dfs = (node) => {
+        if (!node || !node.data) return null;
+        const children = node.childNodes || [];
+        if (!children.length) {
+          return matchLeaf(node.data) ? node : null;
+        }
+        for (const ch of children) {
+          const r = dfs(ch);
+          if (r) return r;
+        }
+        return null;
+      };
+
+      const found = dfs(turbNode);
+      if (!found) return null;
+
+      tree.setCurrentKey(found.data.itemKey);
+      const pathNodes = this.getNodeFullPath(found);
+      return { data: found.data, pathNodes };
+    },
+
+    _sleep(ms) {
+      return new Promise((r) => setTimeout(r, ms));
+    },
+
+    _expandNodeLazy(node) {
+      if (!node) return Promise.resolve();
+      return new Promise((resolve) => {
+        node.expand(() => {
+          this.$nextTick(() => resolve());
+        });
+      });
+    },
+
+    /**
+     * 仅展开到风场 + 风机并高亮风机节点;用于深链未命中末级时仍对齐左侧树。
+     * @returns {Promise<{ pathNodes: object[], measurementPointData: array } | null>}
+     */
+    async expandWindAndTurbine(payload) {
+      const { windTurbineNumber, timeStamp, id, mesurePointName, detectionPointCn, companyCode } =
+        payload || {};
+      const windCode = payload?.windCode || companyCode;
+      if (!windCode || windTurbineNumber === undefined || windTurbineNumber === "") {
+        return null;
+      }
+
+      if (timeStamp) {
+        const t = new Date(String(timeStamp).replace(/-/g, "/"));
+        if (!isNaN(t.getTime())) {
+          const halfDay = 12 * 60 * 60 * 1000;
+          const fmt = (d) => {
+            const pad = (n) => (n < 10 ? "0" + n : "" + n);
+            return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
+          };
+          this.fromObj.beginTime = fmt(new Date(t.getTime() - halfDay));
+          this.fromObj.endTime = fmt(new Date(t.getTime() + halfDay));
+          this.value1 = [this.fromObj.beginTime, this.fromObj.endTime];
+        }
+      }
+
+      await this.handleSearch();
+      await this.$nextTick();
+      await this._sleep(80);
+
+      const tree = this.$refs.tree;
+      if (!tree || !tree.store) return null;
+
+      const root = tree.store.root;
+      const windNode = (root.childNodes || []).find(
+        (n) => n.data && String(n.data.itemKey) === String(windCode),
+      );
+      if (!windNode) return null;
+
+      await this._expandNodeLazy(windNode);
+      const turbNode = (windNode.childNodes || []).find((n) => {
+        if (!n.data) return false;
+        const k = String(n.data.itemKey ?? "").trim();
+        const v = String(n.data.itemValue ?? "").trim();
+        const tgt = String(windTurbineNumber).trim();
+        return k === tgt || v === tgt;
+      });
+      if (!turbNode) return null;
+
+      await this._expandNodeLazy(turbNode);
+      tree.setCurrentKey(turbNode.data.itemKey);
+
+      const syntheticLeaf = {
+        itemKey: id != null && id !== "" ? String(id) : "",
+        itemValue: timeStamp || "",
+        mesurePointName: mesurePointName || "",
+        detectionPointCn: detectionPointCn || "",
+        otherData: {},
+      };
+
+      const pathNodes = [windNode.data, turbNode.data, syntheticLeaf];
+      /** 合并风机下各分类下的测点叶子,避免只用 mp[0] 变成「默认第一条」导致 id 匹配错行 */
+      let measurementPointData = [];
+      const mp = turbNode.data && turbNode.data.nextData;
+      if (Array.isArray(mp) && mp.length && mp[0].nextData) {
+        for (const cat of mp) {
+          if (cat && Array.isArray(cat.nextData)) {
+            measurementPointData.push(...cat.nextData);
+          }
+        }
+      } else if (Array.isArray(mp)) {
+        measurementPointData = mp;
+      }
+
+      return { pathNodes, measurementPointData };
+    },
   },
 };
 </script>

+ 187 - 117
src/views/health/components/malfunction/bearing.vue

@@ -7,35 +7,59 @@
           :data="tableData"
           tooltip-effect="dark"
           style="width: 100%"
-          height="450"
+          height="250"
+          stripe
+          border
         >
           <!-- <el-table-column fixed type="selection" width="55"> </el-table-column> -->
           <el-table-column
-            prop="bearingType"
+            type="index"
             label="序号"
             align="center"
             width="100"
+            :index="bearingTableIndex"
           >
           </el-table-column>
-          <el-table-column prop="timeStamp" label="轴承类型" align="center">
+          <el-table-column prop="brandType" label="型号" align="left">
           </el-table-column>
-          <el-table-column prop="timeStamp" label="时间" align="center">
+          <el-table-column prop="detectionPointCn" label="名称" align="left">
           </el-table-column>
-          <el-table-column prop="samplingFrequency" label="品牌" align="center">
+          <el-table-column label="状态" width="150" align="center">
+            <template slot-scope="scope">
+              <el-tag
+                size="mini"
+                :type="statusTagType(scope.row.status)"
+                effect="dark"
+              >
+                {{ formatStatusText(scope.row.status) }}
+              </el-tag>
+            </template>
           </el-table-column>
-          <el-table-column prop="rotationalSpeed" label="型号" align="center">
-          </el-table-column>
-          <el-table-column prop="rotationalSpeed" label="状态" align="center">
+          <!-- <el-table-column prop="timeStamp" label="时间" align="center">
+          </el-table-column> -->
+          <el-table-column prop="brand" width="150" label="品牌" align="left">
           </el-table-column>
+
+          <!-- <el-table-column prop="" label="操作" align="center">
+            <template slot-scope="scope">
+              <el-button
+                type="text"
+                size="small"
+                @click="handleDetail(scope.row)"
+                >查看振动分析</el-button
+              >
+            </template>
+          </el-table-column> -->
         </el-table>
         <div class="fenye">
-          <p><span>状态码说明:</span>0正常,1报警,2危险,-1未定义</p>
+          <p></p>
+          <!-- <p><span>状态码说明:</span>0正常,1报警,2危险</p> -->
           <el-pagination
             @current-change="handleCurrentChange"
             :current-page="currentPage"
             layout="total, prev, pager, next"
             :total="totalCount"
-            :page-size="10"
+            :page-size="pageSize"
             small
           ></el-pagination>
         </div>
@@ -87,37 +111,42 @@
         </div>
       </div> -->
     </div>
-    <!-- <div class="bottomBox">
+    <div class="bottomBox">
       <div class="BtLeft">
         <h4>轴承状态趋势图</h4>
+        <p style="font-size: 12px; color: #999">
+          <span>状态码说明:</span>0正常,1报警,2危险
+        </p>
         <el-empty
-          v-if="this.xData.length === 0 || this.yData.length === 0"
+          v-if="!trendChartList.length"
           description="暂无数据"
           style="padding: 46px 0"
         ></el-empty>
-        <Eecharts
-          v-else
-          style="height: 300px"
-          :xData="xData"
-          :yData="yData"
-          :yNames="[
-            '轴承内圈状态',
-            '轴承外圈状态',
-            '轴承滚动体状态',
-            '轴承保持架状态',
-          ]"
-          yAxisName="轴承故障状态"
-        ></Eecharts>
+        <div v-else class="mpoint-charts">
+          <div
+            v-for="(chart, index) in trendChartList"
+            :key="`${chart.title}-${index}`"
+            class="mpoint-chart-item"
+          >
+            <!-- <p class="mpoint-chart-title">{{ chart.title }}</p> -->
+            <Eecharts
+              style="height: 260px"
+              :xData="chart.xData"
+              :yData="chart.yData"
+              :yNames="chart.yNames"
+              yAxisName="故障状态"
+              :jumpContext="chart.jumpContext"
+            ></Eecharts>
+          </div>
+        </div>
       </div>
-    </div> -->
+    </div>
   </div>
 </template>
 
 <script>
-import axios from "axios";
-import Eecharts from "./Eecharts.vue";
+import Eecharts from "./mpointEecharts.vue";
 import bingTwo from "./bingTwo.vue";
-import { cacheEntryPossiblyAdded } from "plotly.js-dist";
 export default {
   components: { Eecharts, bingTwo },
   props: {
@@ -141,6 +170,10 @@ export default {
       type: String,
       default: "",
     },
+    echartsdata: {
+      type: Array,
+      default: () => [],
+    },
   },
   data() {
     return {
@@ -156,6 +189,7 @@ export default {
       envelopeTotalAlarmThreshold: "",
       envelopeTotalDangerThreshold: "",
       // 分页
+      pageSize: 10,
       currentPage: 1,
       total: 1,
       xData: [],
@@ -172,6 +206,7 @@ export default {
       dialogVisible: false,
       loading: false, // 控制加载状态
       statistics: {},
+      trendChartList: [],
     };
   },
   created() {},
@@ -183,9 +218,103 @@ export default {
       immediate: true, // 组件创建时立刻执行一次
       deep: true, // 如果 codedata 是复杂对象,建议加上
     },
+    echartsdata: {
+      handler(newVal) {
+        const list = Array.isArray(newVal) ? newVal : [];
+        this.trendChartList = list
+          .map((item) => {
+            const title = item?.pointNameCn || "未命名测点";
+            const inner = Array.isArray(item?.innerDatas)
+              ? item.innerDatas
+              : [];
+            const xData = inner.map((d) => d?.timeStamp || "");
+            const seriesY = inner.map((d) => {
+              const v = d?.status;
+              return v === null || v === undefined || v === ""
+                ? null
+                : Number(v);
+            });
+            const pointIds = inner.map((d) => d?.id);
+            return {
+              title,
+              xData,
+              yData: [seriesY],
+              yNames: [title],
+              jumpContext: {
+                companyCode: this.fieldCode,
+                windCode: this.fieldCode,
+                windTurbineNumber: this.windTurbineNumber,
+                itemKey: item?.itemKey ?? item?.item_key,
+                id: item?.id,
+                mesurePointName: item?.mesurePointName || title,
+                detectionPointCn: item?.pointNameCn || title,
+                rotationalSpeed: item?.rotationalSpeed,
+                samplingFrequency: item?.samplingFrequency,
+                otherData:
+                  item?.otherData && typeof item.otherData === "object"
+                    ? { ...item.otherData }
+                    : undefined,
+                pointIds,
+              },
+            };
+          })
+          .filter((it) => it.xData.length);
+      },
+      immediate: true,
+      deep: true,
+    },
   },
-
+  mounted() {},
   methods: {
+    statusTagType(status) {
+      const code = Number(status);
+      if (code === 0) return "success";
+      if (code === 1) return "warning";
+      if (code === 2) return "danger";
+      return "info";
+    },
+    formatStatusText(status) {
+      if (status === null || status === undefined || status === "") return "/";
+      const code = Number(status);
+      if (code === 0) return "正常";
+      if (code === 1) return "报警";
+      if (code === 2) return "危险";
+      if (code === -1) return "未定义";
+      return String(status);
+    },
+    handleDetail(row) {
+      const itemKey = row.itemKey ?? row.item_key;
+      const payload = {
+        /** 与故障诊断页「单位」一致(selecttree 的 companyCode / codeNumber) */
+        companyCode: this.fieldCode,
+        windCode: this.fieldCode,
+        windTurbineNumber: this.windTurbineNumber,
+        itemKey,
+        id: row.id,
+        mesurePointName: row.mesurePointName,
+        detectionPointCn: row.detectionPointCn,
+        timeStamp: row.timeStamp,
+        /** 频谱特征线需要 RPM → Hz;接口字段可能为 rotationalSpeed / otherData.RPM */
+        rotationalSpeed: row.rotationalSpeed,
+        samplingFrequency: row.samplingFrequency,
+        otherData:
+          row.otherData && typeof row.otherData === "object"
+            ? { ...row.otherData }
+            : undefined,
+      };
+      try {
+        sessionStorage.setItem(
+          "healthVibrationDeepLink",
+          JSON.stringify(payload),
+        );
+      } catch (e) {
+        console.error(e);
+      }
+      this.$router.push({ path: "/home/health/vibration" });
+    },
+    bearingTableIndex(index) {
+      return (this.currentPage - 1) * this.pageSize + index + 1;
+    },
     toggleSelection(rows) {
       if (rows) {
         rows.forEach((row) => {
@@ -200,101 +329,18 @@ export default {
     },
     handleCurrentChange(val) {
       console.log(`当前页: ${val}`);
-      setTimeout(() => {
-        this.automaticDiagnosis();
-      }, 500);
+      // setTimeout(() => {
+      //   this.automaticDiagnosis();
+      // }, 500);
       this.currentPage = val; // 子组件内部更新当前页
       this.$emit("updatePage", this.currentPage); // 通知父组件,把当前页传出去
     },
-
-    automaticDiagnosis() {
-      if (this.tableData.length === 0) {
-        this.$message.warning("当前没有数据,无法进行诊断");
-        this.loading = false; // 确保关闭 loading 状态
-        return; // 直接返回,不发请求
-      }
-      this.loading = true;
-      const ids = this.tableData
-        .map((item) => item.id)
-        .filter((id) => id !== undefined);
-
-      const params = {
-        windCode: this.fieldCode,
-        engine_code: this.windTurbineNumber,
-        autodiagType: "Bearing",
-        ids,
-      };
-
-      const autodiagType = params.autodiagType || "Bearing";
-
-      const loadingInstance = this.$loading({
-        lock: true,
-        text: "自动诊断中...",
-        spinner: "el-icon-loading",
-        background: "rgba(0, 0, 0, 0.7)",
-      });
-
-      axios
-        .post(`/AnalysisMulti/autodiag/${autodiagType}`, params)
-        .then((res) => {
-          if (res.data.code === 405) {
-            this.$message.warning(
-              res.data.message || "当前采集频率不适合进行诊断分析",
-            );
-            return; // 终止后续数据处理逻辑
-          }
-
-          const result = res.data.results;
-          this.statistics = res.data.statistics || {};
-          // x轴数据,时间戳
-          this.xData = this.tableData.map((item) => item.timeStamp);
-          // y轴数据,状态码二维数组
-          this.yData = [
-            result.BPFI?.status_codes || [],
-            result.BPFO?.status_codes || [],
-            result.BSF?.status_codes || [],
-            result.FTF?.status_codes || [],
-          ];
-
-          // 根据 max_status 设置颜色的函数
-          const setColor = (status) => {
-            switch (status) {
-              case 0:
-                return "#8ae359";
-              case 1:
-                return "#eecb5f";
-              case 2:
-                return "#f7715f";
-              default:
-                return "#80808057";
-            }
-          };
-
-          this.bearingStateColors.innerRing = setColor(
-            result.BPFI?.max_status || 0,
-          );
-          this.bearingStateColors.outerRing = setColor(
-            result.BPFO?.max_status || 0,
-          );
-          this.bearingStateColors.rollingElement = setColor(
-            result.BSF?.max_status || 0,
-          );
-          this.bearingStateColors.cage = setColor(result.FTF?.max_status || 0);
-        })
-        .catch((err) => {
-          console.error("诊断失败:", err);
-        })
-        .finally(() => {
-          loadingInstance.close();
-          this.loading = false;
-        });
-    },
-
     reset() {
       // 重置状态
       this.xData = [];
       this.yData = [];
       this.statistics = {};
+      this.trendChartList = [];
       this.bearingStateColors = {
         innerRing: "#80808057",
         outerRing: "#80808057",
@@ -473,6 +519,30 @@ h4 {
   }
 }
 
+.mpoint-charts {
+  display: grid;
+  grid-template-columns: repeat(2, minmax(0, 1fr));
+  gap: 16px;
+}
+
+.mpoint-chart-item {
+  border: 1px solid #f0f0f0;
+  border-radius: 4px;
+  padding: 8px 10px;
+}
+
+.mpoint-chart-title {
+  margin: 0 0 6px;
+  font-size: 13px;
+  color: #303133;
+}
+
+@media (max-width: 1200px) {
+  .mpoint-charts {
+    grid-template-columns: 1fr;
+  }
+}
+
 ::v-deep .el-table__cell {
   padding: 2px 0;
   font-size: 12px;

+ 193 - 108
src/views/health/components/malfunction/dissymmetry.vue

@@ -8,38 +8,54 @@
           tooltip-effect="dark"
           style="width: 100%"
           height="250"
+          stripe
+          border
         >
-          <!-- <el-table-column fixed type="selection" width="55"> </el-table-column> -->
           <el-table-column
-            prop="timeStamp"
-            label="时间"
+            type="index"
+            label="序号"
             align="center"
-            width="150"
+            width="100"
+            :index="bearingTableIndex"
           >
           </el-table-column>
-          <el-table-column
-            prop="samplingFrequency"
-            label="采样频率(Hz)"
-            align="center"
-          >
+          <!-- <el-table-column fixed type="selection" width="55"> </el-table-column> -->
+          <el-table-column prop="detectionPointCn" label="名称" align="left">
           </el-table-column>
-          <el-table-column
-            prop="rotationalSpeed"
-            label="转速(rpm)"
-            align="center"
-          >
+          <!-- <el-table-column prop="timeStamp" label="时间" align="center">
+          </el-table-column> -->
+          <el-table-column label="状态" align="center" width="150">
+            <template slot-scope="scope">
+              <el-tag
+                size="mini"
+                :type="statusTagType(scope.row.status)"
+                effect="dark"
+              >
+                {{ formatStatusText(scope.row.status) }}
+              </el-tag>
+            </template>
           </el-table-column>
+          <!-- <el-table-column prop="" label="操作" align="center">
+            <template slot-scope="scope">
+              <el-button
+                type="text"
+                size="small"
+                @click="handleDetail(scope.row)"
+                >查看振动分析</el-button
+              >
+            </template>
+          </el-table-column> -->
         </el-table>
         <div class="fenye">
-          <p><span>状态码说明:</span>0正常,1报警,2危险,-1未定义</p>
-
+          <p></p>
+          <!-- <p><span>状态码说明:</span>0正常,1报警,2危险</p> -->
           <el-pagination
             small
             @current-change="handleCurrentChange"
             :current-page="currentPage"
             layout="total, prev, pager, next"
             :total="totalCount"
-            :page-size="10"
+            :page-size="pageSize"
           ></el-pagination>
         </div>
       </div>
@@ -81,30 +97,42 @@
         </div>
       </div> -->
     </div>
-    <!-- <div class="bottomBox">
+    <div class="bottomBox">
       <div class="BtLeft">
         <h4>不对中故障状态趋势图</h4>
+        <p style="font-size: 12px; color: #999">
+          <span>状态码说明:</span>0正常,1报警,2危险
+        </p>
         <el-empty
-          v-if="this.xData.length === 0 || this.yData.length === 0"
+          v-if="!trendChartList.length"
           description="暂无数据"
           style="padding: 48px 0"
         ></el-empty>
-        <Eecharts
-          v-else
-          style="height: 310px"
-          :xData="xData"
-          :yData="[yData]"
-          :yNames="['不对中故障']"
-          yAxisName="不对中故障状态"
-        ></Eecharts>
+        <div v-else class="mpoint-charts">
+          <div
+            v-for="(chart, index) in trendChartList"
+            :key="`${chart.title}-${index}`"
+            class="mpoint-chart-item"
+          >
+            <!-- <p class="mpoint-chart-title">{{ chart.title }}</p> -->
+            <Eecharts
+              style="height: 260px"
+              :xData="chart.xData"
+              :yData="chart.yData"
+              :yNames="chart.yNames"
+              yAxisName="故障状态"
+              :jumpContext="chart.jumpContext"
+            ></Eecharts>
+          </div>
+        </div>
       </div>
-    </div> -->
+    </div>
   </div>
 </template>
 
 <script>
 import axios from "axios";
-import Eecharts from "./Eecharts.vue";
+import Eecharts from "./mpointEecharts.vue";
 import bingTwo from "./bingTwo.vue";
 import { cacheEntryPossiblyAdded, log10, promisify } from "plotly.js-dist";
 export default {
@@ -130,6 +158,10 @@ export default {
       type: String,
       default: "",
     },
+    echartsdata: {
+      type: Array,
+      default: () => [],
+    },
   },
   data() {
     return {
@@ -148,6 +180,7 @@ export default {
 
       currentPage: 1,
       total: 1,
+      pageSize: 10,
 
       xData: [],
       yData: [],
@@ -163,6 +196,7 @@ export default {
       dialogVisible: false,
       results: [],
       loading: false, // 控制加载状态
+      trendChartList: [],
     };
   },
   created() {},
@@ -174,9 +208,111 @@ export default {
       immediate: true, // 组件创建时立刻执行一次
       deep: true, // 如果 codedata 是复杂对象,建议加上
     },
+    echartsdata: {
+      handler(newVal) {
+        const list = Array.isArray(newVal) ? newVal : [];
+        this.trendChartList = list
+          .map((item) => {
+            const title = item?.pointNameCn || "未命名测点";
+            const inner = Array.isArray(item?.innerDatas)
+              ? item.innerDatas
+              : [];
+            const xData = inner.map((d) => d?.timeStamp || "");
+            const seriesY = inner.map((d) => {
+              const v = d?.status;
+              return v === null || v === undefined || v === ""
+                ? null
+                : Number(v);
+            });
+            const pointIds = inner.map((d) => d?.id);
+            const pointRows = inner.map((d) => {
+              const pid = d?.id;
+              return (
+                this.tableData.find((r) => String(r?.id) === String(pid)) ||
+                null
+              );
+            });
+            return {
+              title,
+              xData,
+              yData: [seriesY],
+              yNames: [title],
+              jumpContext: {
+                companyCode: this.fieldCode,
+                windCode: this.fieldCode,
+                windTurbineNumber: this.windTurbineNumber,
+                itemKey: item?.itemKey ?? item?.item_key,
+                id: item?.id,
+                mesurePointName: item?.mesurePointName || title,
+                detectionPointCn: item?.pointNameCn || title,
+                rotationalSpeed: item?.rotationalSpeed,
+                samplingFrequency: item?.samplingFrequency,
+                otherData:
+                  item?.otherData && typeof item.otherData === "object"
+                    ? { ...item.otherData }
+                    : undefined,
+                pointIds,
+                pointRows,
+              },
+            };
+          })
+          .filter((it) => it.xData.length);
+      },
+      immediate: true,
+      deep: true,
+    },
   },
 
   methods: {
+    statusTagType(status) {
+      const code = Number(status);
+      if (code === 0) return "success";
+      if (code === 1) return "warning";
+      if (code === 2) return "danger";
+      return "info";
+    },
+    formatStatusText(status) {
+      if (status === null || status === undefined || status === "") return "/";
+      const code = Number(status);
+      if (code === 0) return "正常";
+      if (code === 1) return "报警";
+      if (code === 2) return "危险";
+      if (code === -1) return "未定义";
+      return String(status);
+    },
+    handleDetail(row) {
+      const itemKey = row.itemKey ?? row.item_key;
+      const payload = {
+        /** 与故障诊断页「单位」一致(selecttree 的 companyCode / codeNumber) */
+        companyCode: this.fieldCode,
+        windCode: this.fieldCode,
+        windTurbineNumber: this.windTurbineNumber,
+        itemKey,
+        id: row.id,
+        mesurePointName: row.mesurePointName,
+        detectionPointCn: row.detectionPointCn,
+        timeStamp: row.timeStamp,
+        /** 频谱特征线需要 RPM → Hz;接口字段可能为 rotationalSpeed / otherData.RPM */
+        rotationalSpeed: row.rotationalSpeed,
+        samplingFrequency: row.samplingFrequency,
+        otherData:
+          row.otherData && typeof row.otherData === "object"
+            ? { ...row.otherData }
+            : undefined,
+      };
+      try {
+        sessionStorage.setItem(
+          "healthVibrationDeepLink",
+          JSON.stringify(payload),
+        );
+      } catch (e) {
+        console.error(e);
+      }
+      this.$router.push({ path: "/home/health/vibration" });
+    },
+    bearingTableIndex(index) {
+      return (this.currentPage - 1) * this.pageSize + index + 1;
+    },
     toggleSelection(rows) {
       if (rows) {
         rows.forEach((row) => {
@@ -191,101 +327,26 @@ export default {
     },
     handleCurrentChange(val) {
       console.log(`当前页: ${val}`);
-      setTimeout(() => {
-        this.automaticDiagnosis();
-      }, 500);
+      // setTimeout(() => {
+      //   this.automaticDiagnosis();
+      // }, 500);
 
       this.currentPage = val; // 子组件内部更新当前页
       this.$emit("updatePage", this.currentPage); // 通知父组件,把当前页传出去
     },
-    automaticDiagnosis() {
-      if (this.tableData.length === 0) {
-        this.$message.warning("当前没有数据,无法进行诊断");
-        this.loading = false; // 确保关闭 loading 状态
-        return; // 直接返回,不发请求
-      }
-
-      this.loading = true;
-      const ids = this.tableData
-        .map((item) => item.id)
-        .filter((id) => id !== undefined);
-      const params = {
-        windCode: this.fieldCode,
-        engine_code: this.windTurbineNumber,
-        autodiagType: "Misalignment",
-        ids: ids,
-      };
-      const autodiagType = params.autodiagType || "Misalignment";
-
-      const loadingInstance = this.$loading({
-        lock: true,
-        text: "自动诊断中...",
-        spinner: "el-icon-loading",
-        background: "rgba(0, 0, 0, 0.7)",
-      });
-
-      axios
-        .post(`/AnalysisMulti/autodiag/${autodiagType}`, params)
-        .then((res) => {
-          if (res.data.code === 405) {
-            this.$message.warning({
-              message: "当前采集频率不适合进行诊断分析",
-              duration: 2000,
-            });
-            return; // 停止继续处理数据
-          }
-          this.statistics = res.data.statistics;
-          this.$set(this, "yData", [...res.data.results]);
-          this.$set(
-            this,
-            "xData",
-            this.tableData.map((item) => item.timeStamp),
-          );
-          console.log(this.yData, "yData11");
-          console.log(this.xData, "xData22");
-
-          const maxStatus = res.data.statistics.max_status;
-          if (maxStatus === 0) {
-            this.bearingStateColors.innerRing = "#8ae359";
-          } else if (maxStatus === 1) {
-            this.bearingStateColors.innerRing = "#eecb5f";
-          } else if (maxStatus === 2) {
-            this.bearingStateColors.innerRing = "#f7715f";
-          } else {
-            this.bearingStateColors.innerRing = "#80808057";
-          }
-
-          this.$message.success({
-            message: "诊断完成",
-            duration: 1000, // 1秒后自动关闭
-          });
-        })
-        .catch((error) => {
-          console.error("自动诊断失败:", error);
-          this.bearingStateColors.innerRing = "#80808057";
-          this.$message.error({
-            message: "诊断失败",
-            duration: 1000, // 1秒后自动关闭
-          });
-        })
-        .finally(() => {
-          loadingInstance.close();
-          this.loading = false;
-        });
-    },
 
     reset() {
       this.tableData = [];
       this.xData = [];
       this.yData = [];
       this.statistics = {};
+      this.trendChartList = [];
       this.bearingStateColors = {
         innerRing: "#80808057",
         outerRing: "#80808057",
         rollingElement: "#80808057",
         cage: "#80808057",
       };
-      this.currentPage = 1;
     },
   },
 };
@@ -425,6 +486,30 @@ h4 {
     text-align: right;
   }
 }
+
+.mpoint-charts {
+  display: grid;
+  grid-template-columns: repeat(2, minmax(0, 1fr));
+  gap: 16px;
+}
+
+.mpoint-chart-item {
+  border: 1px solid #f0f0f0;
+  border-radius: 4px;
+  padding: 8px 10px;
+}
+
+.mpoint-chart-title {
+  margin: 0 0 6px;
+  font-size: 13px;
+  color: #303133;
+}
+
+@media (max-width: 1200px) {
+  .mpoint-charts {
+    grid-template-columns: 1fr;
+  }
+}
 ::v-deep .el-table__cell {
   padding: 2px 0;
   font-size: 12px;

+ 202 - 117
src/views/health/components/malfunction/gear.vue

@@ -8,37 +8,54 @@
           tooltip-effect="dark"
           style="width: 100%"
           height="250"
+          stripe
+          border
         >
           <!-- <el-table-column fixed type="selection" width="55"> </el-table-column> -->
           <el-table-column
-            prop="timeStamp"
-            label="时间"
+            type="index"
+            label="序号"
             align="center"
-            width="150"
+            width="100"
+            :index="bearingTableIndex"
           >
           </el-table-column>
-          <el-table-column
-            prop="samplingFrequency"
-            label="采样频率(Hz)"
-            align="center"
-          >
+          <el-table-column prop="detectionPointCn" label="名称" align="left">
           </el-table-column>
-          <el-table-column
-            prop="rotationalSpeed"
-            label="转速(rpm)"
-            align="center"
-          >
+          <!-- <el-table-column prop="timeStamp" label="时间" align="center">
+          </el-table-column> -->
+
+          <el-table-column label="状态" align="center" width="150">
+            <template slot-scope="scope">
+              <el-tag
+                size="mini"
+                type="warning"
+                :type="`${statusTagType(scope.row.status)}`"
+              >
+                {{ formatStatusText(scope.row.status) }}
+              </el-tag>
+            </template>
           </el-table-column>
+          <!-- <el-table-column prop="" label="操作" align="center">
+            <template slot-scope="scope">
+              <el-tag type="warning">标签四</el-tag>
+              <el-button
+                type="text"
+                size="small"
+                @click="handleDetail(scope.row)"
+                >查看振动分析</el-button
+              >
+            </template>
+          </el-table-column> -->
         </el-table>
         <div class="fenye">
-          <p><span>状态码说明:</span>0正常,1报警,2危险,-1未定义</p>
-
+          <p></p>
           <el-pagination
             @current-change="handleCurrentChange"
             :current-page="currentPage"
             layout="total, prev, pager, next"
             :total="totalCount"
-            :page-size="10"
+            :page-size="pageSize"
             small
           ></el-pagination>
         </div>
@@ -83,30 +100,42 @@
         </div>
       </div> -->
     </div>
-    <!-- <div class="bottomBox">
+    <div class="bottomBox">
       <div class="BtLeft">
-        <h4>齿轮状态趋势图</h4>
+        <h4>齿轮状态趋势图:</h4>
+        <p style="font-size: 12px; color: #999">
+          <span>状态码说明:</span>0正常,1报警,2危险
+        </p>
         <el-empty
-          v-if="this.xData.length === 0 || this.yData.length === 0"
+          v-if="!trendChartList.length"
           description="暂无数据"
           style="padding: 46px 0"
         ></el-empty>
-        <Eecharts
-          v-else
-          style="height: 300px"
-          :xData="xData"
-          :yData="yData"
-          :yNames="['齿轮磨损', '齿轮断齿']"
-          yAxisName="齿轮故障状态"
-        ></Eecharts>
+        <div v-else class="mpoint-charts">
+          <div
+            v-for="(chart, index) in trendChartList"
+            :key="`${chart.title}-${index}`"
+            class="mpoint-chart-item"
+          >
+            <!-- <p class="mpoint-chart-title">{{ chart.title }}</p> -->
+            <Eecharts
+              style="height: 260px"
+              :xData="chart.xData"
+              :yData="chart.yData"
+              :yNames="chart.yNames"
+              yAxisName="故障状态"
+              :jumpContext="chart.jumpContext"
+            ></Eecharts>
+          </div>
+        </div>
       </div>
-    </div> -->
+    </div>
   </div>
 </template>
 
 <script>
 import axios from "axios";
-import Eecharts from "./Eecharts.vue";
+import Eecharts from "./mpointEecharts.vue";
 import bingTwo from "./bingTwo.vue";
 import { cacheEntryPossiblyAdded } from "plotly.js-dist";
 export default {
@@ -132,6 +161,10 @@ export default {
       type: String,
       default: "",
     },
+    echartsdata: {
+      type: Array,
+      default: () => [],
+    },
   },
   data() {
     return {
@@ -147,10 +180,9 @@ export default {
       envelopeTotalAlarmThreshold: "",
       envelopeTotalDangerThreshold: "",
       // 分页
-
       currentPage: 1,
       total: 1,
-
+      pageSize: 10,
       xData: [],
       yData: [],
       // 颜色判断
@@ -165,6 +197,7 @@ export default {
       dialogVisible: false,
       loading: false, // 控制加载状态
       statistics: {},
+      trendChartList: [],
     };
   },
   created() {},
@@ -176,9 +209,111 @@ export default {
       immediate: true, // 组件创建时立刻执行一次
       deep: true, // 如果 codedata 是复杂对象,建议加上
     },
+    echartsdata: {
+      handler(newVal) {
+        const list = Array.isArray(newVal) ? newVal : [];
+        this.trendChartList = list
+          .map((item) => {
+            const title = item?.pointNameCn || "未命名测点";
+            const inner = Array.isArray(item?.innerDatas)
+              ? item.innerDatas
+              : [];
+            const xData = inner.map((d) => d?.timeStamp || "");
+            const seriesY = inner.map((d) => {
+              const v = d?.status;
+              return v === null || v === undefined || v === ""
+                ? null
+                : Number(v);
+            });
+            const pointIds = inner.map((d) => d?.id);
+            const pointRows = inner.map((d) => {
+              const pid = d?.id;
+              return (
+                this.tableData.find((r) => String(r?.id) === String(pid)) ||
+                null
+              );
+            });
+            return {
+              title,
+              xData,
+              yData: [seriesY],
+              yNames: [title],
+              jumpContext: {
+                companyCode: this.fieldCode,
+                windCode: this.fieldCode,
+                windTurbineNumber: this.windTurbineNumber,
+                itemKey: item?.itemKey ?? item?.item_key,
+                id: item?.id,
+                mesurePointName: item?.mesurePointName || title,
+                detectionPointCn: item?.pointNameCn || title,
+                rotationalSpeed: item?.rotationalSpeed,
+                samplingFrequency: item?.samplingFrequency,
+                otherData:
+                  item?.otherData && typeof item.otherData === "object"
+                    ? { ...item.otherData }
+                    : undefined,
+                pointIds,
+                pointRows,
+              },
+            };
+          })
+          .filter((it) => it.xData.length);
+      },
+      immediate: true,
+      deep: true,
+    },
   },
 
   methods: {
+    statusTagType(status) {
+      const code = Number(status);
+      if (code === 0) return "success";
+      if (code === 1) return "warning";
+      if (code === 2) return "danger";
+      return "info";
+    },
+    formatStatusText(status) {
+      if (status === null || status === undefined || status === "") return "/";
+      const code = Number(status);
+      if (code === 0) return "正常";
+      if (code === 1) return "报警";
+      if (code === 2) return "危险";
+      if (code === -1) return "未定义";
+      return String(status);
+    },
+    handleDetail(row) {
+      const itemKey = row.itemKey ?? row.item_key;
+      const payload = {
+        /** 与故障诊断页「单位」一致(selecttree 的 companyCode / codeNumber) */
+        companyCode: this.fieldCode,
+        windCode: this.fieldCode,
+        windTurbineNumber: this.windTurbineNumber,
+        itemKey,
+        id: row.id,
+        mesurePointName: row.mesurePointName,
+        detectionPointCn: row.detectionPointCn,
+        timeStamp: row.timeStamp,
+        /** 频谱特征线需要 RPM → Hz;接口字段可能为 rotationalSpeed / otherData.RPM */
+        rotationalSpeed: row.rotationalSpeed,
+        samplingFrequency: row.samplingFrequency,
+        otherData:
+          row.otherData && typeof row.otherData === "object"
+            ? { ...row.otherData }
+            : undefined,
+      };
+      try {
+        sessionStorage.setItem(
+          "healthVibrationDeepLink",
+          JSON.stringify(payload),
+        );
+      } catch (e) {
+        console.error(e);
+      }
+      this.$router.push({ path: "/home/health/vibration" });
+    },
+    bearingTableIndex(index) {
+      return (this.currentPage - 1) * this.pageSize + index + 1;
+    },
     toggleSelection(rows) {
       if (rows) {
         rows.forEach((row) => {
@@ -193,95 +328,21 @@ export default {
     },
     handleCurrentChange(val) {
       console.log(`当前页: ${val}`);
-      setTimeout(() => {
-        this.automaticDiagnosis();
-      }, 500);
+      // setTimeout(() => {
+      //   this.automaticDiagnosis();
+      // }, 500);
       this.currentPage = val; // 子组件内部更新当前页
       this.$emit("updatePage", this.currentPage); // 通知父组件,把当前页传出去
     },
 
-    automaticDiagnosis() {
-      if (this.tableData.length === 0) {
-        this.$message.warning("当前没有数据,无法进行诊断");
-        this.loading = false; // 确保关闭 loading 状态
-        return; // 直接返回,不发请求
-      }
-      this.loading = true;
-      const ids = this.tableData
-        .map((item) => item.id)
-        .filter((id) => id !== undefined);
-
-      const params = {
-        windCode: this.fieldCode,
-        engine_code: this.windTurbineNumber,
-        autodiagType: "Gear",
-        ids,
-      };
-
-      const autodiagType = params.autodiagType || "Gear";
-
-      const loadingInstance = this.$loading({
-        lock: true,
-        text: "自动诊断中...",
-        spinner: "el-icon-loading",
-        background: "rgba(0, 0, 0, 0.7)",
-      });
-
-      axios
-        .post(`/AnalysisMulti/autodiag/${autodiagType}`, params)
-        .then((res) => {
-          if (res.data.code === 400) {
-            this.$message({
-              message: "当前测点无法进行齿轮箱诊断",
-              type: "warning",
-            });
-          } else if (res.data.code === 405) {
-            this.$message({
-              message: "当前采集频率不适合进行诊断分析",
-              type: "warning",
-            });
-          } else {
-            const result = res.data.results || {};
-            this.statistics = res.data.statistics || {};
-            // x轴数据,时间戳
-            this.xData = this.tableData.map((item) => item.timeStamp);
-            // y轴数据,状态码二维数组
-            this.yData = [
-              result.crack?.status_codes || [],
-              result.wear?.status_codes || [],
-            ];
-            // 设置颜色函数(根据 max_status)
-            const setColor = (status) => {
-              switch (status) {
-                case 0:
-                  return "#8ae359"; // 正常
-                case 1:
-                  return "#eecb5f"; // 报警
-                case 2:
-                  return "#f7715f"; // 危险
-                default:
-                  return "#80808057"; // 未知或缺失
-              }
-            };
-
-            // 根据 max_status 设置颜色
-            this.bearingStateColors.innerRing = setColor(
-              result.crack?.max_status ?? -1,
-            );
-            this.bearingStateColors.outerRing = setColor(
-              result.wear?.max_status ?? -1,
-            );
-          }
-        })
-
-        .catch((err) => {
-          console.error("诊断失败:", err);
-        })
-        .finally(() => {
-          loadingInstance.close();
-          this.loading = false;
-        });
-    },
+    // automaticDiagnosis() {
+    //   if (this.tableData.length === 0) {
+    //     this.$message.warning("当前没有数据,无法进行诊断");
+    //     this.loading = false; // 确保关闭 loading 状态
+    //     return; // 直接返回,不发请求
+    //   }
+    //   this.loading = true;
+    // },
 
     reset() {
       this.tableData = [];
@@ -289,7 +350,7 @@ export default {
       this.yData = [];
       this.statistics = {};
       this.result = {};
-      this.currentPage = 1;
+      this.trendChartList = [];
       this.bearingStateColors = {
         innerRing: "#80808057",
         outerRing: "#80808057",
@@ -467,6 +528,30 @@ h4 {
   }
 }
 
+.mpoint-charts {
+  display: grid;
+  grid-template-columns: repeat(2, minmax(0, 1fr));
+  gap: 16px;
+}
+
+.mpoint-chart-item {
+  border: 1px solid #f0f0f0;
+  border-radius: 4px;
+  padding: 8px 10px;
+}
+
+.mpoint-chart-title {
+  margin: 0 0 6px;
+  font-size: 13px;
+  color: #303133;
+}
+
+@media (max-width: 1200px) {
+  .mpoint-charts {
+    grid-template-columns: 1fr;
+  }
+}
+
 ::v-deep .el-table__cell {
   padding: 2px 0;
   font-size: 12px;

+ 191 - 105
src/views/health/components/malfunction/loose.vue

@@ -8,37 +8,55 @@
           tooltip-effect="dark"
           style="width: 100%"
           height="250"
+          stripe
+          border
         >
-          <!-- <el-table-column fixed type="selection" width="55"> </el-table-column> -->
           <el-table-column
-            prop="timeStamp"
-            label="时间"
+            type="index"
+            label="序号"
             align="center"
-            width="150"
+            width="100"
+            :index="bearingTableIndex"
           >
           </el-table-column>
-          <el-table-column
-            prop="samplingFrequency"
-            label="采样频率(Hz)"
-            align="center"
-          >
+          <!-- <el-table-column fixed type="selection" width="55"> </el-table-column> -->
+          <el-table-column prop="detectionPointCn" label="名称" align="left">
           </el-table-column>
-          <el-table-column
-            prop="rotationalSpeed"
-            label="转速(rpm)"
-            align="center"
-          >
+          <!-- <el-table-column prop="timeStamp" label="时间" align="center">
+          </el-table-column> -->
+
+          <el-table-column label="状态" align="center" width="150">
+            <template slot-scope="scope">
+              <el-tag
+                size="mini"
+                :type="statusTagType(scope.row.status)"
+                effect="dark"
+              >
+                {{ formatStatusText(scope.row.status) }}
+              </el-tag>
+            </template>
           </el-table-column>
+          <!-- <el-table-column prop="" label="操作" align="center">
+            <template slot-scope="scope">
+              <el-button
+                type="text"
+                size="small"
+                @click="handleDetail(scope.row)"
+                >查看振动分析</el-button
+              >
+            </template>
+          </el-table-column> -->
         </el-table>
         <div class="fenye">
-          <p><span>状态码说明:</span>0正常,1报警,2危险,-1未定义</p>
+          <p></p>
+          <!-- <p><span>状态码说明:</span>0正常,1报警,2危险</p> -->
           <el-pagination
             small
             @current-change="handleCurrentChange"
             :current-page="currentPage"
             layout="total, prev, pager, next"
             :total="totalCount"
-            :page-size="10"
+            :page-size="pageSize"
           ></el-pagination>
         </div>
       </div>
@@ -80,30 +98,42 @@
         </div>
       </div> -->
     </div>
-    <!-- <div class="bottomBox">
+    <div class="bottomBox">
       <div class="BtLeft">
         <h4>松动故障状态趋势图</h4>
+        <p style="font-size: 12px; color: #999">
+          <span>状态码说明:</span>0正常,1报警,2危险
+        </p>
         <el-empty
-          v-if="this.xData.length === 0 || this.yData.length === 0"
+          v-if="!trendChartList.length"
           description="暂无数据"
           style="padding: 48px 0"
         ></el-empty>
-        <Eecharts
-          v-else
-          style="height: 310px"
-          :xData="xData"
-          :yData="[yData]"
-          :yNames="['松动故障']"
-          yAxisName="松动故障状态"
-        ></Eecharts>
+        <div v-else class="mpoint-charts">
+          <div
+            v-for="(chart, index) in trendChartList"
+            :key="`${chart.title}-${index}`"
+            class="mpoint-chart-item"
+          >
+            <!-- <p class="mpoint-chart-title">{{ chart.title }}</p> -->
+            <Eecharts
+              style="height: 260px"
+              :xData="chart.xData"
+              :yData="chart.yData"
+              :yNames="chart.yNames"
+              yAxisName="故障状态"
+              :jumpContext="chart.jumpContext"
+            ></Eecharts>
+          </div>
+        </div>
       </div>
-    </div> -->
+    </div>
   </div>
 </template>
 
 <script>
 import axios from "axios";
-import Eecharts from "./Eecharts.vue";
+import Eecharts from "./mpointEecharts.vue";
 import bingTwo from "./bingTwo.vue";
 import { cacheEntryPossiblyAdded, log10, promisify } from "plotly.js-dist";
 export default {
@@ -129,6 +159,10 @@ export default {
       type: String,
       default: "",
     },
+    echartsdata: {
+      type: Array,
+      default: () => [],
+    },
   },
   data() {
     return {
@@ -147,6 +181,7 @@ export default {
 
       currentPage: 1,
       total: 1,
+      pageSize: 10,
 
       xData: [],
       yData: [],
@@ -162,6 +197,7 @@ export default {
       dialogVisible: false,
       results: [],
       loading: false, // 控制加载状态
+      trendChartList: [],
     };
   },
   created() {},
@@ -173,9 +209,111 @@ export default {
       immediate: true, // 组件创建时立刻执行一次
       deep: true, // 如果 codedata 是复杂对象,建议加上
     },
+    echartsdata: {
+      handler(newVal) {
+        const list = Array.isArray(newVal) ? newVal : [];
+        this.trendChartList = list
+          .map((item) => {
+            const title = item?.pointNameCn || "未命名测点";
+            const inner = Array.isArray(item?.innerDatas)
+              ? item.innerDatas
+              : [];
+            const xData = inner.map((d) => d?.timeStamp || "");
+            const seriesY = inner.map((d) => {
+              const v = d?.status;
+              return v === null || v === undefined || v === ""
+                ? null
+                : Number(v);
+            });
+            const pointIds = inner.map((d) => d?.id);
+            const pointRows = inner.map((d) => {
+              const pid = d?.id;
+              return (
+                this.tableData.find((r) => String(r?.id) === String(pid)) ||
+                null
+              );
+            });
+            return {
+              title,
+              xData,
+              yData: [seriesY],
+              yNames: [title],
+              jumpContext: {
+                companyCode: this.fieldCode,
+                windCode: this.fieldCode,
+                windTurbineNumber: this.windTurbineNumber,
+                itemKey: item?.itemKey ?? item?.item_key,
+                id: item?.id,
+                mesurePointName: item?.mesurePointName || title,
+                detectionPointCn: item?.pointNameCn || title,
+                rotationalSpeed: item?.rotationalSpeed,
+                samplingFrequency: item?.samplingFrequency,
+                otherData:
+                  item?.otherData && typeof item.otherData === "object"
+                    ? { ...item.otherData }
+                    : undefined,
+                pointIds,
+                pointRows,
+              },
+            };
+          })
+          .filter((it) => it.xData.length);
+      },
+      immediate: true,
+      deep: true,
+    },
   },
 
   methods: {
+    statusTagType(status) {
+      const code = Number(status);
+      if (code === 0) return "success";
+      if (code === 1) return "warning";
+      if (code === 2) return "danger";
+      return "info";
+    },
+    formatStatusText(status) {
+      if (status === null || status === undefined || status === "") return "/";
+      const code = Number(status);
+      if (code === 0) return "正常";
+      if (code === 1) return "报警";
+      if (code === 2) return "危险";
+      if (code === -1) return "未定义";
+      return String(status);
+    },
+    handleDetail(row) {
+      const itemKey = row.itemKey ?? row.item_key;
+      const payload = {
+        /** 与故障诊断页「单位」一致(selecttree 的 companyCode / codeNumber) */
+        companyCode: this.fieldCode,
+        windCode: this.fieldCode,
+        windTurbineNumber: this.windTurbineNumber,
+        itemKey,
+        id: row.id,
+        mesurePointName: row.mesurePointName,
+        detectionPointCn: row.detectionPointCn,
+        timeStamp: row.timeStamp,
+        /** 频谱特征线需要 RPM → Hz;接口字段可能为 rotationalSpeed / otherData.RPM */
+        rotationalSpeed: row.rotationalSpeed,
+        samplingFrequency: row.samplingFrequency,
+        otherData:
+          row.otherData && typeof row.otherData === "object"
+            ? { ...row.otherData }
+            : undefined,
+      };
+      try {
+        sessionStorage.setItem(
+          "healthVibrationDeepLink",
+          JSON.stringify(payload),
+        );
+      } catch (e) {
+        console.error(e);
+      }
+      this.$router.push({ path: "/home/health/vibration" });
+    },
+    bearingTableIndex(index) {
+      return (this.currentPage - 1) * this.pageSize + index + 1;
+    },
     toggleSelection(rows) {
       if (rows) {
         rows.forEach((row) => {
@@ -190,93 +328,17 @@ export default {
     },
     handleCurrentChange(val) {
       console.log(`当前页: ${val}`);
-      setTimeout(() => {
-        this.automaticDiagnosis();
-      }, 500);
 
       this.currentPage = val; // 子组件内部更新当前页
       this.$emit("updatePage", this.currentPage); // 通知父组件,把当前页传出去
     },
-    automaticDiagnosis() {
-      if (this.tableData.length === 0) {
-        this.$message.warning("当前没有数据,无法进行诊断");
-        this.loading = false; // 确保关闭 loading 状态
-        return; // 直接返回,不发请求
-      }
-      this.loading = true;
-      const ids = this.tableData
-        .map((item) => item.id)
-        .filter((id) => id !== undefined);
-      const params = {
-        windCode: this.fieldCode,
-        engine_code: this.windTurbineNumber,
-        autodiagType: "Looseness",
-        ids: ids,
-      };
-      const autodiagType = params.autodiagType || "Looseness";
-
-      const loadingInstance = this.$loading({
-        lock: true,
-        text: "自动诊断中...",
-        spinner: "el-icon-loading",
-        background: "rgba(0, 0, 0, 0.7)",
-      });
-
-      axios
-        .post(`/AnalysisMulti/autodiag/${autodiagType}`, params)
-        .then((res) => {
-          if (res.data.code === 405) {
-            this.$message.warning(
-              res.data.message || "当前采集频率不适合进行诊断分析",
-            );
-            return; // 终止后续数据处理逻辑
-          }
-
-          this.statistics = res.data.statistics;
-          this.$set(this, "yData", [...res.data.results]);
-          this.$set(
-            this,
-            "xData",
-            this.tableData.map((item) => item.timeStamp),
-          );
-          console.log(this.yData, "yData11");
-          console.log(this.xData, "xData22");
-
-          const maxStatus = res.data.statistics.max_status;
-          if (maxStatus === 0) {
-            this.bearingStateColors.innerRing = "#8ae359";
-          } else if (maxStatus === 1) {
-            this.bearingStateColors.innerRing = "#eecb5f";
-          } else if (maxStatus === 2) {
-            this.bearingStateColors.innerRing = "#f7715f";
-          } else {
-            this.bearingStateColors.innerRing = "#80808057";
-          }
-
-          this.$message.success({
-            message: "诊断完成",
-            duration: 1000, // 1秒后自动关闭
-          });
-        })
-        .catch((error) => {
-          console.error("自动诊断失败:", error);
-          this.bearingStateColors.innerRing = "#80808057";
-          this.$message.error({
-            message: "诊断失败",
-            duration: 1000, // 1秒后自动关闭
-          });
-        })
-        .finally(() => {
-          loadingInstance.close();
-          this.loading = false;
-        });
-    },
 
     reset() {
       // 重置状态
       this.xData = [];
       this.yData = [];
       this.statistics = {};
+      this.trendChartList = [];
       this.bearingStateColors = {
         innerRing: "#80808057",
         outerRing: "#80808057",
@@ -423,6 +485,30 @@ h4 {
     text-align: right;
   }
 }
+
+.mpoint-charts {
+  display: grid;
+  grid-template-columns: repeat(2, minmax(0, 1fr));
+  gap: 16px;
+}
+
+.mpoint-chart-item {
+  border: 1px solid #f0f0f0;
+  border-radius: 4px;
+  padding: 8px 10px;
+}
+
+.mpoint-chart-title {
+  margin: 0 0 6px;
+  font-size: 13px;
+  color: #303133;
+}
+
+@media (max-width: 1200px) {
+  .mpoint-charts {
+    grid-template-columns: 1fr;
+  }
+}
 ::v-deep .el-table__cell {
   padding: 2px 0;
   font-size: 12px;

+ 190 - 106
src/views/health/components/malfunction/misalignment.vue

@@ -8,38 +8,54 @@
           tooltip-effect="dark"
           style="width: 100%"
           height="250"
+          stripe
+          border
         >
-          <!-- <el-table-column fixed type="selection" width="55"> </el-table-column> -->
           <el-table-column
-            prop="timeStamp"
-            label="时间"
+            type="index"
+            label="序号"
             align="center"
-            width="150"
+            width="100"
+            :index="bearingTableIndex"
           >
           </el-table-column>
-          <el-table-column
-            prop="samplingFrequency"
-            label="采样频率(Hz)"
-            align="center"
-          >
+          <!-- <el-table-column fixed type="selection" width="55"> </el-table-column> -->
+          <el-table-column prop="detectionPointCn" label="名称" align="left">
           </el-table-column>
-          <el-table-column
-            prop="rotationalSpeed"
-            label="转速(rpm)"
-            align="center"
-          >
+          <!-- <el-table-column prop="timeStamp" label="时间" align="center">
+          </el-table-column> -->
+          <el-table-column label="状态" align="center" width="150">
+            <template slot-scope="scope">
+              <el-tag
+                size="mini"
+                :type="statusTagType(scope.row.status)"
+                effect="dark"
+              >
+                {{ formatStatusText(scope.row.status) }}
+              </el-tag>
+            </template>
           </el-table-column>
+          <!-- <el-table-column prop="" label="操作" align="center">
+            <template slot-scope="scope">
+              <el-button
+                type="text"
+                size="small"
+                @click="handleDetail(scope.row)"
+                >查看振动分析</el-button
+              >
+            </template>
+          </el-table-column> -->
         </el-table>
         <div class="fenye">
-          <p><span>状态码说明:</span>0正常,1报警,2危险,-1未定义</p>
-
+          <!-- <p><span>状态码说明:</span>0正常,1报警,2危险</p> -->
+          <p></p>
           <el-pagination
             small
             @current-change="handleCurrentChange"
             :current-page="currentPage"
             layout="total, prev, pager, next"
             :total="totalCount"
-            :page-size="10"
+            :page-size="pageSize"
           ></el-pagination>
         </div>
       </div>
@@ -81,30 +97,42 @@
         </div>
       </div> -->
     </div>
-    <!-- <div class="bottomBox">
+    <div class="bottomBox">
       <div class="BtLeft">
         <h4>不平衡故障状态趋势图</h4>
+        <p style="font-size: 12px; color: #999">
+          <span>状态码说明:</span>0正常,1报警,2危险
+        </p>
         <el-empty
-          v-if="this.xData.length === 0 || this.yData.length === 0"
+          v-if="!trendChartList.length"
           description="暂无数据"
           style="padding: 48px 0"
         ></el-empty>
-        <Eecharts
-          v-else
-          style="height: 310px"
-          :xData="xData"
-          :yData="[yData]"
-          :yNames="['不平衡故障']"
-          yAxisName="不平衡故障状态"
-        ></Eecharts>
+        <div v-else class="mpoint-charts">
+          <div
+            v-for="(chart, index) in trendChartList"
+            :key="`${chart.title}-${index}`"
+            class="mpoint-chart-item"
+          >
+            <!-- <p class="mpoint-chart-title">{{ chart.title }}</p> -->
+            <Eecharts
+              style="height: 260px"
+              :xData="chart.xData"
+              :yData="chart.yData"
+              :yNames="chart.yNames"
+              yAxisName="故障状态"
+              :jumpContext="chart.jumpContext"
+            ></Eecharts>
+          </div>
+        </div>
       </div>
-    </div> -->
+    </div>
   </div>
 </template>
 
 <script>
 import axios from "axios";
-import Eecharts from "./Eecharts.vue";
+import Eecharts from "./mpointEecharts.vue";
 import bingTwo from "./bingTwo.vue";
 import { cacheEntryPossiblyAdded, log10, promisify } from "plotly.js-dist";
 export default {
@@ -130,6 +158,10 @@ export default {
       type: String,
       default: "",
     },
+    echartsdata: {
+      type: Array,
+      default: () => [],
+    },
   },
   data() {
     return {
@@ -148,6 +180,7 @@ export default {
 
       currentPage: 1,
       total: 1,
+      pageSize: 10,
 
       xData: [],
       yData: [],
@@ -163,6 +196,7 @@ export default {
       dialogVisible: false,
       results: [],
       loading: false, // 控制加载状态
+      trendChartList: [],
     };
   },
   created() {},
@@ -174,9 +208,111 @@ export default {
       immediate: true, // 组件创建时立刻执行一次
       deep: true, // 如果 codedata 是复杂对象,建议加上
     },
+    echartsdata: {
+      handler(newVal) {
+        const list = Array.isArray(newVal) ? newVal : [];
+        this.trendChartList = list
+          .map((item) => {
+            const title = item?.pointNameCn || "未命名测点";
+            const inner = Array.isArray(item?.innerDatas)
+              ? item.innerDatas
+              : [];
+            const xData = inner.map((d) => d?.timeStamp || "");
+            const seriesY = inner.map((d) => {
+              const v = d?.status;
+              return v === null || v === undefined || v === ""
+                ? null
+                : Number(v);
+            });
+            const pointIds = inner.map((d) => d?.id);
+            const pointRows = inner.map((d) => {
+              const pid = d?.id;
+              return (
+                this.tableData.find((r) => String(r?.id) === String(pid)) ||
+                null
+              );
+            });
+            return {
+              title,
+              xData,
+              yData: [seriesY],
+              yNames: [title],
+              jumpContext: {
+                companyCode: this.fieldCode,
+                windCode: this.fieldCode,
+                windTurbineNumber: this.windTurbineNumber,
+                itemKey: item?.itemKey ?? item?.item_key,
+                id: item?.id,
+                mesurePointName: item?.mesurePointName || title,
+                detectionPointCn: item?.pointNameCn || title,
+                rotationalSpeed: item?.rotationalSpeed,
+                samplingFrequency: item?.samplingFrequency,
+                otherData:
+                  item?.otherData && typeof item.otherData === "object"
+                    ? { ...item.otherData }
+                    : undefined,
+                pointIds,
+                pointRows,
+              },
+            };
+          })
+          .filter((it) => it.xData.length);
+      },
+      immediate: true,
+      deep: true,
+    },
   },
 
   methods: {
+    statusTagType(status) {
+      const code = Number(status);
+      if (code === 0) return "success";
+      if (code === 1) return "warning";
+      if (code === 2) return "danger";
+      return "info";
+    },
+    formatStatusText(status) {
+      if (status === null || status === undefined || status === "") return "/";
+      const code = Number(status);
+      if (code === 0) return "正常";
+      if (code === 1) return "报警";
+      if (code === 2) return "危险";
+      if (code === -1) return "未定义";
+      return String(status);
+    },
+    handleDetail(row) {
+      const itemKey = row.itemKey ?? row.item_key;
+      const payload = {
+        /** 与故障诊断页「单位」一致(selecttree 的 companyCode / codeNumber) */
+        companyCode: this.fieldCode,
+        windCode: this.fieldCode,
+        windTurbineNumber: this.windTurbineNumber,
+        itemKey,
+        id: row.id,
+        mesurePointName: row.mesurePointName,
+        detectionPointCn: row.detectionPointCn,
+        timeStamp: row.timeStamp,
+        /** 频谱特征线需要 RPM → Hz;接口字段可能为 rotationalSpeed / otherData.RPM */
+        rotationalSpeed: row.rotationalSpeed,
+        samplingFrequency: row.samplingFrequency,
+        otherData:
+          row.otherData && typeof row.otherData === "object"
+            ? { ...row.otherData }
+            : undefined,
+      };
+      try {
+        sessionStorage.setItem(
+          "healthVibrationDeepLink",
+          JSON.stringify(payload),
+        );
+      } catch (e) {
+        console.error(e);
+      }
+      this.$router.push({ path: "/home/health/vibration" });
+    },
+    bearingTableIndex(index) {
+      return (this.currentPage - 1) * this.pageSize + index + 1;
+    },
     toggleSelection(rows) {
       if (rows) {
         rows.forEach((row) => {
@@ -191,93 +327,17 @@ export default {
     },
     handleCurrentChange(val) {
       console.log(`当前页: ${val}`);
-      setTimeout(() => {
-        this.automaticDiagnosis();
-      }, 500);
 
       this.currentPage = val; // 子组件内部更新当前页
       this.$emit("updatePage", this.currentPage); // 通知父组件,把当前页传出去
     },
-    automaticDiagnosis() {
-      if (this.tableData.length === 0) {
-        this.$message.warning("当前没有数据,无法进行诊断");
-        this.loading = false; // 确保关闭 loading 状态
-        return; // 直接返回,不发请求
-      }
-      this.loading = true;
-      const ids = this.tableData
-        .map((item) => item.id)
-        .filter((id) => id !== undefined);
-      const params = {
-        windCode: this.fieldCode,
-        engine_code: this.windTurbineNumber,
-        autodiagType: "Unbalance",
-        ids: ids,
-      };
-      const autodiagType = params.autodiagType || "Unbalance";
-
-      const loadingInstance = this.$loading({
-        lock: true,
-        text: "自动诊断中...",
-        spinner: "el-icon-loading",
-        background: "rgba(0, 0, 0, 0.7)",
-      });
-
-      axios
-        .post(`/AnalysisMulti/autodiag/${autodiagType}`, params)
-        .then((res) => {
-          if (res.data.code === 405) {
-            this.$message.warning({
-              message: "当前采集频率不适合进行诊断分析",
-              duration: 2000,
-            });
-            return; // 停止继续处理数据
-          }
-
-          this.statistics = res.data.statistics;
-          this.$set(this, "yData", [...res.data.results]);
-          this.$set(
-            this,
-            "xData",
-            this.tableData.map((item) => item.timeStamp),
-          );
-          console.log(this.yData, "yData11");
-          console.log(this.xData, "xData22");
-
-          const maxStatus = res.data.statistics.max_status;
-          if (maxStatus === 0) {
-            this.bearingStateColors.innerRing = "#8ae359";
-          } else if (maxStatus === 1) {
-            this.bearingStateColors.innerRing = "#eecb5f";
-          } else if (maxStatus === 2) {
-            this.bearingStateColors.innerRing = "#f7715f";
-          } else {
-            this.bearingStateColors.innerRing = "#80808057";
-          }
 
-          this.$message.success({
-            message: "诊断完成",
-            duration: 1000, // 1秒后自动关闭
-          });
-        })
-        .catch((error) => {
-          console.error("自动诊断失败:", error);
-          this.bearingStateColors.innerRing = "#80808057";
-          this.$message.error({
-            message: "诊断失败",
-            duration: 1000, // 1秒后自动关闭
-          });
-        })
-        .finally(() => {
-          loadingInstance.close();
-          this.loading = false;
-        });
-    },
     reset() {
       // 重置状态
       this.xData = [];
       this.yData = [];
       this.statistics = {};
+      this.trendChartList = [];
       this.bearingStateColors = {
         innerRing: "#80808057",
         outerRing: "#80808057",
@@ -424,6 +484,30 @@ h4 {
     text-align: right;
   }
 }
+
+.mpoint-charts {
+  display: grid;
+  grid-template-columns: repeat(2, minmax(0, 1fr));
+  gap: 16px;
+}
+
+.mpoint-chart-item {
+  border: 1px solid #f0f0f0;
+  border-radius: 4px;
+  padding: 8px 10px;
+}
+
+.mpoint-chart-title {
+  margin: 0 0 6px;
+  font-size: 13px;
+  color: #303133;
+}
+
+@media (max-width: 1200px) {
+  .mpoint-charts {
+    grid-template-columns: 1fr;
+  }
+}
 ::v-deep .el-table__cell {
   padding: 2px 0;
   font-size: 12px;

+ 217 - 0
src/views/health/components/malfunction/mpointEecharts.vue

@@ -0,0 +1,217 @@
+<!--
+ * @Author: your name
+ * @Date: 2026-04-09 16:13:52
+ * @LastEditTime: 2026-04-10 16:56:18
+ * @LastEditors: MacBookPro
+ * @Description: In User Settings Edit
+ * @FilePath: /performance-test/src/views/health/components/malfunction/cdEecharts.vue
+-->
+<template>
+  <div ref="chartDom" style="width: 100%; height: 100%"></div>
+</template>
+
+<script>
+import * as echarts from "echarts";
+
+export default {
+  name: "LineChart",
+  props: {
+    xData: {
+      type: Array,
+      required: true,
+    },
+    yData: {
+      type: Array, // 二维数组
+      required: true,
+    },
+    yNames: {
+      type: Array,
+      required: true, // 只取第一个元素
+    },
+    yAxisName: {
+      type: String,
+      default: "",
+    },
+    /** 点击坐标点跳转振动分析所需上下文(与 bearing handleDetail 一致) */
+    jumpContext: {
+      type: Object,
+      default: () => ({}),
+    },
+    enablePointJump: {
+      type: Boolean,
+      default: true,
+    },
+  },
+  data() {
+    return {
+      myChart: null,
+    };
+  },
+  mounted() {
+    this.initChart();
+  },
+  beforeDestroy() {
+    if (this.myChart) {
+      this.myChart.off("click", this.handlePointClick);
+    }
+  },
+  watch: {
+    xData: {
+      handler: "initChart",
+      deep: true,
+      immediate: true,
+    },
+    yData: {
+      handler: "initChart",
+      deep: true,
+      immediate: true,
+    },
+    yNames: {
+      handler: "initChart",
+      immediate: true,
+    },
+    yAxisName: {
+      handler: "initChart",
+      immediate: true,
+    },
+  },
+
+  methods: {
+    initChart() {
+      if (!this.$refs.chartDom) return;
+
+      if (!this.myChart) {
+        this.myChart = echarts.init(this.$refs.chartDom);
+        this.myChart.on("click", this.handlePointClick);
+      }
+
+      const colors = ["#02aae9", "#5470C6", "#3CB9B9", "#9966CC"]; // 颜色列表可以根据需要扩展
+
+      const series = this.yData.map((data, index) => ({
+        name: this.yNames[index] || `系列${index + 1}`,
+        type: "line",
+        data: data,
+        lineStyle: {
+          color: colors[index % colors.length],
+        },
+        itemStyle: {
+          color: colors[index % colors.length],
+        },
+      }));
+
+      const option = {
+        tooltip: {
+          trigger: "axis",
+        },
+        legend: {
+          data: this.yNames.slice(0, series.length),
+          top: 10,
+        },
+        grid: {
+          top: 40,
+          bottom: 0,
+          left: 40,
+          right: 20,
+          containLabel: true,
+        },
+        xAxis: {
+          type: "category",
+          data: this.xData,
+        },
+        yAxis: {
+          type: "value",
+          min: 0,
+          max: 2,
+          interval: 1,
+          name: this.yAxisName,
+          axisLabel: {
+            formatter: (value) => `${Math.round(value)}`,
+          },
+        },
+        series: series,
+        color: colors,
+      };
+
+      this.myChart.clear(); // ✅ 清除旧配置
+      this.myChart.setOption(option);
+    },
+    handlePointClick(params) {
+      if (!this.enablePointJump) return;
+      if (!params) return;
+
+      const pointName =
+        params?.seriesName ||
+        this.jumpContext?.detectionPointCn ||
+        this.jumpContext?.mesurePointName ||
+        "";
+      const pointTime =
+        params?.name ||
+        (Array.isArray(params?.value) ? params.value[0] : "") ||
+        "";
+      const idx =
+        params?.dataIndex !== undefined && params?.dataIndex !== null
+          ? Number(params.dataIndex)
+          : -1;
+      const pointIds = Array.isArray(this.jumpContext?.pointIds)
+        ? this.jumpContext.pointIds
+        : [];
+      const pointId =
+        idx >= 0 && idx < pointIds.length ? pointIds[idx] : undefined;
+      const nextItemKey =
+        pointId !== undefined && pointId !== null && pointId !== ""
+          ? pointId
+          : this.jumpContext?.itemKey;
+
+      const idForJump =
+        pointId !== undefined && pointId !== null && pointId !== ""
+          ? pointId
+          : this.jumpContext?.id;
+      const windCodeForJump =
+        this.jumpContext?.windCode || this.jumpContext?.companyCode;
+      const companyCodeForJump =
+        this.jumpContext?.companyCode || this.jumpContext?.windCode;
+      const query = {
+        id:
+          idForJump !== undefined && idForJump !== null
+            ? String(idForJump)
+            : "",
+        timeStamp: pointTime ? String(pointTime) : "",
+        windCode: windCodeForJump ? String(windCodeForJump) : "",
+        companyCode: companyCodeForJump ? String(companyCodeForJump) : "",
+        windTurbineNumber: this.jumpContext?.windTurbineNumber
+          ? String(this.jumpContext.windTurbineNumber)
+          : "",
+        itemKey:
+          nextItemKey !== undefined && nextItemKey !== null
+            ? String(nextItemKey)
+            : "",
+        mesurePointName: this.jumpContext?.mesurePointName || pointName || "",
+        detectionPointCn: this.jumpContext?.detectionPointCn || pointName || "",
+      };
+      const storagePayload = {
+        ...query,
+        rotationalSpeed: this.jumpContext?.rotationalSpeed,
+        otherData:
+          this.jumpContext?.otherData &&
+          typeof this.jumpContext.otherData === "object"
+            ? { ...this.jumpContext.otherData }
+            : undefined,
+      };
+      try {
+        sessionStorage.setItem(
+          "healthVibrationDeepLink",
+          JSON.stringify(storagePayload),
+        );
+      } catch (e) {
+        console.error(e);
+      }
+      this.$router.push({
+        path: "/home/health/vibration",
+        query,
+      });
+    },
+  },
+};
+</script>
+
+<style scoped></style>

+ 21 - 3
src/views/health/components/malfunction/temperature.vue

@@ -8,7 +8,17 @@
           tooltip-effect="dark"
           style="width: 100%"
           height="250"
+          stripe
+          border
         >
+          <el-table-column
+            prop="bearingType"
+            label="序号"
+            align="center"
+            width="100"
+            :index="bearingTableIndex"
+          >
+          </el-table-column>
           <el-table-column prop="time_stamp" label="时间" align="center" />
           <el-table-column
             prop="temp_channel"
@@ -33,7 +43,7 @@
             :current-page="currentPage"
             layout="total, prev, pager, next"
             :total="totalCount"
-            :page-size="500"
+            :page-size="pageSize"
           />
         </div>
       </div>
@@ -82,7 +92,7 @@
       </div> -->
     </div>
 
-    <!-- <div class="bottomBox">
+    <div class="bottomBox">
       <div class="tu" v-for="(item, index) in chartList" :key="index">
         <el-empty v-if="!hasData(item.data)" description="暂无数据" />
         <zhexian
@@ -96,7 +106,7 @@
           :bearingKey="item.bearingKey"
         />
       </div>
-    </div> -->
+    </div>
   </div>
 </template>
 
@@ -137,11 +147,16 @@ export default {
       type: String,
       default: "",
     },
+    echartsdata: {
+      type: Array,
+      default: () => [],
+    },
   },
   data() {
     return {
       tableData: [],
       currentPage: 1,
+      pageSize: 500,
       total: 1,
       result: {},
       bearingStateColors: {
@@ -208,6 +223,9 @@ export default {
     },
   },
   methods: {
+    bearingTableIndex(index) {
+      return (this.currentPage - 1) * this.pageSize + index + 1;
+    },
     updateBearingStateColors() {
       // 四个温度类型对应状态颜色key
       const statusMap = {

+ 301 - 114
src/views/health/components/spectrogramchartsNew.vue

@@ -114,6 +114,24 @@ export default {
       type: Object,
       default: () => ({}),
     },
+    /** 振动页合并 sessionStorage+路由 后的深链快照(可含 rotationalSpeed、otherData.RPM) */
+    deepLinkSpectrumContext: {
+      type: Object,
+      default: null,
+    },
+    /** 已打开图表列表(含 rowData),与深链 id 严格一致时取 RPM;不再使用 measurementPointData */
+    chartRowSnapshots: {
+      type: Array,
+      default: () => [],
+    },
+    routeAnalysisId: {
+      type: String,
+      default: "",
+    },
+    routeAnalysisTime: {
+      type: String,
+      default: "",
+    },
     windCode: {
       type: String,
       default: "",
@@ -181,8 +199,6 @@ export default {
               label: "LSS",
               type: "bearing",
               children: [
-                // 行星啮合频率(本质GMF)
-
                 "lSS NNCF5048 BPFO",
                 "lSS NNCF5048 BPFI",
                 "lSS NNCF5048 BSF",
@@ -245,10 +261,11 @@ export default {
           type: "rotation",
           children: [
             "Speed",
+            "Blade",
             "MainShaft",
             "LSS Planet shaft",
             "IMS Planet shaft",
-            "Rotor, Rotor bars",
+            "Rotor, Rotor bars", //转子
             "固定联轴节HHS",
           ],
         },
@@ -256,8 +273,7 @@ export default {
           label: "结构频率",
           type: "structure",
           children: [
-            "Blade",
-            "Stator, Pole pass freq.",
+            "Stator, Pole pass freq.", //定子
             "Stator, Grid freq.",
             "Rotor, Grid freq.",
           ],
@@ -268,7 +284,7 @@ export default {
           children: [
             "GearIMS",
             "GearHHS",
-            "LSS", //小齿轮
+            "LSS", //小齿轮// 行星啮合频率(本质GMF)
             "IMS", //中间轴
           ],
         },
@@ -299,6 +315,9 @@ export default {
       checkedGB: [],
       checkedValues: [],
       featureMultipleMap: {},
+      /** 与 failureFrequency.json 中各部件的 ratio(传动比)对应到每条故障特征名 */
+      featureRatioMap: {},
+      rpmMissingWarned: false,
     };
   },
   computed: {
@@ -411,6 +430,7 @@ export default {
           this.spectrumList.y.length === this.spectrumList.x.length
         ) {
           this.updateChart(this.spectrumList.y, this.spectrumList.x);
+          this.updateFeatureLinesByGroup();
         }
       },
       immediate: true,
@@ -420,11 +440,17 @@ export default {
       if (!newVal) return;
 
       this.$nextTick(() => {
-        if (this.checkedFeatures.length) {
-          this.updateFeatureLinesByGroup();
-        }
+        this.updateFeatureLinesByGroup();
       });
     },
+    currentRow: {
+      deep: true,
+      handler() {
+        this.$nextTick(() => {
+          this.updateFeatureLinesByGroup();
+        });
+      },
+    },
   },
   mounted() {
     this.$nextTick(() => {
@@ -444,10 +470,128 @@ export default {
     }
   },
   methods: {
+    /** 频率类数值展示:统一保留 6 位小数(特征竖线标签、光标读数等) */
     formatNumericFreq(v) {
       const n = Number(v);
       if (Number.isNaN(n)) return null;
-      return Math.abs(n) >= 1000 ? n.toFixed(2) : n.toFixed(4);
+      return Math.abs(n) >= 1000 ? n.toFixed(2) : n.toFixed(6);
+    },
+    toFinitePositiveNumber(raw) {
+      if (raw === null || raw === undefined || raw === "") return null;
+      if (typeof raw === "number") {
+        return Number.isFinite(raw) && raw > 0 ? raw : null;
+      }
+      if (typeof raw === "string") {
+        const s = raw.trim();
+        if (!s) return null;
+        const direct = Number(s);
+        if (Number.isFinite(direct) && direct > 0) return direct;
+        const m = s.match(/-?\d+(?:\.\d+)?/);
+        if (!m) return null;
+        const n = Number(m[0]);
+        return Number.isFinite(n) && n > 0 ? n : null;
+      }
+      return null;
+    },
+    normalizeIdToken(v) {
+      if (v === undefined || v === null) return "";
+      return String(v).trim();
+    },
+    normalizeTimeToken(v) {
+      if (v === undefined || v === null) return "";
+      return String(v).trim().replace(/\s+/g, " ");
+    },
+    timeTokensEffectivelyEqual(a, b) {
+      const na = this.normalizeTimeToken(a);
+      const nb = this.normalizeTimeToken(b);
+      if (na === nb) return true;
+      const da = Date.parse(String(a).replace(/-/g, "/"));
+      const db = Date.parse(String(b).replace(/-/g, "/"));
+      if (!Number.isFinite(da) || !Number.isFinite(db)) return false;
+      return da === db;
+    },
+    extractRpmFromRow(row) {
+      if (!row) return null;
+      const od = row.otherData || {};
+      const candidates = [
+        row.rotationalSpeed,
+        row.rotationSpeed,
+        od.RPM,
+        od.rpm,
+        od.rotationalSpeed,
+        od.rotationSpeed,
+      ];
+      for (const raw of candidates) {
+        const n = this.toFinitePositiveNumber(raw);
+        if (n != null) return n;
+      }
+      return null;
+    },
+    getStrictAnchor() {
+      const routeId = this.normalizeIdToken(this.routeAnalysisId);
+      const routeTime = this.normalizeTimeToken(this.routeAnalysisTime);
+      if (routeId) {
+        return { id: routeId, time: routeTime };
+      }
+      const cur = this.currentRow || {};
+      const curId = this.normalizeIdToken(cur.id ?? cur.itemKey);
+      const curTime = this.normalizeTimeToken(
+        cur.timeStamp ?? cur.itemValue ?? "",
+      );
+      return { id: curId, time: curTime };
+    },
+    recordMatchesAnchor(row) {
+      const { id: anchorId, time: anchorTime } = this.getStrictAnchor();
+      if (!anchorId || !row) return false;
+      if (this.normalizeIdToken(row.id ?? row.itemKey) !== anchorId) {
+        return false;
+      }
+      if (anchorTime) {
+        return this.timeTokensEffectivelyEqual(
+          row.timeStamp ?? row.itemValue ?? "",
+          anchorTime,
+        );
+      }
+      return true;
+    },
+    /** sessionStorage+路由 合并对象:id(及可选 time)与当前分析一致时才用其中的 RPM */
+    findRpmFromDeepLinkSpectrumContext() {
+      const ctx = this.deepLinkSpectrumContext;
+      if (!ctx || typeof ctx !== "object") return null;
+      const anchorId = this.normalizeIdToken(
+        this.routeAnalysisId || this.currentRow?.id || this.currentRow?.itemKey,
+      );
+      if (!anchorId) return null;
+      const cid = this.normalizeIdToken(ctx.id ?? ctx.itemKey);
+      if (cid !== anchorId) return null;
+      const wantTime = this.normalizeTimeToken(this.routeAnalysisTime);
+      if (wantTime) {
+        if (
+          !this.timeTokensEffectivelyEqual(
+            ctx.timeStamp ?? ctx.itemValue ?? "",
+            wantTime,
+          )
+        ) {
+          return null;
+        }
+      }
+      return this.extractRpmFromRow(ctx);
+    },
+    /** 仅扫历史图表 rowData,不用 measurementPointData(避免默认列表错行) */
+    findRpmFromStrictChartSnapshots() {
+      const { id: anchorId } = this.getStrictAnchor();
+      if (!anchorId) return null;
+      const snapshots = Array.isArray(this.chartRowSnapshots)
+        ? this.chartRowSnapshots
+        : [];
+      for (const item of snapshots) {
+        const rd = item?.rowData;
+        if (rd && this.recordMatchesAnchor(rd)) {
+          const r = this.extractRpmFromRow(rd);
+          if (r != null) return r;
+        }
+      }
+      return null;
     },
     formatCursorPair(freqNum, ampVal) {
       const xl = this.spectrumXLabel;
@@ -469,19 +613,30 @@ export default {
     },
     buildFeatureMultipleMap() {
       const map = {};
+      const ratioMap = {};
       (failureFrequency || []).forEach((part) => {
+        const partRatioRaw = part?.ratio;
+        const partRatio =
+          partRatioRaw != null && partRatioRaw !== ""
+            ? Number(partRatioRaw)
+            : 1;
+        const r = Number.isFinite(partRatio) ? partRatio : 1;
         (part.faultFrequencies || []).forEach((item) => {
           if (!item?.name) return;
           const key = this.normalizeFeatureName(item.name);
           map[key] = Number(item.multiple);
+          ratioMap[key] = r;
         });
       });
       // 兼容页面中的中文别名
       if (map.Blade != null) {
         map.叶片频率 = map.Blade;
+        ratioMap.叶片频率 = ratioMap.Blade != null ? ratioMap.Blade : 1;
       }
       map.转速频率 = 1;
+      ratioMap.转速频率 = ratioMap.Speed != null ? ratioMap.Speed : 1;
       this.featureMultipleMap = map;
+      this.featureRatioMap = ratioMap;
     },
     getFeatureMultipleByName(name) {
       const normalized = this.normalizeFeatureName(name);
@@ -497,6 +652,66 @@ export default {
       }
       return null;
     },
+    /** 传动比:来自 failureFrequency.json 中该特征所属部件的 ratio,缺省为 1 */
+    getFeatureRatioByName(name) {
+      const normalized = this.normalizeFeatureName(name);
+      const m = this.featureRatioMap || {};
+      if (m[normalized] != null && Number.isFinite(Number(m[normalized]))) {
+        return Number(m[normalized]);
+      }
+      const swapped = normalized.includes("GEN NDE")
+        ? normalized.replace("GEN NDE", "GEN-NDE")
+        : normalized.replace("GEN-NDE", "GEN NDE");
+      if (m[swapped] != null && Number.isFinite(Number(m[swapped]))) {
+        return Number(m[swapped]);
+      }
+      return 1;
+    },
+    /** 「齿轮特征」分组下的叶子项:不乘传动比,轴转频 = RPM/60(与轴承/转动等其它分组区分) */
+    isGearGroupFeatureName(name) {
+      const n = this.normalizeFeatureName(name);
+      const gear = this.featureGroups.find((g) => g.type === "gear");
+      const leaves = (gear?.children || []).filter(
+        (c) => typeof c === "string",
+      );
+      return leaves.some((leaf) => this.normalizeFeatureName(leaf) === n);
+    },
+    /**
+     * 特征竖线:非齿轮特征 X = (RPM×传动比/60)×倍频;齿轮特征 X = (RPM/60)×倍频
+     * 转速来源:树节点 otherData.RPM 优先,其次行上 rotationalSpeed(与波形接口一致)
+     */
+    getRotationalSpeedRpm() {
+      const cur = this.currentRow || {};
+      const od = cur.otherData || {};
+      const sp = this.spectrumList || {};
+      const spOd =
+        sp.otherData && typeof sp.otherData === "object" ? sp.otherData : {};
+      const candidates = [
+        od.RPM,
+        od.rpm,
+        od.rotationalSpeed,
+        od.rotationSpeed,
+        cur.rotationalSpeed,
+        cur.rotationSpeed,
+        sp.RPM,
+        sp.rpm,
+        sp.rotationalSpeed,
+        sp.rotationSpeed,
+        spOd.RPM,
+        spOd.rpm,
+        spOd.rotationalSpeed,
+        spOd.rotationSpeed,
+      ];
+      for (const raw of candidates) {
+        const n = this.toFinitePositiveNumber(raw);
+        if (n != null) return n;
+      }
+      const fromDeepLink = this.findRpmFromDeepLinkSpectrumContext();
+      if (fromDeepLink != null) return fromDeepLink;
+      const fromCharts = this.findRpmFromStrictChartSnapshots();
+      if (fromCharts != null) return fromCharts;
+      return null;
+    },
     handleResize() {
       this.chartInstance?.resize();
     },
@@ -510,9 +725,15 @@ export default {
       this.updateFeatureLinesByGroup();
     },
     handleCascaderChange(val) {
-      console.log(this.spectrumListTwo, "spectrumListTwo", this.spectrumList);
+      // console.log(
+      //   this.spectrumListTwo,
+      //   val,
+      //   "spectrumListTwo",
+      //   this.spectrumList,
+      // );
       this.manualFreq = "";
       this.multiple = 1;
+
       if (!val || val.length === 0) {
         this.checkedFeatures = [];
         this.renderFeatureSeries([]);
@@ -522,7 +743,7 @@ export default {
       this.checkedFeatures = val
         .map((item) => item[item.length - 1])
         .filter(Boolean);
-
+      console.log(this.checkedFeatures, "checkedFeatures");
       // 👉 3. 更新图表
       this.updateFeatureLinesByGroup();
     },
@@ -663,27 +884,44 @@ export default {
       });
     },
     updateFeatureLinesByGroup() {
+      console.log(this.spectrumList, "updateFeatureLinesByGroup");
       if (!this.spectrumList) return;
-      console.log(
-        this.spectrumListTwo,
-        "updateFeatureLinesByGroup",
-        this.spectrumList,
-      );
-
-      console.log(this.currentRow, "currentRow");
+      const rpm = this.getRotationalSpeedRpm();
+      console.log(rpm, "rpm");
+      if (rpm == null) {
+        this.renderFeatureSeries([]);
+        if (this.checkedFeatures.length && !this.rpmMissingWarned) {
+          this.rpmMissingWarned = true;
+          this.$message.warning(
+            "当前数据缺少有效转速(RPM),无法计算特征频率标注",
+          );
+        }
+        return;
+      }
+      this.rpmMissingWarned = false;
       const featureLines = this.checkedFeatures
         .map((name) => {
           const multiple = this.getFeatureMultipleByName(name);
           if (multiple == null || Number.isNaN(multiple)) return null;
+          const ratio = this.getFeatureRatioByName(name);
+          console.log(multiple, "multiple");
+          console.log(rpm, ratio, "rpm, ratio");
+          const shaftHz = this.isGearGroupFeatureName(name)
+            ? rpm / 60
+            : (rpm * ratio) / 60;
+          const x = shaftHz * Number(multiple);
+          if (!Number.isFinite(x)) return null;
+          const freqStr = this.formatNumericFreq(x);
+          const xl = this.spectrumXLabel;
+          const labelText =
+            freqStr != null ? `${name}: ${freqStr} ${xl}` : `${name}`;
           return {
-            // Xaxis: Number(multiple),
-            Xaxis:
-              (Number(this.currentRow.otherData.RPM) / 60) * Number(multiple),
-            val: `${name}: ${multiple}`,
+            Xaxis: x,
+            val: labelText,
           };
         })
         .filter(Boolean);
-
+      console.log(featureLines, "featureLines");
       this.renderFeatureSeries(featureLines);
     },
 
@@ -807,6 +1045,7 @@ export default {
         ],
         series: [
           {
+            id: "MAIN_SPECTRUM_SERIES",
             name: "数据系列",
             type: "line",
             data: labels.map((x, i) => [x, data[i]]),
@@ -831,39 +1070,12 @@ export default {
         }
       });
     },
-    // ✅ 生成特征线 series(安全版)
-    generateSeries(featureLines) {
-      // 👉 安全创建 markLine
-      const createMarkLine = (dataSource, color) => {
-        const validData = (dataSource || [])
-          .filter((item) => item && item.Xaxis != null && !isNaN(item.Xaxis))
-          .map(({ Xaxis, val }) => ({
-            xAxis: Number(Xaxis),
-            val,
-          }));
-
-        // ❗ 没数据直接返回 null(后面会过滤)
-        if (!validData.length) return null;
-
-        return {
-          type: "line",
-          markLine: {
-            silent: false,
-            lineStyle: {
-              color,
-              type: "dashed",
-              width: 2,
-            },
-            symbol: ["none", "none"],
-            label: {
-              show: true,
-              position: "end",
-              formatter: ({ data }) => data.val,
-            },
-            data: validData,
-          },
-        };
-      };
+    /**
+     * 特征竖线必须挂在「数据系列」上(与频谱折线同一 series)。
+     * 若用独立的无 data 的 line + markLine,ECharts 无法对齐 x 轴,竖线会全部画在 x=0。
+     */
+    renderFeatureSeries(featureLines) {
+      if (!this.chartInstance) return;
 
       const colors = [
         "#A633FF",
@@ -875,69 +1087,44 @@ export default {
         "#ff7f50",
         "#00bcd4",
       ];
-      // 每个选中特征生成一条标注线
-      const markLines = (featureLines || [])
-        .map((line, idx) => createMarkLine([line], colors[idx % colors.length]))
-        .filter(Boolean); // ✅ 关键:过滤 null
-
-      return [
-        {
-          name: "数据系列",
-          type: "line",
-          data: [],
-        },
-        ...markLines,
-      ];
-    },
 
-    // ✅ 渲染特征线(最终稳定版)
-    renderFeatureSeries(featureLines) {
-      if (!this.chartInstance) return;
-
-      const currentOption = this.chartInstance.getOption();
-      const seriesArr = currentOption.series || [];
-
-      // 👉 基础数据线(getOption 可能在首位出现 null 占位,勿用 [0])
-      const baseSeries =
-        seriesArr.find(
-          (s) => s && s.name === "数据系列" && s.type === "line",
-        ) || seriesArr.find((s) => s);
-
-      // 👉 光标相关 series(不要写 || {} ❗)
-      const cursorLineSeries = seriesArr.find(
-        (s) => s && s.id === "CURSOR_LINE_SERIES",
-      );
-
-      const cursorPointSeries = seriesArr.find(
-        (s) => s && s.id === "CURSOR_POINT_SERIES",
-      );
-
-      const cursorHighLineSeries = seriesArr.find(
-        (s) => s && s.id === "PEAK_REFERENCE_LINE",
-      );
-
-      const singlePointerSeries = seriesArr.find(
-        (s) => s && s.id === "SINGLE_POINTER_LINE",
-      );
-
-      // 👉 生成特征线
-      const featureSeries = this.generateSeries(featureLines);
+      const markLineData = (featureLines || [])
+        .map((line, i) => {
+          const x = Number(line.Xaxis);
+          if (!Number.isFinite(x)) return null;
+          const text = line.val != null ? String(line.val) : "";
+          return {
+            xAxis: x,
+            lineStyle: {
+              color: colors[i % colors.length],
+              type: "dashed",
+              width: 2,
+            },
+            label: {
+              show: true,
+              position: "end",
+              formatter: text,
+            },
+          };
+        })
+        .filter(Boolean);
 
-      // 👉 最终更新(核心:必须过滤)
       this.chartInstance.setOption(
         {
           series: [
-            baseSeries,
-            ...featureSeries.slice(1), // 跳过第一个占位 line
-            cursorLineSeries,
-            cursorPointSeries,
-            cursorHighLineSeries,
-            singlePointerSeries,
-          ].filter((s) => s && s.type), // ✅ 终极保险(必须有)
-        },
-        {
-          replaceMerge: ["series"],
+            {
+              id: "MAIN_SPECTRUM_SERIES",
+              // 特征值仅作为展示,不参与点击交互,避免触发额外事件链
+              markLine: {
+                silent: true,
+                symbol: ["none", "none"],
+                data: markLineData,
+              },
+            },
+          ],
         },
+        false,
+        true,
       );
     },
     // 获取数据

+ 26 - 9
src/views/health/components/tendencychartsNew.vue

@@ -2,7 +2,7 @@
   <div>
     <div class="line-chart" ref="chart"></div>
 
-    <div class="linkageCharts">
+    <div v-if="showLinkage" class="linkageCharts">
       <spectrogramcharts
         :isshow="false"
         :loading="loading"
@@ -45,6 +45,8 @@ export default {
     windCode: { type: String, default: "" },
     chartId: { type: Number, required: true },
     measurementPointData: { type: Array, default: () => [] },
+    /** false 时仅展示顶部趋势折线(与「合并图谱」中上半部分一致,不展示下方频谱/时域联动) */
+    showLinkage: { type: Boolean, default: true },
   },
 
   data() {
@@ -114,16 +116,31 @@ export default {
       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,
-        );
+        const dataIndex = Number(params.dataIndex);
+        const points = this.measurementPointData || [];
+        console.log(params, "ids", points);
+
+        // 优先按 dataIndex 直接映射,避免时间格式差异导致联动失效
+        const byIndex =
+          Number.isInteger(dataIndex) && dataIndex >= 0 && dataIndex < points.length
+            ? points[dataIndex]
+            : null;
+
+        // 兜底:当索引映射失败时,再按时间字符串做宽松匹配
+        const clickTime = String(params.name || "").slice(0, 16);
+        const byTime =
+          byIndex ||
+          points.find((item) => String(item?.itemValue || "").slice(0, 16) === clickTime);
+
+        const match = byTime;
         console.log(match, "match");
-        if (match?.itemKey) ids = [Number(match.itemKey)];
+        if (match?.itemKey) {
+          ids = [Number(match.itemKey)];
+        }
+        // 请求参数时间优先使用测点原始时间,避免 x 轴展示格式造成后端查不到
+        time = match?.itemValue || params.name;
       } else {
-        ids = this.ids;
+        ids = Array.isArray(this.ids) ? this.ids : [Number(this.ids)];
         time = this.windCode;
       }
 

+ 1 - 1
src/views/health/components/timedomainchartsNew.vue

@@ -567,7 +567,7 @@ export default {
             color: "#333",
             padding: [15, 0, 0, 0], // 增加X轴标题和轴线的距离
           },
-          max: xMax,
+          // max: xMax,
           axisLabel: {
             margin: 1, // 增加数值标签和轴线的间距
             formatter: (value) => value,

+ 181 - 270
src/views/health/malfunction.vue

@@ -1,15 +1,5 @@
 <template>
   <div class="global-variable">
-    <!-- tab 切换 -->
-    <el-tabs v-model="activeName" @tab-click="handleClick">
-      <el-tab-pane
-        v-for="item in menuItems"
-        :key="item.component"
-        :label="item.name"
-        :name="item.component"
-      />
-    </el-tabs>
-
     <!-- 查询条件 -->
     <div class="searchbox">
       <p>
@@ -18,9 +8,9 @@
           size="small"
           style="width: 180px"
           placeholder="请选择所属公司"
-          :list="tabConditions[activeTab].parentOpt"
+          :list="queryCondition.parentOpt"
           type="1"
-          v-model="tabConditions[activeTab].companyCode"
+          v-model="queryCondition.companyCode"
           @change="parentChange"
           :defaultParentProps="{
             children: 'children',
@@ -34,12 +24,12 @@
         <el-select
           size="small"
           style="width: 150px"
-          v-model="tabConditions[activeTab].unitvalue"
-          @change="getchedian"
+          v-model="queryCondition.unitvalue"
+          @change="onUnitChange"
           placeholder="请选择"
         >
           <el-option
-            v-for="item in tabConditions[activeTab].unitoptions"
+            v-for="item in queryCondition.unitoptions"
             :key="item.engineCode"
             :label="item.engineName"
             :value="item.engineCode"
@@ -47,45 +37,10 @@
         </el-select>
       </p>
       <p>
-        测点:
-        <el-select
-          v-model="tabConditions[activeTab].monitoringvalue"
-          size="small"
-          clearable
-          placeholder="请选择"
-          :disabled="activeTab === 'Temperature'"
-        >
-          <el-option
-            v-for="item in tabConditions[activeTab].monitoringoptions"
-            :key="item.itemKey"
-            :label="item.itemValue"
-            :value="item.itemKey"
-          />
-        </el-select>
-      </p>
-
-      <!-- <p>
-        采样频率:
-        <el-select
-          v-model="tabConditions[activeTab].frequencyvalue"
-          size="small"
-          clearable
-          placeholder="请选择"
-          :disabled="activeTab === 'Temperature'"
-        >
-          <el-option
-            v-for="item in tabConditions[activeTab].frequencyoptions"
-            :key="item"
-            :label="item"
-            :value="item"
-          />
-        </el-select>
-      </p> -->
-      <p>
         时间:
         <el-date-picker
           size="small"
-          v-model="tabConditions[activeTab].timevalue"
+          v-model="queryCondition.timevalue"
           type="datetimerange"
           :picker-options="pickerOptions"
           range-separator="至"
@@ -102,6 +57,15 @@
         >查询</el-button
       >
     </div>
+    <!-- tab 切换 -->
+    <el-tabs v-model="activeName" @tab-click="handleClick">
+      <el-tab-pane
+        v-for="item in menuItems"
+        :key="item.component"
+        :label="item.name"
+        :name="item.component"
+      />
+    </el-tabs>
 
     <!-- 动态组件内容 -->
     <div class="main-body">
@@ -117,8 +81,9 @@
           "
           :totalCount="tabData[activeTab].totalCount"
           :totalPage="tabData[activeTab].totalPage"
-          :fieldCode="tabConditions[activeTab].companyCode"
-          :windTurbineNumber="tabConditions[activeTab].unitvalue"
+          :echartsdata="tabData[activeTab].echartsdata"
+          :fieldCode="queryCondition.companyCode"
+          :windTurbineNumber="queryCondition.unitvalue"
           @updatePage="handlePageChange"
         />
       </keep-alive>
@@ -132,7 +97,6 @@ import * as XLSX from "xlsx";
 import {
   getSysOrganizationAuthTreeByRoleId,
   windEngineGrouPage,
-  queryDetectionDic,
 } from "@/api/ledger.js";
 import selecttree from "../../components/selecttree.vue";
 import Bearing from "./components/malfunction/bearing.vue";
@@ -228,17 +192,35 @@ export default {
         Loose: {},
         Temperature: {},
       },
+      // 查询条件放在 tab 之上:全局共享,不随 tab 切换清空
+      queryCondition: {
+        unitvalue: "",
+        unitoptions: [],
+        companyCode: "",
+        parentOpt: [],
+        timevalue: [],
+      },
       tabData: {
-        Bearing: { codedata: [], totalCount: 0, totalPage: 0 },
-        Gear: { codedata: [], totalCount: 0, totalPage: 0 },
-        Dissymmetry: { codedata: [], totalCount: 0, totalPage: 0 },
-        Misalignment: { codedata: [], totalCount: 0, totalPage: 0 },
-        Loose: { codedata: [], totalCount: 0, totalPage: 0 },
+        Bearing: { codedata: [], totalCount: 0, totalPage: 0, echartsdata: [] },
+        Gear: { codedata: [], totalCount: 0, totalPage: 0, echartsdata: [] },
+        Dissymmetry: {
+          codedata: [],
+          totalCount: 0,
+          totalPage: 0,
+          echartsdata: [],
+        },
+        Misalignment: {
+          codedata: [],
+          totalCount: 0,
+          totalPage: 0,
+          echartsdata: [],
+        },
+        Loose: { codedata: [], totalCount: 0, totalPage: 0, echartsdata: [] },
         Temperature: {
           codedata: [],
           totalCount: 0,
           totalPage: 0,
-          echartsdata: {},
+          echartsdata: [],
         },
       },
       datePickerOptions: {
@@ -274,6 +256,17 @@ export default {
     }
   },
   methods: {
+    getFunctionTypeByTab(tab) {
+      // 1:轴承诊断;2:齿轮诊断;3:不对中;4:不平衡;5:松动
+      const map = {
+        Bearing: "1",
+        Gear: "2",
+        Dissymmetry: "3",
+        Misalignment: "4",
+        Loose: "5",
+      };
+      return map[tab] || "";
+    },
     defaultCondition() {
       return {
         unitvalue: "",
@@ -281,10 +274,6 @@ export default {
         companyCode: "",
         parentOpt: [],
         timevalue: [],
-        monitoringvalue: "",
-        monitoringoptions: [],
-        frequencyvalue: "",
-        frequencyoptions: [],
       };
     },
     handleClick(tab) {
@@ -295,10 +284,7 @@ export default {
       const res = await getSysOrganizationAuthTreeByRoleId();
       const treedata = res.data;
       const processed = this.processTreeData(treedata);
-
-      for (const key in this.tabConditions) {
-        this.tabConditions[key].parentOpt = processed;
-      }
+      this.queryCondition.parentOpt = processed;
     },
 
     // demo演示所需,必要时可删除
@@ -307,67 +293,21 @@ export default {
       const treedata = res.data;
       const processed = this.processTreeData(treedata);
 
-      for (const key in this.tabConditions) {
-        const tab = this.tabConditions[key];
-        tab.parentOpt = processed;
-
-        // 默认公司
-        tab.companyCode = "WOF046400029";
-
-        // 默认风机列表和选中值
-        tab.unitoptions = [{ engineCode: "WOG01315", engineName: "#37" }];
-        tab.unitvalue = "WOG01315";
-
-        // 默认测点列表和选中值
-        if (key === "Gear") {
-          // Gear tab:保留主轴承 + 齿轮箱测点
-          this.$set(tab, "monitoringoptions", [
-            {
-              itemKey: "gearbox_first_stage_planet_large_ring_radial_vibration",
-              itemValue: "齿轮箱一级行星级大齿圈径向振动",
-            },
-          ]);
-          this.$set(
-            tab,
-            "monitoringvalue",
-            "gearbox_first_stage_planet_large_ring_radial_vibration",
-          );
-        } else {
-          // 其他 tab:只保留主轴承测点
-          this.$set(tab, "monitoringoptions", [
-            {
-              itemKey: "main_bearing_radial_horizontal_vibration",
-              itemValue: "主轴承径向水平振动",
-            },
-          ]);
-          this.$set(
-            tab,
-            "monitoringvalue",
-            "main_bearing_radial_horizontal_vibration",
-          );
-        }
-
-        // 默认采样频率
-        tab.frequencyoptions = ["12800", "25600", "51200"];
-        tab.frequencyvalue = "12800";
-
-        // 默认时间范围
-        tab.timevalue = [
-          new Date("2024-07-01 00:00:00"),
-          new Date("2024-12-31 00:00:00"),
-        ];
-      }
-
-      this.$nextTick(() => {
-        this.tabConditions[this.activeTab].companyCode = "WOF046400029";
-      });
+      this.queryCondition.parentOpt = processed;
+      this.queryCondition.companyCode = "WOF046400029";
+      this.queryCondition.unitoptions = [
+        { engineCode: "WOG01315", engineName: "#37" },
+      ];
+      this.queryCondition.unitvalue = "WOG01315";
+      this.queryCondition.timevalue = [
+        new Date("2024-07-01 00:00:00"),
+        new Date("2024-12-31 00:00:00"),
+      ];
     },
     parentChange(data) {
-      const condition = this.tabConditions[this.activeTab];
+      const condition = this.queryCondition;
       if (!data?.codeNumber) return;
       condition.unitvalue = "";
-      condition.monitoringvalue = "";
-      condition.frequencyvalue = "";
 
       windEngineGrouPage({
         fieldCode: data.codeNumber,
@@ -377,45 +317,10 @@ export default {
         condition.unitoptions = res.data.list;
       });
 
-      axios
-        .get(`/ETLapi/waveData/getAllSamplingFrequency/${data.codeNumber}`)
-        .then((res) => {
-          condition.frequencyoptions = (res.data.datas || [])
-            .map((item) => Number(item))
-            .filter((num) => num >= 2000)
-            .map((num) => num.toString());
-        });
-
       condition.companyCode = data.codeNumber;
     },
-    getchedian(value) {
-      const condition = this.tabConditions[this.activeTab];
-      const companyCode = condition.companyCode;
-
-      axios
-        .post(`/ETLapi/waveData/getAllMesurePointName/${companyCode}/${value}`)
-        .then((res) => {
-          condition.monitoringvalue = "";
-          if (res.data.code === 500) {
-            condition.monitoringoptions = [];
-            this.$message({
-              message: "当前风场不存在测点和采样频率数据" || "未知错误",
-              type: "warning",
-            });
-            return;
-          }
-          if (res.data.code === 200) {
-            const datas = Array.isArray(res.data.datas) ? res.data.datas : [];
-            condition.monitoringoptions = datas;
-            if (datas.length === 0) {
-              this.$message({ message: "暂无数据", type: "warning" });
-            }
-          }
-        })
-        .catch((err) => {
-          console.error("测点请求失败:", err);
-          condition.monitoringoptions = [];
-        });
+    onUnitChange() {
+      // 已移除“测点/采样频率”筛选逻辑:风机切换仅更新选中值
     },
     processTreeData(treeData) {
       const processedData = [];
@@ -434,130 +339,136 @@ export default {
       return processedData;
     },
     onSearchClick() {
-      this.conditions(1, false); // 主动查询,第一页,非分页触发
+      // 主动查询:一次拉取所有 tab 的第一页数据
+      this.menuItems.forEach((item) => {
+        this.fetchTabData(item.component, 1, false);
+      });
     },
     handlePageChange(page) {
-      this.conditions(page, true); // true 表示分页触发
+      this.fetchTabData(this.activeTab, page, true); // 分页只作用当前 tab
     },
+    fetchTabData(tab, page, isPageChange = false) {
+      const condition = this.queryCondition;
+      if (
+        !condition.companyCode ||
+        !condition.unitvalue ||
+        !condition.timevalue?.length
+      ) {
+        return;
+      }
 
-    conditions(page, isPageChange = false) {
-      const tab = this.activeTab;
-      this.$nextTick(() => {
-        const comp = this.$refs.activeComponent;
-        if (comp && typeof comp.reset === "function") {
-          comp.reset();
-        }
-      });
-
-      // 把后面接口请求部分放进延迟里,确保 reset 先执行完成
-      setTimeout(() => {
-        const condition = this.tabConditions[tab];
-
-        if (tab === "Temperature") {
-          const temperature = {
-            windCode: condition.companyCode,
-            windTurbineNumberList: [condition.unitvalue],
-            startTime: this.$formatDateTWO(condition.timevalue[0]),
-            endTime: this.$formatDateTWO(condition.timevalue[1]),
-            pageNo: page,
-            pageSize: 500,
-          };
-
-          const loading = this.$loading({
-            lock: true,
-            text: "温度诊断数据加载中,请稍候…",
-            spinner: "el-icon-loading",
-            background: "rgba(0, 0, 0, 0.8)",
-          });
+      if (tab === "Temperature") {
+        const temperature = {
+          windCode: condition.companyCode,
+          windTurbineNumberList: [condition.unitvalue],
+          startTime: this.$formatDateTWO(condition.timevalue[0]),
+          endTime: this.$formatDateTWO(condition.timevalue[1]),
+          pageNo: page,
+          pageSize: 500,
+        };
+
+        const loading = this.$loading({
+          lock: true,
+          text: "温度诊断数据加载中,请稍候…",
+          spinner: "el-icon-loading",
+          background: "rgba(0, 0, 0, 0.8)",
+        });
 
-          const emptyEchartsData = {
-            gearbox_oil: { timestamps: [], values: [] },
-            generator_drive_end: { timestamps: [], values: [] },
-            generator_nondrive_end: { timestamps: [], values: [] },
-            main_bearing: { timestamps: [], values: [] },
-          };
-
-          const trendRequest = () => {
-            const trendStart = performance.now();
-            return axios
-              .post("/AnalysisMulti/SPRT/trend", temperature)
-              .then((res) => {
-                const trendEnd = performance.now();
-                console.log(
-                  `温度诊断 trend 接口耗时: ${(
-                    (trendEnd - trendStart) /
-                    1000
-                  ).toFixed(2)}s`,
-                );
-                const echartsdata = res.data.data || {};
-                this.tabData[tab].echartsdata = echartsdata;
-              })
-              .catch((err) => {
-                console.error("趋势图请求失败:", err);
-                this.$message.error("温度趋势图数据请求失败,请稍后再试");
-              });
-          };
-
-          const thresholdStart = performance.now();
-          const thresholdReq = axios
-            .post("/AnalysisMulti/temperature/threshold", temperature)
+        const emptyEchartsData = {
+          gearbox_oil: { timestamps: [], values: [] },
+          generator_drive_end: { timestamps: [], values: [] },
+          generator_nondrive_end: { timestamps: [], values: [] },
+          main_bearing: { timestamps: [], values: [] },
+        };
+
+        const trendRequest = () => {
+          const trendStart = performance.now();
+          return axios
+            .post("/AnalysisMulti/SPRT/trend", temperature)
             .then((res) => {
-              const thresholdEnd = performance.now();
+              const trendEnd = performance.now();
               console.log(
-                `温度诊断 threshold 接口耗时: ${(
-                  (thresholdEnd - thresholdStart) /
+                `温度诊断 trend 接口耗时: ${(
+                  (trendEnd - trendStart) /
                   1000
                 ).toFixed(2)}s`,
               );
-
-              const data = res.data.data.records || [];
-              this.tabData[tab].codedata = data;
-              this.tabData[tab].totalCount = res.data.data.totalSize || 0;
-
-              if (!data.length) {
-                this.$message.warning("暂无诊断数据");
-                this.tabData[tab].echartsdata = emptyEchartsData;
-                return Promise.resolve("skip trend");
-              }
-
-              return isPageChange ? Promise.resolve() : trendRequest();
+              const echartsdata = res.data.data || {};
+              this.tabData[tab].echartsdata = echartsdata;
             })
             .catch((err) => {
-              console.error("温度诊断请求失败:", err);
-              this.$message.error("温度诊断数据请求失败,请稍后再试");
+              console.error("趋势图请求失败:", err);
+              this.$message.error("温度趋势图数据请求失败,请稍后再试");
             });
+        };
+        const thresholdStart = performance.now();
+        const thresholdReq = axios
+          .post("/AnalysisMulti/temperature/threshold", temperature)
+          .then((res) => {
+            const thresholdEnd = performance.now();
+            console.log(
+              `温度诊断 threshold 接口耗时: ${(
+                (thresholdEnd - thresholdStart) /
+                1000
+              ).toFixed(2)}s`,
+            );
+
+            const data = res.data.data.records || [];
+            this.tabData[tab].codedata = data;
+            this.tabData[tab].totalCount = res.data.data.totalSize || 0;
+
+            if (!data.length) {
+              this.$message.warning("暂无诊断数据");
+              this.tabData[tab].echartsdata = emptyEchartsData;
+              return Promise.resolve("skip trend");
+            }
 
-          Promise.resolve(thresholdReq).finally(() => {
-            loading.close();
+            return isPageChange ? Promise.resolve() : trendRequest();
+          })
+          .catch((err) => {
+            console.error("温度诊断请求失败:", err);
+            this.$message.error("温度诊断数据请求失败,请稍后再试");
           });
-        } else {
-          const params = {
-            samplingFrequency: condition.frequencyvalue,
-            windCode: condition.companyCode,
-            windTurbineNumberList: [condition.unitvalue],
-            mesureNameList: [condition.monitoringvalue],
-            startTime: this.$formatDateTWO(condition.timevalue[0]),
-            endTime: this.$formatDateTWO(condition.timevalue[1]),
-            pageNo: page,
-            pageSize: 10,
-          };
-
-          const startTime = performance.now();
-          axios
-            .post("/ETLapi/waveData/getMesureDataWithSF", params)
-            .then((res) => {
-              const endTime = performance.now();
-              console.log(
-                `其他 tab 接口请求耗时: ${(endTime - startTime).toFixed(2)}ms`,
-              );
 
-              const data = res.data || {};
-              this.tabData[tab].codedata = data.datas || [];
-              this.tabData[tab].totalCount = data.totalCount || 0;
-              this.tabData[tab].totalPage = data.totalPage || 0;
-            });
-        }
-      }, 300); // 延迟100毫秒执行接口请求逻辑
+        Promise.resolve(thresholdReq).finally(() => {
+          loading.close();
+        });
+      } else {
+        const params = {
+          windCode: condition.companyCode,
+          windTurbineNumber: condition.unitvalue,
+          startTime: this.$formatDateTWO(condition.timevalue[0]),
+          endTime: this.$formatDateTWO(condition.timevalue[1]),
+          functionType: this.getFunctionTypeByTab(tab),
+          pageNo: page,
+          isAsc: true,
+          pageSize: 10,
+        };
+
+        const startTime = performance.now();
+        //请求tab 接口数据
+        axios
+          .post("/transDataWeb/waveData/getByFunctionTypeInData", params)
+          .then((res) => {
+            const endTime = performance.now();
+            console.log(
+              `其他 tab 接口请求耗时: ${(endTime - startTime).toFixed(2)}ms`,
+            );
+
+            const data = res.data || {};
+            this.tabData[tab].codedata = data.datas || [];
+            this.tabData[tab].totalCount = data.totalCount || 0;
+            this.tabData[tab].totalPage = data.totalPage || 0;
+          });
+
+        axios
+          .post("/transDataWeb/waveData/getByFunctionTypeInDataSummary", params)
+          .then((res) => {
+            console.log(res.data, "res.data.data");
+            const data = res.data.datas || [];
+            this.tabData[tab].echartsdata = data;
+          });
+      }
     },
   },
 };

A különbségek nem kerülnek megjelenítésre, a fájl túl nagy
+ 651 - 98
src/views/health/vibration.vue


Nem az összes módosított fájl került megjelenítésre, mert túl sok fájl változott