yusheng пре 1 дан
родитељ
комит
989f2255ba






+ 116 - 0
src/components/GlobalAudioUnlockButton.vue

@@ -0,0 +1,116 @@
+<!-- src/components/GlobalAudioUnlockButton.vue -->
+<template>
+  <div class="audio-control-btn" @click="handleToggle">
+    <span class="icon">{{ buttonIcon }}</span>
+    <span class="tooltip">{{ buttonTooltip }}</span>
+  </div>
+</template>
+
+<script>
+import audioManager from '@/utils/audioManager';
+
+export default {
+  name: 'GlobalAudioUnlockButton',
+  data() {
+    return {
+      // 当前实际静音状态
+      isMuted: audioManager.isMuted,
+      // 是否曾经解锁过(刷新后保留)
+      hasUnlockedBefore: audioManager.hasUnlockedBefore,
+    };
+  },
+  computed: {
+    buttonIcon() {
+      if (this.isMuted) {
+        return '🔇'; // 静音状态
+      } else {
+        return '🔊'; // 有声状态
+      }
+    },
+    buttonTooltip() {
+      if (this.isMuted) {
+        if (this.hasUnlockedBefore) {
+          return '点击恢复声音';
+        } else {
+          return '点击开启声音';
+        }
+      } else {
+        return '声音已开启';
+      }
+    },
+  },
+  methods: {
+    handleToggle() {
+      // 点击按钮,如果当前静音则解除静音,否则静音(可选)
+      if (this.isMuted) {
+        // 解除静音(用户手势触发的)
+        audioManager.unmute();
+        // 更新本地状态
+        this.isMuted = false;
+        this.hasUnlockedBefore = true;
+      } else {
+        // 如果已经开启,点击后静音(也可以注释掉,让用户只能开启不能关闭,避免误操作)
+        audioManager.mute();
+        this.isMuted = true;
+        this.hasUnlockedBefore = false;
+      }
+    },
+  },
+  mounted() {
+    // 可定期检查状态变化(例如其他模块调用 unmute 后更新UI),但单例数据变更后,组件内的 data 不会自动更新,
+    // 所以可以采用 watch 或者事件总线,但为了简化,我们可以在点击时手动更新。
+    // 或者将 isMuted 改为 computed 直接从 audioManager 获取,但需要响应式。
+    // 更稳健:使用 Vue 的 observable 或直接使用 data 并在 unmute/mute 时触发更新。
+    // 但这里因为按钮操作是自己触发的,且其他模块也可能调用,我们可以通过事件总线或 Vuex。
+    // 简单起见,我们不去监听外部变化,只保证自己的点击更新正确。
+    // 如果外部(如导航点击)调用 audioManager.unmute(),按钮图标不会自动刷新。
+    // 解决方案:在 audioManager 中触发事件,或使用 Vuex。
+    // 为保持简洁,这里我们提供一个手动刷新的方法,或者建议用户只通过这个按钮控制。
+    // 如果你的导航点击也需要调用 unmute,可以在调用后调用 this.$refs.btn.updateStatus() 等。
+    // 但推荐只通过这个按钮控制,避免多处控制状态混乱。
+  },
+};
+</script>
+
+<style scoped>
+.audio-control-btn {
+  position: fixed;
+  top: 115px;
+  right: 200px;
+  width: 56px;
+  height: 56px;
+  border-radius: 50%;
+  background: rgba(0, 0, 0, 0.7);
+  color: white;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  font-size: 28px;
+  cursor: pointer;
+  z-index: 9999;
+  box-shadow: 0 2px 12px rgba(0, 0, 0, 0.3);
+  transition: transform 0.2s, background 0.2s;
+  user-select: none;
+}
+.audio-control-btn:hover {
+  transform: scale(1.1);
+  background: rgba(0, 0, 0, 0.9);
+}
+.audio-control-btn .tooltip {
+  position: absolute;
+  bottom: 70px;
+  right: 0;
+  background: rgba(0, 0, 0, 0.8);
+  color: #fff;
+  padding: 4px 12px;
+  border-radius: 12px;
+  font-size: 12px;
+  white-space: nowrap;
+  opacity: 0;
+  pointer-events: none;
+  transition: opacity 0.2s;
+}
+.audio-control-btn:hover .tooltip {
+  opacity: 1;
+}
+</style>

+ 199 - 0
src/utils/audioManager.js

@@ -0,0 +1,199 @@
+// src/utils/audioManager.js
+
+class AudioManager {
+  constructor() {
+    // 从 sessionStorage 读取是否曾经解锁(仅用于UI提示)
+    this.hasUnlockedBefore = sessionStorage.getItem('audioUnlocked') === 'true';
+    // 当前实际静音状态(页面加载后默认为 true)
+    this.isMuted = true;
+    // 存储所有活跃的音频实例及播放状态
+    this.audioInstances = [];
+
+    // 🎯 全局默认播放次数(你可以根据需要改为 1 或 3)
+    this.defaultRepeatCount = 3; // 默认循环3次
+
+    // 被浏览器策略拦截后待重试的播放队列
+    this._pendingPlaybacks = [];
+
+    // 全局用户手势监听(用于重试被拦截的播放)
+    this._setupUserGestureRetry();
+  }
+
+  /**
+   * 停止所有正在播放的音频
+   */
+  stopAll() {
+    this.audioInstances.forEach((item) => {
+      item.audio.pause();
+      item.audio.src = '';
+      item.audio.onended = null; // 清除事件监听
+    });
+    this.audioInstances = [];
+    console.log('🛑 已停止所有音频');
+  }
+
+  /**
+   * 恢复 AudioContext(绕过浏览器自动播放限制)
+   */
+  _resumeAudioContext() {
+    if (this._audioContext && this._audioContext.state === 'suspended') {
+      this._audioContext.resume().catch(() => {});
+    }
+  }
+
+  /**
+   * 设置用户手势后重试被拦截的播放
+   */
+  _setupUserGestureRetry() {
+    const retryAll = () => {
+      if (!this._pendingPlaybacks.length) return;
+      const tasks = [...this._pendingPlaybacks];
+      this._pendingPlaybacks = [];
+      tasks.forEach((fn) => fn());
+    };
+    const handler = () => {
+      retryAll();
+      document.removeEventListener('click', handler);
+      document.removeEventListener('touchstart', handler);
+      document.removeEventListener('keydown', handler);
+    };
+    document.addEventListener('click', handler, { once: true });
+    document.addEventListener('touchstart', handler, { once: true });
+    document.addEventListener('keydown', handler, { once: true });
+  }
+
+  /**
+   * 播放音频(自动停止之前的音频,并支持自定义重复次数)
+   * @param {string} src - 音频文件路径
+   * @param {object} options
+   * @param {number} options.repeatCount - 播放次数(默认使用全局 defaultRepeatCount),设为 1 则只播放一次
+   * @param {boolean} options.loop - 是否无限循环(与 repeatCount 二选一,若同时设置,优先使用 repeatCount)
+   * @param {boolean} options.clearPrevious - 是否停止之前的音频,默认 true
+   * @returns {HTMLAudioElement} 返回 Audio 实例,以便外部控制
+   */
+  play(src, options = {}) {
+    const {
+      repeatCount = this.defaultRepeatCount,
+      loop = false,
+      clearPrevious = true,
+    } = options;
+
+    // 播放新音频前,先停止所有旧音频
+    if (clearPrevious) {
+      this.stopAll();
+    }
+
+    const audio = new Audio(src);
+    // 根据当前静音状态设置
+    audio.muted = this.isMuted;
+
+    // 禁用原生 loop,我们手动控制次数
+    audio.loop = false;
+
+    // 如果用户明确要求无限循环(且没有指定 repeatCount),则使用原生 loop
+    if (loop && !repeatCount) {
+      audio.loop = true;
+    }
+
+    // 计算目标播放次数(至少1次)
+    const targetCount = Math.max(1, repeatCount || 1);
+    let currentCount = 0;
+
+    // 定义 ended 事件处理器(用于重复播放)
+    const onEnded = () => {
+      currentCount++;
+      if (currentCount < targetCount) {
+        // 重新播放
+        audio.play().catch((err) => console.warn('重播失败:', err));
+      } else {
+        // 播放完成,从实例数组中移除
+        const idx = this.audioInstances.findIndex((item) => item.audio === audio);
+        if (idx > -1) {
+          this.audioInstances.splice(idx, 1);
+        }
+        audio.onended = null; // 清理事件
+      }
+    };
+    audio.addEventListener('ended', onEnded);
+
+    // 存储音频实例及元数据(用于后续管理)
+    this.audioInstances.push({
+      audio,
+      currentCount,
+      targetCount,
+      onEnded,
+    });
+
+    // 播放前先尝试恢复 AudioContext(绕过浏览器自动播放限制)
+    this._resumeAudioContext();
+
+    // 开始播放
+    audio.play().catch((err) => {
+      // 被浏览器策略拦截时,保存到待播放队列,等下次用户交互时重试
+      console.warn('音频播放失败(自动播放策略):', err);
+      if (err.name === 'NotAllowedError') {
+        this._pendingPlaybacks.push(() => {
+          audio.play().catch(() => {});
+        });
+      }
+    });
+
+    return audio;
+  }
+
+  /**
+   * 解除全局静音(必须由用户手势触发)
+   */
+  unmute() {
+    if (!this.isMuted) return;
+    this.isMuted = false;
+    this.audioInstances.forEach((item) => {
+      item.audio.muted = false;
+    });
+    sessionStorage.setItem('audioUnlocked', 'true');
+    this.hasUnlockedBefore = true;
+
+    // 解锁后重试被拦截的播放
+    if (this._pendingPlaybacks.length) {
+      const tasks = [...this._pendingPlaybacks];
+      this._pendingPlaybacks = [];
+      tasks.forEach((fn) => fn());
+    }
+
+    console.log('🔊 音频已解除静音');
+  }
+
+  /**
+   * 重新静音
+   */
+  mute() {
+    if (this.isMuted) return;
+    this.isMuted = true;
+    this.audioInstances.forEach((item) => {
+      item.audio.muted = true;
+    });
+    sessionStorage.removeItem('audioUnlocked');
+    this.hasUnlockedBefore = false;
+    console.log('🔇 音频已静音');
+  }
+
+  /**
+   * 获取当前状态(用于UI)
+   */
+  getStatus() {
+    return {
+      isMuted: this.isMuted,
+      hasUnlockedBefore: this.hasUnlockedBefore,
+    };
+  }
+
+  /**
+   * 彻底销毁所有音频(页面卸载时调用)
+   */
+  destroyAll() {
+    this.stopAll();
+  }
+}
+
+// 导出单例
+export default new AudioManager();

+ 419 - 66
src/views/home/yuxin.vue

@@ -62,18 +62,39 @@
             <div class="label device-name" style="left: -0.1%; top: 45%"
               >1#给煤机</div
             >
+
             <div
-              style="left: 4.5%; top: 45%"
+              style="left: 6%; top: 32%"
               class="alarm-badge"
               v-if="gmj1.alarm.alarmLevel"
               :class="{
                 'alarm-yellow': gmj1.alarm.alarmLevel == 1,
                 'alarm-orange': gmj1.alarm.alarmLevel == 2,
-                'alarm-red': gmj1.alarm.alarmLevel == 3
+                'alarm-red': [3, 4, 5].includes(gmj1.alarm.alarmLevel)
               }"
             >
-              <i class="el-icon-bell alarm-icon"></i>
+              <span class="alarm-text">
+                <div v-if="gmj1.alarm && gmj1.alarm?.deviceData">
+                  <div
+                    v-for="(item, index) in gmj1.alarm?.deviceData.filter(
+                      (val) =>
+                        evalFn(val.value + val.operator + val.thresholdValue)
+                    )"
+                    :key="index"
+                  >
+                    点位:{{ item.attributeName }} 差值:{{
+                      parseFloat((item.value - item.thresholdValue).toFixed(3))
+                    }}
+                  </div>
+                </div>
+              </span>
+              <img
+                class="alarm-icon"
+                :src="getAlarmSrc(gmj1.alarm?.alarmLevel)"
+                alt="alarm"
+              />
             </div>
+
             <div class="label data-box" style="left: 7%; top: 41%">{{
               gmj1.频率.value.value + ' ' + gmj1.频率.value.unit
             }}</div>
@@ -82,16 +103,35 @@
               >2#给煤机</div
             >
             <div
-              style="left: 4.5%; top: 65%"
+              style="left: 6%; top: 68%"
               class="alarm-badge"
               v-if="gmj2.alarm.alarmLevel"
               :class="{
                 'alarm-yellow': gmj2.alarm.alarmLevel == 1,
                 'alarm-orange': gmj2.alarm.alarmLevel == 2,
-                'alarm-red': gmj2.alarm.alarmLevel == 3
+                'alarm-red': [3, 4, 5].includes(gmj2.alarm.alarmLevel)
               }"
             >
-              <i class="el-icon-bell alarm-icon"></i>
+              <span class="alarm-text">
+                <div v-if="gmj2.alarm && gmj2.alarm?.deviceData">
+                  <div
+                    v-for="(item, index) in gmj2.alarm?.deviceData.filter(
+                      (val) =>
+                        evalFn(val.value + val.operator + val.thresholdValue)
+                    )"
+                    :key="index"
+                  >
+                    点位:{{ item.attributeName }} 差值:{{
+                      parseFloat((item.value - item.thresholdValue).toFixed(3))
+                    }}
+                  </div>
+                </div>
+              </span>
+              <img
+                class="alarm-icon"
+                :src="getAlarmSrc(gmj2.alarm?.alarmLevel)"
+                alt="alarm"
+              />
             </div>
             <div class="label data-box" style="left: 7%; top: 59.5%">{{
               gmj2.频率.value.value + ' ' + gmj2.频率.value.unit
@@ -111,10 +151,28 @@
               :class="{
                 'alarm-yellow': lt.alarm.alarmLevel == 1,
                 'alarm-orange': lt.alarm.alarmLevel == 2,
-                'alarm-red': lt.alarm.alarmLevel == 3
+                'alarm-red': [3, 4, 5].includes(lt.alarm.alarmLevel)
               }"
             >
-              <i class="el-icon-bell alarm-icon"></i>
+              <span class="alarm-text">
+                <div v-if="lt.alarm && lt.alarm?.deviceData">
+                  <div
+                    v-for="(item, index) in lt.alarm?.deviceData.filter((val) =>
+                      evalFn(val.value + val.operator + val.thresholdValue)
+                    )"
+                    :key="index"
+                  >
+                    点位:{{ item.attributeName }} 差值:{{
+                      parseFloat((item.value - item.thresholdValue).toFixed(3))
+                    }}
+                  </div>
+                </div>
+              </span>
+              <img
+                class="alarm-icon"
+                :src="getAlarmSrc(lt.alarm?.alarmLevel)"
+                alt="alarm"
+              />
             </div>
 
             <!-- 锅炉上部 -->
@@ -172,17 +230,37 @@
               style="left: 46.6%; top: 12.6%; color: #333; text-shadow: none"
               >除尘器</div
             >
+
             <div
-              style="left: 47.5%; top: 16%"
+              style="left: 47.5%; top: 20%"
               class="alarm-badge"
               v-if="ccq.alarm.alarmLevel"
               :class="{
                 'alarm-yellow': ccq.alarm.alarmLevel == 1,
                 'alarm-orange': ccq.alarm.alarmLevel == 2,
-                'alarm-red': ccq.alarm.alarmLevel == 3
+                'alarm-red': [3, 4, 5].includes(ccq.alarm.alarmLevel)
               }"
             >
-              <i class="el-icon-bell alarm-icon"></i>
+              <span class="alarm-text">
+                <div v-if="ccq.alarm && ccq.alarm?.deviceData">
+                  <div
+                    v-for="(item, index) in ccq.alarm?.deviceData.filter(
+                      (val) =>
+                        evalFn(val.value + val.operator + val.thresholdValue)
+                    )"
+                    :key="index"
+                  >
+                    点位:{{ item.attributeName }} 差值:{{
+                      parseFloat((item.value - item.thresholdValue).toFixed(3))
+                    }}
+                  </div>
+                </div>
+              </span>
+              <img
+                class="alarm-icon"
+                :src="getAlarmSrc(ccq.alarm?.alarmLevel)"
+                alt="alarm"
+              />
             </div>
             <!-- 二次风机 -->
             <div class="label device-name" style="left: 59%; top: 43%"
@@ -194,16 +272,35 @@
             >
 
             <div
-              style="left: 60%; top: 62%"
+              style="left: 60%; top: 60%"
               class="alarm-badge"
               v-if="ecfj.alarm.alarmLevel"
               :class="{
                 'alarm-yellow': ecfj.alarm.alarmLevel == 1,
                 'alarm-orange': ecfj.alarm.alarmLevel == 2,
-                'alarm-red': ecfj.alarm.alarmLevel == 3
+                'alarm-red': [3, 4, 5].includes(ecfj.alarm.alarmLevel)
               }"
             >
-              <i class="el-icon-bell alarm-icon"></i>
+              <span class="alarm-text">
+                <div v-if="ecfj.alarm && ecfj.alarm?.deviceData">
+                  <div
+                    v-for="(item, index) in ecfj.alarm?.deviceData.filter(
+                      (val) =>
+                        evalFn(val.value + val.operator + val.thresholdValue)
+                    )"
+                    :key="index"
+                  >
+                    点位:{{ item.attributeName }} 差值:{{
+                      parseFloat((item.value - item.thresholdValue).toFixed(3))
+                    }}
+                  </div>
+                </div>
+              </span>
+              <img
+                class="alarm-icon"
+                :src="getAlarmSrc(ecfj.alarm?.alarmLevel)"
+                alt="alarm"
+              />
             </div>
 
             <!-- 一次风机 -->
@@ -217,17 +314,37 @@
                 ycfj.频率.value.value + ' ' + ycfj.频率.value.unit
               }}</div
             >
+
             <div
-              style="left: 49.5%; top: 88%"
+              style="left: 49.5%; top: 85%"
               class="alarm-badge"
               v-if="ycfj.alarm.alarmLevel"
               :class="{
                 'alarm-yellow': ycfj.alarm.alarmLevel == 1,
                 'alarm-orange': ycfj.alarm.alarmLevel == 2,
-                'alarm-red': ycfj.alarm.alarmLevel == 3
+                'alarm-red': [3, 4, 5].includes(ycfj.alarm.alarmLevel)
               }"
             >
-              <i class="el-icon-bell alarm-icon"></i>
+              <span class="alarm-text">
+                <div v-if="ycfj.alarm && ycfj.alarm?.deviceData">
+                  <div
+                    v-for="(item, index) in ycfj.alarm?.deviceData.filter(
+                      (val) =>
+                        evalFn(val.value + val.operator + val.thresholdValue)
+                    )"
+                    :key="index"
+                  >
+                    点位:{{ item.attributeName }} 差值:{{
+                      parseFloat((item.value - item.thresholdValue).toFixed(3))
+                    }}
+                  </div>
+                </div>
+              </span>
+              <img
+                class="alarm-icon"
+                :src="getAlarmSrc(ycfj.alarm?.alarmLevel)"
+                alt="alarm"
+              />
             </div>
             <!-- 引风机 -->
             <div class="label device-name" style="left: 72%; top: 31%"
@@ -236,17 +353,37 @@
             <div class="label data-box" style="left: 71%; top: 56%"
               >功率: {{ yfj.频率.value.value + ' ' + yfj.频率.value.unit }}</div
             >
+
             <div
-              style="left: 73%; top: 47%"
+              style="left: 73%; top: 45%"
               class="alarm-badge"
               v-if="yfj.alarm.alarmLevel"
               :class="{
                 'alarm-yellow': yfj.alarm.alarmLevel == 1,
                 'alarm-orange': yfj.alarm.alarmLevel == 2,
-                'alarm-red': yfj.alarm.alarmLevel == 3
+                'alarm-red': [3, 4, 5].includes(yfj.alarm.alarmLevel)
               }"
             >
-              <i class="el-icon-bell alarm-icon"></i>
+              <span class="alarm-text">
+                <div v-if="yfj.alarm && yfj.alarm?.deviceData">
+                  <div
+                    v-for="(item, index) in yfj.alarm?.deviceData.filter(
+                      (val) =>
+                        evalFn(val.value + val.operator + val.thresholdValue)
+                    )"
+                    :key="index"
+                  >
+                    点位:{{ item.attributeName }} 差值:{{
+                      parseFloat((item.value - item.thresholdValue).toFixed(3))
+                    }}
+                  </div>
+                </div>
+              </span>
+              <img
+                class="alarm-icon"
+                :src="getAlarmSrc(yfj.alarm?.alarmLevel)"
+                alt="alarm"
+              />
             </div>
           </div>
         </div>
@@ -274,30 +411,50 @@
                 zzq.主蒸汽温度.value.value + ' ' + zzq.主蒸汽温度.value.unit
               }}</div
             >
+
             <div
-              style="left: 6%; top: 17%"
+              style="left: 7%; top: 16.5%"
               class="alarm-badge"
               v-if="zzq.alarm.alarmLevel"
               :class="{
                 'alarm-yellow': zzq.alarm.alarmLevel == 1,
                 'alarm-orange': zzq.alarm.alarmLevel == 2,
-                'alarm-red': zzq.alarm.alarmLevel == 3
+                'alarm-red': [3, 4, 5].includes(zzq.alarm.alarmLevel)
               }"
             >
-              <i class="el-icon-bell alarm-icon"></i>
+              <span class="alarm-text">
+                <div v-if="zzq.alarm && zzq.alarm?.deviceData">
+                  <div
+                    v-for="(item, index) in zzq.alarm?.deviceData.filter(
+                      (val) =>
+                        evalFn(val.value + val.operator + val.thresholdValue)
+                    )"
+                    :key="index"
+                  >
+                    点位:{{ item.attributeName }} 差值:{{
+                      parseFloat((item.value - item.thresholdValue).toFixed(3))
+                    }}
+                  </div>
+                </div>
+              </span>
+              <img
+                class="alarm-icon"
+                :src="getAlarmSrc(zzq.alarm?.alarmLevel)"
+                alt="alarm"
+              />
             </div>
-
             <div
               class="label device-name"
               style="
                 left: 21.5%;
                 top: 25%;
                 font-size: 15px;
-                color: #fff;
+                color: #000;
+                text-shadow: none;
               "
               >高温过热器</div
             >
-  
+
             <!-- 低温过热器 -->
             <div class="label data-box" style="left: 35.9%; top: 0"
               >温度:{{
@@ -318,23 +475,42 @@
                 left: 36%;
                 top: 25%;
                 font-size: 15px;
-                color: #fff;
+                color: #000;
+                text-shadow: none;
               "
               >低温过热器</div
             >
-     
 
             <div
-              style="left: 37%; top: 20%"
-              class="alarm-badge"
+              style="left: 37%; top: 18%"
               v-if="dwgrq.alarm.alarmLevel"
+              class="alarm-badge"
               :class="{
                 'alarm-yellow': dwgrq.alarm.alarmLevel == 1,
                 'alarm-orange': dwgrq.alarm.alarmLevel == 2,
-                'alarm-red': dwgrq.alarm.alarmLevel == 3
+                'alarm-red': [3, 4, 5].includes(dwgrq.alarm.alarmLevel)
               }"
             >
-              <i class="el-icon-bell alarm-icon"></i>
+              <span class="alarm-text">
+                <div v-if="dwgrq.alarm && dwgrq.alarm?.deviceData">
+                  <div
+                    v-for="(item, index) in dwgrq.alarm?.deviceData.filter(
+                      (val) =>
+                        evalFn(val.value + val.operator + val.thresholdValue)
+                    )"
+                    :key="index"
+                  >
+                    点位:{{ item.attributeName }} 差值:{{
+                      parseFloat((item.value - item.thresholdValue).toFixed(3))
+                    }}
+                  </div>
+                </div>
+              </span>
+              <img
+                class="alarm-icon"
+                :src="getAlarmSrc(dwgrq.alarm?.alarmLevel)"
+                alt="alarm"
+              />
             </div>
             <!-- 气包 -->
             <div
@@ -343,22 +519,41 @@
                 left: 53.2%;
                 top: 30%;
                 font-size: 15px;
-                color: #fff;
+                color: #000;
+                text-shadow: none;
               "
               >汽包</div
             >
 
             <div
-              style="left: 52%; top: 29%"
+              style="left: 51%; top: 29%"
               class="alarm-badge"
               v-if="qp.alarm.alarmLevel"
               :class="{
                 'alarm-yellow': qp.alarm.alarmLevel == 1,
                 'alarm-orange': qp.alarm.alarmLevel == 2,
-                'alarm-red': qp.alarm.alarmLevel == 3
+                'alarm-red': [3, 4, 5].includes(qp.alarm.alarmLevel)
               }"
             >
-              <i class="el-icon-bell alarm-icon"></i>
+              <span class="alarm-text">
+                <div v-if="qp.alarm && qp.alarm?.deviceData">
+                  <div
+                    v-for="(item, index) in qp.alarm?.deviceData.filter((val) =>
+                      evalFn(val.value + val.operator + val.thresholdValue)
+                    )"
+                    :key="index"
+                  >
+                    点位:{{ item.attributeName }} 差值:{{
+                      parseFloat((item.value - item.thresholdValue).toFixed(3))
+                    }}
+                  </div>
+                </div>
+              </span>
+              <img
+                class="alarm-icon"
+                :src="getAlarmSrc(qp.alarm?.alarmLevel)"
+                alt="alarm"
+              />
             </div>
             <div class="label data-box" style="left: 53%; top: 40%"
               >汽包液位:{{
@@ -377,20 +572,42 @@
               }}</div
             >
             <!-- 省煤器 -->
-            <div class="label device-name" style="left: 71%; top: 37%"
+            <div
+              class="label device-name"
+              style="left: 71%; top: 37%; font-size: 15px"
               >省煤器</div
             >
+
             <div
-              style="left: 71%; top: 30%"
-              class="alarm-badge"
+              style="left: 71%; top: 29%"
               v-if="smq.alarm.alarmLevel"
+              class="alarm-badge"
               :class="{
                 'alarm-yellow': smq.alarm.alarmLevel == 1,
                 'alarm-orange': smq.alarm.alarmLevel == 2,
-                'alarm-red': smq.alarm.alarmLevel == 3
+                'alarm-red': [3, 4, 5].includes(smq.alarm.alarmLevel)
               }"
             >
-              <i class="el-icon-bell alarm-icon"></i>
+              <span class="alarm-text">
+                <div v-if="smq.alarm && smq.alarm?.deviceData">
+                  <div
+                    v-for="(item, index) in smq.alarm?.deviceData.filter(
+                      (val) =>
+                        evalFn(val.value + val.operator + val.thresholdValue)
+                    )"
+                    :key="index"
+                  >
+                    点位:{{ item.attributeName }} 差值:{{
+                      parseFloat((item.value - item.thresholdValue).toFixed(3))
+                    }}
+                  </div>
+                </div>
+              </span>
+              <img
+                class="alarm-icon"
+                :src="getAlarmSrc(smq.alarm?.alarmLevel)"
+                alt="alarm"
+              />
             </div>
             <div class="label data-box" style="left: 70%; top: 6%"
               >出口水温:
@@ -415,7 +632,31 @@
                 'alarm-red': sxyw.alarm.alarmLevel == 3
               }"
             >
-              <i class="el-icon-bell alarm-icon"></i>
+              <span class="alarm-text">
+                <div
+                  v-if="
+                    item.alarmLogStatusDTO && item.alarmLogStatusDTO?.deviceData
+                  "
+                >
+                  <div
+                    v-for="(item, index) in JSON.parse(
+                      item.alarmLogStatusDTO?.deviceData
+                    ).filter((val) =>
+                      evalFn(val.value + val.operator + val.thresholdValue)
+                    )"
+                    :key="index"
+                  >
+                    点位:{{ item.attributeName }} 差值:{{
+                      parseFloat((item.value - item.thresholdValue).toFixed(3))
+                    }}
+                  </div>
+                </div>
+              </span>
+              <img
+                class="alarm-icon"
+                :src="getAlarmSrc(item.alarmLogStatusDTO?.alarmId)"
+                alt="alarm"
+              />
             </div>
 
             <!-- 给水泵 -->
@@ -436,16 +677,66 @@
             >
 
             <div
-              v-if="gs.alarm.alarmLevel"
-              style="left: 5%; top: 90%"
+              style="left: 91%; top: 72%"
               class="alarm-badge"
+              v-if="yqpf.alarm.alarmLevel"
+              :class="{
+                'alarm-yellow': yqpf.alarm.alarmLevel == 1,
+                'alarm-orange': yqpf.alarm.alarmLevel == 2,
+                'alarm-red': [3, 4, 5].includes(yqpf.alarm.alarmLevel)
+              }"
+            >
+              <span class="alarm-text">
+                <div v-if="yqpf.alarm && yqpf.alarm?.deviceData">
+                  <div
+                    v-for="(item, index) in yqpf.alarm?.deviceData.filter(
+                      (val) =>
+                        evalFn(val.value + val.operator + val.thresholdValue)
+                    )"
+                    :key="index"
+                  >
+                    点位:{{ item.attributeName }} 差值:{{
+                      parseFloat((item.value - item.thresholdValue).toFixed(3))
+                    }}
+                  </div>
+                </div>
+              </span>
+              <img
+                class="alarm-icon"
+                :src="getAlarmSrc(yqpf.alarm?.alarmLevel)"
+                alt="alarm"
+              />
+            </div>
+
+            <div
+              style="left: 10%; top: 90%"
+              class="alarm-badge"
+              v-if="gs.alarm.alarmLevel"
               :class="{
                 'alarm-yellow': gs.alarm.alarmLevel == 1,
                 'alarm-orange': gs.alarm.alarmLevel == 2,
-                'alarm-red': gs.alarm.alarmLevel == 3
+                'alarm-red': [3, 4, 5].includes(gs.alarm.alarmLevel)
               }"
             >
-              <i class="el-icon-bell alarm-icon"></i>
+              <span class="alarm-text">
+                <div v-if="gs.alarm && gs.alarm?.deviceData">
+                  <div
+                    v-for="(item, index) in gs.alarm?.deviceData.filter((val) =>
+                      evalFn(val.value + val.operator + val.thresholdValue)
+                    )"
+                    :key="index"
+                  >
+                    点位:{{ item.attributeName }} 差值:{{
+                      parseFloat((item.value - item.thresholdValue).toFixed(3))
+                    }}
+                  </div>
+                </div>
+              </span>
+              <img
+                class="alarm-icon"
+                :src="getAlarmSrc(yqpf.alarm?.alarmLevel)"
+                alt="alarm"
+              />
             </div>
 
             <div class="label data-box" style="left: 4%; top: 82%"
@@ -492,24 +783,27 @@
         </div>
       </div>
     </div>
+    <GlobalAudioUnlockButton></GlobalAudioUnlockButton>
   </vue-fullscreen>
 </template>
 
 <script>
   import { component } from 'vue-fullscreen';
   import { getRealData, getRealAlarmLogInfo } from '@/api/pcsData/index.js';
-  import C from 'highlight.js/lib/languages/1c';
+  import audioManager from '@/utils/audioManager.js';
+  import GlobalAudioUnlockButton from '@/components/GlobalAudioUnlockButton.vue';
 
   export default {
     name: 'PcsDataDashboard',
     components: {
-      VueFullscreen: component
+      VueFullscreen: component,
+      GlobalAudioUnlockButton
     },
     data() {
       return {
         //烟气排放
         yqpf: {
-          alarm: { alarmLevel: '' },
+          alarm: { alarmLevel: '', deviceData: [] },
           deviceId: {
             5: '2036027830998446082',
             6: '2036033114630303745',
@@ -565,7 +859,7 @@
         },
         //水箱液位
         sxyw: {
-          alarm: { alarmLevel: '' },
+          alarm: { alarmLevel: '', deviceData: [] },
           deviceId: {
             5: '2008477438320357378',
             6: '2008477438257442817',
@@ -583,7 +877,7 @@
         },
         //主给水
         zgs: {
-          alarm: { alarmLevel: '' },
+          alarm: { alarmLevel: '', deviceData: [] },
           deviceId: {
             5: '2048945828386091010',
             6: '2048945828440616962',
@@ -601,7 +895,7 @@
         },
         //给水
         gs: {
-          alarm: { alarmLevel: '' },
+          alarm: { alarmLevel: '', deviceData: [] },
           deviceId: {
             5: '2048945828327370754',
             6: '2048945828289622018',
@@ -628,7 +922,7 @@
         },
         //省煤器
         smq: {
-          alarm: { alarmLevel: '' },
+          alarm: { alarmLevel: '', deviceData: [] },
           deviceId: {
             5: '2036027830256054273',
             6: '2036033113808220162',
@@ -646,7 +940,7 @@
         },
         //气泡
         qp: {
-          alarm: { alarmLevel: '' },
+          alarm: { alarmLevel: '', deviceData: [] },
           deviceId: {
             5: '2036027832722305025',
             6: '2036033116354162689',
@@ -683,7 +977,7 @@
 
         //主蒸汽
         zzq: {
-          alarm: { alarmLevel: '' },
+          alarm: { alarmLevel: '', deviceData: [] },
           deviceId: {
             5: '2036027831287853057',
             6: '2036033114848407554',
@@ -710,7 +1004,7 @@
         },
         //高温过热器
         gwgrq: {
-          alarm: { alarmLevel: '' },
+          alarm: { alarmLevel: '', deviceData: [] },
           deviceId: {
             5: '2036027830004396033',
             6: '2036033113632059394',
@@ -737,7 +1031,7 @@
         },
         //低温过热器
         dwgrq: {
-          alarm: { alarmLevel: '' },
+          alarm: { alarmLevel: '', deviceData: [] },
           deviceId: {
             5: '2036027829865984001',
             6: '2036033113489453058',
@@ -764,7 +1058,7 @@
         },
         //给煤机
         gmj1: {
-          alarm: { alarmLevel: '' },
+          alarm: { alarmLevel: '', deviceData: [] },
           deviceId: {
             5: '2036027831950553090',
             6: '2036033115469164546',
@@ -781,7 +1075,7 @@
           }
         },
         gmj2: {
-          alarm: { alarmLevel: '' },
+          alarm: { alarmLevel: '', deviceData: [] },
           deviceId: {
             5: '2036027831766003713',
             6: '2036033115326558209',
@@ -799,7 +1093,7 @@
         },
         //料层
         lc: {
-          alarm: { alarmLevel: '' },
+          alarm: { alarmLevel: '', deviceData: [] },
           deviceId: {
             5: '2036027832571310081',
             6: '2036033116027006978',
@@ -826,7 +1120,7 @@
         },
         //炉膛
         lt: {
-          alarm: { alarmLevel: '' },
+          alarm: { alarmLevel: '', deviceData: [] },
           deviceId: {
             5: '2036027829618520065',
             6: '2036033113225211905',
@@ -916,7 +1210,7 @@
         },
         //除尘器
         ccq: {
-          alarm: { alarmLevel: '' },
+          alarm: { alarmLevel: '', deviceData: [] },
           deviceId: {
             5: '2008477400542261249',
             6: '2008477400445792258',
@@ -942,7 +1236,7 @@
           }
         },
         ycfj: {
-          alarm: { alarmLevel: '' },
+          alarm: { alarmLevel: '', deviceData: [] },
           deviceId: {
             5: '2008477365637263361',
             6: '2008477365553377282',
@@ -959,7 +1253,7 @@
           }
         },
         ecfj: {
-          alarm: { alarmLevel: '' },
+          alarm: { alarmLevel: '', deviceData: [] },
           deviceId: {
             5: '2008477365368827905',
             6: '2008477365305913345',
@@ -976,7 +1270,7 @@
           }
         },
         yfj: {
-          alarm: { alarmLevel: '' },
+          alarm: { alarmLevel: '', deviceData: [] },
           deviceId: {
             5: '2008477365154918402',
             6: '2008477365087809538',
@@ -1081,6 +1375,61 @@
       Object.values(this.charts).forEach((chart) => chart && chart.dispose());
     },
     methods: {
+      playAlarmSound() {
+        const devices = [
+          'yqpf',
+          'sxyw',
+          'zgs',
+          'gs',
+          'smq',
+          'qp',
+          'zzq',
+          'gwgrq',
+          'dwgrq',
+          'gmj1',
+          'gmj2',
+          'lc',
+          'lt',
+          'ccq',
+          'ycfj',
+          'ecfj',
+          'yfj'
+        ];
+
+        let list = [];
+        devices.forEach((key) => {
+          list.push(this[key].alarm.alarmLevel);
+        });
+
+        const hasHigh = list.some((item) => [3, 4, 5].includes(item));
+        const hasMedium = list.some((item) => item == 2);
+        const hasLow = list.some((item) => item == 1);
+
+        let src = '';
+        if (hasHigh) {
+          src = require('@/assets/r.mp3');
+        } else if (hasMedium) {
+          src = require('@/assets/g.mp3');
+        } else if (hasLow) {
+          src = require('@/assets/y.mp3');
+        }
+
+        console.log(src, 'src');
+        if (src) {
+          console.log(audioManager, 'audioManager');
+          audioManager.play(src, { loop: false });
+        }
+      },
+      evalFn(val) {
+        return eval(val);
+      },
+      getAlarmSrc(level) {
+        if (level == 1) return require('@/assets/y.gif');
+        if (level == 2) return require('@/assets/g.gif');
+        if (level == 3 || level == 4 || level == 5)
+          return require('@/assets/r.gif');
+        return '';
+      },
       init() {
         const devices = [
           'yqpf',
@@ -1114,9 +1463,12 @@
               devices.forEach((key) => {
                 if (item.deviceId == this[key].deviceId[this.currentId]) {
                   this[key].alarm.alarmLevel = item.alarmId;
+                  this[key].alarm.deviceData =
+                    (item.deviceData && JSON.parse(item.deviceData)) || [];
                 }
               });
             });
+            this.playAlarmSound();
           });
         }
       },
@@ -1465,7 +1817,8 @@
     }
 
     .alarm-icon {
-      font-size: 24px;
+      width: 30px;
+      height: 30px;
     }
 
     &.alarm-yellow {

BIN
src/views/home/微信图片_20260526155749_19_349.jpg