From d070e2c417a8f89cc958edfb77d01c18dbf52c17 Mon Sep 17 00:00:00 2001 From: zch Date: Thu, 29 May 2025 18:00:25 +0800 Subject: [PATCH] =?UTF-8?q?feat(=E9=A6=96=E9=A1=B5=E5=92=8C/layout/compone?= =?UTF-8?q?nts/Navbar):=20=E5=BB=BA=E7=AB=8B=20WebSocket=20=E8=BF=9E?= =?UTF-8?q?=E6=8E=A5=E5=B9=B6=E5=A4=84=E7=90=86=E5=AE=9E=E6=97=B6=E6=95=B0?= =?UTF-8?q?=E6=8D=AE=E3=80=82=E6=9B=B4=E6=96=B0=E7=B3=BB=E7=BB=9F=E5=90=8D?= =?UTF-8?q?=E7=A7=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 WebSocket 连接逻辑,实现与服务器的实时通信 - 添加设备数据和告警信息的实时处理功能 - 优化用户界面,展示实时更新的数据 - 调整环境变量和登录界面文本 - 移除不必要的统计刷新操作 --- .env.development | 4 +- .env.production | 4 +- .env.staging | 4 +- package.json | 2 +- src/api/ems/record/recordAlarmData.js | 11 + src/layout/components/Navbar.vue | 501 +++++++++++++++++- src/main.js | 3 + .../base/baseMonitorInfoIOTDevice/index.vue | 13 +- .../ems/record/recordAlarmRule/index.vue | 104 ++-- src/views/index.vue | 309 ++++++++++- src/views/login.vue | 2 +- src/views/register.vue | 2 +- vue.config.js | 2 +- 13 files changed, 874 insertions(+), 87 deletions(-) diff --git a/.env.development b/.env.development index 6216ee5..40e5d75 100644 --- a/.env.development +++ b/.env.development @@ -1,10 +1,10 @@ # 页面标题 -VUE_APP_TITLE = 机场行李系统设备健康监测系统 +VUE_APP_TITLE = 行李健康监测系统 # 开发环境配置 ENV = 'development' -# 机场行李系统设备健康监测系统/开发环境 +# 行李健康监测系统/开发环境 VUE_APP_BASE_API = '/dev-api' # 路由懒加载 diff --git a/.env.production b/.env.production index 9944e41..a6396b8 100644 --- a/.env.production +++ b/.env.production @@ -1,8 +1,8 @@ # 页面标题 -VUE_APP_TITLE = 机场行李系统设备健康监测系统 +VUE_APP_TITLE = 行李健康监测系统 # 生产环境配置 ENV = 'production' -# 机场行李系统设备健康监测系统/生产环境 +# 行李健康监测系统/生产环境 VUE_APP_BASE_API = '/prod-api' diff --git a/.env.staging b/.env.staging index a970b74..c7ca752 100644 --- a/.env.staging +++ b/.env.staging @@ -1,10 +1,10 @@ # 页面标题 -VUE_APP_TITLE = 机场行李系统设备健康监测系统 +VUE_APP_TITLE = 行李健康监测系统 NODE_ENV = production # 测试环境配置 ENV = 'staging' -# 机场行李系统设备健康监测系统/测试环境 +# 行李健康监测系统/测试环境 VUE_APP_BASE_API = '/stage-api' diff --git a/package.json b/package.json index 665a4bf..1031d57 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "ruoyi", "version": "3.8.7", - "description": "机场行李系统设备健康监测系统", + "description": "行李健康监测系统", "author": "机场行李", "license": "MIT", "scripts": { diff --git a/src/api/ems/record/recordAlarmData.js b/src/api/ems/record/recordAlarmData.js index 0f42a37..8dc7588 100644 --- a/src/api/ems/record/recordAlarmData.js +++ b/src/api/ems/record/recordAlarmData.js @@ -58,3 +58,14 @@ export function getAlarmDataTotalCount() { method: 'get', }) } + +// 保存WebSocket告警数据(批量保存) +// 参数alarmDataList应为EmsRecordAlarmData实体数组 +// 每个告警规则对应一条EmsRecordAlarmData记录 +export function saveWebSocketAlarmData(alarmDataList) { + return request({ + url: '/ems/record/recordAlarmData/saveWebSocketAlarmData', + method: 'post', + data: alarmDataList // 传递EmsRecordAlarmData实体数组 + }) +} diff --git a/src/layout/components/Navbar.vue b/src/layout/components/Navbar.vue index 9f763b5..c454b5f 100644 --- a/src/layout/components/Navbar.vue +++ b/src/layout/components/Navbar.vue @@ -192,6 +192,158 @@ + + +
+ +
+

📟 设备信息

+ + +
+ 设备ID: + {{ currentRealtimeAlarm.monitorId }} +
+
+ +
+ 告警时间: + {{ formatAlarmTime(currentRealtimeAlarm.recordTime) }} +
+
+
+
+ + +
+

📊 设备当前数据

+ + +
+ 温度: + {{ currentRealtimeAlarm.deviceParam.temperature }}°C +
+
+ +
+ 湿度: + {{ currentRealtimeAlarm.deviceParam.humidity }}% +
+
+ +
+ 照度: + {{ currentRealtimeAlarm.deviceParam.illuminance }}lx +
+
+ +
+ 噪声: + {{ currentRealtimeAlarm.deviceParam.noise }}dB +
+
+ +
+ 气体浓度: + {{ currentRealtimeAlarm.deviceParam.concentration }}ppm +
+
+ +
+ 振动速度: + {{ currentRealtimeAlarm.deviceParam.vibrationSpeed || currentRealtimeAlarm.deviceParam.VibrationSpeed }}mm/s +
+
+ +
+ 振动位移: + {{ currentRealtimeAlarm.deviceParam.vibrationDisplacement || currentRealtimeAlarm.deviceParam.VibrationDisplacement }}um +
+
+ +
+ 振动加速度: + {{ currentRealtimeAlarm.deviceParam.vibrationAcceleration || currentRealtimeAlarm.deviceParam.VibrationAcceleration }}g +
+
+ +
+ 振动温度: + {{ currentRealtimeAlarm.deviceParam.vibrationTemp || currentRealtimeAlarm.deviceParam.VibrationTemp }}℃ +
+
+
+
+ + +
+

🚨 触发的告警规则

+
+
+
+ {{ rule.ruleName }} + + {{ rule.triggerRule === 0 ? '大于阈值' : '小于阈值' }} + +
+
+ 阈值:{{ rule.triggerValue }} + 监测字段:{{ getFieldName(rule.monitorField) }} + 备注:{{ rule.cause }} +
+
+
+
+ + +
+

📋 告警内容详情

+
+
+ +
+
+
+
+ + +
+ @@ -208,6 +360,7 @@ import RuoYiDoc from '@/components/RuoYi/Doc' import settings from '@/settings' import { handleExceptions, listRecordAlarmData } from '@/api/ems/record/recordAlarmData' import { getEmsAlarmActionStepsByRuleId, getEmsAlarmActionStepsByAlarmInfo } from '@/api/ems/base/emsAlarmActionStep' +import { saveWebSocketAlarmData } from '@/api/ems/record/recordAlarmData' export default { dicts: ['alarm_type', 'alarm_status'], @@ -239,16 +392,27 @@ export default { actionStepsLoading: false, activeTab: 'alarmList', imagePreviewVisible: false, - previewImageUrl: '' + previewImageUrl: '', + + // WebSocket告警相关 + realtimeAlarmDialog: false, + currentRealtimeAlarm: null, + alarmProcessing: false } }, created() { localStorage.setItem('this.alarmDataTotal', 0) + + // 监听WebSocket告警事件 + this.$bus.$on('websocket-alarm', this.handleRealtimeAlarm) + }, + beforeDestroy() { + // 移除事件监听 + this.$bus.$off('websocket-alarm', this.handleRealtimeAlarm) }, mounted() { - // 定时获取提示路由信息 + // 初始获取告警数据,后续完全依赖WebSocket推送 this.getAlarmData() - setInterval(() => this.getAlarmData(), 1000 * 60); }, components: { Breadcrumb, @@ -393,6 +557,215 @@ export default { // 动态拼接当前环境的baseURL const baseURL = process.env.VUE_APP_BASE_API || ''; return baseURL + relativePath; + }, + // 处理WebSocket实时告警 + handleRealtimeAlarm(alarmData) { + console.log('收到实时告警:', alarmData) + this.currentRealtimeAlarm = alarmData + this.realtimeAlarmDialog = true + + // 同时播放提示音(可选) + this.playAlarmSound() + + // 注意:这里不立即保存,等用户操作后再保存 + // this.saveRealtimeAlarmData(alarmData) + }, + // 播放告警提示音 + playAlarmSound() { + try { + // 可以添加音频文件播放 + // const audio = new Audio('/static/alarm.mp3') + // audio.play() + console.log('播放告警提示音') + } catch (error) { + console.error('播放提示音失败:', error) + } + }, + // 保存WebSocket告警数据到数据库 + async saveRealtimeAlarmData(alarmData, alarmStatus = 1) { + try { + // 构建符合EmsRecordAlarmData实体的数据列表 + // 每个触发的告警规则对应一条EmsRecordAlarmData记录 + const alarmDataList = [] + + if (!alarmData.alarmRules || alarmData.alarmRules.length === 0) { + console.warn('告警数据中没有告警规则') + return + } + + // 获取当前时间并格式化为后端期望的格式 + const getCurrentTimeForBackend = () => { + const now = new Date() + const year = now.getFullYear() + const month = String(now.getMonth() + 1).padStart(2, '0') + const day = String(now.getDate()).padStart(2, '0') + const hours = String(now.getHours()).padStart(2, '0') + const minutes = String(now.getMinutes()).padStart(2, '0') + const seconds = String(now.getSeconds()).padStart(2, '0') + return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}` + } + + // 为每个触发的告警规则创建一条记录 + for (const rule of alarmData.alarmRules) { + // 获取对应字段的实际数值 + const actualValue = this.getActualValueFromDeviceParam(alarmData.deviceParam, rule.monitorField) + + const alarmRecord = { + // 设备相关信息 + monitorId: alarmData.monitorId, + collectTime: getCurrentTimeForBackend(), // 使用当前时间并格式化为后端期望格式 + + // 告警类型:根据规则的triggerRule设置(0=大于阈值,1=小于阈值) + alarmType: rule.triggerRule || 0, + + // 告警状态:根据用户操作设置(0=已处理,1=未处理) + alarmStatus: alarmStatus, + + // 告警数据:实际触发告警的数值 + alarmData: actualValue ? String(actualValue) : '', + + // 告警原因:使用字段名称 + cause: this.getFieldName(rule.monitorField), + + // 其他字段可以为空,后端会设置默认值 + operationName: null, + operationTime: null, + notifyUser: null + } + + alarmDataList.push(alarmRecord) + + console.log('构建告警记录:', { + 设备ID: alarmRecord.monitorId, + 告警字段: alarmRecord.cause, + 实际数值: alarmRecord.alarmData, + 告警类型: alarmRecord.alarmType === 0 ? '大于阈值' : '小于阈值', + 规则阈值: rule.triggerValue, + 记录时间: alarmRecord.collectTime, + 处理状态: alarmRecord.alarmStatus === 0 ? '已处理' : '未处理' + }) + } + + if (alarmDataList.length === 0) { + console.warn('没有有效的告警记录可保存') + return + } + + // 发送到后端保存 + const response = await saveWebSocketAlarmData(alarmDataList) + if (response.code === 200) { + console.log('告警数据保存成功:', response.msg) + // 刷新告警数据列表,获取最新数据 + this.getAlarmDataList() + return true + } else { + console.error('告警数据保存失败:', response.msg) + this.$message.error('告警数据保存失败: ' + response.msg) + return false + } + } catch (error) { + console.error('保存告警数据异常:', error) + this.$message.error('保存告警数据异常') + return false + } + }, + + // 从设备参数中获取指定字段的实际数值 + getActualValueFromDeviceParam(deviceParam, monitorField) { + if (!deviceParam || monitorField === null || monitorField === undefined) { + return null + } + + switch (monitorField) { + case 0: // 温度 + return deviceParam.temperature + case 1: // 湿度 + return deviceParam.humidity + case 2: // 振动-速度(mm/s) + return deviceParam.vibrationSpeed || deviceParam.VibrationSpeed + case 3: // 振动-位移(um) + return deviceParam.vibrationDisplacement || deviceParam.VibrationDisplacement + case 4: // 振动-加速度(g) + return deviceParam.vibrationAcceleration || deviceParam.VibrationAcceleration + case 5: // 振动-温度(℃) + return deviceParam.vibrationTemp || deviceParam.VibrationTemp + case 6: // 照度 + return deviceParam.illuminance + case 7: // 噪声 + return deviceParam.noise + case 8: // 气体浓度 + return deviceParam.concentration + default: + console.warn('未知的监测字段:', monitorField) + return null + } + }, + // 稍后处理实时告警(保存为未处理状态) + async closeRealtimeAlarmDialog() { + if (this.currentRealtimeAlarm) { + this.alarmProcessing = true + try { + // 保存告警数据,状态为1(未处理) + const success = await this.saveRealtimeAlarmData(this.currentRealtimeAlarm, 1) + if (success) { + this.$message.info('告警已记录,状态为未处理') + } + } catch (error) { + console.error('保存告警数据失败:', error) + this.$message.error('保存告警数据失败') + } finally { + this.alarmProcessing = false + } + } + + this.realtimeAlarmDialog = false + this.currentRealtimeAlarm = null + }, + + // 确认知晓实时告警(保存为已处理状态) + async processRealtimeAlarm() { + this.alarmProcessing = true + try { + // 保存告警数据,状态为0(已处理) + const success = await this.saveRealtimeAlarmData(this.currentRealtimeAlarm, 0) + if (success) { + this.$message.success('告警已确认知晓并标记为已处理') + } + + // 关闭弹窗 + this.realtimeAlarmDialog = false + this.currentRealtimeAlarm = null + } catch (error) { + console.error('处理告警失败:', error) + this.$message.error('处理告警失败') + } finally { + this.alarmProcessing = false + } + }, + // 格式化告警时间 + formatAlarmTime(time) { + if (!time) return '--' + try { + const date = new Date(time) + return date.toLocaleString('zh-CN') + } catch (error) { + return time + } + }, + // 获取监测字段名称 + getFieldName(fieldCode) { + const fieldMap = { + 0: '温度', + 1: '湿度', + 2: '振动-速度(mm/s)', + 3: '振动-位移(um)', + 4: '振动-加速度(g)', + 5: '振动-温度(℃)', + 6: '照度', + 7: '噪声', + 8: '气体浓度' + } + return fieldMap[fieldCode] || '未知字段' } } } @@ -582,4 +955,126 @@ export default { text-align: center; } +// 实时告警弹窗样式 +.realtime-alarm-dialog { + .el-dialog__header { + background: linear-gradient(135deg, #ff4757, #ff6b7a); + color: white; + padding: 15px 20px; + + .el-dialog__title { + color: white; + font-weight: bold; + font-size: 16px; + } + + .el-dialog__close { + color: white; + + &:hover { + color: #f1f1f1; + } + } + } + + .alarm-content { + max-height: 60vh; + overflow-y: auto; + + .alarm-section { + margin-bottom: 20px; + padding: 15px; + background: #f9f9f9; + border-radius: 8px; + border-left: 4px solid #ff4757; + + .section-title { + margin: 0 0 15px 0; + font-size: 14px; + font-weight: bold; + color: #333; + } + + .info-item, .data-item { + display: flex; + align-items: center; + margin-bottom: 8px; + + .label, .data-label { + font-weight: 500; + color: #666; + min-width: 80px; + } + + .value, .data-value { + color: #333; + font-weight: bold; + } + } + + .alarm-rules { + .rule-item { + background: white; + border: 1px solid #e6e6e6; + border-radius: 6px; + padding: 12px; + margin-bottom: 10px; + + .rule-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 8px; + + .rule-name { + font-weight: bold; + color: #333; + flex: 1; + } + } + + .rule-details { + display: flex; + flex-wrap: wrap; + gap: 15px; + + .detail-item { + font-size: 13px; + color: #666; + + &:before { + content: "• "; + color: #ff4757; + font-weight: bold; + } + } + } + } + } + + .alarm-contents { + .content-item { + margin-bottom: 10px; + + .el-alert { + .el-alert__title { + font-size: 13px; + line-height: 1.4; + } + } + } + } + } + } + + .dialog-footer { + text-align: center; + padding: 20px 0 10px 0; + + .el-button { + min-width: 100px; + } + } +} + diff --git a/src/main.js b/src/main.js index a5042c5..ff38dd0 100644 --- a/src/main.js +++ b/src/main.js @@ -52,6 +52,9 @@ Vue.prototype.selectDictLabels = selectDictLabels Vue.prototype.download = download Vue.prototype.handleTree = handleTree +// 创建事件总线 +Vue.prototype.$bus = new Vue() + // 全局组件挂载 Vue.component('DictTag', DictTag) Vue.component('Pagination', Pagination) diff --git a/src/views/ems/base/baseMonitorInfoIOTDevice/index.vue b/src/views/ems/base/baseMonitorInfoIOTDevice/index.vue index b469240..5b96f4c 100644 --- a/src/views/ems/base/baseMonitorInfoIOTDevice/index.vue +++ b/src/views/ems/base/baseMonitorInfoIOTDevice/index.vue @@ -259,7 +259,7 @@ :label="parseInt(dict.value)">{{ dict.label }} - + @@ -408,16 +408,7 @@ export default { }, }; }, - computed: { - // 是否显示监测字段列(仅温湿度设备和振动设备显示) - showMonitorField() { - return this.currentDevice.monitorType === 6 || this.currentDevice.monitorType === 10; - }, - // 表单中是否显示监测字段 - showMonitorFieldInForm() { - return this.currentDevice.monitorType === 6 || this.currentDevice.monitorType === 10; - } - }, + created() { getBaseEnergyTypeList({}).then(response => { this.energyTypeList = response.data diff --git a/src/views/ems/record/recordAlarmRule/index.vue b/src/views/ems/record/recordAlarmRule/index.vue index dce3599..bcce6b9 100644 --- a/src/views/ems/record/recordAlarmRule/index.vue +++ b/src/views/ems/record/recordAlarmRule/index.vue @@ -100,7 +100,11 @@ - + + + @@ -181,7 +185,7 @@ - + @@ -216,11 +220,11 @@ 添加步骤 - +
- +
步骤 {{ step.stepSequence }}
- -
- +
@@ -261,14 +265,14 @@ placeholder="请输入步骤描述" > - + - +
@@ -286,7 +290,7 @@
将图片拖到此处,或点击上传
只能上传jpg/png文件,且不超过2MB
- +
- +