You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

1442 lines
46 KiB
Vue

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<template>
<div class="navbar">
<hamburger id="hamburger-container" :is-active="sidebar.opened" class="hamburger-container"
@toggleClick="toggleSideBar"
/>
<breadcrumb id="breadcrumb-container" class="breadcrumb-container" v-if="!topNav"/>
<top-nav id="topmenu-container" class="topmenu-container" v-if="topNav"/>
<div class="right-menu">
<template v-if="device!=='mobile'">
<search id="header-search" class="right-menu-item"/>
<!-- <el-tooltip content="源码地址" effect="dark" placement="bottom">-->
<!-- <ruo-yi-git id="ruoyi-git" class="right-menu-item hover-effect" />-->
<!-- </el-tooltip>-->
<!-- <el-tooltip content="文档地址" effect="dark" placement="bottom">-->
<!-- <ruo-yi-doc id="ruoyi-doc" class="right-menu-item hover-effect" />-->
<!-- </el-tooltip>-->
<!-- 异常处理按钮 - 已注释,改用全局处理 -->
<!--
<el-tooltip content="异常处理" class="right-menu-item hover-effect">
<span
class="exceptionHandling"
:class="{ warnTxt: alarmDataList && alarmDataList.length > 0, 'red-yellow-blink': alarmDataList && alarmDataList.length > 0 }"
@click="getAlarmData(true)"
>
<i class="el-icon-message"></i>{{
(alarmDataList && alarmDataList.length > 0 && alarmDataList.length) || ''
}}
</span>
</el-tooltip>
-->
<!-- WebSocket连接状态指示器 - 已注释,改用全局管理 -->
<!--
<el-tooltip :content="websocketStatusText" effect="dark" placement="bottom">
<span class="websocket-status right-menu-item">
<i :class="websocketStatusIcon" :style="{ color: websocketStatusColor }"></i>
</span>
</el-tooltip>
<el-tooltip :content="alarmQueueStatusText" effect="dark" placement="bottom">
<span class="alarm-queue-status right-menu-item" @click="showQueueStatus">
<i class="el-icon-collection" :style="{ color: queueStatusColor }"></i>
<span v-if="alarmQueueLength > 0" class="queue-count">{{ alarmQueueLength }}</span>
</span>
</el-tooltip>
-->
<screenfull id="screenfull" class="right-menu-item hover-effect"/>
<el-tooltip content="布局大小" effect="dark" placement="bottom">
<size-select id="size-select" class="right-menu-item hover-effect"/>
</el-tooltip>
</template>
<el-dropdown class="avatar-container right-menu-item hover-effect" trigger="click">
<div class="avatar-wrapper">
<img :src="avatar" class="user-avatar">
<i class="el-icon-caret-bottom"/>
</div>
<el-dropdown-menu slot="dropdown">
<router-link to="/user/profile">
<el-dropdown-item>个人中心</el-dropdown-item>
</router-link>
<el-dropdown-item @click.native="setting = true">
<span>布局设置</span>
</el-dropdown-item>
<el-dropdown-item divided @click.native="logout">
<span>退出登录</span>
</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
</div>
<!-- 告警对话框 - 已注释,改用全局处理 -->
<!--
<el-dialog
:title="alarmTitle"
:visible.sync="alarmOpen"
width="1000px"
append-to-body
>
<el-tabs v-model="activeTab" type="card">
<el-tab-pane label="告警列表" name="alarmList">
<el-table
v-loading="alarmLoading"
:data="alarmDataList"
@selection-change="handleSelectionChangeAlarm"
@row-click="handleRowClick"
highlight-current-row
>
<el-table-column type="selection" width="55" align="center"/>
<el-table-column label="异常设备" align="center" prop="deviceName"/>
<el-table-column label="异常类型" align="center" prop="cause"/>
<el-table-column label="异常数据" align="center" prop="alarmData"/>
<el-table-column label="异常状态" align="center" prop="alarmStatus">
<template slot-scope="scope">
<dict-tag :options="dict.type.alarm_status" :value="scope.row.alarmStatus"/>
</template>
</el-table-column>
<el-table-column label="记录时间" align="center" prop="collectTime" width="180">
<template slot-scope="scope">
<span>{{ parseTime(scope.row.createTime, '{y}-{m}-{d} {h}:{i}:{s}') }}</span>
</template>
</el-table-column>
<el-table-column label="操作" align="center" width="120">
<template slot-scope="scope">
<el-button
size="mini"
type="text"
icon="el-icon-view"
@click.stop="viewActionSteps(scope.row)"
>查看措施
</el-button>
</template>
</el-table-column>
</el-table>
<pagination
v-show="alarmDataTotal > 0"
:total="alarmDataTotal"
:page.sync="queryParams.pageNum"
:limit.sync="queryParams.pageSize"
@pagination="getAlarmDataList"
/>
</el-tab-pane>
<el-tab-pane label="处置措施" name="actionSteps" :disabled="!currentAlarmData">
<div v-if="currentAlarmData">
<el-alert
:title="`设备:${currentAlarmData.deviceName} | 异常类型:${currentAlarmData.cause} | 异常数据:${currentAlarmData.alarmData}`"
type="warning"
: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
v-for="(step, index) in actionSteps"
: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 type="primary" @click="jumpProcessing" :disabled="objIds.length === 0">
标记已处理 ({{ objIds.length }})
</el-button>
<el-button @click="closeDialog">关 闭</el-button>
</div>
</el-dialog>
-->
<!-- 图片预览对话框 - 已注释,改用全局处理 -->
<!--
<el-dialog
title="图片预览"
:visible.sync="imagePreviewVisible"
width="80%"
append-to-body
center
>
<div class="image-preview-container">
<img :src="previewImageUrl" style="max-width: 100%; max-height: 70vh;"/>
</div>
</el-dialog>
-->
<!-- 实时告警弹窗 - 已注释,使用全局弹窗代替 -->
<!--
<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"
>
<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.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>
</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>
</template>
<script>
import {mapGetters} from 'vuex'
import Breadcrumb from '@/components/Breadcrumb'
import TopNav from '@/components/TopNav'
import Hamburger from '@/components/Hamburger'
import Screenfull from '@/components/Screenfull'
import SizeSelect from '@/components/SizeSelect'
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' // 已注释,改用全局处理
export default {
// dicts: ['alarm_type', 'alarm_status'], // 已注释,改用全局处理
data() {
return {
// 告警对话框相关 - 已注释,改用全局处理
// poolNameList: [],
// poolName: '',
// alarmTitle: '异常处理',
// alarmOpen: false,
// alarmLoading: false,
// alarmDataList: [],
// alarmDataTotal: 0,
// 选中数组
// objIds: [],
// 非单个禁用
// single: true,
// 非多个禁用
// multiple: true,
// 查询参数
// queryParams: {
// pageNum: 1,
// pageSize: 10,
// alarmStatus: '1'
// },
// 措施步骤相关
// currentAlarmData: null,
// showActionSteps: false,
// actionSteps: [],
// actionStepsLoading: false,
// activeTab: 'alarmList',
// imagePreviewVisible: false,
// previewImageUrl: '',
// WebSocket告警相关 - 实时告警弹窗已注释,改用全局弹窗
// realtimeAlarmDialog: false,
// currentRealtimeAlarm: null,
// alarmProcessing: false,
// currentAlarmId: null,
// alarmProcessedCallback: null,
// alarmTimeoutCallback: null,
// websocketStatusText: '',
// websocketStatusIcon: '',
// websocketStatusColor: '',
// alarmQueueStatusText: '',
// queueStatusColor: '',
// alarmQueueLength: 0,
// queueStatusTimer: null // 已注释,改用全局管理
// 实时告警处置措施相关 - 已注释
// realtimeActiveTab: 'alarmDetail',
// realtimeActionSteps: [],
// realtimeActionStepsLoading: false
}
},
created() {
// 初始化告警数据 - 已注释,改用全局处理
// localStorage.setItem('this.alarmDataTotal', 0)
// 监听新的队列化WebSocket告警事件 - 已注释,改用全局弹窗
// this.$bus.$on('websocket-alarm-with-callback', this.handleQueuedRealtimeAlarm)
// 监听自动超时保存事件 - 已注释,改用全局弹窗
// this.$bus.$on('websocket-alarm-auto-save', this.handleAutoSaveAlarm)
// 监听WebSocket连接状态变化 - 已注释,改用全局管理
// this.$bus.$on('websocket-connected', this.onWebSocketConnected)
// this.$bus.$on('websocket-disconnected', this.onWebSocketDisconnected)
// this.$bus.$on('websocket-max-retries-reached', this.onWebSocketMaxRetriesReached)
},
beforeDestroy() {
// 移除所有事件监听 - 实时告警相关已注释
// this.$bus.$off('websocket-alarm-with-callback', this.handleQueuedRealtimeAlarm)
// this.$bus.$off('websocket-alarm-auto-save', this.handleAutoSaveAlarm)
// 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)
// this.queueStatusTimer = null
// }
},
mounted() {
// 初始获取告警数据 - 已注释,改用全局处理
// this.getAlarmData()
// 定期更新告警队列状态 - 已注释,改用全局管理
// this.queueStatusTimer = setInterval(() => {
// this.updateQueueStatus()
// }, 2000) // 每2秒更新一次状态
},
components: {
Breadcrumb,
TopNav,
Hamburger,
Screenfull,
SizeSelect,
Search,
RuoYiGit,
RuoYiDoc
},
computed: {
...mapGetters([
'sidebar',
'avatar',
'device'
]),
setting: {
get() {
return this.$store.state.settings.showSettings
},
set(val) {
this.$store.dispatch('settings/changeSetting', {
key: 'showSettings',
value: val
})
}
},
topNav: {
get() {
return this.$store.state.settings.topNav
}
}
},
methods: {
// 告警数据处理相关方法 - 已注释,改用全局处理
/*
// 打开右下角异常
openAlarm() {
this.$notify({
title: '异常数据提示',
position: 'bottom-right',
message: this.$createElement(
'div',
{
on: {
click: () => {
this.getAlarmData(true)
}
}
},
[this.$createElement('el-button', {}, ['点击查看'])]
)
})
},
// 报警列表
getAlarmData(open) {
this.alarmOpen = open
this.alarmLoading = true
this.getAlarmDataList()
},
getAlarmDataList() {
listRecordAlarmData(this.queryParams).then((response) => {
this.alarmDataList = response.rows
this.alarmDataTotal = response.total
this.alarmLoading = false
if (localStorage.getItem('this.alarmDataTotal') != this.alarmDataTotal) {
localStorage.setItem('this.alarmDataTotal', this.alarmDataTotal)
this.openAlarm()
}
})
},
// 跳转处理
jumpProcessing(row) {
if (this.objIds.length === 0) {
this.$modal.msgWarning('请勾选设备进行异常处理!')
return
}
handleExceptions(this.objIds).then(response => {
this.$modal.msgSuccess('处理成功')
this.alarmOpen = false
})
},
// 多选框选中数据
handleSelectionChangeAlarm(selection) {
this.objIds = selection.map((item) => item.objId)
this.single = selection.length !== 1
this.multiple = !selection.length
},
*/
toggleSideBar() {
this.$store.dispatch('app/toggleSideBar')
},
logout() {
this.$confirm('确定注销并退出系统吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
this.$store.dispatch('LogOut').then(() => {
if (!settings.casEnable) {
location.href = '/index'
}
})
}).catch(() => {
})
},
// 告警对话框交互方法 - 已注释,改用全局处理
/*
handleRowClick(row) {
this.currentAlarmData = row
this.activeTab = 'actionSteps'
this.getActionSteps(row)
},
getActionSteps(alarmData) {
this.actionStepsLoading = true
getEmsAlarmActionStepsByAlarmInfo(alarmData.monitorId, alarmData.cause).then((response) => {
this.actionSteps = response.data || []
this.actionStepsLoading = false
}).catch(() => {
this.actionSteps = []
this.actionStepsLoading = false
})
},
viewActionSteps(row) {
this.currentAlarmData = row
this.activeTab = 'actionSteps'
this.getActionSteps(row)
},
previewImage(url) {
this.previewImageUrl = url
this.imagePreviewVisible = true
},
closeDialog() {
this.alarmOpen = false
this.activeTab = 'alarmList'
},
*/
// 获取完整的图片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;
},
// 处理队列化的WebSocket实时告警 - 已注释,改用全局弹窗
/*
handleQueuedRealtimeAlarm(data) {
console.log('收到队列化实时告警:', 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)
},
// 播放告警提示音
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.noise
case 7: // 照度
return deviceParam.illuminance
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('告警已记录,状态为未处理')
// 调用回调函数通知App.vue处理完成
if (this.alarmProcessedCallback) {
this.alarmProcessedCallback(this.currentAlarmId, 'later')
}
}
} catch (error) {
console.error('保存告警数据失败:', error)
this.$message.error('保存告警数据失败')
} finally {
this.alarmProcessing = false
}
}
this.closeAlarmDialog()
},
// 确认知晓实时告警(保存为已处理状态)
async processRealtimeAlarm() {
this.alarmProcessing = true
try {
// 保存告警数据状态为0已处理
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) {
console.error('处理告警失败:', error)
this.$message.error('处理告警失败')
} finally {
this.alarmProcessing = false
}
},
// 关闭告警弹窗
closeAlarmDialog() {
this.realtimeAlarmDialog = false
this.currentRealtimeAlarm = null
this.currentAlarmId = null
this.alarmProcessedCallback = null
this.alarmTimeoutCallback = null
// 清理实时告警处置措施数据
this.realtimeActiveTab = 'alarmDetail'
this.realtimeActionSteps = []
this.realtimeActionStepsLoading = 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] || '未知字段'
},
// 处理WebSocket连接状态变化 - 已注释,改用全局管理
/*
onWebSocketConnected() {
console.log('WebSocket连接成功')
this.websocketStatusText = 'WebSocket连接成功'
this.websocketStatusIcon = 'el-icon-success'
this.websocketStatusColor = '#67C23A'
},
onWebSocketDisconnected() {
console.log('WebSocket连接断开')
this.websocketStatusText = 'WebSocket连接断开'
this.websocketStatusIcon = 'el-icon-error'
this.websocketStatusColor = '#F56C6C'
},
onWebSocketMaxRetriesReached() {
console.log('WebSocket达到最大重试次数')
this.websocketStatusText = 'WebSocket达到最大重试次数'
this.websocketStatusIcon = 'el-icon-warning'
this.websocketStatusColor = '#E6A23C'
},
// 处理WebSocket自动超时保存事件
handleAutoSaveAlarm(data) {
console.log('收到自动超时保存事件:', data)
const {alarm, status} = data
// 自动保存告警数据
this.saveRealtimeAlarmData(alarm, status).then((success) => {
if (success) {
console.log('自动超时保存成功:', alarm.id)
this.alarmQueueLength = 0
} else {
console.error('自动超时保存失败:', alarm.id)
this.alarmQueueLength = 1
}
})
},
*/
// 显示队列状态 - 已注释,改用全局管理
/*
showQueueStatus() {
// 获取告警队列状态并显示
if (this.$root.getAlarmQueueStatus) {
const status = this.$root.getAlarmQueueStatus()
const message = `
告警队列状态:
• 队列长度:${status.queueLength}
• 正在处理:${status.isProcessing ? '是' : '否'}
• 总接收:${status.totalReceived}
• 已处理:${status.totalProcessed}
• 已丢弃:${status.totalDropped}
• 连接状态:${status.isConnected ? '已连接' : '已断开'}
`
this.$alert(message, '告警队列状态', {
confirmButtonText: '确定',
type: 'info'
})
} else {
this.$message.info('无法获取队列状态信息')
}
},
*/
// 队列状态更新 - 已注释,改用全局管理
/*
updateQueueStatus() {
// 从App.vue获取告警队列状态
if (this.$root.getAlarmQueueStatus) {
const status = this.$root.getAlarmQueueStatus()
this.alarmQueueLength = status.queueLength
// 根据队列长度设置状态
if (status.queueLength === 0) {
this.alarmQueueStatusText = '告警队列为空'
this.queueStatusColor = '#67C23A' // 绿色
} else if (status.queueLength < 5) {
this.alarmQueueStatusText = `告警队列:${status.queueLength}个待处理`
this.queueStatusColor = '#E6A23C' // 黄色
} else {
this.alarmQueueStatusText = `告警队列:${status.queueLength}个待处理(队列较长)`
this.queueStatusColor = '#F56C6C' // 红色
}
// 初始化WebSocket状态如果还未设置
if (!this.websocketStatusText) {
if (status.isConnected) {
this.websocketStatusText = 'WebSocket连接正常'
this.websocketStatusIcon = 'el-icon-success'
this.websocketStatusColor = '#67C23A'
} else {
this.websocketStatusText = 'WebSocket连接断开'
this.websocketStatusIcon = 'el-icon-error'
this.websocketStatusColor = '#F56C6C'
}
}
}
},
// 加载实时告警的处置措施
async loadRealtimeActionSteps(alarm) {
if (!alarm || !alarm.alarmRules || alarm.alarmRules.length === 0) {
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, '个步骤')
} catch (error) {
console.error('加载实时告警处置措施失败:', error)
this.realtimeActionSteps = []
} finally {
this.realtimeActionStepsLoading = false
}
}
*/
}
}
</script>
<style lang="scss" scoped>
@keyframes redYellowBlink {
0% {
color: red;
}
50% {
color: rgb(186, 186, 16);
}
100% {
color: red;
}
}
.red-yellow-blink {
animation: redYellowBlink 1s infinite;
}
.navbar {
height: 50px;
overflow: hidden;
position: relative;
background: #fff;
box-shadow: 0 1px 4px rgba(0, 21, 41, .08);
.hamburger-container {
line-height: 46px;
height: 100%;
float: left;
cursor: pointer;
transition: background .3s;
-webkit-tap-highlight-color: transparent;
&:hover {
background: rgba(0, 0, 0, .025)
}
}
.breadcrumb-container {
float: left;
}
.topmenu-container {
position: absolute;
left: 50px;
}
.errLog-container {
display: inline-block;
vertical-align: top;
}
.right-menu {
float: right;
height: 100%;
line-height: 50px;
&:focus {
outline: none;
}
.right-menu-item {
display: inline-block;
padding: 0 8px;
height: 100%;
font-size: 18px;
color: #5a5e66;
vertical-align: text-bottom;
&.hover-effect {
cursor: pointer;
transition: background .3s;
&:hover {
background: rgba(0, 0, 0, .025)
}
}
}
.avatar-container {
margin-right: 30px;
.avatar-wrapper {
margin-top: 5px;
position: relative;
.user-avatar {
cursor: pointer;
width: 40px;
height: 40px;
border-radius: 10px;
}
.el-icon-caret-bottom {
cursor: pointer;
position: absolute;
right: -20px;
top: 25px;
font-size: 12px;
}
}
}
}
}
// 措施步骤相关样式
.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;
border-radius: 4px;
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;
color: #666;
background: #f9f9f9;
text-align: center;
}
}
}
}
.step-remark {
margin-top: 10px;
}
}
.no-steps {
text-align: center;
padding: 40px;
}
.image-preview-container {
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;
}
}
}
// WebSocket状态和队列状态指示器样式 - 已注释,改用全局管理
.websocket-status {
display: inline-flex;
align-items: center;
cursor: default;
i {
font-size: 16px;
transition: all 0.3s ease;
}
}
.alarm-queue-status {
display: inline-flex;
align-items: center;
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;
right: -8px;
background: #ff4757;
color: white;
font-size: 10px;
padding: 2px 4px;
border-radius: 8px;
min-width: 16px;
height: 16px;
line-height: 12px;
text-align: center;
font-weight: bold;
animation: pulse 2s infinite;
}
}
*/
@keyframes pulse {
0% {
transform: scale(1);
}
50% {
transform: scale(1.2);
}
100% {
transform: scale(1);
}
}
</style>