|
|
|
|
@ -3,6 +3,33 @@
|
|
|
|
|
<router-view />
|
|
|
|
|
<RealtimeAlarmModal />
|
|
|
|
|
<div v-if="offlineAlertActive" class="offline-breath-overlay"></div>
|
|
|
|
|
<div v-if="offlineDialogVisible" class="offline-panel-mask">
|
|
|
|
|
<section class="offline-panel">
|
|
|
|
|
<header class="offline-panel-header">
|
|
|
|
|
<span class="offline-panel-title">离线报警</span>
|
|
|
|
|
<el-button text type="danger" @click="clearOfflineAlarms">关闭</el-button>
|
|
|
|
|
</header>
|
|
|
|
|
<el-table
|
|
|
|
|
ref="offlineAlarmTableRef"
|
|
|
|
|
:data="offlineAlarmRows.length > 8 ? [...offlineAlarmRows, ...offlineAlarmRows] : offlineAlarmRows"
|
|
|
|
|
border
|
|
|
|
|
:show-header="false"
|
|
|
|
|
height="320"
|
|
|
|
|
empty-text="暂无离线报警"
|
|
|
|
|
@mouseenter="offlineTablePaused = true"
|
|
|
|
|
@mouseleave="offlineTablePaused = false"
|
|
|
|
|
>
|
|
|
|
|
<el-table-column label="离线设备">
|
|
|
|
|
<template #default="scope">
|
|
|
|
|
{{ `设备${scope.row.monitorId}离线` }}
|
|
|
|
|
</template>
|
|
|
|
|
</el-table-column>
|
|
|
|
|
</el-table>
|
|
|
|
|
<footer class="offline-panel-footer">
|
|
|
|
|
<el-button type="primary" @click="clearOfflineAlarms">确认</el-button>
|
|
|
|
|
</footer>
|
|
|
|
|
</section>
|
|
|
|
|
</div>
|
|
|
|
|
<el-dialog
|
|
|
|
|
v-model="alarmDetailVisible"
|
|
|
|
|
title="报警信息"
|
|
|
|
|
@ -45,22 +72,20 @@
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
<script setup lang="ts">
|
|
|
|
|
import { ElMessage, ElMessageBox } from 'element-plus';
|
|
|
|
|
import { ElMessage, ElMessageBox, ElNotification } from 'element-plus';
|
|
|
|
|
import { listEmsAlarmActionStep } from '@/api/ems/base/emsAlarmActionStep';
|
|
|
|
|
import { handleExceptions } from '@/api/ems/record/recordAlarmData';
|
|
|
|
|
import RealtimeAlarmModal from '@/components/alarm/RealtimeAlarmModal.vue';
|
|
|
|
|
import { useSettingsStore } from '@/store/modules/settings';
|
|
|
|
|
import { handleThemeStyle } from '@/utils/theme';
|
|
|
|
|
import { useAppStore } from '@/store/modules/app';
|
|
|
|
|
import {
|
|
|
|
|
closeAlarmReminder,
|
|
|
|
|
connectAlarmReminder,
|
|
|
|
|
onAlarmReminderExtra,
|
|
|
|
|
onAlarmReminderOvertime
|
|
|
|
|
} from '@/utils/alarmReminder';
|
|
|
|
|
import { closeAlarmReminder, connectAlarmReminder, onAlarmReminderExtra, onAlarmReminderOvertime } from '@/utils/alarmReminder';
|
|
|
|
|
|
|
|
|
|
const appStore = useAppStore();
|
|
|
|
|
const offlineAlertActive = ref(false);
|
|
|
|
|
const offlineDialogVisible = ref(false);
|
|
|
|
|
const offlineAlarmTableRef = ref();
|
|
|
|
|
const offlineAlarmRows = ref<Array<{ monitorId: string }>>([]);
|
|
|
|
|
const alarmDetailVisible = ref(false);
|
|
|
|
|
const alarmDetailTab = ref('alarm');
|
|
|
|
|
const alarmHandling = ref(false);
|
|
|
|
|
@ -68,7 +93,12 @@ const currentActionSteps = ref<any[]>([]);
|
|
|
|
|
const currentAlarmContents = ref<string[]>([]);
|
|
|
|
|
const alarmDetailQueue = ref<any[]>([]);
|
|
|
|
|
const currentAlarmDetail = ref<any | null>(null);
|
|
|
|
|
const offlineTablePaused = ref(false);
|
|
|
|
|
let offlineAlertTimer: ReturnType<typeof setTimeout> | null = null;
|
|
|
|
|
let notificationTimer: ReturnType<typeof setTimeout> | null = null;
|
|
|
|
|
let offlineTableScrollFrame: number | null = null;
|
|
|
|
|
let offlineTableLastScrollAt = 0;
|
|
|
|
|
const notificationQueue: Array<{ message: string; title: string }> = [];
|
|
|
|
|
|
|
|
|
|
const playAlarmVoice = (message: string) => {
|
|
|
|
|
if (typeof window === 'undefined' || !('speechSynthesis' in window)) {
|
|
|
|
|
@ -97,6 +127,36 @@ const triggerAlarmEffect = () => {
|
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const showAlarmNotification = (message: string, title: string) => {
|
|
|
|
|
notificationQueue.push({ message, title });
|
|
|
|
|
|
|
|
|
|
if (notificationTimer) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const flushNotificationQueue = () => {
|
|
|
|
|
const current = notificationQueue.shift();
|
|
|
|
|
|
|
|
|
|
if (!current) {
|
|
|
|
|
notificationTimer = null;
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ElNotification({
|
|
|
|
|
title: current.title,
|
|
|
|
|
message: current.message,
|
|
|
|
|
type: 'error',
|
|
|
|
|
duration: 0
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
notificationTimer = setTimeout(() => {
|
|
|
|
|
flushNotificationQueue();
|
|
|
|
|
}, 100);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
flushNotificationQueue();
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const showAlarmDialog = (message: string, title: string) => {
|
|
|
|
|
ElMessageBox.alert(message, title, {
|
|
|
|
|
type: 'warning',
|
|
|
|
|
@ -176,6 +236,77 @@ const handleGlobalAlarm = (message: string, title: string) => {
|
|
|
|
|
triggerAlarmEffect();
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const stopOfflineTableScroll = () => {
|
|
|
|
|
if (offlineTableScrollFrame !== null) {
|
|
|
|
|
cancelAnimationFrame(offlineTableScrollFrame);
|
|
|
|
|
offlineTableScrollFrame = null;
|
|
|
|
|
}
|
|
|
|
|
offlineTableLastScrollAt = 0;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const startOfflineTableScroll = async () => {
|
|
|
|
|
if (offlineTableScrollFrame !== null) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!offlineDialogVisible.value || offlineAlarmRows.value.length <= 1) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
await nextTick();
|
|
|
|
|
|
|
|
|
|
const tableEl = offlineAlarmTableRef.value?.$el as HTMLElement | undefined;
|
|
|
|
|
const bodyWrapper = tableEl?.querySelector('.el-scrollbar__wrap') as HTMLElement | null;
|
|
|
|
|
|
|
|
|
|
if (!bodyWrapper || bodyWrapper.scrollHeight <= bodyWrapper.clientHeight) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const scrollStep = (timestamp: number) => {
|
|
|
|
|
if (!offlineDialogVisible.value || offlineAlarmRows.value.length <= 1) {
|
|
|
|
|
stopOfflineTableScroll();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!offlineTableLastScrollAt) {
|
|
|
|
|
offlineTableLastScrollAt = timestamp;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (offlineTablePaused.value) {
|
|
|
|
|
offlineTableLastScrollAt = timestamp;
|
|
|
|
|
} else if (timestamp - offlineTableLastScrollAt >= 16) {
|
|
|
|
|
offlineTableLastScrollAt = timestamp;
|
|
|
|
|
|
|
|
|
|
if (bodyWrapper.scrollTop + bodyWrapper.clientHeight >= bodyWrapper.scrollHeight) {
|
|
|
|
|
bodyWrapper.scrollTop = 0;
|
|
|
|
|
} else {
|
|
|
|
|
bodyWrapper.scrollTop += 1;
|
|
|
|
|
if (bodyWrapper.scrollTop > 40 * offlineAlarmRows.value.length) {
|
|
|
|
|
bodyWrapper.scrollTop -= 40 * offlineAlarmRows.value.length;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
offlineTableScrollFrame = requestAnimationFrame(scrollStep);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
offlineTableScrollFrame = requestAnimationFrame(scrollStep);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const clearOfflineAlarms = () => {
|
|
|
|
|
stopOfflineTableScroll();
|
|
|
|
|
offlineTablePaused.value = false;
|
|
|
|
|
offlineAlarmRows.value = [];
|
|
|
|
|
offlineDialogVisible.value = false;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleOvertimeAlarm = (monitorId: string) => {
|
|
|
|
|
playAlarmVoice(`设备${monitorId}离线`);
|
|
|
|
|
triggerAlarmEffect();
|
|
|
|
|
offlineAlarmRows.value.push({ monitorId });
|
|
|
|
|
offlineDialogVisible.value = true;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const deferAlarmDetail = () => {
|
|
|
|
|
closeCurrentAlarmDetail();
|
|
|
|
|
openNextAlarmDetail();
|
|
|
|
|
@ -217,7 +348,7 @@ onAlarmReminderOvertime((device) => {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
handleGlobalAlarm(`设备${monitorId}离线`, '离线报警');
|
|
|
|
|
handleOvertimeAlarm(monitorId);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
onAlarmReminderExtra((payload) => {
|
|
|
|
|
@ -238,12 +369,28 @@ onMounted(() => {
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
watch([offlineDialogVisible, () => offlineAlarmRows.value.length], ([visible, length]) => {
|
|
|
|
|
if (!visible || length <= 1) {
|
|
|
|
|
stopOfflineTableScroll();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void startOfflineTableScroll();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
onBeforeUnmount(() => {
|
|
|
|
|
if (offlineAlertTimer) {
|
|
|
|
|
clearTimeout(offlineAlertTimer);
|
|
|
|
|
offlineAlertTimer = null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (notificationTimer) {
|
|
|
|
|
clearTimeout(notificationTimer);
|
|
|
|
|
notificationTimer = null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
stopOfflineTableScroll();
|
|
|
|
|
|
|
|
|
|
if (typeof window !== 'undefined' && 'speechSynthesis' in window) {
|
|
|
|
|
window.speechSynthesis.cancel();
|
|
|
|
|
}
|
|
|
|
|
@ -260,6 +407,48 @@ onBeforeUnmount(() => {
|
|
|
|
|
padding: 4px 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.offline-panel-mask {
|
|
|
|
|
position: fixed;
|
|
|
|
|
inset: 0;
|
|
|
|
|
z-index: 3000;
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
justify-content: center;
|
|
|
|
|
pointer-events: none;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.offline-panel {
|
|
|
|
|
width: 560px;
|
|
|
|
|
max-width: calc(100vw - 32px);
|
|
|
|
|
padding: 16px;
|
|
|
|
|
border-radius: 16px;
|
|
|
|
|
background: #fff;
|
|
|
|
|
box-shadow: 0 18px 48px rgba(15, 23, 42, 0.22);
|
|
|
|
|
pointer-events: auto;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.offline-panel-header,
|
|
|
|
|
.offline-panel-footer {
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.offline-panel-header {
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
margin-bottom: 12px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.offline-panel-footer {
|
|
|
|
|
justify-content: flex-end;
|
|
|
|
|
margin-top: 16px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.offline-panel-title {
|
|
|
|
|
font-size: 18px;
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
color: #303133;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.alarm-info-item {
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
line-height: 1.8;
|
|
|
|
|
|