|
|
@@ -1,5 +1,16 @@
|
|
|
<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>
|
|
|
@@ -35,23 +46,45 @@
|
|
|
</template>
|
|
|
<script setup name="AlarmEventList">
|
|
|
import { ref, reactive, watch, onBeforeUnmount } from 'vue'
|
|
|
-import { Search, Refresh } from '@element-plus/icons-vue'
|
|
|
-import { getAlarms, getAlarmOptions } from '@/api/alarm'
|
|
|
+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 { orderStatusType } from '../../pipeNetwork/ops/workOrder/workOrderDict'
|
|
|
import { useAlarmScope } from './utils/useAlarmScope'
|
|
|
-import AlarmDetail from './AlarmDetail.vue'
|
|
|
+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({ ...filters, ...rangeParams(dates.value), ...scope.value, pageNum: pageNum.value, pageSize: pageSize.value, ascending: ascending.value })
|
|
|
+ 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 }
|
|
|
@@ -61,6 +94,7 @@ 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 }
|
|
|
@@ -71,3 +105,35 @@ watch(scopeKey, async () => {
|
|
|
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>
|