|
|
|
|
@ -3,33 +3,85 @@
|
|
|
|
|
<router-view />
|
|
|
|
|
<RealtimeAlarmModal />
|
|
|
|
|
<div v-if="offlineAlertActive" class="offline-breath-overlay"></div>
|
|
|
|
|
<el-dialog
|
|
|
|
|
v-model="alarmDetailVisible"
|
|
|
|
|
title="报警信息"
|
|
|
|
|
width="760px"
|
|
|
|
|
:close-on-click-modal="false"
|
|
|
|
|
:before-close="handleAlarmDetailClose"
|
|
|
|
|
destroy-on-close
|
|
|
|
|
>
|
|
|
|
|
<el-tabs v-model="alarmDetailTab">
|
|
|
|
|
<el-tab-pane label="报警信息" name="alarm">
|
|
|
|
|
<div class="alarm-info-list">
|
|
|
|
|
<div v-if="!currentAlarmContents.length" class="alarm-empty-text">暂无报警信息</div>
|
|
|
|
|
<div v-for="(content, index) in currentAlarmContents" :key="`${index}-${content}`" class="alarm-info-item">
|
|
|
|
|
{{ index + 1 }}. {{ content }}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</el-tab-pane>
|
|
|
|
|
<el-tab-pane label="处理流程" name="steps">
|
|
|
|
|
<div class="alarm-step-panel">
|
|
|
|
|
<div v-if="!currentActionSteps.length" class="alarm-empty-text">当前规则未配置处理步骤</div>
|
|
|
|
|
<el-steps v-else direction="vertical" :space="72" :active="currentActionSteps.length">
|
|
|
|
|
<el-step
|
|
|
|
|
v-for="(step, index) in currentActionSteps"
|
|
|
|
|
:key="step.objId || `${step.stepSequence}-${index}`"
|
|
|
|
|
:title="`第${step.stepSequence ?? '-'}步`"
|
|
|
|
|
:description="step.description || '-'"
|
|
|
|
|
/>
|
|
|
|
|
</el-steps>
|
|
|
|
|
</div>
|
|
|
|
|
</el-tab-pane>
|
|
|
|
|
</el-tabs>
|
|
|
|
|
<template #footer>
|
|
|
|
|
<div class="dialog-footer">
|
|
|
|
|
<el-button @click="deferAlarmDetail">稍后处理</el-button>
|
|
|
|
|
<el-button type="primary" :loading="alarmHandling" @click="confirmAlarmDetail">立即处理</el-button>
|
|
|
|
|
</div>
|
|
|
|
|
</template>
|
|
|
|
|
</el-dialog>
|
|
|
|
|
</el-config-provider>
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
<script setup lang="ts">
|
|
|
|
|
import { ElMessageBox } from 'element-plus';
|
|
|
|
|
import { ElMessage, ElMessageBox } 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, onAlarmReminderOvertime } from '@/utils/alarmReminder';
|
|
|
|
|
import {
|
|
|
|
|
closeAlarmReminder,
|
|
|
|
|
connectAlarmReminder,
|
|
|
|
|
onAlarmReminderExtra,
|
|
|
|
|
onAlarmReminderOvertime
|
|
|
|
|
} from '@/utils/alarmReminder';
|
|
|
|
|
|
|
|
|
|
const appStore = useAppStore();
|
|
|
|
|
const offlineAlertActive = ref(false);
|
|
|
|
|
const alarmDetailVisible = ref(false);
|
|
|
|
|
const alarmDetailTab = ref('alarm');
|
|
|
|
|
const alarmHandling = ref(false);
|
|
|
|
|
const currentActionSteps = ref<any[]>([]);
|
|
|
|
|
const currentAlarmContents = ref<string[]>([]);
|
|
|
|
|
const alarmDetailQueue = ref<any[]>([]);
|
|
|
|
|
const currentAlarmDetail = ref<any | null>(null);
|
|
|
|
|
let offlineAlertTimer: ReturnType<typeof setTimeout> | null = null;
|
|
|
|
|
|
|
|
|
|
const playOfflineVoice = (objid: string) => {
|
|
|
|
|
const playAlarmVoice = (message: string) => {
|
|
|
|
|
if (typeof window === 'undefined' || !('speechSynthesis' in window)) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
window.speechSynthesis.cancel();
|
|
|
|
|
const utterance = new SpeechSynthesisUtterance(`设备${objid}离线`);
|
|
|
|
|
const utterance = new SpeechSynthesisUtterance(message);
|
|
|
|
|
utterance.lang = 'zh-CN';
|
|
|
|
|
window.speechSynthesis.speak(utterance);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const triggerOfflineEffect = () => {
|
|
|
|
|
const triggerAlarmEffect = () => {
|
|
|
|
|
offlineAlertActive.value = false;
|
|
|
|
|
|
|
|
|
|
if (offlineAlertTimer) {
|
|
|
|
|
@ -45,23 +97,138 @@ const triggerOfflineEffect = () => {
|
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const showOfflineDialog = (objid: string) => {
|
|
|
|
|
ElMessageBox.alert(`设备${objid}离线`, '离线报警', {
|
|
|
|
|
const showAlarmDialog = (message: string, title: string) => {
|
|
|
|
|
ElMessageBox.alert(message, title, {
|
|
|
|
|
type: 'warning',
|
|
|
|
|
confirmButtonText: '确定'
|
|
|
|
|
}).catch(() => undefined);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
connectAlarmReminder(() => {});
|
|
|
|
|
onAlarmReminderOvertime((device) => {
|
|
|
|
|
const objid = String(device?.monitorId ?? '');
|
|
|
|
|
if (!objid) {
|
|
|
|
|
const fetchActionStepsByRuleObjId = async (ruleObjId: string) => {
|
|
|
|
|
const response = await listEmsAlarmActionStep({
|
|
|
|
|
pageNum: 1,
|
|
|
|
|
pageSize: 10,
|
|
|
|
|
ruleObjId
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return [...(response.rows ?? [])].sort((a, b) => Number(a.stepSequence || 0) - Number(b.stepSequence || 0));
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const consumeHandleResult = (result?: any) => {
|
|
|
|
|
const updatedCount = Number(result?.updatedCount ?? 0);
|
|
|
|
|
const alreadyHandledCount = Number(result?.alreadyHandledCount ?? 0);
|
|
|
|
|
return updatedCount > 0 || alreadyHandledCount > 0;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const normalizeAlarmContents = (alarmContents: unknown) => {
|
|
|
|
|
if (!Array.isArray(alarmContents)) {
|
|
|
|
|
return [];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return alarmContents.map((item) => String(item ?? '')).filter(Boolean);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const openNextAlarmDetail = () => {
|
|
|
|
|
if (alarmDetailVisible.value || !alarmDetailQueue.value.length) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
playOfflineVoice(objid);
|
|
|
|
|
showOfflineDialog(objid);
|
|
|
|
|
triggerOfflineEffect();
|
|
|
|
|
const nextDetail = alarmDetailQueue.value.shift() || null;
|
|
|
|
|
if (!nextDetail) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
currentAlarmDetail.value = nextDetail;
|
|
|
|
|
currentAlarmContents.value = nextDetail.alarmContents;
|
|
|
|
|
currentActionSteps.value = nextDetail.steps;
|
|
|
|
|
alarmDetailTab.value = 'alarm';
|
|
|
|
|
alarmDetailVisible.value = true;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const closeCurrentAlarmDetail = () => {
|
|
|
|
|
currentAlarmDetail.value = null;
|
|
|
|
|
currentAlarmContents.value = [];
|
|
|
|
|
currentActionSteps.value = [];
|
|
|
|
|
alarmDetailVisible.value = false;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const enqueueAlarmDetails = async (payload: any) => {
|
|
|
|
|
const ruleObjIds = [...new Set((payload?.alarmRules ?? []).map((item) => String(item?.objid ?? '')).filter(Boolean))];
|
|
|
|
|
const alarmContents = normalizeAlarmContents(payload?.alarmContents);
|
|
|
|
|
|
|
|
|
|
for (const ruleObjId of ruleObjIds) {
|
|
|
|
|
const steps = await fetchActionStepsByRuleObjId(ruleObjId);
|
|
|
|
|
alarmDetailQueue.value.push({
|
|
|
|
|
ruleObjId,
|
|
|
|
|
monitorId: String(payload?.deviceParam?.monitorId ?? ''),
|
|
|
|
|
alarmObjId: String(payload?.deviceParam?.objId ?? payload?.deviceParam?.objid ?? ''),
|
|
|
|
|
alarmContents,
|
|
|
|
|
steps
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
openNextAlarmDetail();
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleGlobalAlarm = (message: string, title: string) => {
|
|
|
|
|
playAlarmVoice(message);
|
|
|
|
|
showAlarmDialog(message, title);
|
|
|
|
|
triggerAlarmEffect();
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const deferAlarmDetail = () => {
|
|
|
|
|
closeCurrentAlarmDetail();
|
|
|
|
|
openNextAlarmDetail();
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleAlarmDetailClose = (done: () => void) => {
|
|
|
|
|
deferAlarmDetail();
|
|
|
|
|
done();
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const confirmAlarmDetail = async () => {
|
|
|
|
|
const alarmObjId = currentAlarmDetail.value?.alarmObjId;
|
|
|
|
|
if (!alarmObjId) {
|
|
|
|
|
ElMessage.warning('当前报警缺少处理对象 ID');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
alarmHandling.value = true;
|
|
|
|
|
try {
|
|
|
|
|
const response = await handleExceptions(alarmObjId);
|
|
|
|
|
const success = consumeHandleResult((response as any).data ?? null);
|
|
|
|
|
if (!success) {
|
|
|
|
|
ElMessage.warning((response as any).msg || '报警状态未更新,请稍后重试');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ElMessage.success('报警已处理');
|
|
|
|
|
closeCurrentAlarmDetail();
|
|
|
|
|
openNextAlarmDetail();
|
|
|
|
|
} finally {
|
|
|
|
|
alarmHandling.value = false;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
void connectAlarmReminder(() => {});
|
|
|
|
|
onAlarmReminderOvertime((device) => {
|
|
|
|
|
const monitorId = String(device?.monitorId ?? '');
|
|
|
|
|
if (!monitorId) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
handleGlobalAlarm(`设备${monitorId}离线`, '离线报警');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
onAlarmReminderExtra((payload) => {
|
|
|
|
|
const monitorId = String(payload?.deviceParam?.monitorId ?? '');
|
|
|
|
|
if (!monitorId) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
playAlarmVoice(`设备${monitorId}报警`);
|
|
|
|
|
triggerAlarmEffect();
|
|
|
|
|
void enqueueAlarmDetails(payload);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
onMounted(() => {
|
|
|
|
|
@ -86,6 +253,26 @@ onBeforeUnmount(() => {
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
<style scoped>
|
|
|
|
|
.alarm-info-list,
|
|
|
|
|
.alarm-step-panel {
|
|
|
|
|
max-height: 420px;
|
|
|
|
|
overflow-y: auto;
|
|
|
|
|
padding: 4px 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.alarm-info-item {
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
line-height: 1.8;
|
|
|
|
|
color: #303133;
|
|
|
|
|
word-break: break-all;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.alarm-empty-text {
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
line-height: 1.8;
|
|
|
|
|
color: #909399;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.offline-breath-overlay {
|
|
|
|
|
position: fixed;
|
|
|
|
|
inset: 0;
|
|
|
|
|
|