<template> <div class="alarmBox" :class="getAlarmBoxClass()" :style="`width: ${getAlarmBoxWidth()}px;`" > <div class="columnItem" v-for="columnNumber in $store.state.columnNumber" :key="columnNumber" v-show="alarmList[(columnNumber - 1) * $store.state.alarmShowNumber]" > <div :class="`${ index >= (columnNumber - 1) * $store.state.alarmShowNumber && index < columnNumber * $store.state.alarmShowNumber ? item.class + ' alarmItem animate__animated' : '' }`" v-for="(item, index) in alarmList" :key="index" > <template v-if=" index >= (columnNumber - 1) * $store.state.alarmShowNumber && index < columnNumber * $store.state.alarmShowNumber " > <div class="alarmTitle">{{ item.wpName }} {{ item.code }}</div> <div class="alarmContent"> <div class="contentItem" @click="goToAlertDescPage(item)"> 报警描述: <span class="alertDescCursor">{{ item.description }}</span> </div> <div class="contentItem">报警时间:{{ item.tsName }}</div> </div> <div class="btnBox" :class="`lv${item.lv}BdColor`"> <div class="btnItem" :class="`lv${item.lv}BdColor lv${item.lv}`"> <el-button class="comfirmBtn" size="small" type="text" @click="comfirm(item)" >确认本条</el-button > </div> <div class="btnItem" :class="`lv${item.lv}`"> <el-button class="comfirmBtn" size="small" type="text" @click="comfirmAll" >全部确认</el-button > </div> </div> </template> </div> </div> <div class="collapseBtn" v-if="alarmList?.[0]?.id && $store.state.alarmShowNumber > 0" @click=" () => { isCollapse = !isCollapse; } " > <el-link type="primary" href="javascript:;"> <!-- <el-icon color="#909399"><CaretBottom /></el-icon> --> <span style="margin-left: 6px" v-if="!isCollapse">点此折叠报警</span> <span style="margin-left: 6px; color: var(--el-color-danger)" v-else >报警已折叠,点此恢复</span > </el-link> </div> </div> </template> <script> import { confirmAlart, alarm_history } from "@api/api.js"; import { ElNotification } from "element-plus"; import dayJs from "dayjs"; export default { data() { return { alarmConfigArray: [], alarmList: [], seriousWarning: false, audioElement: null, ws: null, timeConnect: 0, limitConnect: 5, columnNumber: 2, showSocketLog: false, isCollapse: false, requestAlarmHistoryParams: [ { alarmType: "booststation", deviceType: "", }, { alarmType: "inverter", deviceType: "", }, { alarmType: "windturbine", deviceType: "", }, { alarmType: "custom", deviceType: "booststation", }, { alarmType: "custom", deviceType: "inverter", }, { alarmType: "custom", deviceType: "windturbine", }, ], // websocket相关 socketObj: "", // websocket实例对象 //心跳检测 heartCheck: { vueThis: this, // vue实例 timeout: 30000, // 超时时间 timeoutObj: null, // 计时器对象——向后端发送心跳检测 serverTimeoutObj: null, // 计时器对象——等待后端心跳检测的回复 // 心跳检测重置 reset: function () { clearTimeout(this.timeoutObj); clearTimeout(this.serverTimeoutObj); return this; }, // 心跳检测启动 start: function () { this.timeoutObj && clearTimeout(this.timeoutObj); this.serverTimeoutObj && clearTimeout(this.serverTimeoutObj); this.timeoutObj = setTimeout(() => { // 这里向后端发送一个心跳检测,后端收到后,会返回一个心跳回复 this.vueThis.socketObj.send("HeartBeat"); this.showSocketLog && console.log("发送心跳检测"); this.serverTimeoutObj = setTimeout(() => { // 如果超过一定时间还没重置计时器,说明websocket与后端断开了 this.showSocketLog && console.log("未收到心跳检测回复"); // 关闭WebSocket this.vuethis.socketObj && this.socketObj.close(); }, this.timeout); }, this.timeout); }, }, socketReconnectTimer: null, // 计时器对象——重连 socketReconnectLock: false, // WebSocket重连的锁 socketLeaveFlag: false, // 离开标记(解决 退出登录再登录 时出现的 多次相同推送 问题,出现的本质是多次建立了WebSocket连接) }; }, created() { this.getAlarmConfig(); let requestResult = []; this.requestAlarmHistoryParams.forEach(({ alarmType, deviceType }) => { requestResult.push(this.getAlarmHistory(alarmType, deviceType)); }); Promise.all(requestResult) .then((promiseResult) => { promiseResult.forEach(({ data }) => { data?.ls?.forEach((ele) => { this.pushALarmItem(ele); }); }); // this.webSocketInit( // `ws://10.81.3.154:6014/websocket/${this.$store.state.user.userId}_${this.$store.state.user.authToken}` // ); this.createWebSocket(); }) .catch(() => { requestResult.forEach((ele, index) => { ele .then(({ data }) => { data?.ls?.forEach((ele) => { this.pushALarmItem(ele); }); }) .catch((error) => { ElNotification({ type: "error", title: "查询历史未处理报警请求出错!", dangerouslyUseHTMLString: true, message: `<div class="currentRequestErrorNotification"> <p><span>主要参数:</p> <p style="color:var(--el-color-primary)"><span class="errorTitle">alarmType:</span><span class="errorDesc">"${this.requestAlarmHistoryParams[index].alarmType}"</span></p> <p style="color:var(--el-color-primary)"><span class="errorTitle">deviceType:</span><span class="errorDesc">"${this.requestAlarmHistoryParams[index].deviceType}"</span></p> <p style="color:var(--el-color-danger)"><span class="errorTitle">错误正文:</span><span class="errorDesc">${error}</span></p> </div>`, }); }); }); }); }, unmounted() { this.socketLeaveFlag = true; this.socketObj && this.socketObj.close(); }, methods: { getAlarmName(alarmItem) { let alarmName = ""; if (alarmItem.deviceType === "booststation") { alarmName = "升压站报警"; } else if (alarmItem.deviceType === "inverter") { alarmName = "光伏报警"; } else if (alarmItem.deviceType === "windturbine") { alarmName = "设备报警"; } else if (alarmItem.deviceType === "station") { alarmName = "场站"; } if (alarmItem.alarmType === "custom") { alarmName = "自定义报警"; } return alarmName; }, getLvName(alarmItem) { if (alarmItem.rank === 1) { return "低级"; } else if (alarmItem.rank === 2) { return "低中级"; } else if (alarmItem.rank === 3) { return "中级"; } else if (alarmItem.rank === 4) { return "中高级"; } else if (alarmItem.rank === 5) { return "高级"; } }, comfirm(item) { this.$confirm("您确定要执行此操作吗?", "提示", { confirmButtonText: "确定", cancelButtonText: "取消", type: "warning", }).then(() => { item.class = `animate__bounceOutRight lv${item.lv}`; item.confirm = true; setTimeout(() => { item.class = `animate__bounceOutRight hidden lv${item.lv}`; confirmAlart([item]).then((res) => { if (res.code === 200) { this.BASE.showMsg({ type: "success", msg: "确认成功", }); this.$store.commit("removeWarning", item); this.playAudioEffect(); } }); }, 500); }); }, comfirmAll() { this.$confirm("您确定要执行此操作吗?", "提示", { confirmButtonText: "确定", cancelButtonText: "取消", type: "warning", }).then(() => { for (let i = 0; i < this.alarmList.length; i++) { if (!this.alarmList[i].comfirm) { this.alarmList[ i ].class = `animate__bounceOutRight lv${this.alarmList[i].lv}`; this.alarmList[i].confirm = true; setTimeout(() => { this.alarmList[i] && (this.alarmList[i].class = `animate__bounceOutRight hidden lv${ this.alarmList[i]?.lv || 3 }`); }, 500); } } confirmAlart(this.alarmList).then((res) => { if (res.code === 200) { this.BASE.showMsg({ type: "success", msg: "全部确认成功", }); this.alarmList = []; this.$store.commit("emptyWarning"); this.playAudioEffect(); } }); this.playAudioEffect(); }); }, playAudioEffect() { const lv1Config = this.getConfigItem(1); let lv1Play = false; if (lv1Config.isAlarmSound) { lv1Play = this.alarmList.some((ele) => { return ele.lv === 1 && !ele.confirm; }); } const lv2Config = this.getConfigItem(2); let lv2Play = false; if (lv2Config.isAlarmSound) { lv2Play = this.alarmList.some((ele) => { return ele.lv === 2 && !ele.confirm; }); } const lv3Config = this.getConfigItem(3); let lv3Play = false; if (lv3Config.isAlarmSound) { lv3Play = this.alarmList.some((ele) => { return ele.lv === 3 && !ele.confirm; }); } const lv4Config = this.getConfigItem(4); let lv4Play = false; if (lv4Config.isAlarmSound) { lv4Play = this.alarmList.some((ele) => { return ele.lv === 4 && !ele.confirm; }); } const lv5Config = this.getConfigItem(5); let lv5Play = false; if (lv5Config.isAlarmSound) { lv5Play = this.alarmList.some((ele) => { return ele.lv === 5 && !ele.confirm; }); } if (lv5Play && !this.seriousWarning) { this.seriousWarning = true; this.audioElement = new Audio(); this.audioElement.src = "./static/sound/lv5.mp3"; this.audioElement.loop = true; this.audioElement?.play(); } else if ( (lv1Play || lv2Play || lv3Play || lv4Play) && !this.seriousWarning ) { this.audioElement = new Audio(); this.audioElement.src = "./static/sound/lv4.mp3"; this.audioElement.addEventListener("ended", () => { this.audioElement?.removeEventListener( "ended", this.stopPlayAudioEffect ); }); this.audioElement?.play(); } else { if (!this.seriousWarning) { this.stopPlayAudioEffect(); } } }, stopPlayAudioEffect() { this.seriousWarning = false; if (this.audioElement) { this.audioElement.pause(); this.audioElement.currentTime = 0; this.audioElement.loop = false; } this.audioElement = null; }, getAlarmHistory(alarmType, deviceType) { let params = { pageNum: 1, pageSize: 10, alarmId: "", alarmType, deviceType, stationid: "", deviceid: "", modelId: "", components: "", description: "", begin: dayJs().add(-1, "hour").format("YYYY-MM-DD HH:mm:ss"), end: dayJs().format("YYYY-MM-DD HH:mm:ss"), isclose: false, }; if ( params.alarmType == "windturbine" || (params.alarmType == "custom" && params.deviceType == "windturbine") ) { params.stationid = "SXJ_KGDL_DJY_FDC_STA"; } else if ( params.alarmType == "inverter" || (params.alarmType == "custom" && params.deviceType == "inverter") ) { params.stationid = "SXJ_KGDL_JR_GDC_STA"; } return alarm_history(params, 12000); }, // websocket启动 createWebSocket() { let webSocketLink = `ws://10.81.3.154:6014/websocket/${this.$store.state.user.userId}_${this.$store.state.user.authToken}`; // webSocket地址 try { if ("WebSocket" in window) { this.socketObj = new WebSocket(webSocketLink); } // websocket事件绑定 this.socketEventBind(); } catch (e) { this.showSocketLog && console.log("catch" + e); // websocket重连 this.socketReconnect(); } }, // websocket事件绑定 socketEventBind() { // 连接成功建立的回调 this.socketObj.onopen = this.onopenCallback; // 连接发生错误的回调 this.socketObj.onerror = this.onerrorCallback; // 连接关闭的回调 this.socketObj.onclose = this.oncloseCallback; // 向后端发送数据的回调 this.socketObj.onsend = this.onsendCallback; // 接收到消息的回调 this.socketObj.onmessage = this.getMessageCallback; //监听窗口关闭事件,当窗口关闭时,主动去关闭websocket连接,防止连接还没断开就关闭窗口,server端会抛异常。 window.onbeforeunload = () => { this.socketObj.close(); }; }, // websocket重连 socketReconnect() { if (this.socketReconnectLock) { return; } this.socketReconnectLock = true; this.socketReconnectTimer && clearTimeout(this.socketReconnectTimer); this.socketReconnectTimer = setTimeout(() => { this.showSocketLog && console.log("WebSocket:重连中..."); this.socketReconnectLock = false; // websocket启动 this.createWebSocket(); }, 4000); }, // 连接成功建立的回调 onopenCallback(event) { this.showSocketLog && console.log("WebSocket:已连接"); // 心跳检测重置 this.heartCheck.reset().start(); }, // 连接发生错误的回调 onerrorCallback(event) { this.showSocketLog && console.log("WebSocket:发生错误"); // websocket重连 this.socketReconnect(); }, // 连接关闭的回调 oncloseCallback(event) { this.showSocketLog && console.log("WebSocket:已关闭"); // 心跳检测重置 this.heartCheck.reset(); if (!this.socketLeaveFlag) { // 没有离开——重连 // websocket重连 this.socketReconnect(); } else { this.socketObj && this.socketObj.close(); } }, // 向后端发送数据的回调 onsendCallback() { this.showSocketLog && console.log("WebSocket:发送信息给后端"); }, // 接收到消息的回调 getMessageCallback(msg) { // console.log(msg); if (Object.keys(msg) && msg.data == "ok") { // 心跳回复——心跳检测重置 // 收到心跳检测回复就说明连接正常 this.showSocketLog && console.log("收到心跳检测回复"); // 心跳检测重置 this.heartCheck.reset().start(); } else { // 普通推送——正常处理 this.showSocketLog && console.log("收到推送消息"); // 相关处理 let alarmItem = JSON.parse(msg.data); if (alarmItem) { this.pushALarmItem(alarmItem); } } }, // webSocketInit(serveIP) { // if ("WebSocket" in window) { // this.ws = new WebSocket(serveIP); // this.ws.onmessage = (res) => { // let alarmItem = JSON.parse(res.data); // if (alarmItem) { // this.pushALarmItem(alarmItem); // } // }; // this.ws.onclose = () => { // this.ws = null; // }; // this.ws.onopen = () => { // this.timeConnect = 0; // this.showSocketLog && console.log("WebSocket 服务已建立"); // }; // this.ws.onerror = () => { // this.reconnect(serveIP); // }; // } else { // this.BASE.showMsg({ // msg: "当前浏览器不支持 WebSocket ,请更换浏览器后重试", // }); // } // }, pushALarmItem(alarmItem) { const configItem = this.getConfigItem(alarmItem.rank); const alarmOption = { id: alarmItem.id, lv: alarmItem.rank, lvName: this.getLvName(alarmItem), rank: alarmItem.rank, class: `animate__bounceInRight lv${alarmItem.rank}`, isClose: alarmItem.closeTime ? true : alarmItem.endts ? true : false, isCloseName: alarmItem.isClose ? "已解除" : "未解除", alarmId: alarmItem.alarmId, alarmType: alarmItem.alarmType, alarmName: this.getAlarmName(alarmItem), description: alarmItem.description, deviceType: alarmItem.deviceType, oval: alarmItem.oval, triggerType: alarmItem.triggerType, ts: alarmItem.ts, tsName: new Date(alarmItem.ts).formatDate("MM-dd hh:mm:ss"), fullTsName: new Date(alarmItem.ts).formatDate("yyyy-MM-dd hh:mm:ss"), endts: alarmItem.endts ? dayjs(alarmItem.endts).format("YYYY-MM-DD HH:mm:ss") : alarmItem.closeTime ? dayjs(alarmItem.closeTime).format("YYYY-MM-DD HH:mm:ss") : null, // endtsName: // alarmItem.endts > 0 // ? new Date(alarmItem.endts).formatDate("yyyy-MM-dd hh:mm:ss") // : "", endtsName: alarmItem.endts > 0 ? new Date(alarmItem.endts).formatDate("yyyy-MM-dd hh:mm:ss") : "", deviceId: alarmItem.deviceId, faultCause: alarmItem.faultCause, resolvent: alarmItem.resolvent, characteristic: alarmItem.characteristic, code: alarmItem.code, wpName: alarmItem.wpName, stationId: alarmItem.stationid, }; if ( configItem.isAlarmSound || configItem.isAlart || configItem.isContinuousAlarm ) { this.alarmList.push(alarmOption); } this.$store.commit("setWarning", alarmOption); this.alarmList.sort((a, b) => { return b.lv - a.lv; }); this.playAudioEffect(); }, reconnect(serveIP) { if (this.timeConnect < this.limitConnect) { console.log(`webSocket 连接失败,第 ${++this.timeConnect} 次重连`); setTimeout(() => { this.webSocketInit(serveIP); }, 2000); } else { console.log("webSocket 连接已超时"); this.BASE.showMsg({ showClose: true, duration: 0, msg: `webSocket 连接超时,实时报警获取失败`, }); this.ws?.close(); } }, goToAlertDescPage({ deviceId, alarmId, deviceType, alarmType, ts, stationId, }) { if (alarmType == "custom") { this.$router.push( `/safe/customWarning/${deviceId}/${alarmId}/${deviceType}` ); } else if (alarmType == "booststation") { this.$router.push( `/safe/historywaring/${stationId}/${alarmId}/${deviceType}/${ts}` ); } else { this.$router.push( `/safe/historywaring/${deviceId}/${alarmId}/${deviceType}` ); } }, getConfigItem(lv) { return ( this.alarmConfigArray.find((ele) => { return ele.alarmLevel === lv; }) || {} ); }, getAlarmBoxClass() { let classList = []; if (this.alarmList?.length) classList.push("notEmpty"); if (this.isCollapse) classList.push("collapseAlarmBox"); return classList.join(" "); }, getAlarmBoxWidth() { const baseWIdth = 240; let widthStep = 0; for (let i = 0; i < this.$store.state.columnNumber; i++) { if (this.alarmList?.[i * this.$store.state.columnNumber]) { widthStep++; } } return widthStep * baseWIdth; }, getAlarmConfig() { if (localStorage.getItem("alarmConfigArray")) { this.alarmConfigArray = JSON.parse( localStorage.getItem("alarmConfigArray") ); } else { this.alarmConfigArray = [ { id: "1", alarmLevel: 1, isAlart: false, isAlarmSound: false, isContinuousAlarm: false, }, { id: "2", alarmLevel: 2, isAlart: false, isAlarmSound: false, isContinuousAlarm: false, }, { id: "3", alarmLevel: 3, isAlart: true, isAlarmSound: false, isContinuousAlarm: false, }, { id: "4", alarmLevel: 4, isAlart: true, isAlarmSound: true, isContinuousAlarm: false, }, { id: "5", alarmLevel: 5, isAlart: true, isAlarmSound: true, isContinuousAlarm: true, }, ]; localStorage.setItem( "alarmConfigArray", JSON.stringify(this.alarmConfigArray) ); } }, }, watch: { "$store.state.alarmResetFlg"() { // getAlartConfig() // .then((res) => { // this.alarmConfigArray = res.data; // }) // .catch(() => { // this.BASE.showMsg({ // msg: "报警配置获取失败,请重试", // }); // }); this.getAlarmConfig(); }, }, }; </script> <style lang="scss" scoped> .alarmBox { height: calc(100% - 180px); padding: 0 12px 15px 30px; position: absolute; right: 0; bottom: 0; z-index: 1000; display: flex; justify-content: flex-end; align-items: flex-end; pointer-events: none; overflow: hidden; .columnItem { width: 240px; height: 100%; display: flex; flex-direction: column-reverse; align-items: center; font-size: 12px; overflow-y: scroll; border-radius: 8px; margin-left: 4px; transition: 0.2s; .alarmItem { width: 100%; box-sizing: border-box; border-radius: 8px; border: 1px solid #ebeef5; background: #1890ff; margin-bottom: 4px; box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1); color: #fff; pointer-events: auto; cursor: pointer; .alarmTitle { display: flex; justify-content: flex-start; align-content: center; width: calc(100% - 16px); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; padding: 0 8px; margin-top: 8px; } .alarmContent { width: calc(100% - 16px); display: flex; justify-content: flex-start; align-items: flex-start; flex-wrap: wrap; margin-top: 4px; padding: 0 8px; .contentItem { width: 100%; display: flex; justify-content: flex-start; align-items: flex-start; margin-bottom: 2px; .alertDescCursor { width: 160px; cursor: pointer; transition: 0.2s; &:hover { color: var(--el-color-primary); text-decoration: underline; transition: 0.2s; } } &:last-child { margin-bottom: 0; } } } .btnBox { display: flex; width: 100%; justify-content: center; align-items: center; margin-top: 4px; border-top: 1px solid red; .btnItem { width: 50%; display: flex; justify-content: center; align-items: center; padding: 4px 0; .el-button { padding: 0; width: 45%; height: 20px; min-height: 20px; } &.lv1 .el-button, &.lv2 .el-button, &.lv3 .el-button { color: var(--el-color-info) !important; } &.lv4 .el-button { color: rgb(50, 65, 87) !important; } &.lv5 .el-button { color: #242f42; } &:first-child { border-right: 1px solid red; } } } &.lv5 { background: #fef0f0; border: 1px solid #242f42; color: #242f42; } &.lv4 { background: #f0f9eb; border: 1px solid rgb(50, 65, 87); color: rgb(50, 65, 87); } &.lv1, &.lv2, &.lv3 { background: #fdf6ec; border: 1px solid var(--el-color-info); color: var(--el-color-info); } .lv1BdColor, .lv2BdColor, .lv3BdColor { border-color: var(--el-color-info) !important; } .lv4BdColor { border-color: rgb(50, 65, 87) !important; } .lv5BdColor { border-color: #242f42 !important; } &.hidden { height: 0; padding: 0; margin-bottom: 0; border: 0; transition: 0.2s; overflow: hidden; } } &::-webkit-scrollbar { width: 0; /* 隐藏Webkit浏览器的滚动条宽度 */ height: 0; /* 隐藏Webkit浏览器的滚动条高度 */ } } .collapseBtn { right: 12px; bottom: 0; font-size: 14px; color: #909399; display: flex; justify-content: center; align-items: center; white-space: nowrap; position: absolute; pointer-events: auto; user-select: none; cursor: pointer; transition: 0.2s; } &.notEmpty:hover { // background: rgba(0, 0, 0, 0.12); // box-shadow: 0 0 12px rgba(0, 0, 0, 0.12); transition: 0.2s; } &.collapseAlarmBox { width: 0; height: 0; overflow: hidden; } } </style> <style lang="scss"> .currentRequestErrorNotification { width: 100%; display: flex; flex-direction: column; justify-content: flex-start; align-items: flex-start; p { width: 100%; display: flex; justify-content: flex-start; align-items: center; flex-wrap: wrap; .errorTitle { width: 85px; } .errorDesc { width: calc(100% - 85px); } } } </style>