|
|
@@ -192,44 +192,59 @@
|
|
|
:loading="loading"
|
|
|
>
|
|
|
<template #actions>
|
|
|
- <el-input v-model="alarmSearch" class="search-input" placeholder="搜索设备/报警类型" clearable
|
|
|
+ <el-input v-model="query.keyword" class="search-input" placeholder="搜索设备/报警类型/位置" clearable
|
|
|
:prefix-icon="Search"/>
|
|
|
<el-button :icon="Refresh" @click="loadData">刷新</el-button>
|
|
|
+ <el-button :icon="Aim" @click="fitAlarmMap">全图</el-button>
|
|
|
</template>
|
|
|
<template #metrics>
|
|
|
<div class="metric">
|
|
|
<el-icon class="metric-icon"><Bell/></el-icon>
|
|
|
- <span>报警总数</span><strong>{{ mapAlarmCount }}</strong><small>条</small>
|
|
|
+ <span>报警点位</span><strong>{{ mapStatistics.total }}</strong><small>个</small>
|
|
|
</div>
|
|
|
<div class="metric">
|
|
|
<el-icon class="metric-icon"><Warning/></el-icon>
|
|
|
- <span>高等级报警</span><strong class="danger">{{ mapCriticalCount }}</strong><small>条</small>
|
|
|
+ <span>高等级报警</span><strong class="danger">{{ mapStatistics.highLevel }}</strong><small>个</small>
|
|
|
</div>
|
|
|
<div class="metric">
|
|
|
- <el-icon class="metric-icon"><CircleCheck/></el-icon>
|
|
|
- <span>已处理报警</span><strong>{{ mapResolvedCount }}</strong><small>条</small>
|
|
|
+ <el-icon class="metric-icon"><Tickets/></el-icon>
|
|
|
+ <span>有关联工单</span><strong class="primary">{{ mapStatistics.byStatus.workOrder }}</strong><small>个</small>
|
|
|
</div>
|
|
|
<div class="metric">
|
|
|
<el-icon class="metric-icon"><Location/></el-icon>
|
|
|
- <span>地图点位</span><strong>{{ mappedCount }}</strong><small>个</small>
|
|
|
+ <span>离线点位</span><strong>{{ mapStatistics.byStatus.offline }}</strong><small>个</small>
|
|
|
</div>
|
|
|
</template>
|
|
|
<template #controls>
|
|
|
<div class="panel-section alarm-detail">
|
|
|
<h3>选中报警</h3>
|
|
|
<template v-if="selectedPoint.id || selectedPoint.alarmId">
|
|
|
- <div class="detail-name">{{ selectedPoint.deviceCode || selectedPoint.name || '报警点' }}</div>
|
|
|
+ <div class="detail-name">{{ selectedPoint.deviceName || selectedPoint.deviceCode || selectedPoint.name || '报警点' }}</div>
|
|
|
<el-descriptions :column="1" border size="small">
|
|
|
<el-descriptions-item label="报警等级">
|
|
|
<el-tag :type="levelType(selectedPoint.alarmLevel)">{{ selectedPoint.alarmLevelText || levelText(selectedPoint.alarmLevel) }}</el-tag>
|
|
|
</el-descriptions-item>
|
|
|
<el-descriptions-item label="报警类型">{{ selectedPoint.warningType || selectedPoint.alarmType || '-' }}</el-descriptions-item>
|
|
|
<el-descriptions-item label="触发时间">{{ selectedPoint.alarmTime || selectedPoint.createTime || '-' }}</el-descriptions-item>
|
|
|
- <el-descriptions-item label="状态">
|
|
|
- <el-tag :type="Number(selectedPoint.alarmStatus) === 1 ? 'success' : 'danger'">
|
|
|
- {{ Number(selectedPoint.alarmStatus) === 1 ? '已处理' : '未处理' }}
|
|
|
+ <el-descriptions-item label="点位状态">
|
|
|
+ <el-tag :type="statusTagType(selectedPoint)">{{ statusText(selectedPoint) }}</el-tag>
|
|
|
+ </el-descriptions-item>
|
|
|
+ <el-descriptions-item label="设备报警状态">
|
|
|
+ <el-tag :type="selectedPoint.deviceAlarmStatus === undefined ? 'info' : Number(selectedPoint.deviceAlarmStatus) === 1 ? 'danger' : 'success'">
|
|
|
+ {{ selectedPoint.deviceAlarmStatusText || '未获取' }}
|
|
|
+ </el-tag>
|
|
|
+ </el-descriptions-item>
|
|
|
+ <el-descriptions-item label="报警记录状态">
|
|
|
+ <el-tag :type="selectedPoint.recordStatus === undefined ? 'info' : Number(selectedPoint.recordStatus) === 1 ? 'success' : 'danger'">
|
|
|
+ {{ selectedPoint.recordStatusText || '未获取' }}
|
|
|
</el-tag>
|
|
|
</el-descriptions-item>
|
|
|
+ <el-descriptions-item label="关联工单">
|
|
|
+ <span>{{ selectedPoint.orderNo || '-' }}</span>
|
|
|
+ <span v-if="selectedPoint.orderStatus !== null && selectedPoint.orderStatus !== undefined">
|
|
|
+ · {{ orderStatusText(selectedPoint.orderStatus) }}
|
|
|
+ </span>
|
|
|
+ </el-descriptions-item>
|
|
|
<el-descriptions-item label="位置">{{ selectedPoint.equipmentLocation || selectedPoint.location || '-' }}</el-descriptions-item>
|
|
|
</el-descriptions>
|
|
|
<el-button class="detail-action" type="primary" link @click="openDetail(selectedPoint)">查看完整详情</el-button>
|
|
|
@@ -239,28 +254,34 @@
|
|
|
</template>
|
|
|
<template #legend>
|
|
|
<div class="panel-section">
|
|
|
- <h3>报警图层与图例</h3>
|
|
|
+ <h3>点位状态与图例</h3>
|
|
|
<div class="map-legend">
|
|
|
- <div class="map-legend-item"><i class="map-legend-dot status-dot-normal"/>正常</div>
|
|
|
- <div class="map-legend-item"><i class="map-legend-dot status-dot-alarm"/>报警</div>
|
|
|
- <div class="map-legend-item"><i class="map-legend-dot status-dot-warning"/>预警</div>
|
|
|
- <div class="map-legend-item"><i class="map-legend-dot status-dot-work-order"/>关联工单</div>
|
|
|
- <div class="map-legend-item"><i class="map-legend-dot status-dot-offline"/>离线</div>
|
|
|
+ <div class="map-legend-item"><i class="map-legend-dot status-dot-work-order"/>关联工单 {{ mapStatistics.byStatus.workOrder }}</div>
|
|
|
+ <div class="map-legend-item"><i class="map-legend-dot status-dot-alarm"/>报警 {{ mapStatistics.byStatus.alarm }}</div>
|
|
|
+ <div class="map-legend-item"><i class="map-legend-dot status-dot-warning"/>预警 {{ mapStatistics.byStatus.warning }}</div>
|
|
|
+ <div class="map-legend-item"><i class="map-legend-dot status-dot-offline"/>离线 {{ mapStatistics.byStatus.offline }}</div>
|
|
|
+ <div class="map-legend-item"><i class="map-legend-dot status-dot-normal"/>正常 {{ mapStatistics.byStatus.normal }}</div>
|
|
|
</div>
|
|
|
+ <p class="muted legend-note">同一设备多点位合并,按关联工单 > 报警 > 预警 > 离线 > 正常取最高风险状态。</p>
|
|
|
</div>
|
|
|
</template>
|
|
|
<template #results>
|
|
|
<div class="panel-section result-section">
|
|
|
<h3>搜索报警</h3>
|
|
|
- <el-alert v-if="mapError" type="warning" show-icon :closable="false" :title="mapError"/>
|
|
|
- <el-empty v-else-if="!filteredAlarmPoints.length" description="暂无报警点位" :image-size="72"/>
|
|
|
- <div v-else class="device-result-list">
|
|
|
+ <el-alert v-if="mapLoadError" type="error" show-icon :closable="false" :title="mapLoadError">
|
|
|
+ <el-button link type="primary" @click="loadData">重试</el-button>
|
|
|
+ </el-alert>
|
|
|
+ <el-alert v-if="mapEnrichmentWarning" type="warning" show-icon :closable="false"
|
|
|
+ :title="`${mapEnrichmentWarning}未能补全,点位状态按报警记录展示`"/>
|
|
|
+ <el-empty v-if="!mapLoadError && !mapPoints.length" description="暂无报警点位" :image-size="72"/>
|
|
|
+ <el-empty v-if="!mapLoadError && mapPoints.length && !filteredAlarmPoints.length" description="当前筛选下没有匹配的报警点位" :image-size="72"/>
|
|
|
+ <div v-if="!mapLoadError && filteredAlarmPoints.length" class="device-result-list">
|
|
|
<button v-for="point in filteredAlarmPoints" :key="point.id || point.alarmId || point.deviceCode"
|
|
|
type="button" @click="locateAlarmPoint(point)">
|
|
|
<span class="device-name">{{ point.deviceName || point.deviceCode || point.name || '报警点' }}</span>
|
|
|
<span class="device-meta">{{ point.warningType || point.alarmType || '监测报警' }} · {{ point.alarmTime || point.createTime || '-' }}</span>
|
|
|
- <el-tag size="small" :type="levelType(point.alarmLevel)">
|
|
|
- {{ point.alarmLevelText || levelText(point.alarmLevel) }}
|
|
|
+ <el-tag size="small" :type="statusTagType(point)">
|
|
|
+ {{ statusText(point) }}
|
|
|
</el-tag>
|
|
|
</button>
|
|
|
</div>
|
|
|
@@ -273,12 +294,16 @@
|
|
|
cluster-label="报警集合"
|
|
|
:center="{ lng: 110.3965, lat: 28.4638 }"
|
|
|
:zoom="15"
|
|
|
- @point-click="openDetail"
|
|
|
+ @ready="handleAlarmMapReady"
|
|
|
+ @point-click="locateAlarmPoint"
|
|
|
@map-click="detailVisible = false"
|
|
|
@error="mapError = $event"
|
|
|
/>
|
|
|
<el-alert v-if="mapError" class="map-error" type="warning" show-icon :closable="false" :title="mapError"/>
|
|
|
- <el-empty v-if="!gisMapPoints.length && !mapError" class="map-empty" description="当前筛选下没有可显示的报警点位"/>
|
|
|
+ <el-alert v-if="mapLoadError" class="map-error" type="error" show-icon :closable="false" :title="mapLoadError">
|
|
|
+ <el-button link type="primary" @click="loadData">重试</el-button>
|
|
|
+ </el-alert>
|
|
|
+ <el-empty v-else-if="!gisMapPoints.length && !mapError" class="map-empty" description="当前筛选下没有可显示的报警点位"/>
|
|
|
</template>
|
|
|
</WaterGisPageShell>
|
|
|
</el-card>
|
|
|
@@ -354,7 +379,7 @@
|
|
|
import {useRoute, useRouter} from 'vue-router'
|
|
|
import {ElMessage} from 'element-plus'
|
|
|
import {
|
|
|
- Bell, CircleCheck, Clock, DataAnalysis, Download, Finished, MapLocation, PieChart,
|
|
|
+ Aim, Bell, CircleCheck, Clock, DataAnalysis, Download, Finished, MapLocation, PieChart,
|
|
|
Refresh, Search, Tickets, TrendCharts, Warning
|
|
|
} from '@element-plus/icons-vue'
|
|
|
import {saveAs} from 'file-saver'
|
|
|
@@ -365,6 +390,7 @@
|
|
|
getAlarmDetail,
|
|
|
getAlarmFlow,
|
|
|
getAlarmLedgerDashboard,
|
|
|
+ getDeviceGisAllPoints,
|
|
|
listAlarmAudits,
|
|
|
listAlarmHistory,
|
|
|
listAlarmLedger,
|
|
|
@@ -372,7 +398,17 @@
|
|
|
} from '@/api/pipeNetwork/waterSupply'
|
|
|
import GisMapCanvas from '@/components/GisMapCanvas.vue'
|
|
|
import WaterGisPageShell from '@/components/WaterGisPageShell.vue'
|
|
|
- import { getMonitoringPointColor, getMonitoringPointIcon } from '@/utils/gisMapColors'
|
|
|
+ import {
|
|
|
+ getMonitoringPointIcon,
|
|
|
+ getMonitoringPointLabel,
|
|
|
+ getMonitoringPointTagType
|
|
|
+ } from '@/utils/gisMapColors'
|
|
|
+ import {
|
|
|
+ buildAlarmGisPoints,
|
|
|
+ filterAlarmGisPoints,
|
|
|
+ summarizeAlarmGisPoints
|
|
|
+ } from '@/utils/waterAlarmGisModel'
|
|
|
+ import { normalizeDeviceGisPoints, normalizeDeviceRow } from '@/utils/waterSupplyUiModel'
|
|
|
import {normalizeMapPoints, normalizePage, readMonitoringExportError, unwrapResponse} from '@/utils/waterSupplyModel'
|
|
|
import {getRequestErrorMessage} from '@/utils/waterFacilityQuery'
|
|
|
import {orderStatusText} from '@/views/subSystem/pipeNetwork/ops/workOrder/workOrderDict.js'
|
|
|
@@ -438,7 +474,10 @@
|
|
|
let devicePie;
|
|
|
const mapPoints = ref([]);
|
|
|
const mapError = ref('')
|
|
|
- const alarmSearch = ref('')
|
|
|
+ // 报警一张图的数据加载失败与状态补全降级分别提示,避免地图空白时用户无从判断
|
|
|
+ const mapLoadError = ref('')
|
|
|
+ const mapEnrichmentWarning = ref('')
|
|
|
+ const mapReady = ref(false)
|
|
|
const gisMapRef = ref(null)
|
|
|
const loading = ref(false);
|
|
|
const detailVisible = ref(false);
|
|
|
@@ -457,27 +496,21 @@
|
|
|
{label: '已转工单', value: 2},
|
|
|
{label: '已解除', value: 3}
|
|
|
]
|
|
|
- const filteredAlarmPoints = computed(() => {
|
|
|
- const keyword = alarmSearch.value.trim().toLocaleLowerCase();
|
|
|
- if (!keyword) return mapPoints.value;
|
|
|
- return mapPoints.value.filter(point => [
|
|
|
- point.deviceName, point.deviceCode, point.name, point.warningType, point.alarmType
|
|
|
- ].some(value => String(value || '').toLocaleLowerCase().includes(keyword)));
|
|
|
- });
|
|
|
+ const filteredAlarmPoints = computed(() => filterAlarmGisPoints(
|
|
|
+ mapPoints.value,
|
|
|
+ query.value.keyword
|
|
|
+ ));
|
|
|
|
|
|
const gisMapPoints = computed(() => filteredAlarmPoints.value.map(point => ({
|
|
|
...point,
|
|
|
- lng: point._longitude ?? point.longitude,
|
|
|
- lat: point._latitude ?? point.latitude,
|
|
|
- name: point.deviceCode || point.name || '报警点',
|
|
|
- color: getMonitoringPointColor(point),
|
|
|
+ name: point.deviceName || point.deviceCode || point.name || '报警点',
|
|
|
iconUrl: getMonitoringPointIcon(point)
|
|
|
})))
|
|
|
|
|
|
- const mapAlarmCount = computed(() => mapPoints.value.length);
|
|
|
- const mapCriticalCount = computed(() => mapPoints.value.filter(point => Number(point.alarmLevel) <= 2).length);
|
|
|
- const mapResolvedCount = computed(() => mapPoints.value.filter(point => Number(point.alarmStatus) === 1).length);
|
|
|
- const mappedCount = computed(() => mapPoints.value.filter(point => Number.isFinite(Number(point.mapX)) && Number.isFinite(Number(point.mapY))).length)
|
|
|
+ /** 左侧状态总览:五态数量来自点位推导结果,与地图着色使用同一口径 */
|
|
|
+ const mapStatistics = computed(() => summarizeAlarmGisPoints(filteredAlarmPoints.value));
|
|
|
+ const statusText = point => getMonitoringPointLabel(point);
|
|
|
+ const statusTagType = point => getMonitoringPointTagType(point);
|
|
|
/**
|
|
|
* 详情抽屉字段:逐条报警流水,标签统一中文;
|
|
|
* 工单状态复用统一工单字典,避免页面自带一套状态码文本。
|
|
|
@@ -496,8 +529,10 @@
|
|
|
['阈值区间', thresholdRange(row)],
|
|
|
['偏差率', deviationText(row)],
|
|
|
['报警等级', row.alarmLevelText || levelText(row.alarmLevel)],
|
|
|
- ['报警记录状态', row.alarmStatusText],
|
|
|
+ ['报警记录状态', row.recordStatusText || row.alarmStatusText],
|
|
|
['设备报警状态', row.deviceAlarmStatusText],
|
|
|
+ ['设备当前状态', row.currentStatusText],
|
|
|
+ ['在线状态', row.onlineStatusText],
|
|
|
['报警展示状态', row.displayStatusText],
|
|
|
['关联工单号', row.orderNo],
|
|
|
['工单状态', row.orderStatus === null || row.orderStatus === undefined ? '' : orderStatusText(row.orderStatus)],
|
|
|
@@ -580,6 +615,8 @@
|
|
|
|
|
|
async function loadData() {
|
|
|
loading.value = true
|
|
|
+ mapLoadError.value = ''
|
|
|
+ mapEnrichmentWarning.value = ''
|
|
|
try {
|
|
|
if (props.mode === 'manage') {
|
|
|
const [summary, ledger] = await Promise.all([
|
|
|
@@ -591,11 +628,15 @@
|
|
|
} else if (props.mode === 'audit') {
|
|
|
page.value = normalizePage(await listAlarmHistory({...query.value, alarmStatus: 0}))
|
|
|
} else {
|
|
|
- mapPoints.value = normalizeMapPoints(unwrapResponse(await listAlarmMapPoints()) || [])
|
|
|
+ await loadAlarmMapData()
|
|
|
}
|
|
|
} catch (error) {
|
|
|
- page.value = {records: [], total: 0}
|
|
|
- mapPoints.value = []
|
|
|
+ if (props.mode === 'map') {
|
|
|
+ mapLoadError.value = getRequestErrorMessage(error, '报警点位加载失败,请重试')
|
|
|
+ mapPoints.value = []
|
|
|
+ } else {
|
|
|
+ page.value = {records: [], total: 0}
|
|
|
+ }
|
|
|
console.warn(`[${props.title}] 数据加载失败`, error)
|
|
|
ElMessage.error(getRequestErrorMessage(error, '数据加载失败,请检查服务状态'))
|
|
|
} finally {
|
|
|
@@ -605,6 +646,66 @@
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+ /** 报警一张图台账补全条数:与列表页共用同一台账接口,避免同一状态出现两套口径 */
|
|
|
+ const ALARM_MAP_LEDGER_PAGE_SIZE = 200
|
|
|
+ const ALARM_MAP_LEDGER_MAX_PAGES = 50
|
|
|
+
|
|
|
+ async function loadAllAlarmLedgerRows() {
|
|
|
+ const collected = []
|
|
|
+ let total = 0
|
|
|
+ for (let pageNum = 1; pageNum <= ALARM_MAP_LEDGER_MAX_PAGES; pageNum += 1) {
|
|
|
+ const response = await listAlarmLedger({ pageNum, pageSize: ALARM_MAP_LEDGER_PAGE_SIZE })
|
|
|
+ const ledgerPage = normalizePage(response)
|
|
|
+ collected.push(...ledgerPage.records)
|
|
|
+ total = ledgerPage.total
|
|
|
+ if (!ledgerPage.records.length || collected.length >= ledgerPage.total) {
|
|
|
+ if (collected.length < total) throw new Error('报警台账分页未完整返回')
|
|
|
+ return collected
|
|
|
+ }
|
|
|
+ }
|
|
|
+ throw new Error('报警台账分页超过安全上限')
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 报警点位取自专用点位接口,再用报警台账补全设备/记录/工单状态。
|
|
|
+ * 台账或设备点位不可用时只降级对应状态来源,不影响报警点位本身渲染。
|
|
|
+ */
|
|
|
+ async function loadAlarmMapData() {
|
|
|
+ const [pointsResult, ledgerResult, deviceResult] = await Promise.allSettled([
|
|
|
+ listAlarmMapPoints(),
|
|
|
+ loadAllAlarmLedgerRows(),
|
|
|
+ getDeviceGisAllPoints()
|
|
|
+ ])
|
|
|
+ if (pointsResult.status === 'rejected') throw pointsResult.reason
|
|
|
+
|
|
|
+ const alarmPoints = normalizeMapPoints(unwrapResponse(pointsResult.value) || [])
|
|
|
+ const ledgerRows = ledgerResult.status === 'fulfilled' ? ledgerResult.value : []
|
|
|
+ const devicePoints = deviceResult.status === 'fulfilled'
|
|
|
+ ? normalizeDeviceGisPoints(deviceResult.value).map(normalizeDeviceRow)
|
|
|
+ : []
|
|
|
+ mapPoints.value = buildAlarmGisPoints({alarmPoints, ledgerRows, devicePoints})
|
|
|
+
|
|
|
+ const missingSources = [
|
|
|
+ ledgerResult.status === 'rejected' ? '关联工单状态' : '',
|
|
|
+ deviceResult.status === 'rejected' ? '设备在线与运行状态' : ''
|
|
|
+ ].filter(Boolean)
|
|
|
+ mapEnrichmentWarning.value = missingSources.join('、')
|
|
|
+ if (missingSources.length) {
|
|
|
+ console.warn(`[${props.title}] 状态补全降级:${mapEnrichmentWarning.value}`)
|
|
|
+ }
|
|
|
+ await nextTick()
|
|
|
+ fitAlarmMap()
|
|
|
+ }
|
|
|
+
|
|
|
+ function handleAlarmMapReady() {
|
|
|
+ mapReady.value = true
|
|
|
+ if (mapPoints.value.length) nextTick(fitAlarmMap)
|
|
|
+ }
|
|
|
+
|
|
|
+ function fitAlarmMap() {
|
|
|
+ if (mapReady.value) gisMapRef.value?.fitBounds()
|
|
|
+ }
|
|
|
+
|
|
|
const resetQuery = () => {
|
|
|
query.value = {
|
|
|
pageNum: 1,
|
|
|
@@ -619,6 +720,8 @@
|
|
|
}
|
|
|
timeRange.value = []
|
|
|
pendingAlarmId.value = ''
|
|
|
+ mapLoadError.value = ''
|
|
|
+ mapEnrichmentWarning.value = ''
|
|
|
loadData()
|
|
|
}
|
|
|
|
|
|
@@ -785,7 +888,8 @@
|
|
|
props.mode === 'audit' ? listAlarmAudits(id) : Promise.resolve(null),
|
|
|
props.mode === 'audit' ? getAlarmFlow(id) : Promise.resolve(null)
|
|
|
])
|
|
|
- detail.value = unwrapResponse(detailResponse) || row
|
|
|
+ // 台账详情不返回设备在线/运行状态,合并保留点位侧已补全的状态,避免抽屉回退成单一状态
|
|
|
+ detail.value = {...row, ...(unwrapResponse(detailResponse) || {})}
|
|
|
audits.value = unwrapResponse(auditResponse) || []
|
|
|
flow.value = unwrapResponse(flowResponse) || []
|
|
|
} catch (error) {
|
|
|
@@ -795,8 +899,8 @@
|
|
|
|
|
|
function alarmPointFocus(point) {
|
|
|
return {
|
|
|
- lng: point._longitude ?? point.longitude,
|
|
|
- lat: point._latitude ?? point.latitude
|
|
|
+ lng: Number(point.lng ?? point.longitude),
|
|
|
+ lat: Number(point.lat ?? point.latitude)
|
|
|
};
|
|
|
}
|
|
|
|