|
|
<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>
|