feat(首页和/layout/components/Navbar): 建立 WebSocket 连接并处理实时数据。更新系统名称

- 新增 WebSocket 连接逻辑,实现与服务器的实时通信
- 添加设备数据和告警信息的实时处理功能
- 优化用户界面,展示实时更新的数据
- 调整环境变量和登录界面文本
- 移除不必要的统计刷新操作
boardTest
zch 4 weeks ago
parent 7d1a334b02
commit d070e2c417

@ -1,10 +1,10 @@
# 页面标题 # 页面标题
VUE_APP_TITLE = 机场行李系统设备健康监测系统 VUE_APP_TITLE = 行李健康监测系统
# 开发环境配置 # 开发环境配置
ENV = 'development' ENV = 'development'
# 机场行李系统设备健康监测系统/开发环境 # 行李健康监测系统/开发环境
VUE_APP_BASE_API = '/dev-api' VUE_APP_BASE_API = '/dev-api'
# 路由懒加载 # 路由懒加载

@ -1,8 +1,8 @@
# 页面标题 # 页面标题
VUE_APP_TITLE = 机场行李系统设备健康监测系统 VUE_APP_TITLE = 行李健康监测系统
# 生产环境配置 # 生产环境配置
ENV = 'production' ENV = 'production'
# 机场行李系统设备健康监测系统/生产环境 # 行李健康监测系统/生产环境
VUE_APP_BASE_API = '/prod-api' VUE_APP_BASE_API = '/prod-api'

@ -1,10 +1,10 @@
# 页面标题 # 页面标题
VUE_APP_TITLE = 机场行李系统设备健康监测系统 VUE_APP_TITLE = 行李健康监测系统
NODE_ENV = production NODE_ENV = production
# 测试环境配置 # 测试环境配置
ENV = 'staging' ENV = 'staging'
# 机场行李系统设备健康监测系统/测试环境 # 行李健康监测系统/测试环境
VUE_APP_BASE_API = '/stage-api' VUE_APP_BASE_API = '/stage-api'

@ -1,7 +1,7 @@
{ {
"name": "ruoyi", "name": "ruoyi",
"version": "3.8.7", "version": "3.8.7",
"description": "机场行李系统设备健康监测系统", "description": "行李健康监测系统",
"author": "机场行李", "author": "机场行李",
"license": "MIT", "license": "MIT",
"scripts": { "scripts": {

@ -58,3 +58,14 @@ export function getAlarmDataTotalCount() {
method: 'get', method: 'get',
}) })
} }
// 保存WebSocket告警数据批量保存
// 参数alarmDataList应为EmsRecordAlarmData实体数组
// 每个告警规则对应一条EmsRecordAlarmData记录
export function saveWebSocketAlarmData(alarmDataList) {
return request({
url: '/ems/record/recordAlarmData/saveWebSocketAlarmData',
method: 'post',
data: alarmDataList // 传递EmsRecordAlarmData实体数组
})
}

@ -192,6 +192,158 @@
</div> </div>
</el-dialog> </el-dialog>
<!-- 实时告警弹窗 -->
<el-dialog
title="⚠️ 实时告警通知"
:visible.sync="realtimeAlarmDialog"
width="800px"
append-to-body
:close-on-click-modal="false"
:close-on-press-escape="false"
class="realtime-alarm-dialog"
>
<div v-if="currentRealtimeAlarm" class="alarm-content">
<!-- 设备信息 -->
<div class="alarm-section">
<h3 class="section-title">📟 设备信息</h3>
<el-row :gutter="16">
<el-col :span="12">
<div class="info-item">
<span class="label">设备ID</span>
<span class="value">{{ currentRealtimeAlarm.monitorId }}</span>
</div>
</el-col>
<el-col :span="12">
<div class="info-item">
<span class="label">告警时间</span>
<span class="value">{{ formatAlarmTime(currentRealtimeAlarm.recordTime) }}</span>
</div>
</el-col>
</el-row>
</div>
<!-- 设备当前数据 -->
<div class="alarm-section" v-if="currentRealtimeAlarm.deviceParam">
<h3 class="section-title">📊 设备当前数据</h3>
<el-row :gutter="16">
<el-col :span="8" v-if="currentRealtimeAlarm.deviceParam.temperature !== null">
<div class="data-item">
<span class="data-label">温度</span>
<span class="data-value">{{ currentRealtimeAlarm.deviceParam.temperature }}°C</span>
</div>
</el-col>
<el-col :span="8" v-if="currentRealtimeAlarm.deviceParam.humidity !== null">
<div class="data-item">
<span class="data-label">湿度</span>
<span class="data-value">{{ currentRealtimeAlarm.deviceParam.humidity }}%</span>
</div>
</el-col>
<el-col :span="8" v-if="currentRealtimeAlarm.deviceParam.illuminance !== null">
<div class="data-item">
<span class="data-label">照度</span>
<span class="data-value">{{ currentRealtimeAlarm.deviceParam.illuminance }}lx</span>
</div>
</el-col>
<el-col :span="8" v-if="currentRealtimeAlarm.deviceParam.noise !== null">
<div class="data-item">
<span class="data-label">噪声</span>
<span class="data-value">{{ currentRealtimeAlarm.deviceParam.noise }}dB</span>
</div>
</el-col>
<el-col :span="8" v-if="currentRealtimeAlarm.deviceParam.concentration !== null">
<div class="data-item">
<span class="data-label">气体浓度</span>
<span class="data-value">{{ currentRealtimeAlarm.deviceParam.concentration }}ppm</span>
</div>
</el-col>
<el-col :span="8" v-if="currentRealtimeAlarm.deviceParam.vibrationSpeed !== null">
<div class="data-item">
<span class="data-label">振动速度</span>
<span class="data-value">{{ currentRealtimeAlarm.deviceParam.vibrationSpeed || currentRealtimeAlarm.deviceParam.VibrationSpeed }}mm/s</span>
</div>
</el-col>
<el-col :span="8" v-if="currentRealtimeAlarm.deviceParam.vibrationDisplacement !== null || currentRealtimeAlarm.deviceParam.VibrationDisplacement !== null">
<div class="data-item">
<span class="data-label">振动位移</span>
<span class="data-value">{{ currentRealtimeAlarm.deviceParam.vibrationDisplacement || currentRealtimeAlarm.deviceParam.VibrationDisplacement }}um</span>
</div>
</el-col>
<el-col :span="8" v-if="currentRealtimeAlarm.deviceParam.vibrationAcceleration !== null || currentRealtimeAlarm.deviceParam.VibrationAcceleration !== null">
<div class="data-item">
<span class="data-label">振动加速度</span>
<span class="data-value">{{ currentRealtimeAlarm.deviceParam.vibrationAcceleration || currentRealtimeAlarm.deviceParam.VibrationAcceleration }}g</span>
</div>
</el-col>
<el-col :span="8" v-if="currentRealtimeAlarm.deviceParam.vibrationTemp !== null || currentRealtimeAlarm.deviceParam.VibrationTemp !== null">
<div class="data-item">
<span class="data-label">振动温度</span>
<span class="data-value">{{ currentRealtimeAlarm.deviceParam.vibrationTemp || currentRealtimeAlarm.deviceParam.VibrationTemp }}</span>
</div>
</el-col>
</el-row>
</div>
<!-- 触发的告警规则 -->
<div class="alarm-section" v-if="currentRealtimeAlarm.alarmRules && currentRealtimeAlarm.alarmRules.length > 0">
<h3 class="section-title">🚨 触发的告警规则</h3>
<div class="alarm-rules">
<div
v-for="(rule, index) in currentRealtimeAlarm.alarmRules"
:key="index"
class="rule-item"
>
<div class="rule-header">
<span class="rule-name">{{ rule.ruleName }}</span>
<el-tag :type="rule.triggerRule === 0 ? 'danger' : 'warning'" size="small">
{{ rule.triggerRule === 0 ? '大于阈值' : '小于阈值' }}
</el-tag>
</div>
<div class="rule-details">
<span class="detail-item">阈值{{ rule.triggerValue }}</span>
<span class="detail-item">监测字段{{ getFieldName(rule.monitorField) }}</span>
<span class="detail-item" v-if="rule.cause">{{ rule.cause }}</span>
</div>
</div>
</div>
</div>
<!-- 告警内容详情 -->
<div class="alarm-section" v-if="currentRealtimeAlarm.alarmContents && currentRealtimeAlarm.alarmContents.length > 0">
<h3 class="section-title">📋 告警内容详情</h3>
<div class="alarm-contents">
<div
v-for="(content, index) in currentRealtimeAlarm.alarmContents"
:key="index"
class="content-item"
>
<el-alert
:title="content"
type="error"
:closable="false"
show-icon
/>
</div>
</div>
</div>
</div>
<div slot="footer" class="dialog-footer">
<el-button
@click="closeRealtimeAlarmDialog"
:loading="alarmProcessing"
>
稍后处理
</el-button>
<el-button
type="primary"
@click="processRealtimeAlarm"
:loading="alarmProcessing"
>
确认知晓
</el-button>
</div>
</el-dialog>
</div> </div>
</template> </template>
@ -208,6 +360,7 @@ import RuoYiDoc from '@/components/RuoYi/Doc'
import settings from '@/settings' import settings from '@/settings'
import { handleExceptions, listRecordAlarmData } from '@/api/ems/record/recordAlarmData' import { handleExceptions, listRecordAlarmData } from '@/api/ems/record/recordAlarmData'
import { getEmsAlarmActionStepsByRuleId, getEmsAlarmActionStepsByAlarmInfo } from '@/api/ems/base/emsAlarmActionStep' import { getEmsAlarmActionStepsByRuleId, getEmsAlarmActionStepsByAlarmInfo } from '@/api/ems/base/emsAlarmActionStep'
import { saveWebSocketAlarmData } from '@/api/ems/record/recordAlarmData'
export default { export default {
dicts: ['alarm_type', 'alarm_status'], dicts: ['alarm_type', 'alarm_status'],
@ -239,16 +392,27 @@ export default {
actionStepsLoading: false, actionStepsLoading: false,
activeTab: 'alarmList', activeTab: 'alarmList',
imagePreviewVisible: false, imagePreviewVisible: false,
previewImageUrl: '' previewImageUrl: '',
// WebSocket
realtimeAlarmDialog: false,
currentRealtimeAlarm: null,
alarmProcessing: false
} }
}, },
created() { created() {
localStorage.setItem('this.alarmDataTotal', 0) localStorage.setItem('this.alarmDataTotal', 0)
// WebSocket
this.$bus.$on('websocket-alarm', this.handleRealtimeAlarm)
},
beforeDestroy() {
//
this.$bus.$off('websocket-alarm', this.handleRealtimeAlarm)
}, },
mounted() { mounted() {
// // WebSocket
this.getAlarmData() this.getAlarmData()
setInterval(() => this.getAlarmData(), 1000 * 60);
}, },
components: { components: {
Breadcrumb, Breadcrumb,
@ -393,6 +557,215 @@ export default {
// baseURL // baseURL
const baseURL = process.env.VUE_APP_BASE_API || ''; const baseURL = process.env.VUE_APP_BASE_API || '';
return baseURL + relativePath; 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(), // 使
// triggerRule0=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; 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;
}
}
}
</style> </style>

@ -52,6 +52,9 @@ Vue.prototype.selectDictLabels = selectDictLabels
Vue.prototype.download = download Vue.prototype.download = download
Vue.prototype.handleTree = handleTree Vue.prototype.handleTree = handleTree
// 创建事件总线
Vue.prototype.$bus = new Vue()
// 全局组件挂载 // 全局组件挂载
Vue.component('DictTag', DictTag) Vue.component('DictTag', DictTag)
Vue.component('Pagination', Pagination) Vue.component('Pagination', Pagination)

@ -259,7 +259,7 @@
:label="parseInt(dict.value)">{{ dict.label }}</el-radio> :label="parseInt(dict.value)">{{ dict.label }}</el-radio>
</el-radio-group> </el-radio-group>
</el-form-item> </el-form-item>
<el-form-item label="监测字段" prop="monitorField" v-if="showMonitorFieldInForm"> <el-form-item label="监测字段" prop="monitorField" >
<el-select v-model="thresholdForm.monitorField" placeholder="请选择监测字段"> <el-select v-model="thresholdForm.monitorField" placeholder="请选择监测字段">
<el-option v-for="dict in dict.type.monitor_field" :key="dict.value" <el-option v-for="dict in dict.type.monitor_field" :key="dict.value"
:label="dict.label" :value="dict.value" /> :label="dict.label" :value="dict.value" />
@ -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() { created() {
getBaseEnergyTypeList({}).then(response => { getBaseEnergyTypeList({}).then(response => {
this.energyTypeList = response.data this.energyTypeList = response.data

@ -100,7 +100,11 @@
<dict-tag :options="dict.type.trigger_rule" :value="scope.row.triggerRule"/> <dict-tag :options="dict.type.trigger_rule" :value="scope.row.triggerRule"/>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="触发阈值次数" align="center" prop="triggerNumber" v-if="columns[7].visible"/> <el-table-column label="监测字段" align="center" prop="monitorField" v-if="columns[7].visible">
<template slot-scope="scope">
<dict-tag :options="dict.type.monitor_field" :value="scope.row.monitorField"/>
</template>
</el-table-column>
<el-table-column label="时间范围(分)" align="center" prop="timeRange" v-if="columns[8].visible"/> <el-table-column label="时间范围(分)" align="center" prop="timeRange" v-if="columns[8].visible"/>
<el-table-column label="触发阈值量" align="center" prop="triggerValue" v-if="columns[9].visible"/> <el-table-column label="触发阈值量" align="center" prop="triggerValue" v-if="columns[9].visible"/>
<el-table-column label="通知用户" align="center" prop="notifyUser" v-if="columns[10].visible"/> <el-table-column label="通知用户" align="center" prop="notifyUser" v-if="columns[10].visible"/>
@ -181,7 +185,7 @@
</el-radio> </el-radio>
</el-radio-group> </el-radio-group>
</el-form-item> </el-form-item>
<el-form-item label="监测字段" prop="monitorField" v-if="selectedMonitorType === 6 || selectedMonitorType === 10"> <el-form-item label="监测字段" prop="monitorField" >
<el-select v-model="form.monitorField" placeholder="请选择监测字段"> <el-select v-model="form.monitorField" placeholder="请选择监测字段">
<el-option v-for="dict in dict.type.monitor_field" :key="dict.value" :label="dict.label" :value="dict.value"/> <el-option v-for="dict in dict.type.monitor_field" :key="dict.value" :label="dict.label" :value="dict.value"/>
</el-select> </el-select>
@ -216,11 +220,11 @@
添加步骤 添加步骤
</el-button> </el-button>
</div> </div>
<div v-if="actionStepsList.length === 0" class="empty-steps"> <div v-if="actionStepsList.length === 0" class="empty-steps">
<el-empty description="暂无处置措施,请点击添加步骤"></el-empty> <el-empty description="暂无处置措施,请点击添加步骤"></el-empty>
</div> </div>
<div v-else class="steps-list"> <div v-else class="steps-list">
<div <div
v-for="(step, index) in actionStepsList" v-for="(step, index) in actionStepsList"
@ -231,26 +235,26 @@
<div class="step-header"> <div class="step-header">
<span class="step-number">步骤 {{ step.stepSequence }}</span> <span class="step-number">步骤 {{ step.stepSequence }}</span>
<div class="step-controls"> <div class="step-controls">
<el-button <el-button
v-if="index > 0" v-if="index > 0"
type="text" type="text"
size="mini" size="mini"
icon="el-icon-top" icon="el-icon-top"
@click="moveStepUp(index)" @click="moveStepUp(index)"
title="上移" title="上移"
></el-button> ></el-button>
<el-button <el-button
v-if="index < actionStepsList.length - 1" v-if="index < actionStepsList.length - 1"
type="text" type="text"
size="mini" size="mini"
icon="el-icon-bottom" icon="el-icon-bottom"
@click="moveStepDown(index)" @click="moveStepDown(index)"
title="下移" title="下移"
></el-button> ></el-button>
<el-button type="danger" size="mini" icon="el-icon-delete" @click="removeActionStep(index)" circle></el-button> <el-button type="danger" size="mini" icon="el-icon-delete" @click="removeActionStep(index)" circle></el-button>
</div> </div>
</div> </div>
<div class="step-content"> <div class="step-content">
<el-form :model="step" label-width="100px" size="small"> <el-form :model="step" label-width="100px" size="small">
<el-form-item label="步骤描述"> <el-form-item label="步骤描述">
@ -261,14 +265,14 @@
placeholder="请输入步骤描述" placeholder="请输入步骤描述"
></el-input> ></el-input>
</el-form-item> </el-form-item>
<el-form-item label="备注"> <el-form-item label="备注">
<el-input <el-input
v-model="step.remark" v-model="step.remark"
placeholder="请输入备注信息" placeholder="请输入备注信息"
></el-input> ></el-input>
</el-form-item> </el-form-item>
<el-form-item label="参考图片"> <el-form-item label="参考图片">
<div class="image-upload-section"> <div class="image-upload-section">
<!-- 图片上传 --> <!-- 图片上传 -->
@ -286,7 +290,7 @@
<div class="el-upload__text">将图片拖到此处<em>点击上传</em></div> <div class="el-upload__text">将图片拖到此处<em>点击上传</em></div>
<div class="el-upload__tip" slot="tip">只能上传jpg/png文件且不超过2MB</div> <div class="el-upload__tip" slot="tip">只能上传jpg/png文件且不超过2MB</div>
</el-upload> </el-upload>
<!-- 图片列表 --> <!-- 图片列表 -->
<div v-if="step.stepImages && step.stepImages.length > 0" class="image-list"> <div v-if="step.stepImages && step.stepImages.length > 0" class="image-list">
<div <div
@ -321,7 +325,7 @@
</div> </div>
</div> </div>
</div> </div>
<div slot="footer" class="dialog-footer"> <div slot="footer" class="dialog-footer">
<el-button type="primary" @click="saveActionSteps" :loading="actionStepsSaving"> <el-button type="primary" @click="saveActionSteps" :loading="actionStepsSaving">
保存措施 保存措施
@ -415,15 +419,15 @@ export default {
{ key: 4, label: `记录时间`, visible: false }, { key: 4, label: `记录时间`, visible: false },
{ key: 5, label: `能源类型`, visible: false }, { key: 5, label: `能源类型`, visible: false },
{ key: 6, label: `触发规则`, visible: true }, { key: 6, label: `触发规则`, visible: true },
{ key: 7, label: `触发阈值次数`, visible: false }, { key: 7, label: `监测字段`, visible: true },
{ key: 8, label: `时间范围(分)`, visible: false }, { key: 8, label: `时间范围(分)`, visible: false },
{ key: 9, label: `触发阈值量`, visible: true }, { key: 9, label: `触发阈值量`, visible: true },
{ key: 10, label: `通知用户`, visible: false }, { key: 10, label: `通知用户`, visible: false },
{ key: 11, label: `备注`, visible: true }, { key: 11, label: `备注`, visible: true },
{ key: 12, label: `创建人`, visible: false }, // { key: 12, label: ``, visible: false },
{ key: 13, label: `创建时间`, visible: true }, // { key: 13, label: ``, visible: true },
{ key: 14, label: `更新人`, visible: false }, // { key: 14, label: ``, visible: false },
{ key: 15, label: `更新时间`, visible: false } // { key: 15, label: ``, visible: false }
], ],
actionStepsTitle: '', actionStepsTitle: '',
actionStepsOpen: false, actionStepsOpen: false,
@ -595,10 +599,10 @@ export default {
}); });
}, },
addActionStep() { addActionStep() {
const newSequence = this.actionStepsList.length > 0 const newSequence = this.actionStepsList.length > 0
? Math.max(...this.actionStepsList.map(s => s.stepSequence || 0)) + 1 ? Math.max(...this.actionStepsList.map(s => s.stepSequence || 0)) + 1
: 1; : 1;
const newStep = { const newStep = {
tempId: Date.now() + Math.random(), // ID tempId: Date.now() + Math.random(), // ID
ruleObjId: this.currentRuleObjId, ruleObjId: this.currentRuleObjId,
@ -607,7 +611,7 @@ export default {
remark: '', remark: '',
stepImages: [] stepImages: []
}; };
this.actionStepsList.push(newStep); this.actionStepsList.push(newStep);
}, },
removeActionStep(index) { removeActionStep(index) {
@ -629,7 +633,7 @@ export default {
if (!step.stepImages) { if (!step.stepImages) {
step.stepImages = []; step.stepImages = [];
} }
// URL // URL
let relativePath = res.url; let relativePath = res.url;
if (res.url.startsWith('http')) { if (res.url.startsWith('http')) {
@ -637,14 +641,14 @@ export default {
const urlObj = new URL(res.url); const urlObj = new URL(res.url);
relativePath = urlObj.pathname; relativePath = urlObj.pathname;
} }
const newImage = { const newImage = {
tempId: Date.now() + Math.random(), tempId: Date.now() + Math.random(),
imageUrl: relativePath, // imageUrl: relativePath, //
imageSequence: step.stepImages.length + 1, imageSequence: step.stepImages.length + 1,
description: '' description: ''
}; };
step.stepImages.push(newImage); step.stepImages.push(newImage);
this.$message.success('图片上传成功'); this.$message.success('图片上传成功');
} else { } else {
@ -687,14 +691,14 @@ export default {
return; return;
} }
} }
this.actionStepsSaving = true; this.actionStepsSaving = true;
// ID // ID
const stepsToSave = this.actionStepsList.map(step => { const stepsToSave = this.actionStepsList.map(step => {
const stepData = { ...step }; const stepData = { ...step };
delete stepData.tempId; delete stepData.tempId;
if (stepData.stepImages && stepData.stepImages.length > 0) { if (stepData.stepImages && stepData.stepImages.length > 0) {
stepData.stepImages = stepData.stepImages.map(img => { stepData.stepImages = stepData.stepImages.map(img => {
const imgData = { ...img }; const imgData = { ...img };
@ -702,10 +706,10 @@ export default {
return imgData; return imgData;
}); });
} }
return stepData; return stepData;
}); });
batchSaveActionSteps(this.currentRuleObjId, stepsToSave).then(response => { batchSaveActionSteps(this.currentRuleObjId, stepsToSave).then(response => {
this.$message.success('措施步骤保存成功'); this.$message.success('措施步骤保存成功');
this.actionStepsOpen = false; this.actionStepsOpen = false;
@ -748,12 +752,12 @@ export default {
// URL // URL
getFullImageUrl(relativePath) { getFullImageUrl(relativePath) {
if (!relativePath) return ''; if (!relativePath) return '';
// URL // URL
if (relativePath.startsWith('http')) { if (relativePath.startsWith('http')) {
return relativePath; return relativePath;
} }
// baseURL // baseURL
const baseURL = process.env.VUE_APP_BASE_API || ''; const baseURL = process.env.VUE_APP_BASE_API || '';
return baseURL + relativePath; return baseURL + relativePath;
@ -793,7 +797,7 @@ export default {
border: 1px solid #e4e7ed; border: 1px solid #e4e7ed;
border-radius: 8px; border-radius: 8px;
background: #fafafa; background: #fafafa;
.step-header { .step-header {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
@ -802,29 +806,29 @@ export default {
background: #f0f9ff; background: #f0f9ff;
border-bottom: 1px solid #e4e7ed; border-bottom: 1px solid #e4e7ed;
border-radius: 8px 8px 0 0; border-radius: 8px 8px 0 0;
.step-number { .step-number {
font-size: 16px; font-size: 16px;
font-weight: bold; font-weight: bold;
color: #409EFF; color: #409EFF;
} }
.step-controls { .step-controls {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 5px; gap: 5px;
.el-button--text { .el-button--text {
color: #606266; color: #606266;
font-size: 16px; font-size: 16px;
&:hover { &:hover {
color: #409EFF; color: #409EFF;
} }
} }
} }
} }
.step-content { .step-content {
padding: 20px; padding: 20px;
} }
@ -837,44 +841,44 @@ export default {
height: 120px; height: 120px;
margin-bottom: 20px; margin-bottom: 20px;
} }
.image-list { .image-list {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
gap: 15px; gap: 15px;
margin-top: 15px; margin-top: 15px;
.image-item { .image-item {
width: 200px; width: 200px;
border: 1px solid #e4e7ed; border: 1px solid #e4e7ed;
border-radius: 6px; border-radius: 6px;
overflow: hidden; overflow: hidden;
background: white; background: white;
.image-preview { .image-preview {
height: 150px; height: 150px;
cursor: pointer; cursor: pointer;
position: relative; position: relative;
overflow: hidden; overflow: hidden;
img { img {
width: 100%; width: 100%;
height: 100%; height: 100%;
object-fit: cover; object-fit: cover;
transition: transform 0.3s; transition: transform 0.3s;
} }
&:hover img { &:hover img {
transform: scale(1.05); transform: scale(1.05);
} }
} }
.image-info { .image-info {
padding: 10px; padding: 10px;
display: flex; display: flex;
align-items: center; align-items: center;
gap: 8px; gap: 8px;
.el-input { .el-input {
flex: 1; flex: 1;
} }
@ -885,7 +889,7 @@ export default {
.image-preview-container { .image-preview-container {
text-align: center; text-align: center;
img { img {
border-radius: 6px; border-radius: 6px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);

@ -190,7 +190,7 @@
<script> <script>
import { getLatestRecords, getLatestRecordsByParentId } from '@/api/ems/record/recordIotenvInstant' import { getLatestRecords, getLatestRecordsByParentId } from '@/api/ems/record/recordIotenvInstant'
import {getAlarmDataTotalCount} from "@/api/ems/record/recordAlarmData"; import {getAlarmDataTotalCount, saveWebSocketAlarmData} from "@/api/ems/record/recordAlarmData";
import {getEmsRecordAlarmRuleTotalCount} from "@/api/ems/record/recordAlarmRule"; import {getEmsRecordAlarmRuleTotalCount} from "@/api/ems/record/recordAlarmRule";
import { getMonitorInfoTree } from '@/api/ems/base/baseMonitorInfo'; import { getMonitorInfoTree } from '@/api/ems/base/baseMonitorInfo';
@ -200,7 +200,15 @@ export default {
return { return {
loading: false, loading: false,
deviceList: [], deviceList: [],
refreshTimer: null, // refreshTimer: null, // 使
// WebSocket
websocket: null,
websocketUrl: 'ws://119.45.202.115:7181/ws',
isWebSocketConnected: false,
reconnectTimer: null,
reconnectAttempts: 0,
maxReconnectAttempts: 5,
alarmRuleTotalCount: 0, alarmRuleTotalCount: 0,
alarmDataTotalCount: 0, alarmDataTotalCount: 0,
totalDeviceCount: 0, // totalDeviceCount: 0, //
@ -229,21 +237,163 @@ export default {
}, },
mounted() { mounted() {
this.loadDeviceTree() this.loadDeviceTree()
this.loadStatistics() // this.loadStatistics() //
// 1 this.initWebSocket() // WebSocket
this.refreshTimer = setInterval(() => {
if (this.selectedNodeId !== null) {
this.loadDeviceDataByNode(this.selectedNodeId)
}
this.loadStatistics()
}, 60000)
}, },
beforeDestroy() { beforeDestroy() {
if (this.refreshTimer) { // WebSocket
clearInterval(this.refreshTimer) if (this.reconnectTimer) {
clearInterval(this.reconnectTimer)
} }
// WebSocket
this.closeWebSocket()
}, },
methods: { methods: {
// WebSocket
initWebSocket() {
console.log('正在建立WebSocket连接...')
try {
this.websocket = new WebSocket(this.websocketUrl)
this.websocket.onopen = this.onWebSocketOpen
this.websocket.onmessage = this.onWebSocketMessage
this.websocket.onclose = this.onWebSocketClose
this.websocket.onerror = this.onWebSocketError
} catch (error) {
console.error('WebSocket连接失败:', error)
this.handleReconnect()
}
},
// WebSocket
onWebSocketOpen(event) {
console.log('WebSocket连接已建立')
this.isWebSocketConnected = true
this.reconnectAttempts = 0
//
if (this.reconnectTimer) {
clearInterval(this.reconnectTimer)
this.reconnectTimer = null
}
this.$message.success('实时数据连接已建立')
},
// WebSocket
onWebSocketMessage(event) {
try {
const data = JSON.parse(event.data)
console.log('收到WebSocket数据:', data)
if (data.deviceParam) {
//
this.handleDeviceData(data)
}
if (data.isFlag === 1 && data.alarmRules && data.alarmContents) {
// - 线Navbar
this.$bus.$emit('websocket-alarm', data)
// -
this.alarmDataTotalCount += data.alarmRules.length
console.log('实时更新告警统计,新增告警数量:', data.alarmRules.length, '当前总数:', this.alarmDataTotalCount)
}
} catch (error) {
console.error('解析WebSocket数据失败:', error)
}
},
// WebSocket
onWebSocketClose(event) {
console.log('WebSocket连接已关闭', event)
this.isWebSocketConnected = false
if (event.code !== 1000) { //
this.$message.warning('实时数据连接已断开,正在尝试重连...')
this.handleReconnect()
}
},
// WebSocket
onWebSocketError(event) {
console.error('WebSocket连接错误:', event)
this.isWebSocketConnected = false
this.handleReconnect()
},
//
handleReconnect() {
if (this.reconnectAttempts >= this.maxReconnectAttempts) {
console.log('达到最大重连次数,停止重连')
this.$message.error('实时数据连接失败,请刷新页面重试')
return
}
this.reconnectAttempts++
console.log(`${this.reconnectAttempts}次重连尝试...`)
this.reconnectTimer = setTimeout(() => {
this.initWebSocket()
}, 3000 * this.reconnectAttempts) //
},
// WebSocket
closeWebSocket() {
if (this.websocket) {
this.websocket.close(1000, '页面关闭')
this.websocket = null
}
this.isWebSocketConnected = false
},
//
handleDeviceData(data) {
const deviceParam = data.deviceParam
if (!deviceParam || !deviceParam.monitorId) {
return
}
//
if (this.selectedNodeId !== null) {
this.updateDeviceDataInList(deviceParam)
} else {
//
console.log('收到设备数据但未选中节点:', deviceParam.monitorId)
}
},
//
updateDeviceDataInList(deviceParam) {
const index = this.deviceList.findIndex(device => device.monitorId === deviceParam.monitorId)
if (index !== -1) {
//
const updatedDevice = {
...this.deviceList[index],
temperature: deviceParam.temperature,
humidity: deviceParam.humidity,
illuminance: deviceParam.illuminance,
noise: deviceParam.noise,
concentration: deviceParam.concentration,
vibrationSpeed: deviceParam.vibrationSpeed || deviceParam.VibrationSpeed,
vibrationDisplacement: deviceParam.vibrationDisplacement || deviceParam.VibrationDisplacement,
vibrationAcceleration: deviceParam.vibrationAcceleration || deviceParam.VibrationAcceleration,
vibrationTemp: deviceParam.vibrationTemp || deviceParam.VibrationTemp,
collectTime: deviceParam.collectTime,
recodeTime: deviceParam.recordTime
}
// 使Vue.set
this.$set(this.deviceList, index, updatedDevice)
console.log('更新设备数据:', deviceParam.monitorId)
} else {
console.log('设备不在当前节点列表中:', deviceParam.monitorId)
}
},
async loadStatistics() { async loadStatistics() {
try { try {
this.alarmDataTotalCount = await getAlarmDataTotalCount(); this.alarmDataTotalCount = await getAlarmDataTotalCount();
@ -298,7 +448,8 @@ export default {
if (this.selectedNodeId !== null) { if (this.selectedNodeId !== null) {
this.loadDeviceDataByNode(this.selectedNodeId) this.loadDeviceDataByNode(this.selectedNodeId)
} }
this.loadStatistics() // WebSocket
// this.loadStatistics()
}, },
// //
@ -396,6 +547,138 @@ export default {
} }
}, },
// WebSocket
async saveRealtimeAlarmData(alarmData) {
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(), // 使
// triggerRule0=1=
alarmType: rule.triggerRule || 0,
// 1=
alarmStatus: 1,
//
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
})
}
if (alarmDataList.length === 0) {
console.warn('没有有效的告警记录可保存')
return
}
//
const response = await saveWebSocketAlarmData(alarmDataList)
if (response.code === 200) {
console.log('告警数据保存成功:', response.msg)
// WebSocket
// this.getAlarmDataList()
} else {
console.error('告警数据保存失败:', response.msg)
this.$message.error('告警数据保存失败: ' + response.msg)
}
} catch (error) {
console.error('保存告警数据异常:', error)
this.$message.error('保存告警数据异常')
}
},
//
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
}
},
//
getFieldName(fieldCode) {
const fieldMap = {
0: '温度',
1: '湿度',
2: '振动-速度(mm/s)',
3: '振动-位移(um)',
4: '振动-加速度(g)',
5: '振动-温度(℃)',
6: '照度',
7: '噪声',
8: '气体浓度'
}
return fieldMap[fieldCode] || '未知字段'
}
} }
} }
</script> </script>

@ -1,7 +1,7 @@
<template> <template>
<div class="login"> <div class="login">
<el-form ref="loginForm" :model="loginForm" :rules="loginRules" class="login-form"> <el-form ref="loginForm" :model="loginForm" :rules="loginRules" class="login-form">
<h3 class="title">机场行李系统设备健康监测系统</h3> <h3 class="title">行李健康监测系统</h3>
<el-form-item prop="username"> <el-form-item prop="username">
<el-input <el-input
v-model="loginForm.username" v-model="loginForm.username"

@ -1,7 +1,7 @@
<template> <template>
<div class="register"> <div class="register">
<el-form ref="registerForm" :model="registerForm" :rules="registerRules" class="register-form"> <el-form ref="registerForm" :model="registerForm" :rules="registerRules" class="register-form">
<h3 class="title">机场行李系统设备健康监测系统</h3> <h3 class="title">行李健康监测系统</h3>
<el-form-item prop="username"> <el-form-item prop="username">
<el-input v-model="registerForm.username" type="text" auto-complete="off" placeholder="账号"> <el-input v-model="registerForm.username" type="text" auto-complete="off" placeholder="账号">
<svg-icon slot="prefix" icon-class="user" class="el-input__icon input-icon" /> <svg-icon slot="prefix" icon-class="user" class="el-input__icon input-icon" />

@ -7,7 +7,7 @@ function resolve(dir) {
const CompressionPlugin = require('compression-webpack-plugin') const CompressionPlugin = require('compression-webpack-plugin')
const name = process.env.VUE_APP_TITLE || '机场行李系统设备健康监测系统' // 网页标题 const name = process.env.VUE_APP_TITLE || '行李健康监测系统' // 网页标题
const port = process.env.port || process.env.npm_config_port || 8200 // 端口 const port = process.env.port || process.env.npm_config_port || 8200 // 端口

Loading…
Cancel
Save