|
|
@@ -0,0 +1,952 @@
|
|
|
+<template>
|
|
|
+ <div class="app-container alarm-container">
|
|
|
+ <!-- 实时告警横幅 -->
|
|
|
+ <div v-if="urgentAlarms.length > 0" class="urgent-banner">
|
|
|
+ <div class="urgent-icon">
|
|
|
+ <i class="el-icon-warning"></i>
|
|
|
+ </div>
|
|
|
+ <div class="urgent-content">
|
|
|
+ <el-carousel height="36px" direction="vertical" :autoplay="true" :interval="3000" indicator-position="none">
|
|
|
+ <el-carousel-item v-for="item in urgentAlarms" :key="item.alarmId">
|
|
|
+ <span class="urgent-text" @click="handleView(item)">
|
|
|
+ 【紧急】{{ item.alarmMsg || item.targetName + ' - ' + item.attrName + '异常' }}
|
|
|
+ <span class="urgent-time">{{ item.alarmTime }}</span>
|
|
|
+ </span>
|
|
|
+ </el-carousel-item>
|
|
|
+ </el-carousel>
|
|
|
+ </div>
|
|
|
+ <el-button type="text" class="urgent-btn" @click="showUrgentList">查看全部({{ urgentAlarms.length }})</el-button>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 统计卡片 -->
|
|
|
+ <div class="stats-cards">
|
|
|
+ <div class="stat-card active" @click="filterByStatus(null)">
|
|
|
+ <div class="stat-icon">
|
|
|
+ <i class="el-icon-bell"></i>
|
|
|
+ </div>
|
|
|
+ <div class="stat-content">
|
|
|
+ <div class="stat-value">{{ statsData.totalCount || 0 }}</div>
|
|
|
+ <div class="stat-label">告警总数</div>
|
|
|
+ </div>
|
|
|
+ <div class="stat-trend" v-if="statsData.todayCount">
|
|
|
+ 今日 +{{ statsData.todayCount }}
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div class="stat-card danger" @click="filterByStatus(0)">
|
|
|
+ <div class="stat-icon">
|
|
|
+ <i class="el-icon-warning-outline"></i>
|
|
|
+ </div>
|
|
|
+ <div class="stat-content">
|
|
|
+ <div class="stat-value">{{ statsData.activeCount || 0 }}</div>
|
|
|
+ <div class="stat-label">活动告警</div>
|
|
|
+ </div>
|
|
|
+ <div class="stat-badge" v-if="statsData.activeCount > 0"></div>
|
|
|
+ </div>
|
|
|
+ <div class="stat-card warning" @click="filterByLevel(3)">
|
|
|
+ <div class="stat-icon">
|
|
|
+ <i class="el-icon-s-opportunity"></i>
|
|
|
+ </div>
|
|
|
+ <div class="stat-content">
|
|
|
+ <div class="stat-value">{{ statsData.urgentCount || 0 }}</div>
|
|
|
+ <div class="stat-label">紧急告警</div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div class="stat-card success">
|
|
|
+ <div class="stat-icon">
|
|
|
+ <i class="el-icon-circle-check"></i>
|
|
|
+ </div>
|
|
|
+ <div class="stat-content">
|
|
|
+ <div class="stat-value">{{ statsData.handleRate || 0 }}%</div>
|
|
|
+ <div class="stat-label">处理率</div>
|
|
|
+ </div>
|
|
|
+ <div class="stat-progress">
|
|
|
+ <el-progress :percentage="statsData.handleRate || 0" :show-text="false" :stroke-width="4" />
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 搜索区域 -->
|
|
|
+ <el-card class="search-card" shadow="hover">
|
|
|
+ <el-form :model="queryParams" ref="queryForm" size="small" :inline="true" v-show="showSearch" label-width="80px">
|
|
|
+ <el-form-item label="目标名称" prop="targetName">
|
|
|
+ <el-input v-model="queryParams.targetName" placeholder="设备/设施名称" clearable style="width: 160px"
|
|
|
+ @keyup.enter.native="handleQuery" />
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="告警级别" prop="alarmLevel">
|
|
|
+ <el-select v-model="queryParams.alarmLevel" placeholder="全部级别" clearable style="width: 120px">
|
|
|
+ <el-option v-for="item in ALARM_LEVEL_OPTIONS" :key="item.value" :label="item.label" :value="item.value">
|
|
|
+ <span :style="{ color: item.color }">{{ item.label }}</span>
|
|
|
+ </el-option>
|
|
|
+ </el-select>
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="告警状态" prop="alarmStatus">
|
|
|
+ <el-select v-model="queryParams.alarmStatus" placeholder="全部状态" clearable style="width: 120px">
|
|
|
+ <el-option v-for="item in ALARM_STATUS_OPTIONS" :key="item.value" :label="item.label" :value="item.value" />
|
|
|
+ </el-select>
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="告警来源" prop="alarmSource">
|
|
|
+ <el-select v-model="queryParams.alarmSource" placeholder="全部来源" clearable style="width: 120px">
|
|
|
+ <el-option v-for="item in ALARM_SOURCE_OPTIONS" :key="item.value" :label="item.label" :value="item.value" />
|
|
|
+ </el-select>
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="告警时间">
|
|
|
+ <el-date-picker v-model="dateRange" type="datetimerange" range-separator="至"
|
|
|
+ start-placeholder="开始时间" end-placeholder="结束时间" style="width: 340px"
|
|
|
+ value-format="yyyy-MM-dd HH:mm:ss" :default-time="['00:00:00', '23:59:59']" />
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="子系统" prop="subsystemCode">
|
|
|
+ <el-select v-model="queryParams.subsystemCode" placeholder="全部子系统" clearable style="width: 140px">
|
|
|
+ <el-option v-for="item in subsystemOptions" :key="item.systemCode"
|
|
|
+ :label="item.systemName" :value="item.systemCode" />
|
|
|
+ </el-select>
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item>
|
|
|
+ <el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
|
|
|
+ <el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
|
|
|
+ </el-form-item>
|
|
|
+ </el-form>
|
|
|
+ </el-card>
|
|
|
+
|
|
|
+ <!-- 操作工具栏 -->
|
|
|
+ <el-card class="table-card" shadow="hover">
|
|
|
+ <div slot="header" class="card-header">
|
|
|
+ <div class="card-title-area">
|
|
|
+ <span class="card-title">
|
|
|
+ <i class="el-icon-message-solid"></i> 告警列表
|
|
|
+ </span>
|
|
|
+ <el-radio-group v-model="statusFilter" size="mini" @change="handleStatusFilter">
|
|
|
+ <el-radio-button :label="null">全部</el-radio-button>
|
|
|
+ <el-radio-button :label="0">
|
|
|
+ 活动 <el-badge :value="statsData.activeCount" :max="99" class="status-badge" v-if="statsData.activeCount > 0" />
|
|
|
+ </el-radio-button>
|
|
|
+ <el-radio-button :label="1">已确认</el-radio-button>
|
|
|
+ <el-radio-button :label="2">处置中</el-radio-button>
|
|
|
+ <el-radio-button :label="'ended'">已结束</el-radio-button>
|
|
|
+ </el-radio-group>
|
|
|
+ </div>
|
|
|
+ <div class="card-actions">
|
|
|
+ <el-button type="primary" plain icon="el-icon-plus" size="mini" @click="handleReport"
|
|
|
+ v-hasPermi="['ems:alarm:add']">手动上报</el-button>
|
|
|
+ <el-button type="success" plain icon="el-icon-check" size="mini" :disabled="multiple"
|
|
|
+ @click="handleBatchConfirm" v-hasPermi="['ems:alarm:handle']">批量确认</el-button>
|
|
|
+ <el-button type="danger" plain icon="el-icon-delete" size="mini" :disabled="multiple"
|
|
|
+ @click="handleDelete" v-hasPermi="['ems:alarm:remove']">删除</el-button>
|
|
|
+ <el-button type="warning" plain icon="el-icon-download" size="mini" @click="handleExport"
|
|
|
+ v-hasPermi="['ems:alarm:export']">导出</el-button>
|
|
|
+ <el-tooltip content="刷新数据" placement="top">
|
|
|
+ <el-button circle icon="el-icon-refresh" size="mini" @click="getList" />
|
|
|
+ </el-tooltip>
|
|
|
+ <right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 告警表格 -->
|
|
|
+ <el-table v-loading="loading" :data="alarmList" @selection-change="handleSelectionChange"
|
|
|
+ border stripe highlight-current-row :row-class-name="tableRowClassName"
|
|
|
+ @row-dblclick="handleView">
|
|
|
+ <el-table-column type="selection" width="50" align="center" />
|
|
|
+ <el-table-column label="告警级别" align="center" width="90">
|
|
|
+ <template slot-scope="scope">
|
|
|
+ <el-tag :type="getAlarmLevelInfo(scope.row.alarmLevel).type" size="small" effect="dark"
|
|
|
+ :class="{ 'blink': scope.row.alarmStatus === 0 && scope.row.alarmLevel === 3 }">
|
|
|
+ {{ getAlarmLevelInfo(scope.row.alarmLevel).label }}
|
|
|
+ </el-tag>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="告警状态" align="center" width="90">
|
|
|
+ <template slot-scope="scope">
|
|
|
+ <el-tag :type="getAlarmStatusInfo(scope.row.alarmStatus).type" size="small" effect="light">
|
|
|
+ {{ getAlarmStatusInfo(scope.row.alarmStatus).label }}
|
|
|
+ </el-tag>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="告警目标" min-width="180" show-overflow-tooltip>
|
|
|
+ <template slot-scope="scope">
|
|
|
+ <div class="target-info">
|
|
|
+ <i :class="getTargetTypeIcon(scope.row.targetType)" class="target-icon"></i>
|
|
|
+ <div class="target-detail">
|
|
|
+ <el-link type="primary" @click="handleView(scope.row)" class="target-name">
|
|
|
+ {{ scope.row.targetName || scope.row.targetCode }}
|
|
|
+ </el-link>
|
|
|
+ <span class="target-location" v-if="scope.row.location">{{ scope.row.location }}</span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="告警内容" min-width="200" show-overflow-tooltip>
|
|
|
+ <template slot-scope="scope">
|
|
|
+ <div class="alarm-content">
|
|
|
+ <span class="alarm-msg">{{ scope.row.alarmMsg || formatAlarmContent(scope.row) }}</span>
|
|
|
+ <span class="alarm-value" v-if="scope.row.attrValue">
|
|
|
+ 当前值: <code>{{ scope.row.attrValue }}</code>
|
|
|
+ </span>
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="告警来源" align="center" width="100">
|
|
|
+ <template slot-scope="scope">
|
|
|
+ <el-tooltip :content="getAlarmSourceInfo(scope.row.alarmSource).label">
|
|
|
+ <i :class="getAlarmSourceInfo(scope.row.alarmSource).icon"
|
|
|
+ :style="{ color: getAlarmSourceInfo(scope.row.alarmSource).color, fontSize: '18px' }"></i>
|
|
|
+ </el-tooltip>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="告警时间" align="center" width="160">
|
|
|
+ <template slot-scope="scope">
|
|
|
+ <div class="time-info">
|
|
|
+ <span class="time-main">{{ formatTime(scope.row.alarmTime) }}</span>
|
|
|
+ <span class="time-duration" v-if="scope.row.duration">
|
|
|
+ 持续 {{ formatDuration(scope.row.duration) }}
|
|
|
+ </span>
|
|
|
+ <span class="repeat-count" v-if="scope.row.repeatCount > 1">
|
|
|
+ <el-badge :value="scope.row.repeatCount" type="warning" />
|
|
|
+ </span>
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="子系统" prop="subsystemName" width="100" show-overflow-tooltip />
|
|
|
+ <el-table-column label="操作" align="center" width="200" fixed="right" class-name="small-padding fixed-width">
|
|
|
+ <template slot-scope="scope">
|
|
|
+ <template v-if="!isEndedAlarm(scope.row.alarmStatus)">
|
|
|
+ <el-button v-if="scope.row.alarmStatus === 0" size="mini" type="text" icon="el-icon-check"
|
|
|
+ @click="handleConfirm(scope.row)" v-hasPermi="['ems:alarm:handle']">确认</el-button>
|
|
|
+ <el-button size="mini" type="text" icon="el-icon-s-tools"
|
|
|
+ @click="handleProcess(scope.row)" v-hasPermi="['ems:alarm:handle']">处置</el-button>
|
|
|
+ <el-dropdown @command="(cmd) => handleCommand(cmd, scope.row)" trigger="click">
|
|
|
+ <el-button size="mini" type="text" icon="el-icon-more"></el-button>
|
|
|
+ <el-dropdown-menu slot="dropdown">
|
|
|
+ <el-dropdown-item command="resolve" icon="el-icon-circle-check">标记解决</el-dropdown-item>
|
|
|
+ <el-dropdown-item command="close" icon="el-icon-circle-close">关闭告警</el-dropdown-item>
|
|
|
+ <el-dropdown-item command="view" icon="el-icon-view" divided>查看详情</el-dropdown-item>
|
|
|
+ </el-dropdown-menu>
|
|
|
+ </el-dropdown>
|
|
|
+ </template>
|
|
|
+ <template v-else>
|
|
|
+ <el-button size="mini" type="text" icon="el-icon-view" @click="handleView(scope.row)">详情</el-button>
|
|
|
+ <el-button size="mini" type="text" icon="el-icon-delete" class="delete-btn"
|
|
|
+ @click="handleDelete(scope.row)" v-hasPermi="['ems:alarm:remove']">删除</el-button>
|
|
|
+ </template>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ </el-table>
|
|
|
+
|
|
|
+ <!-- 分页 -->
|
|
|
+ <pagination v-show="total > 0" :total="total" :page.sync="queryParams.pageNum"
|
|
|
+ :limit.sync="queryParams.pageSize" @pagination="getList" />
|
|
|
+ </el-card>
|
|
|
+
|
|
|
+ <!-- 告警详情抽屉 -->
|
|
|
+ <alarm-detail ref="alarmDetail" @refresh="getList" />
|
|
|
+
|
|
|
+ <!-- 告警处置对话框 -->
|
|
|
+ <alarm-handle ref="alarmHandle" @success="handleSuccess" />
|
|
|
+
|
|
|
+ <!-- 手动上报对话框 -->
|
|
|
+ <alarm-report ref="alarmReport" @success="handleSuccess" />
|
|
|
+
|
|
|
+ <!-- 紧急告警列表对话框 -->
|
|
|
+ <el-dialog title="紧急告警列表" :visible.sync="urgentDialogVisible" width="900px" append-to-body>
|
|
|
+ <el-table :data="urgentAlarms" border stripe max-height="400">
|
|
|
+ <el-table-column label="告警目标" prop="targetName" min-width="150" show-overflow-tooltip />
|
|
|
+ <el-table-column label="告警内容" min-width="200" show-overflow-tooltip>
|
|
|
+ <template slot-scope="scope">
|
|
|
+ {{ scope.row.alarmMsg || formatAlarmContent(scope.row) }}
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="告警时间" prop="alarmTime" width="160" />
|
|
|
+ <el-table-column label="操作" width="120" align="center">
|
|
|
+ <template slot-scope="scope">
|
|
|
+ <el-button type="text" size="small" @click="handleUrgentProcess(scope.row)">立即处置</el-button>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ </el-table>
|
|
|
+ <div slot="footer">
|
|
|
+ <el-button @click="urgentDialogVisible = false">关 闭</el-button>
|
|
|
+ <el-button type="primary" @click="handleBatchConfirmUrgent">全部确认</el-button>
|
|
|
+ </div>
|
|
|
+ </el-dialog>
|
|
|
+ </div>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script>
|
|
|
+import { listAlarm, listActiveAlarm, delAlarm, confirmAlarm, batchConfirmAlarm, getAlarmStatsOverview, exportAlarm } from '@/api/alarm/alarm'
|
|
|
+import { listSubsystem } from '@/api/adapter/subsystem'
|
|
|
+import { ALARM_LEVEL_OPTIONS, ALARM_STATUS_OPTIONS, ALARM_SOURCE_OPTIONS, getAlarmLevelInfo, getAlarmStatusInfo, getAlarmSourceInfo, getTargetTypeIcon, isEndedAlarm } from '@/enums/alarmEnum'
|
|
|
+import AlarmDetail from './components/AlarmDetail.vue'
|
|
|
+import AlarmHandle from './components/AlarmHandle.vue'
|
|
|
+import AlarmReport from './components/AlarmReport.vue'
|
|
|
+import { parseTime } from '@/utils/ruoyi'
|
|
|
+
|
|
|
+export default {
|
|
|
+ name: 'Alarm',
|
|
|
+ components: { AlarmDetail, AlarmHandle, AlarmReport },
|
|
|
+ data() {
|
|
|
+ return {
|
|
|
+ loading: false,
|
|
|
+ showSearch: true,
|
|
|
+ total: 0,
|
|
|
+ alarmList: [],
|
|
|
+ ids: [],
|
|
|
+ alarmIds: [],
|
|
|
+ single: true,
|
|
|
+ multiple: true,
|
|
|
+ dateRange: [],
|
|
|
+ queryParams: {
|
|
|
+ pageNum: 1,
|
|
|
+ pageSize: 20,
|
|
|
+ targetName: null,
|
|
|
+ alarmLevel: null,
|
|
|
+ alarmStatus: null,
|
|
|
+ alarmSource: null,
|
|
|
+ subsystemCode: null,
|
|
|
+ startRecTime: null,
|
|
|
+ endRecTime: null
|
|
|
+ },
|
|
|
+ statusFilter: null,
|
|
|
+ statsData: {
|
|
|
+ totalCount: 0,
|
|
|
+ activeCount: 0,
|
|
|
+ urgentCount: 0,
|
|
|
+ handleRate: 0,
|
|
|
+ todayCount: 0
|
|
|
+ },
|
|
|
+ urgentAlarms: [],
|
|
|
+ urgentDialogVisible: false,
|
|
|
+ subsystemOptions: [],
|
|
|
+ ALARM_LEVEL_OPTIONS,
|
|
|
+ ALARM_STATUS_OPTIONS,
|
|
|
+ ALARM_SOURCE_OPTIONS,
|
|
|
+ // 自动刷新定时器
|
|
|
+ refreshTimer: null
|
|
|
+ }
|
|
|
+ },
|
|
|
+ created() {
|
|
|
+ this.loadSubsystems()
|
|
|
+ this.getList()
|
|
|
+ this.loadStats()
|
|
|
+ this.loadUrgentAlarms()
|
|
|
+ // 启动自动刷新
|
|
|
+ this.startAutoRefresh()
|
|
|
+ },
|
|
|
+ beforeDestroy() {
|
|
|
+ // 清除定时器
|
|
|
+ this.stopAutoRefresh()
|
|
|
+ },
|
|
|
+ methods: {
|
|
|
+ getAlarmLevelInfo,
|
|
|
+ getAlarmStatusInfo,
|
|
|
+ getAlarmSourceInfo,
|
|
|
+ getTargetTypeIcon,
|
|
|
+ isEndedAlarm,
|
|
|
+
|
|
|
+ /** 加载子系统选项 */
|
|
|
+ async loadSubsystems() {
|
|
|
+ try {
|
|
|
+ const res = await listSubsystem({})
|
|
|
+ this.subsystemOptions = res.rows || res.data || []
|
|
|
+ } catch (e) {
|
|
|
+ console.error('加载子系统失败', e)
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ /** 加载统计数据 */
|
|
|
+ async loadStats() {
|
|
|
+ try {
|
|
|
+ const res = await getAlarmStatsOverview({})
|
|
|
+ const data = res.data || {}
|
|
|
+ this.statsData = {
|
|
|
+ totalCount: data.totalCount || 0,
|
|
|
+ activeCount: data.activeCount || 0,
|
|
|
+ urgentCount: data.urgentCount || 0,
|
|
|
+ handleRate: data.handleRate?.rate || 0,
|
|
|
+ todayCount: data.todayCount || 0
|
|
|
+ }
|
|
|
+ } catch (e) {
|
|
|
+ console.error('加载统计失败', e)
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ /** 加载紧急告警 */
|
|
|
+ async loadUrgentAlarms() {
|
|
|
+ try {
|
|
|
+ const res = await listActiveAlarm()
|
|
|
+ const allActive = res.data || []
|
|
|
+ this.urgentAlarms = allActive.filter(a => a.alarmLevel === 3 && a.alarmStatus === 0)
|
|
|
+ } catch (e) {
|
|
|
+ console.error('加载紧急告警失败', e)
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ /** 查询列表 */
|
|
|
+ getList() {
|
|
|
+ this.loading = true
|
|
|
+ // 处理时间范围
|
|
|
+ if (this.dateRange && this.dateRange.length === 2) {
|
|
|
+ this.queryParams.startRecTime = this.dateRange[0]
|
|
|
+ this.queryParams.endRecTime = this.dateRange[1]
|
|
|
+ } else {
|
|
|
+ this.queryParams.startRecTime = null
|
|
|
+ this.queryParams.endRecTime = null
|
|
|
+ }
|
|
|
+
|
|
|
+ listAlarm(this.queryParams).then(res => {
|
|
|
+ this.alarmList = res.rows || []
|
|
|
+ this.total = res.total || 0
|
|
|
+ }).finally(() => {
|
|
|
+ this.loading = false
|
|
|
+ })
|
|
|
+ },
|
|
|
+
|
|
|
+ /** 搜索 */
|
|
|
+ handleQuery() {
|
|
|
+ this.queryParams.pageNum = 1
|
|
|
+ this.getList()
|
|
|
+ },
|
|
|
+
|
|
|
+ /** 重置 */
|
|
|
+ resetQuery() {
|
|
|
+ this.dateRange = []
|
|
|
+ this.statusFilter = null
|
|
|
+ this.resetForm('queryForm')
|
|
|
+ this.handleQuery()
|
|
|
+ },
|
|
|
+
|
|
|
+ /** 状态筛选 */
|
|
|
+ handleStatusFilter(val) {
|
|
|
+ if (val === 'ended') {
|
|
|
+ // 已结束包括:已解决、已关闭、已恢复
|
|
|
+ this.queryParams.alarmStatus = null
|
|
|
+ // 需要后端支持或特殊处理
|
|
|
+ } else {
|
|
|
+ this.queryParams.alarmStatus = val
|
|
|
+ }
|
|
|
+ this.handleQuery()
|
|
|
+ },
|
|
|
+
|
|
|
+ /** 按状态筛选 */
|
|
|
+ filterByStatus(status) {
|
|
|
+ this.statusFilter = status
|
|
|
+ this.queryParams.alarmStatus = status
|
|
|
+ this.handleQuery()
|
|
|
+ },
|
|
|
+
|
|
|
+ /** 按级别筛选 */
|
|
|
+ filterByLevel(level) {
|
|
|
+ this.queryParams.alarmLevel = level
|
|
|
+ this.handleQuery()
|
|
|
+ },
|
|
|
+
|
|
|
+ /** 多选框选中数据 */
|
|
|
+ handleSelectionChange(selection) {
|
|
|
+ this.ids = selection.map(item => item.id)
|
|
|
+ this.alarmIds = selection.map(item => item.alarmId)
|
|
|
+ this.single = selection.length !== 1
|
|
|
+ this.multiple = !selection.length
|
|
|
+ },
|
|
|
+
|
|
|
+ /** 表格行样式 */
|
|
|
+ tableRowClassName({ row }) {
|
|
|
+ if (row.alarmStatus === 0) {
|
|
|
+ if (row.alarmLevel === 3) return 'urgent-row'
|
|
|
+ if (row.alarmLevel === 2) return 'important-row'
|
|
|
+ return 'active-row'
|
|
|
+ }
|
|
|
+ if (row.alarmStatus >= 3) return 'ended-row'
|
|
|
+ return ''
|
|
|
+ },
|
|
|
+
|
|
|
+ /** 格式化告警内容 */
|
|
|
+ formatAlarmContent(row) {
|
|
|
+ const attrName = row.attrName || row.attrKey || '属性'
|
|
|
+ return `${row.targetName || '设备'} ${attrName}异常`
|
|
|
+ },
|
|
|
+
|
|
|
+ /** 格式化时间 */
|
|
|
+ formatTime(time) {
|
|
|
+ return parseTime(time, '{y}-{m}-{d} {h}:{i}')
|
|
|
+ },
|
|
|
+
|
|
|
+ /** 格式化持续时长 */
|
|
|
+ formatDuration(seconds) {
|
|
|
+ if (!seconds) return ''
|
|
|
+ if (seconds < 60) return `${seconds}秒`
|
|
|
+ if (seconds < 3600) return `${Math.floor(seconds / 60)}分钟`
|
|
|
+ if (seconds < 86400) return `${Math.floor(seconds / 3600)}小时`
|
|
|
+ return `${Math.floor(seconds / 86400)}天`
|
|
|
+ },
|
|
|
+
|
|
|
+ /** 查看详情 */
|
|
|
+ handleView(row) {
|
|
|
+ this.$refs.alarmDetail.open(row.alarmId)
|
|
|
+ },
|
|
|
+
|
|
|
+ /** 确认告警 */
|
|
|
+ handleConfirm(row) {
|
|
|
+ this.$modal.confirm(`确认告警【${row.targetName}】吗?`).then(() => {
|
|
|
+ return confirmAlarm(row.alarmId, '已确认')
|
|
|
+ }).then(() => {
|
|
|
+ this.$modal.msgSuccess('确认成功')
|
|
|
+ this.handleSuccess()
|
|
|
+ }).catch(() => {})
|
|
|
+ },
|
|
|
+
|
|
|
+ /** 处置告警 */
|
|
|
+ handleProcess(row) {
|
|
|
+ this.$refs.alarmHandle.open(row, 'handle')
|
|
|
+ },
|
|
|
+
|
|
|
+ /** 下拉菜单命令 */
|
|
|
+ handleCommand(cmd, row) {
|
|
|
+ switch (cmd) {
|
|
|
+ case 'resolve':
|
|
|
+ this.$refs.alarmHandle.open(row, 'resolve')
|
|
|
+ break
|
|
|
+ case 'close':
|
|
|
+ this.$refs.alarmHandle.open(row, 'close')
|
|
|
+ break
|
|
|
+ case 'view':
|
|
|
+ this.handleView(row)
|
|
|
+ break
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ /** 手动上报 */
|
|
|
+ handleReport() {
|
|
|
+ this.$refs.alarmReport.open()
|
|
|
+ },
|
|
|
+
|
|
|
+ /** 批量确认 */
|
|
|
+ handleBatchConfirm() {
|
|
|
+ if (this.alarmIds.length === 0) {
|
|
|
+ this.$modal.msgWarning('请选择要确认的告警')
|
|
|
+ return
|
|
|
+ }
|
|
|
+ this.$modal.confirm(`确认选中的 ${this.alarmIds.length} 条告警吗?`).then(() => {
|
|
|
+ return batchConfirmAlarm(this.alarmIds)
|
|
|
+ }).then(res => {
|
|
|
+ this.$modal.msgSuccess(res.msg || '批量确认成功')
|
|
|
+ this.handleSuccess()
|
|
|
+ }).catch(() => {})
|
|
|
+ },
|
|
|
+
|
|
|
+ /** 删除 */
|
|
|
+ handleDelete(row) {
|
|
|
+ const ids = row.id ? [row.id] : this.ids
|
|
|
+ this.$modal.confirm(`确认删除选中的告警记录吗?`).then(() => {
|
|
|
+ return delAlarm(ids)
|
|
|
+ }).then(() => {
|
|
|
+ this.$modal.msgSuccess('删除成功')
|
|
|
+ this.getList()
|
|
|
+ }).catch(() => {})
|
|
|
+ },
|
|
|
+
|
|
|
+ /** 导出 */
|
|
|
+ handleExport() {
|
|
|
+ this.download('/ems/alarm/export', {
|
|
|
+ ...this.queryParams
|
|
|
+ }, `alarm_${new Date().getTime()}.xlsx`)
|
|
|
+ },
|
|
|
+
|
|
|
+ /** 处理成功回调 */
|
|
|
+ handleSuccess() {
|
|
|
+ this.getList()
|
|
|
+ this.loadStats()
|
|
|
+ this.loadUrgentAlarms()
|
|
|
+ },
|
|
|
+
|
|
|
+ /** 显示紧急告警列表 */
|
|
|
+ showUrgentList() {
|
|
|
+ this.urgentDialogVisible = true
|
|
|
+ },
|
|
|
+
|
|
|
+ /** 处置紧急告警 */
|
|
|
+ handleUrgentProcess(row) {
|
|
|
+ this.urgentDialogVisible = false
|
|
|
+ this.$refs.alarmHandle.open(row, 'handle')
|
|
|
+ },
|
|
|
+
|
|
|
+ /** 批量确认紧急告警 */
|
|
|
+ handleBatchConfirmUrgent() {
|
|
|
+ const ids = this.urgentAlarms.map(a => a.alarmId)
|
|
|
+ batchConfirmAlarm(ids).then(res => {
|
|
|
+ this.$modal.msgSuccess('批量确认成功')
|
|
|
+ this.urgentDialogVisible = false
|
|
|
+ this.handleSuccess()
|
|
|
+ })
|
|
|
+ },
|
|
|
+
|
|
|
+ /** 启动自动刷新 */
|
|
|
+ startAutoRefresh() {
|
|
|
+ this.refreshTimer = setInterval(() => {
|
|
|
+ this.loadStats()
|
|
|
+ this.loadUrgentAlarms()
|
|
|
+ }, 30000) // 30秒刷新一次
|
|
|
+ },
|
|
|
+
|
|
|
+ /** 停止自动刷新 */
|
|
|
+ stopAutoRefresh() {
|
|
|
+ if (this.refreshTimer) {
|
|
|
+ clearInterval(this.refreshTimer)
|
|
|
+ this.refreshTimer = null
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+</script>
|
|
|
+
|
|
|
+<style lang="scss" scoped>
|
|
|
+.alarm-container {
|
|
|
+ padding: 20px;
|
|
|
+ background: #f5f7fa;
|
|
|
+ min-height: calc(100vh - 84px);
|
|
|
+}
|
|
|
+
|
|
|
+// 紧急告警横幅
|
|
|
+.urgent-banner {
|
|
|
+ background: linear-gradient(135deg, #ff6b6b 0%, #ee5a5a 100%);
|
|
|
+ border-radius: 8px;
|
|
|
+ padding: 12px 20px;
|
|
|
+ margin-bottom: 16px;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ color: #fff;
|
|
|
+ box-shadow: 0 4px 12px rgba(255, 107, 107, 0.3);
|
|
|
+ animation: pulse 2s infinite;
|
|
|
+
|
|
|
+ .urgent-icon {
|
|
|
+ font-size: 24px;
|
|
|
+ margin-right: 12px;
|
|
|
+ animation: shake 0.5s infinite;
|
|
|
+ }
|
|
|
+
|
|
|
+ .urgent-content {
|
|
|
+ flex: 1;
|
|
|
+ overflow: hidden;
|
|
|
+
|
|
|
+ ::v-deep .el-carousel__item {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ .urgent-text {
|
|
|
+ cursor: pointer;
|
|
|
+ font-size: 14px;
|
|
|
+
|
|
|
+ &:hover {
|
|
|
+ text-decoration: underline;
|
|
|
+ }
|
|
|
+
|
|
|
+ .urgent-time {
|
|
|
+ margin-left: 16px;
|
|
|
+ opacity: 0.8;
|
|
|
+ font-size: 12px;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ .urgent-btn {
|
|
|
+ color: #fff;
|
|
|
+ font-weight: 600;
|
|
|
+
|
|
|
+ &:hover {
|
|
|
+ color: #ffeb3b;
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+@keyframes pulse {
|
|
|
+ 0%, 100% { opacity: 1; }
|
|
|
+ 50% { opacity: 0.9; }
|
|
|
+}
|
|
|
+
|
|
|
+@keyframes shake {
|
|
|
+ 0%, 100% { transform: rotate(0deg); }
|
|
|
+ 25% { transform: rotate(-5deg); }
|
|
|
+ 75% { transform: rotate(5deg); }
|
|
|
+}
|
|
|
+
|
|
|
+// 统计卡片
|
|
|
+.stats-cards {
|
|
|
+ display: grid;
|
|
|
+ grid-template-columns: repeat(4, 1fr);
|
|
|
+ gap: 16px;
|
|
|
+ margin-bottom: 16px;
|
|
|
+}
|
|
|
+
|
|
|
+.stat-card {
|
|
|
+ background: #fff;
|
|
|
+ border-radius: 12px;
|
|
|
+ padding: 20px;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05);
|
|
|
+ cursor: pointer;
|
|
|
+ transition: all 0.3s;
|
|
|
+ position: relative;
|
|
|
+ overflow: hidden;
|
|
|
+
|
|
|
+ &:hover {
|
|
|
+ transform: translateY(-4px);
|
|
|
+ box-shadow: 0 8px 24px 0 rgba(0, 0, 0, 0.1);
|
|
|
+ }
|
|
|
+
|
|
|
+ &::before {
|
|
|
+ content: '';
|
|
|
+ position: absolute;
|
|
|
+ top: 0;
|
|
|
+ left: 0;
|
|
|
+ width: 4px;
|
|
|
+ height: 100%;
|
|
|
+ }
|
|
|
+
|
|
|
+ &.active::before { background: linear-gradient(180deg, #667eea 0%, #764ba2 100%); }
|
|
|
+ &.danger::before { background: linear-gradient(180deg, #f56c6c 0%, #e6a23c 100%); }
|
|
|
+ &.warning::before { background: linear-gradient(180deg, #e6a23c 0%, #f56c6c 100%); }
|
|
|
+ &.success::before { background: linear-gradient(180deg, #67c23a 0%, #409eff 100%); }
|
|
|
+}
|
|
|
+
|
|
|
+.stat-icon {
|
|
|
+ width: 56px;
|
|
|
+ height: 56px;
|
|
|
+ border-radius: 12px;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ margin-right: 16px;
|
|
|
+
|
|
|
+ i {
|
|
|
+ font-size: 28px;
|
|
|
+ color: #fff;
|
|
|
+ }
|
|
|
+
|
|
|
+ .active & { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); }
|
|
|
+ .danger & { background: linear-gradient(135deg, #f56c6c 0%, #e6a23c 100%); }
|
|
|
+ .warning & { background: linear-gradient(135deg, #e6a23c 0%, #f56c6c 100%); }
|
|
|
+ .success & { background: linear-gradient(135deg, #67c23a 0%, #409eff 100%); }
|
|
|
+}
|
|
|
+
|
|
|
+.stat-content {
|
|
|
+ flex: 1;
|
|
|
+}
|
|
|
+
|
|
|
+.stat-value {
|
|
|
+ font-size: 28px;
|
|
|
+ font-weight: 700;
|
|
|
+ color: #303133;
|
|
|
+ line-height: 1.2;
|
|
|
+}
|
|
|
+
|
|
|
+.stat-label {
|
|
|
+ font-size: 14px;
|
|
|
+ color: #909399;
|
|
|
+ margin-top: 4px;
|
|
|
+}
|
|
|
+
|
|
|
+.stat-trend {
|
|
|
+ position: absolute;
|
|
|
+ top: 12px;
|
|
|
+ right: 12px;
|
|
|
+ font-size: 12px;
|
|
|
+ color: #67c23a;
|
|
|
+ background: rgba(103, 194, 58, 0.1);
|
|
|
+ padding: 2px 8px;
|
|
|
+ border-radius: 10px;
|
|
|
+}
|
|
|
+
|
|
|
+.stat-badge {
|
|
|
+ position: absolute;
|
|
|
+ top: 10px;
|
|
|
+ right: 10px;
|
|
|
+ width: 10px;
|
|
|
+ height: 10px;
|
|
|
+ background: #f56c6c;
|
|
|
+ border-radius: 50%;
|
|
|
+ animation: blink 1s infinite;
|
|
|
+}
|
|
|
+
|
|
|
+.stat-progress {
|
|
|
+ position: absolute;
|
|
|
+ bottom: 0;
|
|
|
+ left: 0;
|
|
|
+ right: 0;
|
|
|
+ padding: 0 20px 10px;
|
|
|
+
|
|
|
+ ::v-deep .el-progress-bar__outer {
|
|
|
+ background: #ebeef5;
|
|
|
+ }
|
|
|
+
|
|
|
+ ::v-deep .el-progress-bar__inner {
|
|
|
+ background: linear-gradient(90deg, #67c23a 0%, #409eff 100%);
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+@keyframes blink {
|
|
|
+ 0%, 100% { opacity: 1; }
|
|
|
+ 50% { opacity: 0.3; }
|
|
|
+}
|
|
|
+
|
|
|
+// 搜索卡片
|
|
|
+.search-card {
|
|
|
+ margin-bottom: 16px;
|
|
|
+ border-radius: 8px;
|
|
|
+
|
|
|
+ ::v-deep .el-card__body {
|
|
|
+ padding: 15px 20px 5px;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 表格卡片
|
|
|
+.table-card {
|
|
|
+ border-radius: 8px;
|
|
|
+
|
|
|
+ .card-header {
|
|
|
+ display: flex;
|
|
|
+ justify-content: space-between;
|
|
|
+ align-items: center;
|
|
|
+ flex-wrap: wrap;
|
|
|
+ gap: 12px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .card-title-area {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 16px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .card-title {
|
|
|
+ font-size: 16px;
|
|
|
+ font-weight: 600;
|
|
|
+ color: #303133;
|
|
|
+
|
|
|
+ i {
|
|
|
+ margin-right: 8px;
|
|
|
+ color: #f56c6c;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ .status-badge {
|
|
|
+ margin-left: 4px;
|
|
|
+
|
|
|
+ ::v-deep .el-badge__content {
|
|
|
+ height: 16px;
|
|
|
+ line-height: 16px;
|
|
|
+ padding: 0 5px;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ .card-actions {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 8px;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 目标信息
|
|
|
+.target-info {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+
|
|
|
+ .target-icon {
|
|
|
+ font-size: 20px;
|
|
|
+ color: #409eff;
|
|
|
+ margin-right: 10px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .target-detail {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ }
|
|
|
+
|
|
|
+ .target-name {
|
|
|
+ font-weight: 500;
|
|
|
+ }
|
|
|
+
|
|
|
+ .target-location {
|
|
|
+ font-size: 12px;
|
|
|
+ color: #909399;
|
|
|
+ margin-top: 2px;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 告警内容
|
|
|
+.alarm-content {
|
|
|
+ .alarm-msg {
|
|
|
+ display: block;
|
|
|
+ color: #303133;
|
|
|
+ }
|
|
|
+
|
|
|
+ .alarm-value {
|
|
|
+ display: block;
|
|
|
+ font-size: 12px;
|
|
|
+ color: #909399;
|
|
|
+ margin-top: 4px;
|
|
|
+
|
|
|
+ code {
|
|
|
+ background: #f5f7fa;
|
|
|
+ padding: 1px 6px;
|
|
|
+ border-radius: 3px;
|
|
|
+ color: #e6a23c;
|
|
|
+ font-family: 'Monaco', 'Menlo', monospace;
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 时间信息
|
|
|
+.time-info {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ align-items: center;
|
|
|
+
|
|
|
+ .time-main {
|
|
|
+ color: #606266;
|
|
|
+ }
|
|
|
+
|
|
|
+ .time-duration {
|
|
|
+ font-size: 12px;
|
|
|
+ color: #909399;
|
|
|
+ margin-top: 2px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .repeat-count {
|
|
|
+ margin-top: 4px;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 表格行样式
|
|
|
+::v-deep {
|
|
|
+ .urgent-row {
|
|
|
+ background: rgba(245, 108, 108, 0.08) !important;
|
|
|
+ }
|
|
|
+
|
|
|
+ .important-row {
|
|
|
+ background: rgba(230, 162, 60, 0.08) !important;
|
|
|
+ }
|
|
|
+
|
|
|
+ .active-row {
|
|
|
+ background: rgba(64, 158, 255, 0.05) !important;
|
|
|
+ }
|
|
|
+
|
|
|
+ .ended-row {
|
|
|
+ color: #c0c4cc;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 闪烁标签
|
|
|
+.blink {
|
|
|
+ animation: tagBlink 1s infinite;
|
|
|
+}
|
|
|
+
|
|
|
+@keyframes tagBlink {
|
|
|
+ 0%, 100% { opacity: 1; }
|
|
|
+ 50% { opacity: 0.5; }
|
|
|
+}
|
|
|
+
|
|
|
+// 删除按钮
|
|
|
+.delete-btn {
|
|
|
+ color: #f56c6c !important;
|
|
|
+
|
|
|
+ &:hover {
|
|
|
+ color: #f78989 !important;
|
|
|
+ }
|
|
|
+}
|
|
|
+</style>
|