|
@@ -1,179 +0,0 @@
|
|
|
-<template>
|
|
|
|
|
- <div class="alarm-page alarm-gis-page">
|
|
|
|
|
- <header class="gis-page-heading">
|
|
|
|
|
- <div><h1>当前报警 GIS 一张图</h1><p>按设备查看报警分布,关联监测曲线与报警记录</p></div>
|
|
|
|
|
- <span class="gis-view-label"><el-icon><MapLocation /></el-icon>{{ viewLabel }}</span>
|
|
|
|
|
- </header>
|
|
|
|
|
-
|
|
|
|
|
- <section class="gis-filter-card">
|
|
|
|
|
- <el-form class="gis-filters" label-position="left" label-width="72px" @submit.prevent="load">
|
|
|
|
|
- <el-form-item label="设备搜索" class="gis-search-field"><el-input v-model="keyword" clearable :prefix-icon="Search" placeholder="请输入设备编码/名称" @keyup.enter="load" /></el-form-item>
|
|
|
|
|
- <el-form-item label="报警等级"><el-select v-model="level" clearable placeholder="全部等级"><el-option v-for="v in options.alarmLevels || []" :key="v.value" :value="v.value" :label="v.label" /></el-select></el-form-item>
|
|
|
|
|
- <div class="gis-filter-actions"><el-button type="primary" :icon="Search" :loading="loading" @click="load">查询</el-button><el-button :icon="Refresh" @click="reset">重置</el-button></div>
|
|
|
|
|
- </el-form>
|
|
|
|
|
- </section>
|
|
|
|
|
-
|
|
|
|
|
- <div class="gis-level-metrics" aria-label="当前查询结果的设备等级统计" :aria-busy="loading">
|
|
|
|
|
- <article v-for="item in levelMetrics" :key="item.value" class="gis-level-metric" :style="levelStyle(item.value)">
|
|
|
|
|
- <span class="gis-grade-badge">{{ item.value }}</span>
|
|
|
|
|
- <div class="gis-metric-label">{{ item.label }}</div>
|
|
|
|
|
- <div class="gis-metric-value"><span>报警设备</span><strong>{{ loading || error ? '—' : item.count }}<small>台</small></strong></div>
|
|
|
|
|
- </article>
|
|
|
|
|
- </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>
|
|
|
|
|
-
|
|
|
|
|
- <section class="gis-workspace">
|
|
|
|
|
- <header class="gis-workspace-heading"><div><el-icon><MapLocation /></el-icon><h2>报警设备分布</h2><span>{{ devices.length }} 台设备 / {{ rows.length }} 条报警</span></div></header>
|
|
|
|
|
- <div class="gis-workspace-body">
|
|
|
|
|
- <aside class="gis-device-pane" aria-label="报警设备列表" v-loading="loading">
|
|
|
|
|
- <div class="gis-list-heading"><span>筛选结果 <b>{{ devices.length }}</b> 台</span><span>等级优先</span></div>
|
|
|
|
|
- <div class="gis-device-list">
|
|
|
|
|
- <article v-for="device in devices" :key="device.key" :class="['gis-device-item', { 'is-selected': selected?.key === device.key }]" :style="levelStyle(device.alarmLevel)">
|
|
|
|
|
- <div class="gis-device-title"><span class="gis-grade-badge">{{ device.alarmLevel }}</span><button type="button" class="gis-device-name" @click="choose(device)">{{ device.name }}</button></div>
|
|
|
|
|
- <p class="gis-device-code">{{ device.deviceCode }}</p>
|
|
|
|
|
- <div class="gis-device-location"><el-icon><Location /></el-icon><span>{{ device.equipmentLocation || '暂无安装位置' }}</span></div>
|
|
|
|
|
- <div class="gis-device-meta"><span>{{ levelLabel(device.alarmLevel) }}</span><span>{{ device.alarms.length }} 条报警</span></div>
|
|
|
|
|
- <div class="gis-device-actions"><span v-if="!device.hasCoordinates" class="gis-coordinate-warning">缺少有效坐标</span><span v-else class="gis-coordinate-ready"><i></i>已关联点位</span><div><el-button link type="primary" :disabled="!device.hasCoordinates || !mapReady" @click="locate(device)">定位</el-button><el-button link type="primary" @click="choose(device)">详情<el-icon><ArrowRight /></el-icon></el-button></div></div>
|
|
|
|
|
- </article>
|
|
|
|
|
- <el-empty v-if="!devices.length && !loading" :description="error ? '设备列表加载失败' : '暂无匹配的报警设备'" :image-size="72"><el-button v-if="!error" @click="reset">重置筛选</el-button></el-empty>
|
|
|
|
|
- </div>
|
|
|
|
|
- </aside>
|
|
|
|
|
-
|
|
|
|
|
- <div class="gis-map-stage">
|
|
|
|
|
- <GisMapCanvas :key="mapKey" ref="mapRef" :points="points" :marker-size="36" :map-style-id="''" @ready="ready" @error="failMap" @point-click="choose" @cluster-click="cluster = $event.points || []" />
|
|
|
|
|
- <div class="gis-map-status" role="status"><i :class="{ 'is-ready': mapReady, 'is-error': mapError }"></i>{{ mapError ? '底图未就绪 · 列表可用' : mapReady ? '地图已就绪' : '正在加载底图' }}</div>
|
|
|
|
|
- <div class="gis-map-tools" aria-label="地图工具">
|
|
|
|
|
- <el-tooltip content="查看全部点位" placement="bottom"><el-button :icon="Aim" :disabled="!mapReady || !points.length" aria-label="查看全部点位" @click="mapRef?.fitBounds()" /></el-tooltip>
|
|
|
|
|
- <el-tooltip content="放大地图" placement="bottom"><el-button :icon="Plus" :disabled="!mapReady" aria-label="放大地图" @click="mapRef?.zoomIn()" /></el-tooltip>
|
|
|
|
|
- <el-tooltip content="缩小地图" placement="bottom"><el-button :icon="Minus" :disabled="!mapReady" aria-label="缩小地图" @click="mapRef?.zoomOut()" /></el-tooltip>
|
|
|
|
|
- </div>
|
|
|
|
|
- <div v-if="!mapReady" class="gis-map-fallback">
|
|
|
|
|
- <span class="gis-fallback-icon"><el-icon :class="{ 'is-loading': !mapError }"><Warning v-if="mapError" /><Loading v-else /></el-icon></span>
|
|
|
|
|
- <h3>{{ mapError ? '底图暂时无法加载' : '正在连接地图服务' }}</h3>
|
|
|
|
|
- <p>{{ mapError || '地图加载中,设备列表与详情可继续查看。' }}</p>
|
|
|
|
|
- <el-button v-if="mapError" type="primary" :icon="Refresh" @click="retryMap">重试底图</el-button>
|
|
|
|
|
- <span v-if="mapError" class="gis-fallback-note">设备资料、报警记录与监测曲线仍可独立查看</span>
|
|
|
|
|
- </div>
|
|
|
|
|
- <div class="gis-map-bottom"><div class="gis-map-legend" aria-label="报警等级图例"><span v-for="item in options.alarmLevels || []" :key="item.value" :style="levelStyle(item.value)"><i></i>{{ item.label }}</span></div><span v-if="missingCoordinates" class="gis-map-note">{{ missingCoordinates }} 台设备缺少有效坐标</span></div>
|
|
|
|
|
- </div>
|
|
|
|
|
- </div>
|
|
|
|
|
- <footer class="gis-workspace-footer"><span><el-icon><Location /></el-icon>{{ selected ? '已选中:' + selected.name + ' · ' + selected.deviceCode : '选择设备查看详情,点击定位查看所在位置' }}</span><span>点位数字代表报警等级</span></footer>
|
|
|
|
|
- </section>
|
|
|
|
|
-
|
|
|
|
|
- <el-drawer v-model="detailVisible" size="min(680px, 100vw)" append-to-body destroy-on-close class="alarm-gis-drawer">
|
|
|
|
|
- <template #header><div class="gis-drawer-heading"><span>设备详情</span><h2>{{ selected?.name }}</h2></div></template>
|
|
|
|
|
- <div v-if="selected" class="gis-detail-content">
|
|
|
|
|
- <div class="gis-detail-banner"><span class="gis-detail-grade" :style="levelStyle(selected.alarmLevel)"><span class="gis-grade-badge">{{ selected.alarmLevel }}</span>{{ levelLabel(selected.alarmLevel) }}</span><span>{{ selected.alarms.length }} 条报警 · 当前查询结果</span></div>
|
|
|
|
|
- <dl class="gis-detail-grid"><div><dt>设备编码</dt><dd>{{ selected.deviceCode }}</dd></div><div><dt>最近报警时间</dt><dd>{{ latestAlarm?.createTime || '—' }}</dd></div><div class="is-wide"><dt>安装位置</dt><dd>{{ selected.equipmentLocation || '—' }}</dd></div></dl>
|
|
|
|
|
- <div v-if="latestAlarm && hasActualValue(latestAlarm)" class="gis-snapshot"><div><span>最近报警触发值 · {{ latestAlarm.warningType || latestAlarm.warningCode }}</span><strong>{{ valueText(latestAlarm.actualValue, latestAlarm.warningUnit) }}</strong></div><div><span>触发时阈值</span><p>{{ thresholdText(latestAlarm) }}</p></div></div>
|
|
|
|
|
-
|
|
|
|
|
- <el-tabs v-model="tab" class="gis-detail-tabs">
|
|
|
|
|
- <el-tab-pane label="监测曲线" name="curve">
|
|
|
|
|
- <div class="gis-panel-heading"><h3>监测趋势</h3><span>真实采集数据</span></div>
|
|
|
|
|
- <el-select v-model="curveId" :disabled="!curveChoices.length" placeholder="选择报警指标" class="gis-curve-select"><el-option v-for="row in curveChoices" :key="row.id" :value="row.id" :label="`${row.warningType || row.warningCode} · ${row.alarmNo || row.createTime}`" /></el-select>
|
|
|
|
|
- <AlarmChart v-if="detailVisible && tab === 'curve' && currentCurve" :alarm-id="currentCurve.id" :scope="scope" />
|
|
|
|
|
- <el-empty v-if="!curveChoices.length" description="暂无有实际值的报警指标" :image-size="70" />
|
|
|
|
|
- </el-tab-pane>
|
|
|
|
|
- <el-tab-pane label="报警记录" name="alarms">
|
|
|
|
|
- <div class="gis-panel-heading"><h3>本设备报警记录</h3><span>共 {{ historyTotal }} 条 · 时间倒序</span></div>
|
|
|
|
|
- <el-alert v-if="historyError" :title="historyError" type="error" :closable="false"><el-button link @click="loadHistory">重试</el-button></el-alert>
|
|
|
|
|
- <div v-loading="historyLoading" class="gis-alarm-history">
|
|
|
|
|
- <article v-for="row in history" :key="row.id" class="gis-history-item" :style="levelStyle(row.alarmLevel)">
|
|
|
|
|
- <div class="gis-history-heading"><el-tag size="small" effect="light" :type="levelTone(row.alarmLevel)">{{ row.alarmLevelName || levelLabel(row.alarmLevel) }}</el-tag><el-tag size="small" effect="light" :type="monitorStatusTone(row.alarmStatus)">{{ row.monitorStatusName }}</el-tag></div>
|
|
|
|
|
- <p class="gis-history-description">{{ row.alarmContent || '—' }}</p>
|
|
|
|
|
- <dl><dt>报警单号</dt><dd>{{ row.alarmNo || '—' }}</dd><dt>报警时间</dt><dd>{{ row.createTime || '—' }}</dd><dt>监测指标</dt><dd>{{ row.warningType || row.warningCode || '—' }}</dd></dl>
|
|
|
|
|
- <div class="gis-history-actions"><el-button link type="primary" @click="detailRef.open(row.id)">报警详情</el-button><el-button v-if="hasActualValue(row)" link type="primary" @click="curveId = row.id; tab = 'curve'">查看曲线</el-button></div>
|
|
|
|
|
- </article>
|
|
|
|
|
- <el-empty v-if="!history.length && !historyLoading && !historyError" description="暂无报警记录" :image-size="60" />
|
|
|
|
|
- </div>
|
|
|
|
|
- <pagination v-show="historyTotal > 0" v-model:page="historyPage" v-model:limit="historySize" :total="historyTotal" :auto-scroll="false" layout="total, prev, pager, next" @pagination="loadHistory" />
|
|
|
|
|
- </el-tab-pane>
|
|
|
|
|
- <el-tab-pane label="设备信息" name="device">
|
|
|
|
|
- <div class="gis-panel-heading"><h3>设备档案</h3><span>关联设备台账</span></div>
|
|
|
|
|
- <dl class="gis-detail-grid"><div><dt>设备名称</dt><dd>{{ selected.name }}</dd></div><div><dt>设备编码</dt><dd>{{ selected.deviceCode }}</dd></div><div class="is-wide"><dt>安装位置</dt><dd>{{ selected.equipmentLocation || '—' }}</dd></div><div><dt>经度</dt><dd>{{ selected.hasCoordinates ? selected.lng : '暂无有效坐标' }}</dd></div><div><dt>纬度</dt><dd>{{ selected.hasCoordinates ? selected.lat : '暂无有效坐标' }}</dd></div></dl>
|
|
|
|
|
- <section v-if="selected.equipmentImage" class="gis-device-images"><h3>设备图片</h3><AlarmAttachments :value="selected.equipmentImage" :base-url="baseUrl" /></section>
|
|
|
|
|
- </el-tab-pane>
|
|
|
|
|
- </el-tabs>
|
|
|
|
|
- </div>
|
|
|
|
|
- <template #footer><div class="gis-drawer-footer"><el-button @click="detailVisible = false">关闭</el-button></div></template>
|
|
|
|
|
- </el-drawer>
|
|
|
|
|
- <el-dialog :model-value="cluster.length > 0" title="聚合点位" width="min(440px, 94vw)" @close="cluster = []"><div class="gis-cluster-list"><button v-for="device in cluster" :key="device.key" type="button" @click="choose(device); cluster = []"><span>{{ device.name }}</span><small>{{ device.alarms.length }} 条报警</small><el-icon><ArrowRight /></el-icon></button></div></el-dialog>
|
|
|
|
|
- <AlarmDetail ref="detailRef" />
|
|
|
|
|
- </div>
|
|
|
|
|
-</template>
|
|
|
|
|
-
|
|
|
|
|
-<script setup name="WaterSupplyAlarmGIS">
|
|
|
|
|
-import { ref, computed, watch, nextTick, onBeforeUnmount } from 'vue'
|
|
|
|
|
-import { Search, Refresh, MapLocation, Location, Aim, Plus, Minus, ArrowRight, Warning, Loading } from '@element-plus/icons-vue'
|
|
|
|
|
-import GisMapCanvas from '@/components/GisMapCanvas.vue'
|
|
|
|
|
-import { getAlarmMap, getAlarmOptions, getAlarms } from '@/api/alarm'
|
|
|
|
|
-import AlarmDetail from '../../alarm/AlarmDetail.vue'
|
|
|
|
|
-import AlarmChart from '../../alarm/AlarmChart.vue'
|
|
|
|
|
-import AlarmAttachments from '../../alarm/AlarmAttachments.vue'
|
|
|
|
|
-import { alarmCurveChoices, groupAlarmDevices, levelColor, levelTone, monitorStatusTone } from '../../alarm/common/js/alarmOperationsModel'
|
|
|
|
|
-import { hasActualValue, thresholdText, valueText } from '../../alarm/common/js/alarmReviewModel'
|
|
|
|
|
-import { useAlarmScope } from '../../alarm/common/js/useAlarmScope'
|
|
|
|
|
-import '../../alarm/common/styles/alarmOperations.scss'
|
|
|
|
|
-
|
|
|
|
|
-const { scope, scopeKey } = useAlarmScope()
|
|
|
|
|
-const viewLabel = computed(() => ({ 1: '供水监测视图', 3: '燃气监测视图', 4: '窨井监测视图' }[scope.value.rootTypeId] || '设备监测视图'))
|
|
|
|
|
-const baseUrl = import.meta.env.VITE_APP_BASE_API
|
|
|
|
|
-const keyword = ref(''), level = ref(), options = ref({}), rows = ref([]), selected = ref(null), tab = ref('curve'), curveId = ref('')
|
|
|
|
|
-const loading = ref(false), error = ref(''), mapError = ref(''), mapReady = ref(false), mapKey = ref(0), mapRef = ref(), detailRef = ref(), cluster = ref([])
|
|
|
|
|
-const detailVisible = ref(false)
|
|
|
|
|
-const devices = computed(() => groupAlarmDevices(rows.value).sort((a, b) => Number(a.alarmLevel) - Number(b.alarmLevel) || String(a.deviceCode).localeCompare(String(b.deviceCode))))
|
|
|
|
|
-const points = computed(() => devices.value.filter(d => d.hasCoordinates))
|
|
|
|
|
-const missingCoordinates = computed(() => devices.value.length - points.value.length)
|
|
|
|
|
-const levelMetrics = computed(() => (options.value.alarmLevels || []).map(item => ({ ...item, count: devices.value.filter(device => Number(device.alarmLevel) === Number(item.value)).length })))
|
|
|
|
|
-const latestAlarm = computed(() => selected.value?.alarms[0])
|
|
|
|
|
-const levelStyle = value => ({ '--grade-color': levelColor(value), '--grade-tint': levelColor(value) + '14' })
|
|
|
|
|
-const levelLabel = value => options.value.alarmLevels?.find(item => Number(item.value) === Number(value))?.label || '未知等级'
|
|
|
|
|
-const history = ref([]), historyPage = ref(1), historySize = ref(10), historyTotal = ref(0), historyLoading = ref(false), historyError = ref('')
|
|
|
|
|
-const curveChoices = computed(() => alarmCurveChoices([...(selected.value?.alarms || []), ...history.value]))
|
|
|
|
|
-const currentCurve = computed(() => curveChoices.value.find(row => row.id === curveId.value))
|
|
|
|
|
-watch(curveChoices, choices => { if (!choices.some(row => row.id === curveId.value)) curveId.value = choices[0]?.id || '' })
|
|
|
|
|
-
|
|
|
|
|
-let historyGeneration = 0
|
|
|
|
|
-async function loadHistory() {
|
|
|
|
|
- const device = selected.value
|
|
|
|
|
- if (!device) return
|
|
|
|
|
- const token = ++historyGeneration; historyLoading.value = true; historyError.value = ''
|
|
|
|
|
- try {
|
|
|
|
|
- const result = await getAlarms({ ...scope.value, deviceCode: device.deviceCode, pageNum: historyPage.value, pageSize: historySize.value })
|
|
|
|
|
- if (token === historyGeneration) { history.value = result.data.records || []; historyTotal.value = result.data.total || 0 }
|
|
|
|
|
- } catch (e) { if (token === historyGeneration) { historyError.value = e.message || '设备报警记录加载失败'; history.value = [] } }
|
|
|
|
|
- finally { if (token === historyGeneration) historyLoading.value = false }
|
|
|
|
|
-}
|
|
|
|
|
-watch(selected, device => { ++historyGeneration; history.value = []; historyTotal.value = 0; historyPage.value = 1; historyError.value = ''; historyLoading.value = false; if (device) loadHistory() })
|
|
|
|
|
-let generation = 0
|
|
|
|
|
-async function load() {
|
|
|
|
|
- const token = ++generation; loading.value = true; error.value = ''; detailVisible.value = false; selected.value = null; cluster.value = []
|
|
|
|
|
- try {
|
|
|
|
|
- const result = await getAlarmMap({ ...scope.value, keyword: keyword.value, alarmLevel: level.value, alarmStatus: 0 })
|
|
|
|
|
- if (token === generation) { rows.value = result.data || []; await nextTick(); mapRef.value?.fitBounds() }
|
|
|
|
|
- } catch (e) { if (token === generation) { error.value = e.message || '点位加载失败'; rows.value = [] } }
|
|
|
|
|
- finally { if (token === generation) loading.value = false }
|
|
|
|
|
-}
|
|
|
|
|
-function locate(device) { selected.value = device; if (device.hasCoordinates && mapReady.value) mapRef.value?.locatePoint(device) }
|
|
|
|
|
-function choose(device) {
|
|
|
|
|
- selected.value = device
|
|
|
|
|
- curveId.value = device.alarms.find(hasActualValue)?.id || ''
|
|
|
|
|
- tab.value = curveId.value ? 'curve' : 'alarms'
|
|
|
|
|
- detailVisible.value = true
|
|
|
|
|
- if (device.hasCoordinates && mapReady.value) mapRef.value?.locatePoint(device)
|
|
|
|
|
-}
|
|
|
|
|
-function ready() { mapError.value = ''; mapReady.value = true; mapRef.value?.fitBounds() }
|
|
|
|
|
-function failMap(message) { mapReady.value = false; mapError.value = message || '请检查网络或地图服务配置,稍后重试。' }
|
|
|
|
|
-function retryMap() { mapError.value = ''; mapReady.value = false; ++mapKey.value }
|
|
|
|
|
-function reset() { keyword.value = ''; level.value = undefined; return load() }
|
|
|
|
|
-watch(scopeKey, async () => {
|
|
|
|
|
- ++generation; rows.value = []; selected.value = null; detailVisible.value = false; loading.value = false; error.value = ''; options.value = {}
|
|
|
|
|
- const key = scopeKey.value
|
|
|
|
|
- try { const result = await getAlarmOptions({ ...scope.value }); if (key === scopeKey.value) options.value = result.data || {} }
|
|
|
|
|
- catch (e) { if (key === scopeKey.value) { error.value = e.message || '查询选项加载失败'; return } }
|
|
|
|
|
- if (key === scopeKey.value) await reset()
|
|
|
|
|
-}, { immediate: true })
|
|
|
|
|
-onBeforeUnmount(() => { ++generation; ++historyGeneration })
|
|
|
|
|
-</script>
|
|
|
|
|
-
|
|
|
|
|
-<style scoped lang="scss" src="./alarmGIS.scss"></style>
|
|
|