|
|
|
|
@ -37,11 +37,6 @@
|
|
|
|
|
<el-option v-for="dict in dict.type.is_flag" :key="dict.value" :label="dict.label" :value="dict.value" />
|
|
|
|
|
</el-select>
|
|
|
|
|
</el-form-item>
|
|
|
|
|
<el-form-item label="通知组" prop="notifyGroupId">
|
|
|
|
|
<el-select v-model="queryParams.notifyGroupId" placeholder="请选择通知组" clearable filterable style="width: 220px">
|
|
|
|
|
<el-option v-for="group in groupOptions" :key="group.id" :label="group.groupName" :value="group.id" />
|
|
|
|
|
</el-select>
|
|
|
|
|
</el-form-item>
|
|
|
|
|
<el-form-item class="query-actions">
|
|
|
|
|
<el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
|
|
|
|
|
<el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
|
|
|
|
|
@ -58,14 +53,6 @@
|
|
|
|
|
<span class="panel-badge warm">措施联动</span>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="notify-entry-bar">
|
|
|
|
|
<div class="notify-entry-text">通知组与通知成员的配置入口已经接回规则页,保存时会同步回填通知对象摘要,避免规则挂空通知链。</div>
|
|
|
|
|
<div class="notify-entry-actions">
|
|
|
|
|
<el-button link type="primary" @click="openNotifyGroupPage">配置通知组</el-button>
|
|
|
|
|
<el-button link type="primary" @click="openNotifyUserPage">配置通知成员</el-button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<el-row :gutter="10" class="toolbar-row">
|
|
|
|
|
<el-col :span="1.5">
|
|
|
|
|
<el-button type="primary" plain icon="el-icon-plus" size="mini" @click="handleAdd" v-hasPermi="['ems/record:recordAlarmRule:add']"
|
|
|
|
|
@ -125,7 +112,8 @@
|
|
|
|
|
<el-table-column label="恢复下限" align="center" prop="recoverLower" v-if="columns[8].visible" />
|
|
|
|
|
<el-table-column label="回差" align="center" prop="hysteresis" v-if="columns[9].visible" />
|
|
|
|
|
<el-table-column label="持续秒数" align="center" prop="durationSec" v-if="columns[10].visible" />
|
|
|
|
|
<el-table-column label="告警级别" align="center" prop="alarmLevel" v-if="columns[11].visible" />
|
|
|
|
|
<!-- 告警级别暂时停用:规则页不再暴露这个入口,避免和统一阈值配置形成双口径。 -->
|
|
|
|
|
<!-- <el-table-column label="告警级别" align="center" prop="alarmLevel" v-if="columns[11].visible" /> -->
|
|
|
|
|
<el-table-column label="启用状态" align="center" prop="isEnable" v-if="columns[12].visible">
|
|
|
|
|
<template #default="scope">
|
|
|
|
|
<dict-tag :options="dict.type.is_flag" :value="scope.row.isEnable" />
|
|
|
|
|
@ -216,31 +204,54 @@
|
|
|
|
|
<el-form-item label="恢复下限" prop="recoverLower">
|
|
|
|
|
<el-input-number v-model="form.recoverLower" placeholder="请输入恢复下限" :precision="4" />
|
|
|
|
|
</el-form-item>
|
|
|
|
|
<el-form-item label="回差" prop="hysteresis">
|
|
|
|
|
<el-form-item prop="hysteresis">
|
|
|
|
|
<template #label>
|
|
|
|
|
<span class="field-label-with-hint">
|
|
|
|
|
回差
|
|
|
|
|
<el-tooltip placement="top" effect="dark">
|
|
|
|
|
<template #content>
|
|
|
|
|
<div class="field-tooltip-content">
|
|
|
|
|
<p><strong>回差(Hysteresis / 死区)</strong></p>
|
|
|
|
|
<p>在告警阈值附近设置的"缓冲区间",防止测量值在阈值线上抖动导致告警反复触发/恢复(告警抖动)。</p>
|
|
|
|
|
<p>规则:越界后只有回落到 (告警上限 − 回差) 以下,或 (告警下限 + 回差) 以上,才判定为恢复。</p>
|
|
|
|
|
<p>示例:告警上限 = 100,回差 = 5 → 值 ≥ 100 触发告警,只有值 ≤ 95 才视为恢复。</p>
|
|
|
|
|
<p>单位与被测量一致;留空或 0 表示不启用回差保护。</p>
|
|
|
|
|
</div>
|
|
|
|
|
</template>
|
|
|
|
|
<el-icon class="field-hint-icon"><QuestionFilled /></el-icon>
|
|
|
|
|
</el-tooltip>
|
|
|
|
|
</span>
|
|
|
|
|
</template>
|
|
|
|
|
<el-input-number v-model="form.hysteresis" placeholder="请输入回差" :precision="4" />
|
|
|
|
|
<div class="field-hint">防止告警抖动,如告警上限=100、回差=5 → 值≤95 才视为恢复</div>
|
|
|
|
|
</el-form-item>
|
|
|
|
|
<el-form-item label="持续秒数" prop="durationSec">
|
|
|
|
|
<el-form-item prop="durationSec">
|
|
|
|
|
<template #label>
|
|
|
|
|
<span class="field-label-with-hint">
|
|
|
|
|
持续秒数
|
|
|
|
|
<el-tooltip placement="top" effect="dark">
|
|
|
|
|
<template #content>
|
|
|
|
|
<div class="field-tooltip-content">
|
|
|
|
|
<p><strong>持续秒数(Duration Seconds)</strong></p>
|
|
|
|
|
<p>测量值必须<strong>连续</strong>越界达到该秒数,才会正式触发告警,用于过滤瞬时尖峰造成的误报。</p>
|
|
|
|
|
<p>规则:越界计时累计 ≥ 该秒数才落库并推送;中途恢复则计时清零。</p>
|
|
|
|
|
<p>示例:持续秒数 = 30 → 连续 28 秒越界后回落不告警;连续 30 秒越界在第 30 秒触发告警。</p>
|
|
|
|
|
<p>单位:秒;留空或 0 表示不延迟,单采样越界即触发。</p>
|
|
|
|
|
</div>
|
|
|
|
|
</template>
|
|
|
|
|
<el-icon class="field-hint-icon"><QuestionFilled /></el-icon>
|
|
|
|
|
</el-tooltip>
|
|
|
|
|
</span>
|
|
|
|
|
</template>
|
|
|
|
|
<el-input-number v-model="form.durationSec" placeholder="请输入持续秒数" :precision="0" />
|
|
|
|
|
<div class="field-hint">过滤瞬时尖峰,需连续越界≥该秒数才触发告警</div>
|
|
|
|
|
</el-form-item>
|
|
|
|
|
<!-- 告警级别暂时停用:先收敛到单一规则口径,避免运维误以为这里还会参与实时判定。 -->
|
|
|
|
|
<!--
|
|
|
|
|
<el-form-item label="告警级别" prop="alarmLevel">
|
|
|
|
|
<el-input v-model="form.alarmLevel" placeholder="请输入告警级别" />
|
|
|
|
|
</el-form-item>
|
|
|
|
|
<el-form-item label="通知组" prop="notifyGroupId">
|
|
|
|
|
<el-select
|
|
|
|
|
v-model="form.notifyGroupId"
|
|
|
|
|
placeholder="请选择通知组"
|
|
|
|
|
clearable
|
|
|
|
|
filterable
|
|
|
|
|
style="width: 100%"
|
|
|
|
|
@change="handleNotifyGroupChange"
|
|
|
|
|
>
|
|
|
|
|
<el-option v-for="group in groupOptions" :key="group.id" :label="group.groupName" :value="group.id" />
|
|
|
|
|
</el-select>
|
|
|
|
|
</el-form-item>
|
|
|
|
|
<el-form-item label="通知对象摘要" prop="notifyUser">
|
|
|
|
|
<el-input v-model="form.notifyUser" type="textarea" :rows="2" placeholder="可手工补充通知对象;选择通知组后会自动回填成员摘要" />
|
|
|
|
|
<div class="field-tip">这里保留 notifyUser 摘要字段,是为了兼容旧台账展示;后端会以 notifyGroupId 作为通知配置的主校验入口。</div>
|
|
|
|
|
</el-form-item>
|
|
|
|
|
-->
|
|
|
|
|
<el-form-item label="启用状态" prop="isEnable">
|
|
|
|
|
<el-radio-group v-model="form.isEnable">
|
|
|
|
|
<el-radio v-for="dict in dict.type.is_flag" :key="dict.value" :label="dict.value">{{ dict.label }}</el-radio>
|
|
|
|
|
@ -364,9 +375,8 @@
|
|
|
|
|
|
|
|
|
|
<script setup lang="ts">
|
|
|
|
|
import { ElMessage, ElMessageBox } from 'element-plus';
|
|
|
|
|
import { QuestionFilled } from '@element-plus/icons-vue';
|
|
|
|
|
import { useDict } from '@/utils/dict';
|
|
|
|
|
import { listAlarmNotifyGroupAll } from '@/api/ems/base/alarmNotifyGroup';
|
|
|
|
|
import { listAlarmNotifyGroupUser } from '@/api/ems/base/alarmNotifyGroupUser';
|
|
|
|
|
import {
|
|
|
|
|
listRecordAlarmRule,
|
|
|
|
|
getRecordAlarmRule,
|
|
|
|
|
@ -376,8 +386,6 @@ import {
|
|
|
|
|
} from '@/api/ems/record/recordAlarmRule';
|
|
|
|
|
import { listBaseMonitorInfo } from '@/api/ems/base/baseMonitorInfo';
|
|
|
|
|
import { getEmsAlarmActionStepsByRuleId, batchSaveActionSteps } from '@/api/ems/base/emsAlarmActionStep';
|
|
|
|
|
import type { AlarmNotifyGroupVO } from '@/api/ems/base/alarmNotifyGroup/types';
|
|
|
|
|
import type { AlarmNotifyGroupUserVO } from '@/api/ems/base/alarmNotifyGroupUser/types';
|
|
|
|
|
import { getToken } from '@/utils/auth';
|
|
|
|
|
|
|
|
|
|
defineOptions({
|
|
|
|
|
@ -417,9 +425,6 @@ const createQueryParams = () => ({
|
|
|
|
|
durationSec: null,
|
|
|
|
|
alarmLevel: null,
|
|
|
|
|
isEnable: null,
|
|
|
|
|
notifyGroupId: null,
|
|
|
|
|
notifyUser: null,
|
|
|
|
|
nickName: null,
|
|
|
|
|
cause: null
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
@ -446,10 +451,7 @@ const createFormData = () => ({
|
|
|
|
|
hysteresis: null,
|
|
|
|
|
durationSec: null,
|
|
|
|
|
alarmLevel: null,
|
|
|
|
|
notifyGroupId: null,
|
|
|
|
|
isEnable: '0',
|
|
|
|
|
notifyUser: null,
|
|
|
|
|
nickName: null,
|
|
|
|
|
cause: null,
|
|
|
|
|
createBy: null,
|
|
|
|
|
createTime: null,
|
|
|
|
|
@ -479,8 +481,6 @@ const state = reactive({
|
|
|
|
|
open: false,
|
|
|
|
|
// 设备列表
|
|
|
|
|
monitorList: [],
|
|
|
|
|
groupOptions: [] as AlarmNotifyGroupVO[],
|
|
|
|
|
groupUserMap: {} as Record<string, AlarmNotifyGroupUserVO[]>,
|
|
|
|
|
selectedMonitorType: null,
|
|
|
|
|
// 新增:用于存储选中设备的 monitorType
|
|
|
|
|
// 查询参数
|
|
|
|
|
@ -592,7 +592,6 @@ const {
|
|
|
|
|
columns,
|
|
|
|
|
currentRuleObjId,
|
|
|
|
|
form,
|
|
|
|
|
groupOptions,
|
|
|
|
|
ids,
|
|
|
|
|
imagePreviewVisible,
|
|
|
|
|
loading,
|
|
|
|
|
@ -612,17 +611,6 @@ const {
|
|
|
|
|
uploadHeaders
|
|
|
|
|
} = toRefs(state);
|
|
|
|
|
|
|
|
|
|
const resolveNotifyUsers = (groupId?: string | number | null) => {
|
|
|
|
|
if (groupId === null || groupId === undefined || groupId === '') {
|
|
|
|
|
return '';
|
|
|
|
|
}
|
|
|
|
|
const users = state.groupUserMap[String(groupId)] || [];
|
|
|
|
|
return users
|
|
|
|
|
.map((item) => item.nickName || item.userName || item.phone || item.email)
|
|
|
|
|
.filter(Boolean)
|
|
|
|
|
.join('、');
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// 这里补充展示型统计卡,帮助运维先看到规则覆盖面,不改变任何列表查询和保存逻辑。
|
|
|
|
|
const overviewStats = computed(() => {
|
|
|
|
|
const rules = recordAlarmRuleList.value;
|
|
|
|
|
@ -711,9 +699,6 @@ const handleUpdate = (row) => {
|
|
|
|
|
if (selectedMonitor) {
|
|
|
|
|
selectedMonitorType.value = selectedMonitor.monitorType;
|
|
|
|
|
}
|
|
|
|
|
if (form.value.notifyGroupId) {
|
|
|
|
|
handleNotifyGroupChange(form.value.notifyGroupId);
|
|
|
|
|
}
|
|
|
|
|
open.value = true;
|
|
|
|
|
title.value = '修改异常告警规则';
|
|
|
|
|
});
|
|
|
|
|
@ -737,14 +722,6 @@ const submitForm = () => {
|
|
|
|
|
form.value.triggerValue = form.value.alarmLower;
|
|
|
|
|
}
|
|
|
|
|
form.value.isEnable = form.value.isEnable ?? '0';
|
|
|
|
|
if (form.value.notifyGroupId) {
|
|
|
|
|
const notifySummary = resolveNotifyUsers(form.value.notifyGroupId);
|
|
|
|
|
if (!notifySummary && !form.value.notifyUser) {
|
|
|
|
|
ElMessage.warning('当前通知组没有可用成员,请先补充通知成员后再保存规则');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
form.value.notifyUser = form.value.notifyUser || notifySummary;
|
|
|
|
|
}
|
|
|
|
|
if (form.value.objId != null) {
|
|
|
|
|
updateRecordAlarmRule(form.value).then((response) => {
|
|
|
|
|
proxy?.$modal.msgSuccess('修改成功');
|
|
|
|
|
@ -792,49 +769,6 @@ const getMonitorList = () => {
|
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const loadNotifyGroupOptions = async () => {
|
|
|
|
|
const response = await listAlarmNotifyGroupAll();
|
|
|
|
|
groupOptions.value = (((response as any).data ?? (response as any).rows ?? []) || []) as AlarmNotifyGroupVO[];
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const loadNotifyGroupUsers = async () => {
|
|
|
|
|
const response = await listAlarmNotifyGroupUser({
|
|
|
|
|
pageNum: 1,
|
|
|
|
|
pageSize: 1000
|
|
|
|
|
} as any);
|
|
|
|
|
const users = (((response as any).rows ?? (response as any).data ?? []) || []) as AlarmNotifyGroupUserVO[];
|
|
|
|
|
const nextMap: Record<string, AlarmNotifyGroupUserVO[]> = {};
|
|
|
|
|
users.forEach((item) => {
|
|
|
|
|
const groupId = item.groupId;
|
|
|
|
|
if (groupId === null || groupId === undefined || groupId === '') {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
const key = String(groupId);
|
|
|
|
|
if (!nextMap[key]) {
|
|
|
|
|
nextMap[key] = [];
|
|
|
|
|
}
|
|
|
|
|
nextMap[key].push(item);
|
|
|
|
|
});
|
|
|
|
|
state.groupUserMap = nextMap;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleNotifyGroupChange = (groupId?: string | number | null) => {
|
|
|
|
|
if (!groupId) {
|
|
|
|
|
form.value.notifyUser = null;
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
const notifySummary = resolveNotifyUsers(groupId);
|
|
|
|
|
form.value.notifyUser = notifySummary || form.value.notifyUser;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const openNotifyGroupPage = () => {
|
|
|
|
|
proxy?.$tab.openPage('/ems/base/alarmNotifyGroup', '通知组配置');
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const openNotifyUserPage = () => {
|
|
|
|
|
proxy?.$tab.openPage('/ems/base/alarmNotifyGroupUser', '通知成员配置');
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleMonitorChange = (monitorId) => {
|
|
|
|
|
// monitorId这里实际传入的是monitorCode值
|
|
|
|
|
const selectedMonitor = monitorList.value.find((m) => m.monitorCode === monitorId);
|
|
|
|
|
@ -1067,8 +1001,6 @@ const getFullImageUrl = (relativePath) => {
|
|
|
|
|
onMounted(() => {
|
|
|
|
|
getList();
|
|
|
|
|
getMonitorList();
|
|
|
|
|
loadNotifyGroupOptions();
|
|
|
|
|
loadNotifyGroupUsers();
|
|
|
|
|
});
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
@ -1237,29 +1169,6 @@ onMounted(() => {
|
|
|
|
|
background: linear-gradient(90deg, rgba(59, 130, 246, 0.08), rgba(236, 72, 153, 0.08));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.notify-entry-bar {
|
|
|
|
|
align-items: center;
|
|
|
|
|
background: rgba(249, 239, 225, 0.7);
|
|
|
|
|
border: 1px solid rgba(190, 141, 103, 0.18);
|
|
|
|
|
border-radius: 18px;
|
|
|
|
|
display: flex;
|
|
|
|
|
gap: 12px;
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
margin-bottom: 12px;
|
|
|
|
|
padding: 12px 16px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.notify-entry-text {
|
|
|
|
|
color: #6f5b51;
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
line-height: 1.7;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.notify-entry-actions {
|
|
|
|
|
display: flex;
|
|
|
|
|
gap: 10px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.data-table {
|
|
|
|
|
:deep(.el-table__header-wrapper th) {
|
|
|
|
|
background: #f5f7ff;
|
|
|
|
|
@ -1304,11 +1213,44 @@ onMounted(() => {
|
|
|
|
|
background: #fafbff;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.field-tip {
|
|
|
|
|
.field-label-with-hint {
|
|
|
|
|
display: inline-flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
gap: 4px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.field-hint-icon {
|
|
|
|
|
color: #6366f1;
|
|
|
|
|
cursor: help;
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
|
|
|
|
|
&:hover {
|
|
|
|
|
color: #4338ca;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.field-hint {
|
|
|
|
|
color: var(--el-text-color-secondary);
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
line-height: 1.6;
|
|
|
|
|
margin-top: 6px;
|
|
|
|
|
line-height: 1.5;
|
|
|
|
|
margin-top: 4px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.field-tooltip-content {
|
|
|
|
|
max-width: 320px;
|
|
|
|
|
line-height: 1.65;
|
|
|
|
|
|
|
|
|
|
p {
|
|
|
|
|
margin: 0 0 6px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
p:last-child {
|
|
|
|
|
margin-bottom: 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
strong {
|
|
|
|
|
color: #fdd08a;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.action-steps-container {
|
|
|
|
|
@ -1499,10 +1441,5 @@ onMounted(() => {
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
align-items: flex-start;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.notify-entry-bar {
|
|
|
|
|
align-items: flex-start;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
</style>
|
|
|
|
|
|