| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139 |
- <template>
- <div class="alarm-page">
- <section v-loading="statisticsLoading" class="alarm-list-statistics" aria-label="报警统计" :aria-busy="statisticsLoading">
- <div v-for="card in statisticsCards" :key="card.key" class="alarm-list-stat-card" :class="`is-${card.tone}`">
- <div class="stat-content">
- <div class="stat-label">{{ card.label }}</div>
- <div class="stat-value">{{ statistics?.[card.key] ?? '—' }}</div>
- <div class="stat-caption">{{ card.caption }}</div>
- </div>
- <el-icon class="stat-icon" aria-hidden="true"><component :is="card.icon" /></el-icon>
- </div>
- <el-alert v-if="statisticsError" class="statistics-error" :title="statisticsError" type="error" show-icon :closable="false"><el-button link type="primary" @click="load">重新加载</el-button></el-alert>
- </section>
- <el-card shadow="never" class="alarm-filter-card"><el-form :model="filters" inline class="alarm-filters" @submit.prevent="search">
- <el-form-item label="报警单号"><el-input v-model="filters.alarmNo" clearable placeholder="请输入报警单号" @keyup.enter="search" /></el-form-item>
- <el-form-item label="设备搜索"><el-input v-model="filters.keyword" clearable placeholder="请输入设备编码/名称" @keyup.enter="search" /></el-form-item>
- <el-form-item label="报警等级"><el-select v-model="filters.alarmLevel" clearable placeholder="全部等级"><el-option v-for="v in options.alarmLevels" :key="v.value" :label="v.label" :value="v.value" /></el-select></el-form-item>
- <el-form-item label="监测状态"><el-select v-model="filters.alarmStatus" clearable placeholder="全部状态"><el-option v-for="v in options.monitorStatuses" :key="v.value" :label="v.label" :value="v.value" /></el-select></el-form-item>
- <el-form-item label="报警时间"><el-date-picker v-model="dates" type="daterange" value-format="YYYY-MM-DD" start-placeholder="开始日期" end-placeholder="结束日期" range-separator="至" /></el-form-item>
- <el-form-item><el-button type="primary" :icon="Search" @click="search">查询</el-button><el-button :icon="Refresh" @click="reset">重置</el-button></el-form-item>
- </el-form></el-card>
- <el-card shadow="never"><div class="alarm-toolbar"><h3>报警事件</h3><span class="muted">共 {{ total }} 条</span></div>
- <el-alert v-if="error" :title="error" type="error" show-icon :closable="false" class="load-error"><el-button link @click="load">重新加载</el-button></el-alert>
- <el-table v-loading="loading" :data="rows" border stripe row-key="id" :header-cell-style="{ background: '#f5f7fa', color: '#606266' }" :default-sort="{ prop: 'createTime', order: 'descending' }" @sort-change="sort">
- <el-table-column prop="alarmNo" label="报警单号" width="210" show-overflow-tooltip><template #default="{ row }">{{ row.alarmNo || '—' }}</template></el-table-column>
- <el-table-column prop="deviceCode" label="设备编码" width="200" show-overflow-tooltip />
- <el-table-column prop="deviceName" label="设备名称" min-width="155" show-overflow-tooltip />
- <el-table-column prop="equipmentLocation" label="安装位置" min-width="200" show-overflow-tooltip />
- <el-table-column label="报警描述" min-width="270"><template #default="{ row }"><div>{{ alarmDescription(row) }}</div><div v-if="hasActualValue(row)" class="muted">实际值:{{ valueText(row.actualValue, row.warningUnit) }};阈值:{{ thresholdText(row) }}</div></template></el-table-column>
- <el-table-column label="报警等级" width="105" align="center"><template #default="{ row }"><el-tag :type="levelTone(row.alarmLevel)">{{ row.alarmLevelName }}</el-tag></template></el-table-column>
- <el-table-column prop="createTime" label="报警时间" width="180" sortable="custom" />
- <el-table-column label="监测状态" width="100" align="center"><template #default="{ row }"><el-tag :type="row.alarmStatus === 0 ? 'danger' : 'success'">{{ row.monitorStatusName }}</el-tag></template></el-table-column>
- <el-table-column prop="auditStatusName" label="审核结果" width="105" align="center">
- <template #default="{ row }"><el-tag effect="light" :type="auditStatusTone(row.auditStatus)">{{ row.auditStatusName || '未知' }}</el-tag></template>
- </el-table-column>
- <el-table-column prop="orderStatusName" label="处置状态" width="100" align="center">
- <template #default="{ row }"><el-tag effect="light" :type="row.workOrderId != null && row.orderStatus != null ? orderStatusType(row.orderStatus) : 'info'">{{ row.orderStatusName || '未转工单' }}</el-tag></template>
- </el-table-column>
- <el-table-column prop="receiveUserName" label="责任人" width="100"><template #default="{ row }">{{ row.receiveUserName || '未分配' }}</template></el-table-column>
- <el-table-column label="操作" width="80" fixed="right" align="center"><template #default="{ row }"><el-button link type="primary" @click="detailRef.open(row.id)">详情</el-button></template></el-table-column>
- <template #empty><el-empty :description="error ? '数据加载失败' : '暂无匹配报警'" /></template>
- </el-table>
- <pagination v-show="total > 0" v-model:page="pageNum" v-model:limit="pageSize" :total="total" @pagination="load" />
- </el-card><AlarmDetail ref="detailRef" />
- </div>
- </template>
- <script setup name="AlarmEventList">
- import { ref, reactive, watch, onBeforeUnmount } from 'vue'
- import { Search, Refresh, Document, Warning, CircleCheck } from '@element-plus/icons-vue'
- import { getAlarms, getAlarmOptions, getAlarmStatistics } from '@/api/alarm'
- import { alarmDescription, hasActualValue, valueText, thresholdText } from './utils/alarmReviewModel'
- import { auditStatusTone, levelTone, rangeParams } from './utils/alarmOperationsModel'
- import { orderStatusType } from '../../pipeNetwork/ops/workOrder/workOrderDict'
- import { useAlarmScope } from './utils/useAlarmScope'
- import AlarmDetail from '../AlarmDetail.vue'
- import './styles/alarmOperations.scss'
- const { scope, scopeKey, route } = useAlarmScope()
- const empty = () => ({ alarmNo: '', keyword: '', alarmLevel: undefined, alarmStatus: undefined })
- const filters = reactive(empty()), dates = ref([]), options = ref({}), rows = ref([]), total = ref(0)
- const pageNum = ref(1), pageSize = ref(10), ascending = ref(false), loading = ref(false), error = ref(''), detailRef = ref()
- const statistics = ref(null), statisticsLoading = ref(false), statisticsError = ref('')
- const statisticsCards = [
- { key: 'total', label: '报警总数', caption: '当前查询范围 · 按事件计数', tone: 'primary', icon: Document },
- { key: 'abnormal', label: '监测异常', caption: '当前查询范围 · 按事件计数', tone: 'danger', icon: Warning },
- { key: 'recovered', label: '监测已恢复', caption: '监测恢复不代表处置完成', tone: 'success', icon: CircleCheck }
- ]
- let generation = 0
- async function loadStatistics(params, token) {
- statisticsLoading.value = true; statisticsError.value = ''; statistics.value = null
- try {
- const result = await getAlarmStatistics(params)
- if (token !== generation) return
- statistics.value = (result.data || []).reduce((counts, row) => ({
- total: counts.total + Number(row.total || 0),
- abnormal: counts.abnormal + Number(row.abnormal || 0),
- recovered: counts.recovered + Number(row.recovered || 0)
- }), { total: 0, abnormal: 0, recovered: 0 })
- } catch (e) { if (token === generation) statisticsError.value = e.message || '统计加载失败,请重试' }
- finally { if (token === generation) statisticsLoading.value = false }
- }
- async function load() {
- const token = ++generation; loading.value = true; error.value = ''
- const params = { ...filters, ...rangeParams(dates.value), ...scope.value }
- // 统计覆盖全部筛选结果,独立加载,不阻塞列表显示。
- loadStatistics(params, token)
- try {
- const result = await getAlarms({ ...params, pageNum: pageNum.value, pageSize: pageSize.value, ascending: ascending.value })
- if (token === generation) { rows.value = result.data.records || []; total.value = result.data.total || 0 }
- } catch (e) { if (token === generation) { rows.value = []; total.value = 0; error.value = e.message || '加载失败' } }
- finally { if (token === generation) loading.value = false }
- }
- function search() { pageNum.value = 1; return load() }
- function reset() { Object.assign(filters, empty()); dates.value = []; search() }
- function sort({ order }) { ascending.value = order === 'ascending'; search() }
- watch(scopeKey, async () => {
- ++generation; rows.value = []; total.value = 0; loading.value = false
- statistics.value = null; statisticsLoading.value = false; statisticsError.value = ''
- Object.assign(filters, empty()); dates.value = []; options.value = {}
- const key = scopeKey.value
- try { const result = await getAlarmOptions({ ...scope.value }); if (key === scopeKey.value) options.value = result.data || {} } catch (e) { error.value = e.message }
- if (key !== scopeKey.value) return
- await search()
- if (route.query.alarmId) detailRef.value?.open(route.query.alarmId)
- }, { immediate: true })
- onBeforeUnmount(() => { ++generation })
- watch(() => route.query.alarmId, id => { if (id) detailRef.value?.open(id) })
- </script>
- <style scoped lang="scss">
- .alarm-list-statistics {
- display: grid;
- grid-template-columns: repeat(3, minmax(0, 1fr));
- gap: 16px;
- margin-bottom: 20px;
- }
- .alarm-list-stat-card {
- --stat-color: var(--el-color-primary);
- display: flex;
- align-items: center;
- justify-content: space-between;
- gap: 16px;
- padding: 22px 24px;
- background: #fff;
- border: 1px solid var(--el-border-color-light);
- border-radius: 8px;
- &.is-danger { --stat-color: var(--el-color-danger); }
- &.is-success { --stat-color: var(--el-color-success); }
- }
- .stat-label { color: #303133; font-size: 14px; line-height: 22px; }
- .stat-value { margin: 4px 0; color: var(--stat-color); font-size: 30px; font-weight: 600; line-height: 36px; font-variant-numeric: tabular-nums; }
- .stat-caption { color: #909399; font-size: 12px; line-height: 20px; }
- .stat-icon { flex-shrink: 0; color: var(--stat-color); font-size: 26px; }
- .statistics-error { grid-column: 1 / -1; }
- @media (max-width: 768px) {
- .alarm-list-statistics { grid-template-columns: 1fr; gap: 12px; }
- .alarm-list-stat-card { padding: 16px 20px; }
- }
- </style>
|