增加告警页面(带bug)

This commit is contained in:
2025-11-16 17:34:22 +08:00
parent 5df3ec4da9
commit 8df89576ab
2 changed files with 578 additions and 0 deletions

View File

@@ -0,0 +1,245 @@
<template>
<div class="alarm-list">
<el-card>
<template #header>
<div class="card-header">
<div class="title-container">
<h2 class="page-title">告警管理</h2>
<el-button type="text" @click="loadAlarms()" class="refresh-btn" title="刷新告警列表">
<el-icon :size="20"><Refresh /></el-icon>
</el-button>
</div>
</div>
</template>
<el-tabs v-model="activeTab" @tab-click="handleTabClick">
<el-tab-pane label="未解决告警" name="unresolved"></el-tab-pane>
<el-tab-pane label="已解决告警" name="resolved"></el-tab-pane>
</el-tabs>
<!-- 加载状态 -->
<div v-if="loading" class="loading">
<el-skeleton animated />
</div>
<!-- 错误状态 -->
<div v-else-if="error" class="error">
<el-alert
title="获取告警数据失败"
:description="error"
type="error"
show-icon
closable
@close="error = null"
/>
<el-button type="primary" @click="loadAlarms()" class="retry-btn">重新加载</el-button>
</div>
<!-- 告警列表 -->
<el-table v-else :data="alarmData" style="width: 100%" :fit="true" table-layout="auto">
<el-table-column prop="alarm_summary" label="告警摘要" min-width="150" />
<el-table-column prop="level" label="告警级别" min-width="100">
<template #default="scope">{{ formatSeverity(scope.row.level) }}</template>
</el-table-column>
<el-table-column prop="source_type" label="告警来源类型" min-width="120">
<template #default="scope">{{ formatSourceType(scope.row.source_type) }}</template>
</el-table-column>
<el-table-column prop="source_id" label="告警来源ID" min-width="100" />
<el-table-column prop="trigger_time" label="触发时间" min-width="180">
<template #default="scope">{{ formatTime(scope.row.trigger_time) }}</template>
</el-table-column>
<el-table-column v-if="activeTab === 'resolved'" prop="resolve_time" label="解决时间" min-width="180">
<template #default="scope">{{ formatTime(scope.row.resolve_time) }}</template>
</el-table-column>
<el-table-column v-if="activeTab === 'unresolved'" label="操作" min-width="100" align="center">
<template #default="scope">
<el-button size="small" @click="handleIgnore(scope.row)" :disabled="scope.row.is_ignored">
{{ scope.row.is_ignored ? '已忽略' : '忽略' }}
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<el-pagination
v-if="!loading && !error"
class="pagination-container"
:current-page="pagination.currentPage"
:page-size="pagination.pageSize"
:total="pagination.total"
layout="total, sizes, prev, pager, next, jumper"
@size-change="handleSizeChange"
@current-change="handlePageChange"
/>
</el-card>
</div>
</template>
<script>
import { Refresh } from '@element-plus/icons-vue';
import { AlarmApi } from '../../api/alarm.js';
import { AlarmSourceType, SeverityLevel } from '../../enums.js';
import { ElMessageBox } from 'element-plus';
export default {
name: 'AlarmList',
components: {
Refresh,
},
data() {
return {
alarmData: [],
loading: false,
error: null,
activeTab: 'unresolved',
pagination: {
currentPage: 1,
pageSize: 10,
total: 0,
},
};
},
async mounted() {
await this.loadAlarms();
},
methods: {
/**
* 加载告警列表数据
* @param {string} [tabName] - 可选参数当前激活的tab名称用于确保调用正确的API
*/
async loadAlarms(tabName) {
this.loading = true;
this.error = null;
try {
const params = {
page: this.pagination.currentPage,
page_size: this.pagination.pageSize,
order_by: 'trigger_time DESC',
};
let response;
// 使用传入的tabName或组件的activeTab来判断
const currentTab = tabName || this.activeTab;
if (currentTab === 'unresolved') {
response = await AlarmApi.getActiveAlarms(params);
} else {
response = await AlarmApi.getHistoricalAlarms(params);
}
this.alarmData = response.list || [];
this.pagination.total = response.pagination?.total || 0;
} catch (err) {
this.error = err.message || '未知错误';
console.error('加载告警列表失败:', err);
} finally {
this.loading = false;
}
},
/**
* 处理标签页切换事件
* @param {object} tab - 当前激活的tab对象
*/
handleTabClick(tab) {
this.pagination.currentPage = 1;
this.loadAlarms(tab.paneName); // 显式传递paneName
},
handleSizeChange(newSize) {
this.pagination.pageSize = newSize;
this.loadAlarms();
},
handlePageChange(newPage) {
this.pagination.currentPage = newPage;
this.loadAlarms();
},
async handleIgnore(alarm) {
try {
const { value } = await ElMessageBox.prompt('请输入忽略时长(分钟)', '忽略告警', {
confirmButtonText: '确定',
cancelButtonText: '取消',
inputType: 'number',
inputPattern: /^[1-9]\d*$/,
inputErrorMessage: '请输入正整数',
});
const duration = parseInt(value, 10);
await AlarmApi.snoozeAlarm(alarm.id, { duration_minutes: duration });
this.$message.success('告警已忽略');
await this.loadAlarms();
} catch (err) {
if (err !== 'cancel') {
this.$message.error('操作失败: ' + (err.message || '未知错误'));
}
}
},
formatTime(timeStr) {
if (!timeStr) return '-';
return new Date(timeStr).toLocaleString();
},
formatSeverity(level) {
return SeverityLevel[level] || level;
},
formatSourceType(type) {
return AlarmSourceType[type] || type;
}
},
};
</script>
<style scoped>
.alarm-list {
padding: 20px;
max-width: 1200px;
margin: 0 auto;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 15px 0;
}
.title-container {
display: flex;
align-items: center;
gap: 5px;
}
.page-title {
margin: 0;
font-size: 1.5rem;
font-weight: bold;
line-height: 1;
}
.refresh-btn {
color: black;
background-color: transparent;
padding: 0;
width: 24px;
height: 24px;
display: flex;
align-items: center;
justify-content: center;
border: none;
}
.loading {
padding: 20px 0;
}
.error {
padding: 20px 0;
text-align: center;
}
.retry-btn {
margin-top: 15px;
}
.pagination-container {
margin-top: 20px;
display: flex;
justify-content: flex-end;
}
</style>

View File

@@ -0,0 +1,333 @@
<template>
<div class="threshold-alarm-list">
<el-card>
<template #header>
<div class="card-header">
<div class="title-container">
<h2 class="page-title">阈值告警配置</h2>
<el-button type="text" @click="loadData()" class="refresh-btn" title="刷新列表">
<el-icon :size="20"><Refresh /></el-icon>
</el-button>
</div>
<el-button type="primary" @click="handleAddRule">新增规则</el-button>
</div>
</template>
<el-tabs v-model="activeTab" @tab-click="handleTabClick">
<!-- 区域告警规则 -->
<el-tab-pane label="区域告警" name="area">
<el-table :data="areaAlarms.list" v-loading="loading">
<el-table-column prop="id" label="ID" width="80" />
<el-table-column prop="area_controller_id" label="区域主控ID" min-width="120" />
<el-table-column prop="sensor_type" label="传感器类型" min-width="120">
<template #default="scope">{{ formatSensorType(scope.row.sensor_type) }}</template>
</el-table-column>
<el-table-column prop="level" label="告警等级" min-width="100">
<template #default="scope">{{ formatSeverity(scope.row.level) }}</template>
</el-table-column>
<el-table-column label="触发条件" min-width="150">
<template #default="scope">
{{ `${scope.row.operator} ${scope.row.thresholds}` }}
</template>
</el-table-column>
<el-table-column label="操作" width="150" align="center">
<template #default="scope">
<el-button size="small" @click="handleEditRule(scope.row)">编辑</el-button>
<el-button size="small" type="danger" @click="handleDeleteRule(scope.row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<el-pagination
class="pagination-container"
:current-page="areaAlarms.pagination.currentPage"
:page-size="areaAlarms.pagination.pageSize"
:total="areaAlarms.pagination.total"
layout="total, sizes, prev, pager, next, jumper"
@size-change="handleSizeChange"
@current-change="handlePageChange"
/>
</el-tab-pane>
<!-- 设备告警规则 -->
<el-tab-pane label="设备告警" name="device">
<el-table :data="deviceAlarms.list" v-loading="loading">
<el-table-column prop="id" label="ID" width="80" />
<el-table-column prop="device_id" label="设备ID" min-width="120" />
<el-table-column prop="sensor_type" label="传感器类型" min-width="120">
<template #default="scope">{{ formatSensorType(scope.row.sensor_type) }}</template>
</el-table-column>
<el-table-column prop="level" label="告警等级" min-width="100">
<template #default="scope">{{ formatSeverity(scope.row.level) }}</template>
</el-table-column>
<el-table-column label="触发条件" min-width="150">
<template #default="scope">
{{ `${scope.row.operator} ${scope.row.thresholds}` }}
</template>
</el-table-column>
<el-table-column label="操作" width="150" align="center">
<template #default="scope">
<el-button size="small" @click="handleEditRule(scope.row)">编辑</el-button>
<el-button size="small" type="danger" @click="handleDeleteRule(scope.row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<el-pagination
class="pagination-container"
:current-page="deviceAlarms.pagination.currentPage"
:page-size="deviceAlarms.pagination.pageSize"
:total="deviceAlarms.pagination.total"
layout="total, sizes, prev, pager, next, jumper"
@size-change="handleSizeChange"
@current-change="handlePageChange"
/>
</el-tab-pane>
</el-tabs>
</el-card>
<!-- 新增/编辑对话框 -->
<el-dialog v-model="dialogVisible" :title="isEdit ? '编辑规则' : '新增规则'" width="500px" @close="resetForm">
<el-form :model="form" ref="ruleForm" label-width="100px">
<el-form-item v-if="activeTab === 'area'" label="区域主控ID" prop="area_controller_id" required>
<el-input v-model.number="form.area_controller_id" :disabled="isEdit"></el-input>
</el-form-item>
<el-form-item v-if="activeTab === 'device'" label="设备ID" prop="device_id" required>
<el-input v-model.number="form.device_id" :disabled="isEdit"></el-input>
</el-form-item>
<el-form-item label="传感器类型" prop="sensor_type" required>
<el-select v-model="form.sensor_type" placeholder="请选择" :disabled="isEdit">
<el-option v-for="(label, key) in SensorType" :key="key" :label="label" :value="key"></el-option>
</el-select>
</el-form-item>
<el-form-item label="告警等级" prop="level" required>
<el-select v-model="form.level" placeholder="请选择">
<el-option v-for="(label, key) in SeverityLevel" :key="key" :label="label" :value="key"></el-option>
</el-select>
</el-form-item>
<el-form-item label="操作符" prop="operator" required>
<el-select v-model="form.operator" placeholder="请选择">
<el-option v-for="(label, key) in Operator" :key="key" :label="label" :value="key"></el-option>
</el-select>
</el-form-item>
<el-form-item label="阈值" prop="thresholds" required>
<el-input-number v-model="form.thresholds"></el-input-number>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="submitForm">确定</el-button>
</template>
</el-dialog>
</div>
</template>
<script>
import { Refresh } from '@element-plus/icons-vue';
import { AlarmApi } from '../../api/alarm.js';
import { SensorType, SeverityLevel, Operator } from '../../enums.js';
export default {
name: 'ThresholdAlarmList',
components: {
Refresh,
},
data() {
return {
SensorType,
SeverityLevel,
Operator,
activeTab: 'area',
loading: false,
areaAlarms: {
list: [],
pagination: { currentPage: 1, pageSize: 10, total: 0 },
},
deviceAlarms: {
list: [],
pagination: { currentPage: 1, pageSize: 10, total: 0 },
},
dialogVisible: false,
isEdit: false,
form: {
id: null,
area_controller_id: null,
device_id: null,
sensor_type: '',
level: '',
operator: '',
thresholds: 0,
},
};
},
async mounted() {
await this.loadData();
},
methods: {
/**
* 加载阈值告警数据
* @param {string} [tabName] - 可选参数当前激活的tab名称用于确保调用正确的API
*/
async loadData(tabName) {
this.loading = true;
try {
const currentTab = tabName || this.activeTab;
if (currentTab === 'area') {
const { currentPage, pageSize } = this.areaAlarms.pagination;
const response = await AlarmApi.getAreaThresholdAlarms({ page: currentPage, page_size: pageSize });
this.areaAlarms.list = response.list || [];
this.areaAlarms.pagination.total = response.pagination?.total || 0;
} else {
const { currentPage, pageSize } = this.deviceAlarms.pagination;
const response = await AlarmApi.getDeviceThresholdAlarms({ page: currentPage, page_size: pageSize });
this.deviceAlarms.list = response.list || [];
this.deviceAlarms.pagination.total = response.pagination?.total || 0;
}
} catch (error) {
this.$message.error('加载告警规则失败: ' + (error.message || '未知错误'));
console.error('加载告警规则失败:', error);
} finally {
this.loading = false;
}
},
/**
* 处理标签页切换事件
* @param {object} tab - 当前激活的tab对象
*/
handleTabClick(tab) {
// 重置分页到第一页
if (tab.paneName === 'area') {
this.areaAlarms.pagination.currentPage = 1;
} else {
this.deviceAlarms.pagination.currentPage = 1;
}
this.loadData(tab.paneName); // 显式传递paneName
},
handleSizeChange(newSize) {
const pagination = this.activeTab === 'area' ? this.areaAlarms.pagination : this.deviceAlarms.pagination;
pagination.pageSize = newSize;
this.loadData();
},
handlePageChange(newPage) {
const pagination = this.activeTab === 'area' ? this.areaAlarms.pagination : this.deviceAlarms.pagination;
pagination.currentPage = newPage;
this.loadData();
},
handleAddRule() {
this.isEdit = false;
this.resetForm();
this.dialogVisible = true;
},
handleEditRule(rule) {
this.isEdit = true;
this.form = { ...rule };
this.dialogVisible = true;
},
async handleDeleteRule(rule) {
try {
await this.$confirm('确认删除这条规则吗?', '提示', { type: 'warning' });
if (this.activeTab === 'area') {
await AlarmApi.deleteAreaThresholdAlarm(rule.id);
} else {
// 注意:设备告警删除接口需要 sensor_type
await AlarmApi.deleteDeviceThresholdAlarm(rule.id, { sensor_type: rule.sensor_type });
}
this.$message.success('删除成功');
await this.loadData();
} catch (error) {
if (error !== 'cancel') {
this.$message.error('删除失败: ' + (error.message || '未知错误'));
}
}
},
async submitForm() {
try {
await this.$refs.ruleForm.validate();
const apiCall = this.isEdit ? this.updateRule : this.createRule;
await apiCall();
this.dialogVisible = false;
this.$message.success(this.isEdit ? '更新成功' : '创建成功');
} catch (error) {
if (error) { // validation error
console.log('表单验证失败');
}
} finally {
await this.loadData();
}
},
async createRule() {
const { id, ...requestBody } = this.form;
if (this.activeTab === 'area') {
await AlarmApi.createAreaThresholdAlarm(requestBody);
} else {
await AlarmApi.createDeviceThresholdAlarm(requestBody);
}
},
async updateRule() {
const { id, ...requestBody } = this.form;
if (this.activeTab === 'area') {
await AlarmApi.updateAreaThresholdAlarm(id, requestBody);
} else {
await AlarmApi.updateDeviceThresholdAlarm(id, requestBody);
}
},
resetForm() {
this.form = {
id: null,
area_controller_id: null,
device_id: null,
sensor_type: '',
level: '',
operator: '',
thresholds: 0,
};
if (this.$refs.ruleForm) {
this.$refs.ruleForm.resetFields();
}
},
formatSensorType(type) {
return SensorType[type] || type;
},
formatSeverity(level) {
return SeverityLevel[level] || level;
},
},
};
</script>
<style scoped>
.threshold-alarm-list {
padding: 20px;
max-width: 1200px;
margin: 0 auto;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.title-container {
display: flex;
align-items: center;
gap: 5px;
}
.page-title {
margin: 0;
font-size: 1.5rem;
font-weight: bold;
}
.refresh-btn {
color: black;
background-color: transparent;
padding: 0;
width: 24px;
height: 24px;
display: flex;
align-items: center;
justify-content: center;
border: none;
}
.pagination-container {
margin-top: 20px;
display: flex;
justify-content: flex-end;
}
</style>