list.vue 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139
  1. <template>
  2. <div class="alarm-page">
  3. <section v-loading="statisticsLoading" class="alarm-list-statistics" aria-label="报警统计" :aria-busy="statisticsLoading">
  4. <div v-for="card in statisticsCards" :key="card.key" class="alarm-list-stat-card" :class="`is-${card.tone}`">
  5. <div class="stat-content">
  6. <div class="stat-label">{{ card.label }}</div>
  7. <div class="stat-value">{{ statistics?.[card.key] ?? '—' }}</div>
  8. <div class="stat-caption">{{ card.caption }}</div>
  9. </div>
  10. <el-icon class="stat-icon" aria-hidden="true"><component :is="card.icon" /></el-icon>
  11. </div>
  12. <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>
  13. </section>
  14. <el-card shadow="never" class="alarm-filter-card"><el-form :model="filters" inline class="alarm-filters" @submit.prevent="search">
  15. <el-form-item label="报警单号"><el-input v-model="filters.alarmNo" clearable placeholder="请输入报警单号" @keyup.enter="search" /></el-form-item>
  16. <el-form-item label="设备搜索"><el-input v-model="filters.keyword" clearable placeholder="请输入设备编码/名称" @keyup.enter="search" /></el-form-item>
  17. <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>
  18. <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>
  19. <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>
  20. <el-form-item><el-button type="primary" :icon="Search" @click="search">查询</el-button><el-button :icon="Refresh" @click="reset">重置</el-button></el-form-item>
  21. </el-form></el-card>
  22. <el-card shadow="never"><div class="alarm-toolbar"><h3>报警事件</h3><span class="muted">共 {{ total }} 条</span></div>
  23. <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>
  24. <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">
  25. <el-table-column prop="alarmNo" label="报警单号" width="210" show-overflow-tooltip><template #default="{ row }">{{ row.alarmNo || '—' }}</template></el-table-column>
  26. <el-table-column prop="deviceCode" label="设备编码" width="200" show-overflow-tooltip />
  27. <el-table-column prop="deviceName" label="设备名称" min-width="155" show-overflow-tooltip />
  28. <el-table-column prop="equipmentLocation" label="安装位置" min-width="200" show-overflow-tooltip />
  29. <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>
  30. <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>
  31. <el-table-column prop="createTime" label="报警时间" width="180" sortable="custom" />
  32. <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>
  33. <el-table-column prop="auditStatusName" label="审核结果" width="105" align="center">
  34. <template #default="{ row }"><el-tag effect="light" :type="auditStatusTone(row.auditStatus)">{{ row.auditStatusName || '未知' }}</el-tag></template>
  35. </el-table-column>
  36. <el-table-column prop="orderStatusName" label="处置状态" width="100" align="center">
  37. <template #default="{ row }"><el-tag effect="light" :type="row.workOrderId != null && row.orderStatus != null ? orderStatusType(row.orderStatus) : 'info'">{{ row.orderStatusName || '未转工单' }}</el-tag></template>
  38. </el-table-column>
  39. <el-table-column prop="receiveUserName" label="责任人" width="100"><template #default="{ row }">{{ row.receiveUserName || '未分配' }}</template></el-table-column>
  40. <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>
  41. <template #empty><el-empty :description="error ? '数据加载失败' : '暂无匹配报警'" /></template>
  42. </el-table>
  43. <pagination v-show="total > 0" v-model:page="pageNum" v-model:limit="pageSize" :total="total" @pagination="load" />
  44. </el-card><AlarmDetail ref="detailRef" />
  45. </div>
  46. </template>
  47. <script setup name="AlarmEventList">
  48. import { ref, reactive, watch, onBeforeUnmount } from 'vue'
  49. import { Search, Refresh, Document, Warning, CircleCheck } from '@element-plus/icons-vue'
  50. import { getAlarms, getAlarmOptions, getAlarmStatistics } from '@/api/alarm'
  51. import { alarmDescription, hasActualValue, valueText, thresholdText } from './utils/alarmReviewModel'
  52. import { auditStatusTone, levelTone, rangeParams } from './utils/alarmOperationsModel'
  53. import { orderStatusType } from '../../pipeNetwork/ops/workOrder/workOrderDict'
  54. import { useAlarmScope } from './utils/useAlarmScope'
  55. import AlarmDetail from '../AlarmDetail.vue'
  56. import './styles/alarmOperations.scss'
  57. const { scope, scopeKey, route } = useAlarmScope()
  58. const empty = () => ({ alarmNo: '', keyword: '', alarmLevel: undefined, alarmStatus: undefined })
  59. const filters = reactive(empty()), dates = ref([]), options = ref({}), rows = ref([]), total = ref(0)
  60. const pageNum = ref(1), pageSize = ref(10), ascending = ref(false), loading = ref(false), error = ref(''), detailRef = ref()
  61. const statistics = ref(null), statisticsLoading = ref(false), statisticsError = ref('')
  62. const statisticsCards = [
  63. { key: 'total', label: '报警总数', caption: '当前查询范围 · 按事件计数', tone: 'primary', icon: Document },
  64. { key: 'abnormal', label: '监测异常', caption: '当前查询范围 · 按事件计数', tone: 'danger', icon: Warning },
  65. { key: 'recovered', label: '监测已恢复', caption: '监测恢复不代表处置完成', tone: 'success', icon: CircleCheck }
  66. ]
  67. let generation = 0
  68. async function loadStatistics(params, token) {
  69. statisticsLoading.value = true; statisticsError.value = ''; statistics.value = null
  70. try {
  71. const result = await getAlarmStatistics(params)
  72. if (token !== generation) return
  73. statistics.value = (result.data || []).reduce((counts, row) => ({
  74. total: counts.total + Number(row.total || 0),
  75. abnormal: counts.abnormal + Number(row.abnormal || 0),
  76. recovered: counts.recovered + Number(row.recovered || 0)
  77. }), { total: 0, abnormal: 0, recovered: 0 })
  78. } catch (e) { if (token === generation) statisticsError.value = e.message || '统计加载失败,请重试' }
  79. finally { if (token === generation) statisticsLoading.value = false }
  80. }
  81. async function load() {
  82. const token = ++generation; loading.value = true; error.value = ''
  83. const params = { ...filters, ...rangeParams(dates.value), ...scope.value }
  84. // 统计覆盖全部筛选结果,独立加载,不阻塞列表显示。
  85. loadStatistics(params, token)
  86. try {
  87. const result = await getAlarms({ ...params, pageNum: pageNum.value, pageSize: pageSize.value, ascending: ascending.value })
  88. if (token === generation) { rows.value = result.data.records || []; total.value = result.data.total || 0 }
  89. } catch (e) { if (token === generation) { rows.value = []; total.value = 0; error.value = e.message || '加载失败' } }
  90. finally { if (token === generation) loading.value = false }
  91. }
  92. function search() { pageNum.value = 1; return load() }
  93. function reset() { Object.assign(filters, empty()); dates.value = []; search() }
  94. function sort({ order }) { ascending.value = order === 'ascending'; search() }
  95. watch(scopeKey, async () => {
  96. ++generation; rows.value = []; total.value = 0; loading.value = false
  97. statistics.value = null; statisticsLoading.value = false; statisticsError.value = ''
  98. Object.assign(filters, empty()); dates.value = []; options.value = {}
  99. const key = scopeKey.value
  100. try { const result = await getAlarmOptions({ ...scope.value }); if (key === scopeKey.value) options.value = result.data || {} } catch (e) { error.value = e.message }
  101. if (key !== scopeKey.value) return
  102. await search()
  103. if (route.query.alarmId) detailRef.value?.open(route.query.alarmId)
  104. }, { immediate: true })
  105. onBeforeUnmount(() => { ++generation })
  106. watch(() => route.query.alarmId, id => { if (id) detailRef.value?.open(id) })
  107. </script>
  108. <style scoped lang="scss">
  109. .alarm-list-statistics {
  110. display: grid;
  111. grid-template-columns: repeat(3, minmax(0, 1fr));
  112. gap: 16px;
  113. margin-bottom: 20px;
  114. }
  115. .alarm-list-stat-card {
  116. --stat-color: var(--el-color-primary);
  117. display: flex;
  118. align-items: center;
  119. justify-content: space-between;
  120. gap: 16px;
  121. padding: 22px 24px;
  122. background: #fff;
  123. border: 1px solid var(--el-border-color-light);
  124. border-radius: 8px;
  125. &.is-danger { --stat-color: var(--el-color-danger); }
  126. &.is-success { --stat-color: var(--el-color-success); }
  127. }
  128. .stat-label { color: #303133; font-size: 14px; line-height: 22px; }
  129. .stat-value { margin: 4px 0; color: var(--stat-color); font-size: 30px; font-weight: 600; line-height: 36px; font-variant-numeric: tabular-nums; }
  130. .stat-caption { color: #909399; font-size: 12px; line-height: 20px; }
  131. .stat-icon { flex-shrink: 0; color: var(--stat-color); font-size: 26px; }
  132. .statistics-error { grid-column: 1 / -1; }
  133. @media (max-width: 768px) {
  134. .alarm-list-statistics { grid-template-columns: 1fr; gap: 12px; }
  135. .alarm-list-stat-card { padding: 16px 20px; }
  136. }
  137. </style>