yusheng 9 stundas atpakaļ
vecāks
revīzija
0925068e99

BIN
src/assets/g.gif


BIN
src/assets/g.mp3


BIN
src/assets/r.gif


BIN
src/assets/r.mp3


BIN
src/assets/y.gif


BIN
src/assets/y.mp3


+ 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: 75px;
+  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>

+ 147 - 0
src/utils/audioManager.js

@@ -0,0 +1,147 @@
+// 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次
+  }
+
+  /**
+   * 停止所有正在播放的音频
+   */
+  stopAll() {
+    this.audioInstances.forEach((item) => {
+      item.audio.pause();
+      item.audio.src = '';
+      item.audio.onended = null; // 清除事件监听
+    });
+    this.audioInstances = [];
+    console.log('🛑 已停止所有音频');
+  }
+
+  /**
+   * 播放音频(自动停止之前的音频,并支持自定义重复次数)
+   * @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,
+    });
+
+    // 开始播放
+    audio.play().catch((err) => {
+      // 静音状态下一般不会报错,但若报错则忽略
+      console.warn('音频播放失败(自动播放策略):', err);
+    });
+
+    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;
+    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();

+ 164 - 11
src/views/equipmentOperationMonitoring/index.vue

@@ -110,7 +110,7 @@
               </div>
 
               <!-- 更新时间 -->
-              <div class="update-time">更新时间:{{ updateTime }}</div>
+              <div class="update-time">更新时间:{{ updateTime }} </div>
 
               <!-- 卡片视图 -->
               <div
@@ -158,10 +158,36 @@
                             item.alarmLogStatusDTO?.alarmId == 4
                         }"
                       >
-                        <span class="alarm-text">{{
-                          getAlarmLevelText(item.alarmLogStatusDTO?.alarmId)
-                        }}</span>
-                        <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>
                       <div class="card-header">
                         <div>{{item.postName|}}</div>
@@ -240,6 +266,7 @@
         </ele-split-layout>
       </el-card>
     </div>
+    <GlobalAudioUnlockButton></GlobalAudioUnlockButton>
   </vue-fullscreen>
 </template>
 
@@ -253,9 +280,16 @@
   import { businessStatus } from '@/utils/dict/warehouse';
   import DeptSelect from '@/components/CommomSelect/dept-selectNew.vue';
   import { component } from 'vue-fullscreen';
+  import audioManager  from '@/utils/audioManager.js';
+  
+  import GlobalAudioUnlockButton from '@/components/GlobalAudioUnlockButton.vue';
   export default {
     mixins: [dictMixins, tableColumnsMixin],
-    components: { DeptSelect, VueFullscreen: component },
+    components: {
+      DeptSelect,
+      VueFullscreen: component,
+      GlobalAudioUnlockButton
+    },
     data() {
       return {
         // 搜索表单
@@ -279,6 +313,7 @@
           children: 'children',
           label: 'name'
         },
+        src: '',
         // 分页参数
         pageNum: 1,
         pageSize: 10,
@@ -290,7 +325,9 @@
         categoryLevelId: '',
         rootCategoryLevelId: '',
         loadingMore: false,
-        loading: false
+        loading: false,
+        audioInstance: null,
+        pollingTimer: null
       };
     },
     computed: {},
@@ -332,6 +369,20 @@
           parentIdField: 'parentId'
         });
       });
+      // 3分钟轮询
+      this.pollingTimer = setInterval(() => {
+        this.pollDeviceData();
+      }, 3 * 60 * 1000);
+    },
+    beforeDestroy() {
+      if (this.pollingTimer) {
+        clearInterval(this.pollingTimer);
+        this.pollingTimer = null;
+      }
+      if (this.audioInstance) {
+        this.audioInstance.pause();
+        this.audioInstance = null;
+      }
     },
     methods: {
       activeTabChange() {
@@ -365,6 +416,13 @@
         };
         return textMap[level] || '';
       },
+      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 '';
+      },
       loadDeviceData(isLoadMore = false) {
         if (this.loadingMore) return;
         this.loadingMore = true;
@@ -421,6 +479,7 @@
               this.deviceData = list;
             }
             this.total = res.count;
+            this.playAlarmSound();
             console.log(res);
           })
           .catch(() => {
@@ -478,12 +537,96 @@
         };
         return textMap[status] || status;
       },
+      playAlarmSound() {
+        const data = this.deviceData;
+        const hasHigh = data.some((item) =>
+          [3, 4, 5].includes(item.alarmLogStatusDTO?.alarmId)
+        );
+        const hasMedium = data.some(
+          (item) => item.alarmLogStatusDTO?.alarmId == 2
+        );
+        const hasLow = data.some(
+          (item) => item.alarmLogStatusDTO?.alarmId == 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 });
+        }
+      },
+      // 3分钟轮询请求
+      pollDeviceData() {
+        const size = this.deviceData.length;
+        if (!size) return;
+        querySubstanceRunningMonitor({
+          pageNum: 1,
+          size: size,
+          categoryLevelId: this.categoryLevelId,
+          rootCategoryLevelId: this.rootCategoryLevelId,
+          areaId: this.areaId,
+          keyWord: this.keyWord,
+          postId: this.postId
+        })
+          .then((res) => {
+            const list = res.list.map((item) => {
+              let iotList = [];
+              if (item.iotPointDataList) {
+                item.iotPointDataList.forEach((element) => {
+                  let data = item.iotModel.properties.find(
+                    (iotModel) => iotModel.identifier == element.identifier
+                  );
+                  if (data) {
+                    iotList.push({
+                      ...element,
+                      dataType: data.dataType
+                    });
+                  }
+                });
+              }
+              item['iotList'] = item.iotDashboardPoint.length
+                ? iotList.filter((iotListItem) =>
+                    item.iotDashboardPoint.find(
+                      (Point) =>
+                        Point.identifier == iotListItem.identifier &&
+                        Point.checked1
+                    )
+                  )
+                : iotList.filter((iotListItem, index) => index < 4);
+              return item;
+            });
+            this.deviceData = list;
+            this.total = res.count;
+            this.playAlarmSound();
+          })
+          .catch(() => {});
+      },
 
       // 搜索
       handleSearch() {
         this.pageNum = 1;
         this.loadDeviceData();
       },
+      evalFn(val) {
+        return eval(val);
+      },
+      getDeviceData(data) {
+        console.log(data, 'data');
+        if (data) {
+          return JSON.parse(data);
+        } else {
+          return [];
+        }
+      },
       // 重置
       handleReset() {
         this.keyWord = '';
@@ -826,13 +969,13 @@
 
           .alarm-badge {
             position: absolute;
-            top: 10px;
-            right: 10px;
+            top: 15px;
+            right: 130px;
             display: flex;
             align-items: center;
             gap: 4px;
             z-index: 2;
-            animation: alarm-pulse 1.5s infinite;
+            //animation: alarm-pulse 1.5s infinite;
 
             .alarm-text {
               font-size: 12px;
@@ -840,7 +983,8 @@
             }
 
             .alarm-icon {
-              font-size: 18px;
+              width: 30px;
+              height: 30px;
             }
 
             &.alarm-yellow {
@@ -1060,6 +1204,15 @@
     }
   }
 
+  @keyframes alarm-pulse {
+    0%,
+    100% {
+      transform: scale(1);
+    }
+    50% {
+      transform: scale(1.15);
+    }
+  }
   @keyframes alarm-pulse {
     0%,
     100% {

+ 14 - 2
src/views/warning/warningMessage/index.vue

@@ -88,8 +88,16 @@
         >
           <template slot-scope="{ row }">
             <div v-if="row._deviceData && row._deviceData.length">
-              <div v-for="(item, index) in row._deviceData" :key="index">
-                点位:{{ item.attributeName}}&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;    告警值:{{ item.thresholdValue }}&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;  当前值: {{ item.value }} &nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 差值:{{
+              <div
+                v-for="(item, index) in row._deviceData.filter((val) =>
+                  evalFn(val.value + val.operator + val.thresholdValue)
+                )"
+                :key="index"
+              >
+                点位:{{ item.attributeName }}&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
+                告警值:{{ item.thresholdValue }}&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
+                当前值: {{ item.value }} &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
+                差值:{{
                   parseFloat((item.value - item.thresholdValue).toFixed(3))
                 }}
               </div>
@@ -283,6 +291,10 @@
       openEdit(row, type) {
         this.$refs.processDialogRef.open(row, type);
       },
+      evalFn(val) {
+        console.log(val,'val')
+        return eval(val);
+      },
       triggerCount(row) {
         getDetail(row.id).then((res) => {
           let list = Array.isArray(res) ? res : [];

+ 2 - 2
vue.config.js

@@ -40,8 +40,8 @@ module.exports = {
         // target: 'http://192.168.1.139:18086', // 粟
         // target: 'http://192.168.1.132:18086', // 徐
         // target: 'http://192.168.1.125:18086', //本
-        // target: 'http://192.168.1.116:18086', // 赵沙金
-        target: 'http://192.168.1.251:18086', // 测试环境
+        // target: 'http://192.168.1.251:18086', // 赵沙金
+        target: 'http://114.116.248.196:86/api/', // 测试环境
         changeOrigin: true, // 只有这个值为true的情况下 才表示开启跨域
         pathRewrite: {
           '^/api': ''