通知告警规则引擎

This commit is contained in:
“hsc”
2026-08-28 15:23:45 +08:00
parent 43f1dde4e2
commit c83bdbeb16
6 changed files with 444 additions and 4 deletions
+3 -1
View File
@@ -5,8 +5,10 @@
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>设备管理系统</title>
<script type="module" crossorigin src="/assets/index-1sWe50d-.js"></script>
<script type="module" crossorigin src="/assets/index-RVBj8t6x.js"></script>
<link rel="modulepreload" crossorigin href="/assets/rolldown-runtime-hePW80VL.js">
<link rel="modulepreload" crossorigin href="/assets/runtime-core.esm-bundler-DZ3WZ9y9.js">
<link rel="modulepreload" crossorigin href="/assets/message-ddUl4JIs.js">
<link rel="stylesheet" crossorigin href="/assets/index-BpBnxyAz.css">
</head>
<body>
+26
View File
@@ -0,0 +1,26 @@
import request from '@/utils/request'
// 告警规则列表
export function getAlertRuleList() {
return request.get('/inspection/alert-rule/list')
}
// 新增告警规则
export function addAlertRule(data) {
return request.post('/inspection/alert-rule/add', data)
}
// 修改告警规则
export function updateAlertRule(data) {
return request.put('/inspection/alert-rule/update', data)
}
// 删除告警规则
export function deleteAlertRule(id) {
return request.delete(`/inspection/alert-rule/${id}`)
}
// 启用/停用告警规则
export function setAlertRuleEnabled(id, enabled) {
return request.put(`/inspection/alert-rule/${id}/enabled`, null, { params: { enabled } })
}
+26
View File
@@ -0,0 +1,26 @@
import request from '@/utils/request'
// 转发规则列表
export function getDataForwardList() {
return request.get('/inspection/data-forward/list')
}
// 新增转发规则
export function addDataForward(data) {
return request.post('/inspection/data-forward/add', data)
}
// 修改转发规则
export function updateDataForward(data) {
return request.put('/inspection/data-forward/update', data)
}
// 删除转发规则
export function deleteDataForward(id) {
return request.delete(`/inspection/data-forward/${id}`)
}
// 启用/停用转发规则
export function setDataForwardEnabled(id, enabled) {
return request.put(`/inspection/data-forward/${id}/enabled`, null, { params: { enabled } })
}
+6
View File
@@ -162,6 +162,12 @@ const routes = [
name: 'AlertMessage',
component: () => import('@/views/inspection/alert-message/Index.vue'),
meta: { title: '消息告警', icon: 'ChatDotRound' }
},
{
path: 'data-forward',
name: 'DataForward',
component: () => import('@/views/inspection/data-forward/Index.vue'),
meta: { title: '数据转发', icon: 'Promotion' }
}
]
},
+186 -3
View File
@@ -2,13 +2,196 @@
<div class="page-container">
<el-card>
<template #header>
<span>告警规则</span>
<div class="card-header">
<span>告警规则{{ rules.length }} </span>
<el-button type="primary" icon="Plus" @click="openDialog()">新增规则</el-button>
</div>
</template>
<el-empty description="告警规则 - 功能开发中" />
<el-table :data="rules" v-loading="loading" stripe>
<el-table-column prop="RuleName" label="规则名称" min-width="140" show-overflow-tooltip />
<el-table-column label="适用设备" width="120">
<template #default="{ row }">
<el-tag v-if="!row.DeviceType" type="info">全部设备</el-tag>
<span v-else>{{ deviceTypeLabel(row.DeviceType) }}</span>
</template>
</el-table-column>
<el-table-column label="触发条件" min-width="160">
<template #default="{ row }">
{{ metricLabel(row.Metric) }} {{ row.Operator }} {{ row.Threshold }}
</template>
</el-table-column>
<el-table-column label="告警级别" width="100">
<template #default="{ row }">
<el-tag :type="levelTagType(row.AlarmLevel)">{{ row.AlarmLevel }}</el-tag>
</template>
</el-table-column>
<el-table-column label="启用" width="80">
<template #default="{ row }">
<el-switch :model-value="row.Enabled" @change="onToggleEnabled(row, $event)" />
</template>
</el-table-column>
<el-table-column prop="TriggerCount" label="触发次数" width="90" align="center" />
<el-table-column label="最近触发" width="160">
<template #default="{ row }">{{ fmtTime(row.LastAlarmTime) }}</template>
</el-table-column>
<el-table-column label="创建时间" width="160">
<template #default="{ row }">{{ fmtTime(row.CreateTime) }}</template>
</el-table-column>
<el-table-column label="操作" width="130" fixed="right">
<template #default="{ row }">
<el-button link type="primary" @click="openDialog(row)">编辑</el-button>
<el-button link type="danger" @click="onDelete(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
</el-card>
<el-dialog v-model="dialogVisible" :title="form.Id ? '编辑告警规则' : '新增告警规则'" width="520px" destroy-on-close>
<el-form ref="formRef" :model="form" :rules="formRules" label-width="90px">
<el-form-item label="规则名称" prop="RuleName">
<el-input v-model="form.RuleName" placeholder="如:温度箱超温报警" maxlength="100" />
</el-form-item>
<el-form-item label="适用设备" prop="DeviceType">
<el-select v-model="form.DeviceType" placeholder="全部设备" clearable style="width: 100%">
<el-option v-for="d in deviceTypes" :key="d.value" :label="d.label" :value="d.value" />
</el-select>
</el-form-item>
<el-form-item label="监测指标" prop="Metric">
<el-select v-model="form.Metric" placeholder="请选择监测指标" style="width: 100%">
<el-option v-for="m in metrics" :key="m.value" :label="m.label" :value="m.value" />
</el-select>
</el-form-item>
<el-form-item label="触发条件" required>
<div style="display: flex; gap: 8px; width: 100%">
<el-select v-model="form.Operator" placeholder="运算符" style="width: 45%" prop="Operator">
<el-option v-for="op in operators" :key="op" :label="op" :value="op" />
</el-select>
<el-input-number v-model="form.Threshold" :precision="1" :step="1" style="width: 55%" placeholder="阈值" />
</div>
</el-form-item>
<el-form-item label="告警级别" prop="AlarmLevel">
<el-select v-model="form.AlarmLevel" placeholder="请选择告警级别" style="width: 100%">
<el-option v-for="l in levels" :key="l" :label="l" :value="l" />
</el-select>
</el-form-item>
<el-form-item label="启用">
<el-switch v-model="form.Enabled" />
</el-form-item>
<el-form-item label="备注">
<el-input v-model="form.Remark" type="textarea" :rows="2" maxlength="500" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" :loading="saving" @click="onSubmit">确定</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup>
// TODO: 实现告警规则功能
import { ref, reactive, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import {
getAlertRuleList, addAlertRule, updateAlertRule,
deleteAlertRule, setAlertRuleEnabled
} from '@/api/alertRule'
const loading = ref(false)
const saving = ref(false)
const rules = ref([])
const dialogVisible = ref(false)
const formRef = ref(null)
const deviceTypes = [
{ value: 'TemperatureBox', label: '温度箱' },
{ value: 'ChargeCabinet', label: '充放电柜' },
{ value: 'AuxiliaryDevice', label: '辅助设备' }
]
const metrics = [
{ value: 'Temperature', label: '温度' },
{ value: 'Humidity', label: '湿度' }
]
const operators = ['>', '>=', '<', '<=', '=', '≠']
const levels = ['信息', '警告', '紧急']
const defaultForm = () => ({
Id: null, RuleName: '', DeviceType: '', Metric: 'Temperature',
Operator: '>', Threshold: 80, AlarmLevel: '警告', Enabled: true, Remark: ''
})
const form = reactive(defaultForm())
const formRules = {
RuleName: [{ required: true, message: '请输入规则名称', trigger: 'blur' }],
Metric: [{ required: true, message: '请选择监测指标', trigger: 'change' }],
AlarmLevel: [{ required: true, message: '请选择告警级别', trigger: 'change' }]
}
const deviceTypeLabel = (v) => deviceTypes.find(d => d.value === v)?.label || v
const metricLabel = (v) => metrics.find(m => m.value === v)?.label || v
const levelTagType = (l) => ({ '信息': 'info', '警告': 'warning', '紧急': 'danger' }[l] || 'info')
const fmtTime = (t) => t ? String(t).replace('T', ' ').slice(0, 19) : '-'
async function fetchList() {
loading.value = true
try {
rules.value = (await getAlertRuleList()) || []
} catch (e) { /* 拦截器已提示 */ } finally {
loading.value = false
}
}
function openDialog(row) {
Object.assign(form, defaultForm(), row || {})
dialogVisible.value = true
}
async function onSubmit() {
await formRef.value.validate()
saving.value = true
try {
// 后端 Id 是 long 不接受 null:新增时剔除 Id 字段,否则报 400
const payload = { ...form }
if (!payload.Id) delete payload.Id
if (payload.Id) {
await updateAlertRule(payload)
ElMessage.success('修改成功')
} else {
await addAlertRule(payload)
ElMessage.success('新增成功')
}
dialogVisible.value = false
fetchList()
} catch (e) { /* 拦截器已提示 */ } finally {
saving.value = false
}
}
async function onToggleEnabled(row, enabled) {
try {
await setAlertRuleEnabled(row.Id, enabled)
row.Enabled = enabled
ElMessage.success(enabled ? '已启用' : '已停用')
} catch (e) { /* 拦截器已提示 */ }
}
async function onDelete(row) {
await ElMessageBox.confirm(`确定删除规则「${row.RuleName}」吗?`, '提示', { type: 'warning' })
try {
await deleteAlertRule(row.Id)
ElMessage.success('删除成功')
fetchList()
} catch (e) { /* 拦截器已提示 */ }
}
onMounted(fetchList)
</script>
<style scoped>
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
}
</style>
+197
View File
@@ -0,0 +1,197 @@
<template>
<div class="page-container">
<el-card>
<template #header>
<div class="card-header">
<span>数据转发规则{{ rules.length }} </span>
<el-button type="primary" icon="Plus" @click="openDialog()">新增规则</el-button>
</div>
</template>
<el-alert type="info" :closable="false" style="margin-bottom: 12px"
title="按设定的间隔,将设备数据通过消息通知方式定时转发到目标地址" />
<el-table :data="rules" v-loading="loading" stripe>
<el-table-column prop="ForwardName" label="规则名称" min-width="140" show-overflow-tooltip />
<el-table-column label="源设备" width="120">
<template #default="{ row }">
<el-tag v-if="!row.SourceDeviceType" type="info">全部设备</el-tag>
<span v-else>{{ deviceTypeLabel(row.SourceDeviceType) }}</span>
</template>
</el-table-column>
<el-table-column label="转发间隔" width="100" align="center">
<template #default="{ row }">{{ row.ForwardInterval }} </template>
</el-table-column>
<el-table-column label="通知方式" width="110">
<template #default="{ row }">
<el-tag :type="notifyTagType(row.NotifyType)">{{ notifyLabel(row.NotifyType) }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="TargetAddress" label="目标地址" min-width="180" show-overflow-tooltip />
<el-table-column label="启用" width="80">
<template #default="{ row }">
<el-switch :model-value="row.Enabled" @change="onToggleEnabled(row, $event)" />
</template>
</el-table-column>
<el-table-column label="最近转发" width="160">
<template #default="{ row }">{{ fmtTime(row.LastForwardTime) }}</template>
</el-table-column>
<el-table-column label="创建时间" width="160">
<template #default="{ row }">{{ fmtTime(row.CreateTime) }}</template>
</el-table-column>
<el-table-column label="操作" width="130" fixed="right">
<template #default="{ row }">
<el-button link type="primary" @click="openDialog(row)">编辑</el-button>
<el-button link type="danger" @click="onDelete(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
</el-card>
<el-dialog v-model="dialogVisible" :title="form.Id ? '编辑转发规则' : '新增转发规则'" width="520px" destroy-on-close>
<el-form ref="formRef" :model="form" :rules="formRules" label-width="90px">
<el-form-item label="规则名称" prop="ForwardName">
<el-input v-model="form.ForwardName" placeholder="如:温度箱数据转发到运维邮箱" maxlength="100" />
</el-form-item>
<el-form-item label="源设备" prop="SourceDeviceType">
<el-select v-model="form.SourceDeviceType" placeholder="全部设备" clearable style="width: 100%">
<el-option v-for="d in deviceTypes" :key="d.value" :label="d.label" :value="d.value" />
</el-select>
</el-form-item>
<el-form-item label="转发间隔" prop="ForwardInterval">
<el-input-number v-model="form.ForwardInterval" :min="1" :step="10" style="width: 100%" />
<div class="form-tip">单位每隔多久转发一次数据</div>
</el-form-item>
<el-form-item label="通知方式" prop="NotifyType">
<el-select v-model="form.NotifyType" placeholder="请选择通知方式" style="width: 100%">
<el-option v-for="n in notifyTypes" :key="n.value" :label="n.label" :value="n.value" />
</el-select>
</el-form-item>
<el-form-item label="目标地址" prop="TargetAddress">
<el-input v-model="form.TargetAddress" placeholder="邮箱 / 手机号 / 接口URL" maxlength="200" />
</el-form-item>
<el-form-item label="启用">
<el-switch v-model="form.Enabled" />
</el-form-item>
<el-form-item label="备注">
<el-input v-model="form.Remark" type="textarea" :rows="2" maxlength="500" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" :loading="saving" @click="onSubmit">确定</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import {
getDataForwardList, addDataForward, updateDataForward,
deleteDataForward, setDataForwardEnabled
} from '@/api/dataForward'
const loading = ref(false)
const saving = ref(false)
const rules = ref([])
const dialogVisible = ref(false)
const formRef = ref(null)
const deviceTypes = [
{ value: 'TemperatureBox', label: '温度箱' },
{ value: 'ChargeCabinet', label: '充放电柜' },
{ value: 'AuxiliaryDevice', label: '辅助设备' }
]
const notifyTypes = [
{ value: 'Email', label: '邮件' },
{ value: 'Sms', label: '短信' },
{ value: 'Http', label: 'HTTP 推送' }
]
const defaultForm = () => ({
Id: null, ForwardName: '', SourceDeviceType: '', ForwardInterval: 60,
NotifyType: 'Email', TargetAddress: '', Enabled: true, Remark: ''
})
const form = reactive(defaultForm())
const formRules = {
ForwardName: [{ required: true, message: '请输入规则名称', trigger: 'blur' }],
ForwardInterval: [{ required: true, message: '请输入转发间隔', trigger: 'blur' }],
NotifyType: [{ required: true, message: '请选择通知方式', trigger: 'change' }],
TargetAddress: [{ required: true, message: '请输入目标地址', trigger: 'blur' }]
}
const deviceTypeLabel = (v) => deviceTypes.find(d => d.value === v)?.label || v
const notifyLabel = (v) => notifyTypes.find(n => n.value === v)?.label || v
const notifyTagType = (v) => ({ Email: 'primary', Sms: 'success', Http: 'warning' }[v] || 'info')
const fmtTime = (t) => t ? String(t).replace('T', ' ').slice(0, 19) : '-'
async function fetchList() {
loading.value = true
try {
rules.value = (await getDataForwardList()) || []
} catch (e) { /* 拦截器已提示 */ } finally {
loading.value = false
}
}
function openDialog(row) {
Object.assign(form, defaultForm(), row || {})
dialogVisible.value = true
}
async function onSubmit() {
await formRef.value.validate()
saving.value = true
try {
// 后端 Id 是 long 不接受 null:新增时剔除 Id 字段,否则报 400
const payload = { ...form }
if (!payload.Id) delete payload.Id
if (payload.Id) {
await updateDataForward(payload)
ElMessage.success('修改成功')
} else {
await addDataForward(payload)
ElMessage.success('新增成功')
}
dialogVisible.value = false
fetchList()
} catch (e) { /* 拦截器已提示 */ } finally {
saving.value = false
}
}
async function onToggleEnabled(row, enabled) {
try {
await setDataForwardEnabled(row.Id, enabled)
row.Enabled = enabled
ElMessage.success(enabled ? '已启用' : '已停用')
} catch (e) { /* 拦截器已提示 */ }
}
async function onDelete(row) {
await ElMessageBox.confirm(`确定删除规则「${row.ForwardName}」吗?`, '提示', { type: 'warning' })
try {
await deleteDataForward(row.Id)
ElMessage.success('删除成功')
fetchList()
} catch (e) { /* 拦截器已提示 */ }
}
onMounted(fetchList)
</script>
<style scoped>
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.form-tip {
font-size: 12px;
color: #909399;
line-height: 1.4;
}
</style>