|
|
|
|
@ -1,10 +1,50 @@
|
|
|
|
|
<template>
|
|
|
|
|
<div class="app-container">
|
|
|
|
|
<!-- 预警通知弹窗 -->
|
|
|
|
|
<el-notification
|
|
|
|
|
v-for="alert in alertNotifications"
|
|
|
|
|
:key="alert.alertId"
|
|
|
|
|
:title="getAlertTitle(alert)"
|
|
|
|
|
:message="alert.alertContent"
|
|
|
|
|
:type="getAlertType(alert)"
|
|
|
|
|
:duration="0"
|
|
|
|
|
position="top-right"
|
|
|
|
|
@close="removeAlertNotification(alert.alertId)"
|
|
|
|
|
style="margin-bottom: 10px;"
|
|
|
|
|
>
|
|
|
|
|
<div slot="default" class="alert-notification-content">
|
|
|
|
|
<div class="alert-item">
|
|
|
|
|
<span class="alert-label">设备:</span>
|
|
|
|
|
<span class="alert-value">{{ alert.deviceName }} ({{ alert.deviceCode }})</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="alert-item">
|
|
|
|
|
<span class="alert-label">参数:</span>
|
|
|
|
|
<span class="alert-value">{{ alert.paramName }}</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="alert-item">
|
|
|
|
|
<span class="alert-label">当前值:</span>
|
|
|
|
|
<span class="alert-value alert-value-danger">{{ alert.alertValue }}</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="alert-item">
|
|
|
|
|
<span class="alert-label">阈值:</span>
|
|
|
|
|
<span class="alert-value">{{ alert.thresholdValue }}</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="alert-actions">
|
|
|
|
|
<el-button size="mini" type="primary" @click="viewAlertDetail(alert)">查看详情</el-button>
|
|
|
|
|
<el-button size="mini" @click="removeAlertNotification(alert.alertId)">关闭</el-button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</el-notification>
|
|
|
|
|
|
|
|
|
|
<!-- 页面头部 -->
|
|
|
|
|
<div class="page-header">
|
|
|
|
|
<div class="page-title">设备工艺参数监控</div>
|
|
|
|
|
<div class="page-actions">
|
|
|
|
|
<span class="page-time">{{ nowTimeStr }}</span>
|
|
|
|
|
<!-- 预警提醒图标 -->
|
|
|
|
|
<el-badge :value="unreadAlertCount" :hidden="unreadAlertCount === 0" class="alert-badge">
|
|
|
|
|
<el-button type="danger" size="mini" icon="el-icon-warning" circle @click="showAlertPanel = !showAlertPanel" />
|
|
|
|
|
</el-badge>
|
|
|
|
|
<el-switch
|
|
|
|
|
v-model="autoRefresh"
|
|
|
|
|
active-text="自动刷新开"
|
|
|
|
|
@ -17,6 +57,54 @@
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- 预警面板(可折叠) -->
|
|
|
|
|
<div v-if="showAlertPanel" class="alert-panel">
|
|
|
|
|
<div class="alert-panel-header">
|
|
|
|
|
<span class="alert-panel-title">
|
|
|
|
|
<i class="el-icon-warning-outline"></i>
|
|
|
|
|
实时预警 ({{ unreadAlertCount }} 条未处理)
|
|
|
|
|
</span>
|
|
|
|
|
<div class="alert-panel-actions">
|
|
|
|
|
<el-button v-if="alertList.length > 0" type="text" size="mini" icon="el-icon-check" @click="handleMarkAllProcessed">全部标记已处理</el-button>
|
|
|
|
|
<el-button type="text" size="mini" icon="el-icon-close" @click="showAlertPanel = false" />
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="alert-list" v-loading="alertLoading">
|
|
|
|
|
<div v-if="alertList.length === 0" class="no-alert">
|
|
|
|
|
<i class="el-icon-success"></i>
|
|
|
|
|
<span>暂无预警信息</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div
|
|
|
|
|
v-for="alert in alertList"
|
|
|
|
|
:key="alert.alertId"
|
|
|
|
|
:class="['alert-item', 'alert-level-' + alert.alertLevel]"
|
|
|
|
|
>
|
|
|
|
|
<div class="alert-item-header">
|
|
|
|
|
<el-tag :type="getAlertTagType(alert)" size="mini">{{ getAlertLevelText(alert) }}</el-tag>
|
|
|
|
|
<span class="alert-time">{{ parseTime(alert.alertTime, '{h}:{i}:{s}') }}</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="alert-item-content">
|
|
|
|
|
<div class="alert-content-row">
|
|
|
|
|
<span class="alert-label">设备:</span>
|
|
|
|
|
<span class="alert-value">{{ alert.deviceName }} ({{ alert.deviceCode }})</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="alert-content-row">
|
|
|
|
|
<span class="alert-label">参数:</span>
|
|
|
|
|
<span class="alert-value">{{ alert.paramName }}</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="alert-content-row">
|
|
|
|
|
<span class="alert-label">内容:</span>
|
|
|
|
|
<span class="alert-value">{{ alert.alertContent }}</span>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="alert-item-actions">
|
|
|
|
|
<el-button size="mini" type="text" @click="viewAlertDetail(alert)">详情</el-button>
|
|
|
|
|
<el-button size="mini" type="text" @click="markAsRead(alert)">标记已读</el-button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- 主体内容:左侧设备列表 + 右侧参数展示 -->
|
|
|
|
|
<div class="main-content">
|
|
|
|
|
<!-- 左侧设备列表 -->
|
|
|
|
|
@ -112,6 +200,7 @@
|
|
|
|
|
<script>
|
|
|
|
|
import { getLatestVal } from "@/api/baseDeviceParamVal/val";
|
|
|
|
|
import { getDeviceLedgerList } from "@/api/base/deviceLedger";
|
|
|
|
|
import { listProcessAlert, handleProcessAlert, checkThresholdAlert, batchMarkAsProcessed } from "@/api/base/processAlert";
|
|
|
|
|
|
|
|
|
|
export default {
|
|
|
|
|
name: "Val",
|
|
|
|
|
@ -134,6 +223,14 @@ export default {
|
|
|
|
|
deviceParams: [],
|
|
|
|
|
paramLoading: false,
|
|
|
|
|
paramSearchKey: '',
|
|
|
|
|
// 预警相关
|
|
|
|
|
alertList: [],
|
|
|
|
|
alertNotifications: [],
|
|
|
|
|
alertLoading: false,
|
|
|
|
|
showAlertPanel: false,
|
|
|
|
|
alertTimer: null,
|
|
|
|
|
alertCheckIntervalMs: 30000, // 30秒检查一次预警
|
|
|
|
|
lastAlertIds: [], // 记录上次的预警ID,用于判断是否有新预警
|
|
|
|
|
};
|
|
|
|
|
},
|
|
|
|
|
computed: {
|
|
|
|
|
@ -166,16 +263,23 @@ export default {
|
|
|
|
|
if (this.deviceParams.length === 0) return '-';
|
|
|
|
|
const t = this.deviceParams[0].recordTime || this.deviceParams[0].collectTime;
|
|
|
|
|
return t ? this.parseTime(t, '{y}-{m}-{d} {h}:{i}:{s}') : '-';
|
|
|
|
|
},
|
|
|
|
|
// 未读预警数量
|
|
|
|
|
unreadAlertCount() {
|
|
|
|
|
return this.alertList.filter(alert => alert.alertStatus === '0').length;
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
created() {
|
|
|
|
|
this.tickClock();
|
|
|
|
|
this.loadDeviceList();
|
|
|
|
|
this.loadAllLatestParams(); // 默认加载所有设备最新参数
|
|
|
|
|
this.loadAlerts(); // 加载预警列表
|
|
|
|
|
if (this.autoRefresh) this.startAutoRefresh();
|
|
|
|
|
this.startAlertCheck(); // 开始定时检查预警
|
|
|
|
|
},
|
|
|
|
|
beforeDestroy() {
|
|
|
|
|
this.stopAutoRefresh();
|
|
|
|
|
this.stopAlertCheck();
|
|
|
|
|
if (this.clockTimer) {
|
|
|
|
|
clearInterval(this.clockTimer);
|
|
|
|
|
this.clockTimer = null;
|
|
|
|
|
@ -211,12 +315,28 @@ export default {
|
|
|
|
|
this.refreshTimer = null;
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
/** 开始定时检查预警 */
|
|
|
|
|
startAlertCheck() {
|
|
|
|
|
this.stopAlertCheck();
|
|
|
|
|
this.alertTimer = setInterval(() => {
|
|
|
|
|
this.checkAndLoadAlerts();
|
|
|
|
|
}, this.alertCheckIntervalMs);
|
|
|
|
|
},
|
|
|
|
|
/** 停止定时检查预警 */
|
|
|
|
|
stopAlertCheck() {
|
|
|
|
|
if (this.alertTimer) {
|
|
|
|
|
clearInterval(this.alertTimer);
|
|
|
|
|
this.alertTimer = null;
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
/** 自动刷新切换 */
|
|
|
|
|
onAutoRefreshChange(val) {
|
|
|
|
|
if (val) {
|
|
|
|
|
this.startAutoRefresh();
|
|
|
|
|
this.startAlertCheck();
|
|
|
|
|
} else {
|
|
|
|
|
this.stopAutoRefresh();
|
|
|
|
|
this.stopAlertCheck();
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
/** 手动刷新 */
|
|
|
|
|
@ -274,6 +394,109 @@ export default {
|
|
|
|
|
this.paramLoading = false;
|
|
|
|
|
this.deviceParams = [];
|
|
|
|
|
});
|
|
|
|
|
},
|
|
|
|
|
/** 加载预警列表 */
|
|
|
|
|
loadAlerts() {
|
|
|
|
|
this.alertLoading = true;
|
|
|
|
|
listProcessAlert({
|
|
|
|
|
pageNum: 1,
|
|
|
|
|
pageSize: 20,
|
|
|
|
|
alertStatus: '0' // 只查询未处理的预警
|
|
|
|
|
}).then(response => {
|
|
|
|
|
this.alertList = response.rows || [];
|
|
|
|
|
this.alertLoading = false;
|
|
|
|
|
}).catch(() => {
|
|
|
|
|
this.alertLoading = false;
|
|
|
|
|
this.alertList = [];
|
|
|
|
|
});
|
|
|
|
|
},
|
|
|
|
|
/** 检查并加载预警(定时调用) */
|
|
|
|
|
checkAndLoadAlerts() {
|
|
|
|
|
// 先检查阈值,生成预警记录
|
|
|
|
|
checkThresholdAlert().then(() => {
|
|
|
|
|
// 然后加载预警列表
|
|
|
|
|
const currentAlertIds = this.alertList.map(a => a.alertId);
|
|
|
|
|
this.loadAlerts().then(() => {
|
|
|
|
|
// 检查是否有新的预警
|
|
|
|
|
const newAlerts = this.alertList.filter(alert => !currentAlertIds.includes(alert.alertId));
|
|
|
|
|
if (newAlerts.length > 0) {
|
|
|
|
|
// 显示新预警通知
|
|
|
|
|
newAlerts.forEach(alert => {
|
|
|
|
|
this.showAlertNotification(alert);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}).catch(() => {
|
|
|
|
|
// 即使检查失败,也加载预警列表
|
|
|
|
|
this.loadAlerts();
|
|
|
|
|
});
|
|
|
|
|
},
|
|
|
|
|
/** 显示预警通知 */
|
|
|
|
|
showAlertNotification(alert) {
|
|
|
|
|
// 避免重复显示
|
|
|
|
|
if (this.alertNotifications.find(n => n.alertId === alert.alertId)) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
this.alertNotifications.push(alert);
|
|
|
|
|
},
|
|
|
|
|
/** 移除预警通知 */
|
|
|
|
|
removeAlertNotification(alertId) {
|
|
|
|
|
const index = this.alertNotifications.findIndex(n => n.alertId === alertId);
|
|
|
|
|
if (index > -1) {
|
|
|
|
|
this.alertNotifications.splice(index, 1);
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
/** 获取预警标题 */
|
|
|
|
|
getAlertTitle(alert) {
|
|
|
|
|
return alert.alertLevel === '3' ? '【紧急预警】' : alert.alertLevel === '2' ? '【重要预警】' : '【一般预警】';
|
|
|
|
|
},
|
|
|
|
|
/** 获取预警类型 */
|
|
|
|
|
getAlertType(alert) {
|
|
|
|
|
return alert.alertLevel === '3' ? 'error' : alert.alertLevel === '2' ? 'warning' : 'info';
|
|
|
|
|
},
|
|
|
|
|
/** 获取预警标签类型 */
|
|
|
|
|
getAlertTagType(alert) {
|
|
|
|
|
return alert.alertLevel === '3' ? 'danger' : alert.alertLevel === '2' ? 'warning' : 'info';
|
|
|
|
|
},
|
|
|
|
|
/** 获取预警级别文本 */
|
|
|
|
|
getAlertLevelText(alert) {
|
|
|
|
|
return alert.alertLevel === '3' ? '紧急' : alert.alertLevel === '2' ? '重要' : '一般';
|
|
|
|
|
},
|
|
|
|
|
/** 查看预警详情 */
|
|
|
|
|
viewAlertDetail(alert) {
|
|
|
|
|
this.$router.push({
|
|
|
|
|
path: '/base/processAlert',
|
|
|
|
|
query: { alertId: alert.alertId }
|
|
|
|
|
});
|
|
|
|
|
},
|
|
|
|
|
/** 标记为已读 */
|
|
|
|
|
markAsRead(alert) {
|
|
|
|
|
handleProcessAlert({
|
|
|
|
|
alertId: alert.alertId,
|
|
|
|
|
alertStatus: '1',
|
|
|
|
|
handleResult: '已查看'
|
|
|
|
|
}).then(() => {
|
|
|
|
|
this.$message.success('标记成功');
|
|
|
|
|
this.loadAlerts();
|
|
|
|
|
});
|
|
|
|
|
},
|
|
|
|
|
/** 全部标记为已处理 */
|
|
|
|
|
handleMarkAllProcessed() {
|
|
|
|
|
const alertIds = this.alertList.map(alert => alert.alertId);
|
|
|
|
|
if (alertIds.length === 0) {
|
|
|
|
|
this.$message.warning('暂无预警需要处理');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
this.$confirm('确认将全部 ' + alertIds.length + ' 条预警标记为已处理?', '提示', {
|
|
|
|
|
confirmButtonText: '确定',
|
|
|
|
|
cancelButtonText: '取消',
|
|
|
|
|
type: 'warning'
|
|
|
|
|
}).then(() => {
|
|
|
|
|
batchMarkAsProcessed(alertIds).then(() => {
|
|
|
|
|
this.$message.success('标记成功');
|
|
|
|
|
this.loadAlerts();
|
|
|
|
|
});
|
|
|
|
|
}).catch(() => {});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
@ -305,6 +528,142 @@ export default {
|
|
|
|
|
opacity: 0.9;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* 预警徽章 */
|
|
|
|
|
.alert-badge {
|
|
|
|
|
margin-right: 4px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* 预警面板 */
|
|
|
|
|
.alert-panel {
|
|
|
|
|
background: #fff;
|
|
|
|
|
border-radius: 6px;
|
|
|
|
|
border: 1px solid #e8e8e8;
|
|
|
|
|
margin-bottom: 12px;
|
|
|
|
|
max-height: 400px;
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
}
|
|
|
|
|
.alert-panel-header {
|
|
|
|
|
display: flex;
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
align-items: center;
|
|
|
|
|
padding: 10px 16px;
|
|
|
|
|
border-bottom: 1px solid #f0f0f0;
|
|
|
|
|
background: #fafafa;
|
|
|
|
|
}
|
|
|
|
|
.alert-panel-title {
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
color: #333;
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
gap: 6px;
|
|
|
|
|
}
|
|
|
|
|
.alert-panel-title i {
|
|
|
|
|
color: #f56c6c;
|
|
|
|
|
}
|
|
|
|
|
.alert-panel-actions {
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
gap: 8px;
|
|
|
|
|
}
|
|
|
|
|
.alert-list {
|
|
|
|
|
max-height: 340px;
|
|
|
|
|
overflow-y: auto;
|
|
|
|
|
padding: 8px;
|
|
|
|
|
}
|
|
|
|
|
.no-alert {
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
align-items: center;
|
|
|
|
|
justify-content: center;
|
|
|
|
|
padding: 40px 20px;
|
|
|
|
|
color: #67c23a;
|
|
|
|
|
}
|
|
|
|
|
.no-alert i {
|
|
|
|
|
font-size: 36px;
|
|
|
|
|
margin-bottom: 8px;
|
|
|
|
|
}
|
|
|
|
|
.alert-item {
|
|
|
|
|
background: #fff;
|
|
|
|
|
border: 1px solid #e8e8e8;
|
|
|
|
|
border-radius: 6px;
|
|
|
|
|
padding: 10px;
|
|
|
|
|
margin-bottom: 8px;
|
|
|
|
|
border-left: 4px solid #909399;
|
|
|
|
|
}
|
|
|
|
|
.alert-item.alert-level-1 {
|
|
|
|
|
border-left-color: #909399;
|
|
|
|
|
background: #f5f7fa;
|
|
|
|
|
}
|
|
|
|
|
.alert-item.alert-level-2 {
|
|
|
|
|
border-left-color: #e6a23c;
|
|
|
|
|
background: #fdf6ec;
|
|
|
|
|
}
|
|
|
|
|
.alert-item.alert-level-3 {
|
|
|
|
|
border-left-color: #f56c6c;
|
|
|
|
|
background: #fef0f0;
|
|
|
|
|
}
|
|
|
|
|
.alert-item-header {
|
|
|
|
|
display: flex;
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
align-items: center;
|
|
|
|
|
margin-bottom: 8px;
|
|
|
|
|
}
|
|
|
|
|
.alert-time {
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
color: #999;
|
|
|
|
|
}
|
|
|
|
|
.alert-item-content {
|
|
|
|
|
margin-bottom: 8px;
|
|
|
|
|
}
|
|
|
|
|
.alert-content-row {
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
margin-bottom: 4px;
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
}
|
|
|
|
|
.alert-label {
|
|
|
|
|
color: #999;
|
|
|
|
|
min-width: 40px;
|
|
|
|
|
}
|
|
|
|
|
.alert-value {
|
|
|
|
|
color: #333;
|
|
|
|
|
flex: 1;
|
|
|
|
|
}
|
|
|
|
|
.alert-item-actions {
|
|
|
|
|
display: flex;
|
|
|
|
|
justify-content: flex-end;
|
|
|
|
|
gap: 8px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* 预警通知内容 */
|
|
|
|
|
.alert-notification-content {
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
}
|
|
|
|
|
.alert-notification-content .alert-item {
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
margin-bottom: 6px;
|
|
|
|
|
}
|
|
|
|
|
.alert-notification-content .alert-label {
|
|
|
|
|
color: #999;
|
|
|
|
|
min-width: 50px;
|
|
|
|
|
}
|
|
|
|
|
.alert-notification-content .alert-value {
|
|
|
|
|
color: #333;
|
|
|
|
|
flex: 1;
|
|
|
|
|
}
|
|
|
|
|
.alert-notification-content .alert-value-danger {
|
|
|
|
|
color: #f56c6c;
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
}
|
|
|
|
|
.alert-notification-content .alert-actions {
|
|
|
|
|
margin-top: 10px;
|
|
|
|
|
display: flex;
|
|
|
|
|
justify-content: flex-end;
|
|
|
|
|
gap: 8px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* 主体布局 */
|
|
|
|
|
.main-content {
|
|
|
|
|
display: flex;
|
|
|
|
|
|