修改看板

boardTest
夜笙歌 8 months ago
parent 493b0a0cb8
commit a988a368bc

@ -1,16 +1,18 @@
<template>
<div id="app">
<router-view />
<theme-picker />
<router-view/>
<theme-picker/>
<alm-modal/>
</div>
</template>
<script>
import ThemePicker from "@/components/ThemePicker";
import AlmModal from "@/components/AlmModal";
export default {
name: "App",
components: { ThemePicker },
components: {ThemePicker,AlmModal},
metaInfo() {
return {
title: this.$store.state.settings.dynamicTitle && this.$store.state.settings.title,
@ -23,7 +25,7 @@ export default {
return {
// WebSocket
websocket: null,
websocketUrl: 'ws://10.42.0.1:7181/ws',
websocketUrl: 'ws://119.45.202.115:7181/ws',
isWebSocketConnected: false,
reconnectTimer: null,
reconnectAttempts: 0,
@ -278,7 +280,7 @@ export default {
if (alarmData.deviceParam) {
//
if (alarmData.deviceParam.temperature &&
(alarmData.deviceParam.temperature > 50 || alarmData.deviceParam.temperature < -20)) {
(alarmData.deviceParam.temperature > 50 || alarmData.deviceParam.temperature < -20)) {
priority += 10 //
}

@ -0,0 +1,8 @@
import request from '@/utils/request'
// 查询采集设备信息列表
export function getByAlarmInfo(query) {
return request({
url: '/ems/base/emsAlarmActionStep/getByAlarmInfo', method: 'post', data: query
})
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

After

Width:  |  Height:  |  Size: 27 KiB

@ -0,0 +1,507 @@
<template>
<div>
<el-dialog
title="⚠️ 实时告警通知"
:visible.sync="realtimeAlarmDialog"
width="900px"
append-to-body
:close-on-click-modal="false"
:close-on-press-escape="false"
class="realtime-alarm-dialog"
@close="closeRealtimeAlarmDialog"
>
<el-tabs v-model="realtimeActiveTab" type="card">
<!-- 告警详情标签页 -->
<el-tab-pane label="告警详情" name="alarmDetail">
<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.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 class="alarm-section"
v-if="currentRealtimeAlarm.alarmContents && currentRealtimeAlarm.alarmContents.length > 0">
<h3 class="section-title">🔎 故障预测内容 </h3>
<div class="alarm-contents">
<el-alert
:title="PredictionContent()"
type="warning"
:closable="false"
show-icon
/>
</div>
</div>
</div>
</el-tab-pane>
<!-- 处置措施标签页 -->
<el-tab-pane label="处置措施" name="realtimeActionSteps">
<div v-if="currentRealtimeAlarm">
<el-alert
:title="`设备:${currentRealtimeAlarm.monitorId} | 告警时间:${formatAlarmTime(currentRealtimeAlarm.recordTime)}`"
type="warning"
:closable="false"
style="margin-bottom: 20px;"
/>
<div v-loading="realtimeActionStepsLoading">
<div v-if="realtimeActionSteps.length === 0" class="no-steps">
<el-empty description="该告警规则暂无配置处置措施"></el-empty>
</div>
<div v-else>
<el-timeline>
<el-timeline-item
v-for="(step, index) in realtimeActionSteps"
:key="step.objId"
:timestamp="`步骤 ${step.stepSequence}`"
placement="top"
type="primary"
size="large"
>
<el-card class="step-card">
<div slot="header" class="clearfix">
<span class="step-title">{{ step.stepSequence }}</span>
</div>
<!-- 步骤描述 -->
<div class="step-description">
<p>{{ step.description }}</p>
</div>
<!-- 步骤图片 -->
<div v-if="step.stepImages && step.stepImages.length > 0" class="step-images">
<div class="images-title">参考图片</div>
<div class="image-gallery">
<div
v-for="image in step.stepImages"
:key="image.objId"
class="image-item"
@click="previewImage(getFullImageUrl(image.imageUrl))"
>
<img :src="getFullImageUrl(image.imageUrl)" :alt="image.description"/>
<div v-if="image.description" class="image-desc">{{ image.description }}</div>
</div>
</div>
</div>
<!-- 步骤备注 -->
<div v-if="step.remark" class="step-remark">
<el-tag type="info" size="small">备注{{ step.remark }}</el-tag>
</div>
</el-card>
</el-timeline-item>
</el-timeline>
</div>
</div>
</div>
</el-tab-pane>
</el-tabs>
<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 class="almReminder" @click="openAlm" v-if="almData.filter(e=>e.isWaiting).length>0"></div>
</div>
</template>
<script>
import {getByAlarmInfo} from "@/api/board";
import {saveWebSocketAlarmData} from "@/api/ems/record/recordAlarmData";
import icon5 from "@/assets/images/icon5.jpg";
export default {
data() {
return {
icon5,
almData: [],
//
realtimeAlarmDialog: false,
// tabName
realtimeActiveTab: 'alarmDetail',
//
realtimeActionSteps: [],
// loading
realtimeActionStepsLoading: false,
//
currentRealtimeAlarm: null,
// loading
alarmProcessing: false,
}
},
mounted() {
this.$bus.$on('websocket-device-data', (e) => {
if (e.alarmContents.length > 0) {
e.alarmRules.forEach((item, index) => {
if (!this.almData.find(v => v.monitorId === e.monitorId && v.alarmRules[0].objid === item.objid)) {
this.almData.push({...e, alarmRules: [item], alarmContents: [e.alarmContents[index]], isWaiting: false})
}
})
}
})
// setInterval(() => {
// let e = {
// "monitorId": "T0002_0101",
// "isFlag": 1,
// "deviceParam": {
// "objid": 1927987087563427800,
// "monitorId": "T0002_0101",
// "temperature": 27.68,
// "humidity": 0,
// "illuminance": 0,
// "noise": 0,
// "concentration": 0,
// "VibrationSpeed": 0,
// "VibrationDisplacement": 0,
// "VibrationAcceleration": 0,
// "VibrationTemp": 0,
// "collectTime": "2024-09-03T04:47:06",
// "recordTime": "2025-05-29T15:15:16.3016212+08:00"
// },
// "alarmRules": [
// {
// "objid": 30020,
// "monitorId": "T0002_0101 ",
// "ruleId": null,
// "ruleName": "T0002_0101 20",
// "triggerRule": 0,
// "monitorField": 0,
// "triggerValue": 20,
// "cause": "-",
// "alarmId": 202506051636001,
// },
// {
// "objid": 30021,
// "monitorId": "T0002_0101 ",
// "ruleId": null,
// "ruleName": "T0002_0101 30",
// "triggerRule": 1,
// "monitorField": 0,
// "triggerValue": 30,
// "cause": "-",
// "alarmId": 202506051636001,
// }
// ],
// "alarmContents": [
// "T0002_01012025-05-29 15:15:16T0002_0101 20,:,:20.00,:-",
// "T0002_01012025-05-29 15:15:16T0002_0101 30,:,:30.00,:-"
// ],
// "recordTime": 1748566080572
// }
//
// if (e.alarmContents.length > 0) {
// e.alarmRules.forEach((item, index) => {
// if (!this.almData.find(v => v.monitorId === e.monitorId && v.alarmRules[0].objid === item.objid)) {
// this.almData.push({...e, alarmRules: [item], alarmContents: [e.alarmContents[index]], isWaiting: false})
// }
// })
// }
//
// }, 3000)
},
watch: {
almData: {
handler() {
if (this.almData.filter(e => !e.isWaiting).length > 0 && !this.realtimeAlarmDialog) {
let arr = this.almData.sort((a, b) => a.alarmRules[0].objid - b.alarmRules[0].objid)
let data = arr.filter(v => !v.isWaiting)
if (data.length > 0) {
this.currentRealtimeAlarm = data[0]
this.realtimeAlarmDialog = true
this.getAlm(data[0])
}
}
},
deep: true,
}
},
methods: {
openAlm() {
let arr = this.almData.sort((a, b) => a.alarmRules[0].objid - b.alarmRules[0].objid)
if (arr.length > 0) {
this.currentRealtimeAlarm = arr[0]
this.realtimeAlarmDialog = true
this.getAlm(arr[0])
}
},
//
formatAlarmTime(time) {
if (!time) return '--'
try {
const date = new Date(time)
return date.toLocaleString('zh-CN')
} catch (error) {
return time
}
},
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.noise
case 7: //
return deviceParam.illuminance
case 8: //
return deviceParam.concentration
default:
console.warn('未知的监测字段:', monitorField)
return null
}
},
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)
}
if (alarmDataList.length === 0) {
console.warn('没有有效的告警记录可保存')
return
}
//
const response = await saveWebSocketAlarmData(alarmDataList)
if (response.code === 200) {
console.log('告警数据保存成功:', response.msg)
return true
} else {
this.$message.error('告警数据保存失败: ' + response.msg)
return false
}
} catch (error) {
this.$message.error('保存告警数据异常')
return false
}
},
async processRealtimeAlarm() {
this.alarmProcessing = true
try {
// 0
const success = await this.saveRealtimeAlarmData(this.currentRealtimeAlarm, 0)
if (success) {
this.$message.success('告警已确认知晓并标记为已处理')
let data = this.currentRealtimeAlarm
let index = this.almData.findIndex(v => v.monitorId === data.monitorId && v.alarmRules[0].objid === data.alarmRules[0].objid)
this.almData.splice(index, 1)
this.realtimeAlarmDialog = false
this.currentRealtimeAlarm = null
this.realtimeActiveTab = 'alarmDetail'
this.realtimeActionSteps = []
}
} catch (error) {
console.error('处理告警失败:', error)
this.$message.error('处理告警失败')
} finally {
this.alarmProcessing = false
}
},
getFieldName(fieldCode) {
const fieldMap = {
0: '温度',
1: '湿度',
2: '振动-速度(mm/s)',
3: '振动-位移(um)',
4: '振动-加速度(g)',
5: '振动-温度(℃)',
6: '噪音',
7: '照度',
8: '气体浓度'
}
return fieldMap[fieldCode] || '未知字段'
},
getAlm(e) {
let cause = this.getFieldName(e.alarmRules[0].monitorField)
getByAlarmInfo({
monitorId: e.monitorId,
cause
}).then(v => {
console.log(v)
this.realtimeActionSteps = v.data || []
})
},
closeRealtimeAlarmDialog() {
let data = this.currentRealtimeAlarm
let index = this.almData.findIndex(v => v.monitorId === data.monitorId && v.alarmRules[0].objid === data.alarmRules[0].objid)
this.$set(this.almData[index], 'isWaiting', true)
this.realtimeAlarmDialog = false
this.currentRealtimeAlarm = null
this.realtimeActiveTab = 'alarmDetail'
this.realtimeActionSteps = []
},
PredictionContent() {
if (this.currentRealtimeAlarm.monitorId === 'E0012_4300' && this.currentRealtimeAlarm.alarmRules[0].monitorField === 1) {
return '故障预测:空气湿度过大'
}
let data = this.currentRealtimeAlarm.monitorId
if (data) {
let AA = data.split('_')[1].slice(0, 2)
const sortNum = parseInt(AA, 10)
const overloadTypes = [1, 2, 20, 17, 26, 10, 33, 5, 15, 9, 30, 27, 29, 18, 24, 22, 7, 31, 28, 6]
const electricalOverloadTypes = [14, 21, 23, 12, 13, 32, 4, 3, 8]
//
if (overloadTypes.includes(sortNum)) {
return '故障预测:过载'
} else if (electricalOverloadTypes.includes(sortNum)) {
return '故障预测:电器元器件过载'
} else if (sortNum >= 35 && sortNum <= 38) {
return '故障预测:轴承故障'
} else if (sortNum >= 39 && sortNum <= 41) {
return '故障预测:机械磨损'
}
} else {
return ''
}
}
}
}
</script>
<style lang="less">
.almReminder {
position: fixed;
top: calc(15% + 2vw);
display: none;
right: 4%;
transform: translate(50%, -50%);
width: 3vw;
height: 3vw;
background: url("~@/assets/images/icon5.jpg") no-repeat;
background-size: 100% 100%;
animation: Zoom 2s infinite;
}
@keyframes Zoom {
0% {
width: 3vw;
height: 3vw;
}
50% {
width: 4vw;
height: 4vw;
}
100% {
width: 3vw;
height: 3vw;
}
}
</style>

@ -110,7 +110,8 @@
type="text"
icon="el-icon-view"
@click.stop="viewActionSteps(scope.row)"
>查看措施</el-button>
>查看措施
</el-button>
</template>
</el-table-column>
</el-table>
@ -132,12 +133,12 @@
:closable="false"
style="margin-bottom: 20px;"
/>
<div v-loading="actionStepsLoading">
<div v-if="actionSteps.length === 0" class="no-steps">
<el-empty description="该告警规则暂无配置处置措施"></el-empty>
</div>
<div v-else>
<el-timeline>
<el-timeline-item
@ -152,12 +153,12 @@
<div slot="header" class="clearfix">
<span class="step-title">{{ step.stepSequence }}</span>
</div>
<!-- 步骤描述 -->
<div class="step-description">
<p>{{ step.description }}</p>
</div>
<!-- 步骤图片 -->
<div v-if="step.stepImages && step.stepImages.length > 0" class="step-images">
<div class="images-title">参考图片</div>
@ -168,12 +169,12 @@
class="image-item"
@click="previewImage(getFullImageUrl(image.imageUrl))"
>
<img :src="getFullImageUrl(image.imageUrl)" :alt="image.description" />
<img :src="getFullImageUrl(image.imageUrl)" :alt="image.description"/>
<div v-if="image.description" class="image-desc">{{ image.description }}</div>
</div>
</div>
</div>
<!-- 步骤备注 -->
<div v-if="step.remark" class="step-remark">
<el-tag type="info" size="small">备注{{ step.remark }}</el-tag>
@ -204,7 +205,7 @@
center
>
<div class="image-preview-container">
<img :src="previewImageUrl" style="max-width: 100%; max-height: 70vh;" />
<img :src="previewImageUrl" style="max-width: 100%; max-height: 70vh;"/>
</div>
</el-dialog>
@ -278,36 +279,48 @@
<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>
<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">
<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>
<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">
<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>
<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">
<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>
<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">
<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"
<div
v-for="(rule, index) in currentRealtimeAlarm.alarmRules"
:key="index"
class="rule-item"
>
@ -327,11 +340,12 @@
</div>
<!-- 告警内容详情 -->
<div class="alarm-section" v-if="currentRealtimeAlarm.alarmContents && currentRealtimeAlarm.alarmContents.length > 0">
<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"
<div
v-for="(content, index) in currentRealtimeAlarm.alarmContents"
:key="index"
class="content-item"
>
@ -356,12 +370,12 @@
:closable="false"
style="margin-bottom: 20px;"
/>
<div v-loading="realtimeActionStepsLoading">
<div v-if="realtimeActionSteps.length === 0" class="no-steps">
<el-empty description="该告警规则暂无配置处置措施"></el-empty>
</div>
<div v-else>
<el-timeline>
<el-timeline-item
@ -376,12 +390,12 @@
<div slot="header" class="clearfix">
<span class="step-title">{{ step.stepSequence }}</span>
</div>
<!-- 步骤描述 -->
<div class="step-description">
<p>{{ step.description }}</p>
</div>
<!-- 步骤图片 -->
<div v-if="step.stepImages && step.stepImages.length > 0" class="step-images">
<div class="images-title">参考图片</div>
@ -392,12 +406,12 @@
class="image-item"
@click="previewImage(getFullImageUrl(image.imageUrl))"
>
<img :src="getFullImageUrl(image.imageUrl)" :alt="image.description" />
<img :src="getFullImageUrl(image.imageUrl)" :alt="image.description"/>
<div v-if="image.description" class="image-desc">{{ image.description }}</div>
</div>
</div>
</div>
<!-- 步骤备注 -->
<div v-if="step.remark" class="step-remark">
<el-tag type="info" size="small">备注{{ step.remark }}</el-tag>
@ -412,14 +426,14 @@
</el-tabs>
<div slot="footer" class="dialog-footer">
<el-button
<el-button
@click="closeRealtimeAlarmDialog"
:loading="alarmProcessing"
>
稍后处理
</el-button>
<el-button
type="primary"
<el-button
type="primary"
@click="processRealtimeAlarm"
:loading="alarmProcessing"
>
@ -432,7 +446,7 @@
</template>
<script>
import { mapGetters } from 'vuex'
import {mapGetters} from 'vuex'
import Breadcrumb from '@/components/Breadcrumb'
import TopNav from '@/components/TopNav'
import Hamburger from '@/components/Hamburger'
@ -442,9 +456,9 @@ import Search from '@/components/HeaderSearch'
import RuoYiGit from '@/components/RuoYi/Git'
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'
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'],
@ -477,7 +491,7 @@ export default {
activeTab: 'alarmList',
imagePreviewVisible: false,
previewImageUrl: '',
// WebSocket
realtimeAlarmDialog: false,
currentRealtimeAlarm: null,
@ -492,7 +506,7 @@ export default {
queueStatusColor: '',
alarmQueueLength: 0,
queueStatusTimer: null,
//
realtimeActiveTab: 'alarmDetail',
realtimeActionSteps: [],
@ -501,7 +515,7 @@ export default {
},
created() {
localStorage.setItem('this.alarmDataTotal', 0)
// WebSocket
this.$bus.$on('websocket-alarm-with-callback', this.handleQueuedRealtimeAlarm)
//
@ -518,7 +532,7 @@ export default {
this.$bus.$off('websocket-connected', this.onWebSocketConnected)
this.$bus.$off('websocket-disconnected', this.onWebSocketDisconnected)
this.$bus.$off('websocket-max-retries-reached', this.onWebSocketMaxRetriesReached)
//
if (this.queueStatusTimer) {
clearInterval(this.queueStatusTimer)
@ -528,7 +542,7 @@ export default {
mounted() {
// WebSocket
this.getAlarmData()
//
this.queueStatusTimer = setInterval(() => {
this.updateQueueStatus()
@ -668,12 +682,12 @@ export default {
// URL
getFullImageUrl(relativePath) {
if (!relativePath) return '';
// URL
if (relativePath.startsWith('http')) {
return relativePath;
}
// baseURL
const baseURL = process.env.VUE_APP_BASE_API || '';
return baseURL + relativePath;
@ -681,27 +695,27 @@ export default {
// WebSocket
handleQueuedRealtimeAlarm(data) {
console.log('收到队列化实时告警:', data)
//
const { alarm, onProcessed, onTimeout } = data
const {alarm, onProcessed, onTimeout} = data
this.currentRealtimeAlarm = alarm
this.currentAlarmId = alarm.id
this.alarmProcessedCallback = onProcessed
this.alarmTimeoutCallback = onTimeout
//
this.realtimeActiveTab = 'alarmDetail'
//
this.realtimeAlarmDialog = true
//
this.playAlarmSound()
//
this.loadRealtimeActionSteps(alarm)
console.log('告警弹窗已显示告警ID:', alarm.id, '优先级:', alarm.priority)
},
//
@ -721,12 +735,12 @@ export default {
// EmsRecordAlarmData
// EmsRecordAlarmData
const alarmDataList = []
if (!alarmData.alarmRules || alarmData.alarmRules.length === 0) {
console.warn('告警数据中没有告警规则')
return
}
//
const getCurrentTimeForBackend = () => {
const now = new Date()
@ -738,37 +752,37 @@ export default {
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,
@ -779,12 +793,12 @@ export default {
处理状态: alarmRecord.alarmStatus === 0 ? '已处理' : '未处理'
})
}
if (alarmDataList.length === 0) {
console.warn('没有有效的告警记录可保存')
return
}
//
const response = await saveWebSocketAlarmData(alarmDataList)
if (response.code === 200) {
@ -803,13 +817,13 @@ export default {
return false
}
},
//
getActualValueFromDeviceParam(deviceParam, monitorField) {
if (!deviceParam || monitorField === null || monitorField === undefined) {
return null
}
switch (monitorField) {
case 0: //
return deviceParam.temperature
@ -843,7 +857,7 @@ export default {
const success = await this.saveRealtimeAlarmData(this.currentRealtimeAlarm, 1)
if (success) {
this.$message.info('告警已记录,状态为未处理')
// App.vue
if (this.alarmProcessedCallback) {
this.alarmProcessedCallback(this.currentAlarmId, 'later')
@ -856,10 +870,10 @@ export default {
this.alarmProcessing = false
}
}
this.closeAlarmDialog()
},
//
async processRealtimeAlarm() {
this.alarmProcessing = true
@ -868,13 +882,13 @@ export default {
const success = await this.saveRealtimeAlarmData(this.currentRealtimeAlarm, 0)
if (success) {
this.$message.success('告警已确认知晓并标记为已处理')
// App.vue
if (this.alarmProcessedCallback) {
this.alarmProcessedCallback(this.currentAlarmId, 'processed')
}
}
//
this.closeAlarmDialog()
} catch (error) {
@ -891,7 +905,7 @@ export default {
this.currentAlarmId = null
this.alarmProcessedCallback = null
this.alarmTimeoutCallback = null
//
this.realtimeActiveTab = 'alarmDetail'
this.realtimeActionSteps = []
@ -944,8 +958,8 @@ export default {
// WebSocket
handleAutoSaveAlarm(data) {
console.log('收到自动超时保存事件:', data)
const { alarm, status } = data
const {alarm, status} = data
//
this.saveRealtimeAlarmData(alarm, status).then((success) => {
if (success) {
@ -982,9 +996,9 @@ export default {
// App.vue
if (this.$root.getAlarmQueueStatus) {
const status = this.$root.getAlarmQueueStatus()
this.alarmQueueLength = status.queueLength
//
if (status.queueLength === 0) {
this.alarmQueueStatusText = '告警队列为空'
@ -996,7 +1010,7 @@ export default {
this.alarmQueueStatusText = `告警队列:${status.queueLength}个待处理(队列较长)`
this.queueStatusColor = '#F56C6C' //
}
// WebSocket
if (!this.websocketStatusText) {
if (status.isConnected) {
@ -1017,13 +1031,13 @@ export default {
this.realtimeActionSteps = []
return
}
this.realtimeActionStepsLoading = true
try {
// 使
const firstRule = alarm.alarmRules[0]
const fieldName = this.getFieldName(firstRule.monitorField)
const response = await getEmsAlarmActionStepsByAlarmInfo(alarm.monitorId, fieldName)
this.realtimeActionSteps = response.data || []
console.log('实时告警处置措施加载成功:', this.realtimeActionSteps.length, '个步骤')
@ -1146,37 +1160,37 @@ export default {
//
.step-card {
margin-bottom: 20px;
.step-title {
font-weight: bold;
font-size: 16px;
color: #409EFF;
}
.step-description {
margin-bottom: 15px;
p {
line-height: 1.6;
margin: 0;
color: #606266;
}
}
.step-images {
margin-bottom: 15px;
.images-title {
font-size: 14px;
color: #909399;
margin-bottom: 10px;
}
.image-gallery {
display: flex;
flex-wrap: wrap;
gap: 10px;
.image-item {
cursor: pointer;
border: 2px solid #f0f0f0;
@ -1184,19 +1198,19 @@ export default {
overflow: hidden;
transition: all 0.3s;
max-width: 200px;
&:hover {
border-color: #409EFF;
transform: scale(1.02);
}
img {
width: 100%;
height: 150px;
object-fit: cover;
display: block;
}
.image-desc {
padding: 8px;
font-size: 12px;
@ -1207,7 +1221,7 @@ export default {
}
}
}
.step-remark {
margin-top: 10px;
}
@ -1228,57 +1242,57 @@ export default {
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;
@ -1286,29 +1300,29 @@ export default {
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;
@ -1318,11 +1332,11 @@ export default {
}
}
}
.alarm-contents {
.content-item {
margin-bottom: 10px;
.el-alert {
.el-alert__title {
font-size: 13px;
@ -1333,11 +1347,11 @@ export default {
}
}
}
.dialog-footer {
text-align: center;
padding: 20px 0 10px 0;
.el-button {
min-width: 100px;
}
@ -1349,7 +1363,7 @@ export default {
display: inline-flex;
align-items: center;
cursor: default;
i {
font-size: 16px;
transition: all 0.3s ease;
@ -1362,16 +1376,16 @@ export default {
position: relative;
cursor: pointer;
transition: all 0.3s ease;
&:hover {
transform: scale(1.1);
}
i {
font-size: 16px;
transition: all 0.3s ease;
}
.queue-count {
position: absolute;
top: -8px;

@ -4,234 +4,6 @@
:style="`width:100%;height:100%;background-image:url(${bg});background-repeat: no-repeat;background-size: 100% 100%;`">
</div>
<div class="title">青岛胶东机场行李输送线</div>
<div class="item1">
<div class="alm">
<div v-for="i in Object.keys(almData.E0013)">
<div v-for="ii in almData.E0013[i]" class="almSpan">
{{ ii }}
</div>
</div>
<div v-for="i in Object.keys(almData.E0014)">
<div v-for="ii in almData.E0014[i]" class="almSpan">
{{ ii }}
</div>
</div>
</div>
<div class="line"></div>
<div class="table1">
<div class="th1">设备名称</div>
<div class="th2">设备参数</div>
</div>
<div class="table2">
<div style="border-bottom: 1px solid #244b9f;" v-for="i in Object.keys(deviceData.E0013)">
<div class="td1">{{ i }}</div>
<div class="td2">
<div v-for="ii in Object.keys(deviceData.E0013[i])" v-if="deviceData.E0013[i][ii]>0">
<div>
<div class="info1">
{{ deviceMap[ii] }}
</div>
<div class="span">
:
</div>
<div class="info2">
{{ deviceData.E0013[i][ii] }}
</div>
</div>
</div>
</div>
</div>
<div style="border-bottom: 1px solid #244b9f;" v-for="i in Object.keys(deviceData.E0014)">
<div class="td1">{{ i }}</div>
<div class="td2">
<div v-for="ii in Object.keys(deviceData.E0014[i])" v-if="deviceData.E0014[i][ii]>0">
<div>
<div class="info1">
{{ deviceMap[ii] }}
</div>
<div class="span">
:
</div>
<div class="info2">
{{ deviceData.E0014[i][ii] }}
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="item2">
<div class="alm">
<div v-for="i in Object.keys(almData.E0002)">
<div v-for="ii in almData.E0002[i]" class="almSpan">
{{ ii }}
</div>
</div>
<div v-for="i in Object.keys(almData.E0003)">
<div v-for="ii in almData.E0003[i]" class="almSpan">
{{ ii }}
</div>
</div>
<div v-for="i in Object.keys(almData.E0004)">
<div v-for="ii in almData.E0004[i]" class="almSpan">
{{ ii }}
</div>
</div>
</div>
<div class="line"></div>
<div class="table1">
<div class="th1">设备名称</div>
<div class="th2">设备参数</div>
</div>
<div class="table2">
<div style="border-bottom: 1px solid #244b9f;" v-for="i in Object.keys(deviceData.E0002)">
<div class="td1">{{ i }}</div>
<div class="td2">
<div v-for="ii in Object.keys(deviceData.E0002[i])" v-if="deviceData.E0002[i][ii]>0">
<div>
<div class="info1">
{{ deviceMap[ii] }}
</div>
<div class="span">
:
</div>
<div class="info2">
{{ deviceData.E0002[i][ii] }}
</div>
</div>
</div>
</div>
</div>
<div style="border-bottom: 1px solid #244b9f;" v-for="i in Object.keys(deviceData.E0003)">
<div class="td1">{{ i }}</div>
<div class="td2">
<div v-for="ii in Object.keys(deviceData.E0003[i])" v-if="deviceData.E0003[i][ii]>0">
<div>
<div class="info1">
{{ deviceMap[ii] }}
</div>
<div class="span">
:
</div>
<div class="info2">
{{ deviceData.E0003[i][ii] }}
</div>
</div>
</div>
</div>
</div>
<div style="border-bottom: 1px solid #244b9f;" v-for="i in Object.keys(deviceData.E0004)">
<div class="td1">{{ i }}</div>
<div class="td2">
<div v-for="ii in Object.keys(deviceData.E0004[i])" v-if="deviceData.E0004[i][ii]>0">
<div>
<div class="info1">
{{ deviceMap[ii] }}
</div>
<div class="span">
:
</div>
<div class="info2">
{{ deviceData.E0004[i][ii] }}
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="item3">
<div class="alm">
<div v-for="i in Object.keys(almData.E0001)">
<div v-for="ii in almData.E0001[i]" class="almSpan">
{{ ii }}
</div>
</div>
</div>
<div class="line"></div>
<div class="table1">
<div class="th1">设备名称</div>
<div class="th2">设备参数</div>
</div>
<div class="table2">
<div style="border-bottom: 1px solid #244b9f;" v-for="i in Object.keys(deviceData.E0001)">
<div class="td1">{{ i }}</div>
<div class="td2">
<div v-for="ii in Object.keys(deviceData.E0001[i])" v-if="deviceData.E0001[i][ii]>0">
<div>
<div class="info1">
{{ deviceMap[ii] }}
</div>
<div class="span">
:
</div>
<div class="info2">
{{ deviceData.E0001[i][ii] }}
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="item4">
<div class="alm">
<div v-for="i in Object.keys(almData.E0005)">
<div v-for="ii in almData.E0005[i]" class="almSpan">
{{ ii }}
</div>
</div>
<div v-for="i in Object.keys(almData.E0006)">
<div v-for="ii in almData.E0006[i]" class="almSpan">
{{ ii }}
</div>
</div>
</div>
<div class="line"></div>
<div class="table1">
<div class="th1">设备名称</div>
<div class="th2">设备参数</div>
</div>
<div class="table2">
<div style="border-bottom: 1px solid #244b9f;" v-for="i in Object.keys(deviceData.E0005)">
<div class="td1">{{ i }}</div>
<div class="td2">
<div v-for="ii in Object.keys(deviceData.E0005[i])" v-if="deviceData.E0005[i][ii]>0">
<div>
<div class="info1">
{{ deviceMap[ii] }}
</div>
<div class="span">
:
</div>
<div class="info2">
{{ deviceData.E0005[i][ii] }}
</div>
</div>
</div>
</div>
</div>
<div style="border-bottom: 1px solid #244b9f;" v-for="i in Object.keys(deviceData.E0006)">
<div class="td1">{{ i }}</div>
<div class="td2">
<div v-for="ii in Object.keys(deviceData.E0006[i])" v-if="deviceData.E0006[i][ii]>0">
<div>
<div class="info1">
{{ deviceMap[ii] }}
</div>
<div class="span">
:
</div>
<div class="info2">
{{ deviceData.E0006[i][ii] }}
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="leftBottom">
<el-button-group size="mini">
@ -249,50 +21,26 @@
</el-button-group>
<div style="margin: 4px 0 ;">
<img :src="icon1" alt="" style="width: 2vw">
<span style="line-height: 2vw;color: #fff;vertical-align: top">温度设备</span>
<span style="line-height: 2vw;color: #fff;vertical-align: top;font-size: 12px">温度设备</span>
</div>
<div style="margin: 4px 0 ;">
<img :src="icon2" alt="" style="width: 2vw">
<span style="line-height: 2vw;color: #fff;vertical-align: top">湿度设备</span>
<span style="line-height: 2vw;color: #fff;vertical-align: top;;font-size: 12px">湿度设备</span>
</div>
<div style="margin: 4px 0 ;">
<img :src="icon3" alt="" style="width: 2vw">
<span style="line-height: 2vw;color: #fff;vertical-align: top">温湿度设备</span>
<span style="line-height: 2vw;color: #fff;vertical-align: top;;font-size: 12px">温湿度设备</span>
</div>
<div style="margin: 4px 0 ;">
<img :src="icon4" alt="" style="width: 2vw">
<span style="line-height: 2vw;color: #fff;vertical-align: top">振动设备</span>
<span style="line-height: 2vw;color: #fff;vertical-align: top;;font-size: 12px">振动设备</span>
</div>
<div style="margin: 4px 0 ;">
<img :src="icon5" alt="" style="width: 2vw">
<span style="line-height: 2vw;color: #fff;vertical-align: top">报警设备</span>
<span style="line-height: 2vw;color: #fff;vertical-align: top;;font-size: 12px">报警设备</span>
</div>
</div>
<div class="scTable1">
<div style="background-color: #094170">
<div class="scrollTable" style="font-weight: bold;">
报警信息
</div>
</div>
<vue-seamless-scroll
:class-option="chart1TableOption"
:data="almTableData"
style="height: calc(100% - 33px);overflow:hidden; "
>
<div
v-for="(item, index) in almTableData"
:key="index"
>
<div :style='"background-color:" + ((index % 2 === 0)? "#053460":"#032d57") '>
<div
class="scrollTable">
{{ item.ruleName }}
</div>
</div>
</div>
</vue-seamless-scroll>
</div>
<div v-if="isNo">
<div class="id id1">15.01</div>
<div class="id id2">15.03</div>
@ -1013,6 +761,7 @@
</div>
</div>
</div>
</div>
</template>
@ -1024,6 +773,8 @@ import icon3 from '@/assets/images/icon3.jpg'
import icon4 from '@/assets/images/icon4.jpg'
import icon5 from '@/assets/images/icon5.jpg'
import vueSeamlessScroll from 'vue-seamless-scroll'
import {getByAlarmInfo} from "@/api/board";
import {saveWebSocketAlarmData} from "@/api/ems/record/recordAlarmData";
export default {
components: {vueSeamlessScroll},
@ -1047,17 +798,6 @@ export default {
'E0013': {},
'E0014': {},
},
almData: {
'E0001': {},
'E0002': {},
'E0003': {},
'E0004': {},
'E0005': {},
'E0006': {},
'E0013': {},
'E0014': {},
},
almTableData: [],
chart1TableOption: {
step: 0.5, //
limitMoveNum: 12, // this.dataList.length
@ -1079,68 +819,34 @@ export default {
vibrationDisplacement: '振动-位移',
vibrationAcceleration: '振动-加速度',
vibrationTemp: '振动-温度',
}
},
}
},
mounted() {
this.$bus.$on('websocket-device-data', (e) => {
let deviceID = e?.deviceParam?.monitorId?.split('_')?.[0]
if (Object.keys(this.deviceData).includes(deviceID)) {
this.$set(this.deviceData[deviceID], e.deviceParam.monitorId, {
temperature: e.deviceParam.temperature || 0,
humidity: e.deviceParam.humidity || 0,
illuminance: e.deviceParam.illuminance || 0,
noise: e.deviceParam.noise || 0,
concentration: e.deviceParam.concentration || 0,
vibrationSpeed: e.deviceParam.vibrationSpeed || 0,
vibrationDisplacement: e.deviceParam.vibrationDisplacement || 0,
vibrationAcceleration: e.deviceParam.vibrationAcceleration || 0,
vibrationTemp: e.deviceParam.vibrationTemp || 0,
})
// this.$set(this.almData[deviceID], e.deviceParam.monitorId, e.alarmRules)
this.$set(this.almData[deviceID], e.deviceParam.monitorId, [
{
"objid": 30020,
"monitorId": "T0002_0101 ",
"ruleId": null,
"ruleName": "T0002_0101 表的温度大于阈值 20",
"triggerRule": 0,
"monitorField": 0,
"triggerValue": 20,
"cause": "温度过高-备注"
},
{
"objid": 30021,
"monitorId": "T0002_0101 ",
"ruleId": null,
"ruleName": "T0002_0101表的温度小于阈值 30",
"triggerRule": 1,
"monitorField": 0,
"triggerValue": 30,
"cause": "温度过低-备注"
}
])
let list = []
Object.values(this.almData).forEach((item) => {
Object.values(item).forEach((item1) => {
item1.forEach((item2) => {
list.push(item2)
})
})
})
this.almTableData = list
// this.$set(this.almData[deviceID], e.deviceParam.monitorId, [''])
}
let deviceID = e?.deviceParam?.monitorId?.split('_')?.[0]
if (Object.keys(this.deviceData).includes(deviceID)) {
this.$set(this.deviceData[deviceID], e.deviceParam.monitorId, {
temperature: e.deviceParam.temperature || 0,
humidity: e.deviceParam.humidity || 0,
illuminance: e.deviceParam.illuminance || 0,
noise: e.deviceParam.noise || 0,
concentration: e.deviceParam.concentration || 0,
vibrationSpeed: e.deviceParam.vibrationSpeed || 0,
vibrationDisplacement: e.deviceParam.vibrationDisplacement || 0,
vibrationAcceleration: e.deviceParam.vibrationAcceleration || 0,
vibrationTemp: e.deviceParam.vibrationTemp || 0,
})
}
)
})
},
methods:
{}
methods: {}
}
</script>
<style lang="less" scoped>
.deviceInfo {
position: absolute;
bottom: calc(2vw + 2vh);
@ -1723,4 +1429,11 @@ export default {
bottom: 4.5%;
left: 2.5%;
}
</style>
<style>
.almReminder {
display: block;
}
</style>

Loading…
Cancel
Save