|
@@ -1,929 +0,0 @@
|
|
|
-<!--监测报警一张图-->
|
|
|
|
|
-<template>
|
|
|
|
|
- <div class="gis-alert-dashboard">
|
|
|
|
|
- <!-- 顶部状态栏 -->
|
|
|
|
|
- <div class="top-bar">
|
|
|
|
|
-
|
|
|
|
|
- <div class="alert-stats">
|
|
|
|
|
- <div class="stat" @click="focusLayer('critical')">
|
|
|
|
|
- <span class="stat-value critical">{{ criticalCount }}</span>
|
|
|
|
|
- <span class="stat-label">严重报警</span>
|
|
|
|
|
- </div>
|
|
|
|
|
- <div class="stat" @click="focusLayer('warning')">
|
|
|
|
|
- <span class="stat-value warning">{{ warningCount }}</span>
|
|
|
|
|
- <span class="stat-label">一般报警</span>
|
|
|
|
|
- </div>
|
|
|
|
|
- <div class="stat" @click="focusLayer('normal')">
|
|
|
|
|
- <span class="stat-value">{{ normalCount }}</span>
|
|
|
|
|
- <span class="stat-label">正常井盖</span>
|
|
|
|
|
- </div>
|
|
|
|
|
- <div class="stat" @click="focusLayer('offline')">
|
|
|
|
|
- <span class="stat-value offline">{{ offlineCount }}</span>
|
|
|
|
|
- <span class="stat-label">离线设备</span>
|
|
|
|
|
- </div>
|
|
|
|
|
- </div>
|
|
|
|
|
- </div>
|
|
|
|
|
-
|
|
|
|
|
- <div class="main-layout">
|
|
|
|
|
- <!-- 左侧图层控制面板 -->
|
|
|
|
|
- <div class="layer-control">
|
|
|
|
|
- <div class="layer-header">
|
|
|
|
|
- <el-icon><Monitor /></el-icon> 态势图层
|
|
|
|
|
- </div>
|
|
|
|
|
- <div class="layer-filter-panel">
|
|
|
|
|
- <el-input
|
|
|
|
|
- v-model="queryParams.keyword"
|
|
|
|
|
- clearable
|
|
|
|
|
- :prefix-icon="Search"
|
|
|
|
|
- placeholder="设备编号/名称/位置模糊查询"
|
|
|
|
|
- @keyup.enter="handleSearch"
|
|
|
|
|
- />
|
|
|
|
|
- <div class="filter-actions">
|
|
|
|
|
- <el-button type="primary" size="small" @click="handleSearch">查询</el-button>
|
|
|
|
|
- <el-button size="small" :icon="RefreshRight" @click="handleReset">重置</el-button>
|
|
|
|
|
- </div>
|
|
|
|
|
- <div class="filter-summary">
|
|
|
|
|
- <el-tag size="small" effect="plain">搜索结果 {{ totalDevices }} 台</el-tag>
|
|
|
|
|
- <el-tag size="small" type="success" effect="plain">当前显示 {{ displayDevices.length }} 台</el-tag>
|
|
|
|
|
- <el-tag v-if="activeQuickFilter" size="small" type="warning" effect="plain">态势筛选: {{ quickFilterLabel }}</el-tag>
|
|
|
|
|
- </div>
|
|
|
|
|
- </div>
|
|
|
|
|
- <el-tree
|
|
|
|
|
- ref="layerTreeRef"
|
|
|
|
|
- :data="layerTreeData"
|
|
|
|
|
- show-checkbox
|
|
|
|
|
- check-strictly
|
|
|
|
|
- node-key="id"
|
|
|
|
|
- :default-checked-keys="checkedKeys"
|
|
|
|
|
- :default-expand-all="true"
|
|
|
|
|
- @check="onLayerChange"
|
|
|
|
|
- class="layer-tree"
|
|
|
|
|
- >
|
|
|
|
|
- <template #default="{ node, data }">
|
|
|
|
|
- <span class="layer-node">
|
|
|
|
|
- <el-icon v-if="data.icon"><component :is="data.icon" /></el-icon>
|
|
|
|
|
- <span>{{ node.label }}</span>
|
|
|
|
|
- <el-badge v-if="data.alertCount" :value="data.alertCount" type="danger" class="layer-badge" />
|
|
|
|
|
- </span>
|
|
|
|
|
- </template>
|
|
|
|
|
- </el-tree>
|
|
|
|
|
- <div class="legend-panel">
|
|
|
|
|
- <div class="legend-title">图例说明</div>
|
|
|
|
|
- <div><span class="legend-marker critical-dot"></span> 严重报警</div>
|
|
|
|
|
- <div><span class="legend-marker warning-dot"></span> 一般报警</div>
|
|
|
|
|
- <div><span class="legend-marker normal-dot"></span> 正常在线</div>
|
|
|
|
|
- <div><span class="legend-marker offline-dot"></span> 离线/停用</div>
|
|
|
|
|
- <div><span class="legend-marker alarm-pulse"></span> 实时报警闪烁</div>
|
|
|
|
|
- </div>
|
|
|
|
|
- <div class="time-selector">
|
|
|
|
|
- <el-date-picker v-model="timeRange" type="datetimerange" range-separator="至" size="small" :shortcuts="shortcuts" start-placeholder="开始" end-placeholder="结束" />
|
|
|
|
|
- </div>
|
|
|
|
|
- <div class="panel-tip">已选图层会实时过滤地图点位,支持结合关键字一起查询。</div>
|
|
|
|
|
- </div>
|
|
|
|
|
-
|
|
|
|
|
- <!-- 右侧GIS地图核心区 -->
|
|
|
|
|
- <div class="gis-map-area">
|
|
|
|
|
- <div class="map-toolbar">
|
|
|
|
|
- <el-button-group>
|
|
|
|
|
- <el-button size="small" :icon="ZoomIn" @click="zoomIn">放大</el-button>
|
|
|
|
|
- <el-button size="small" :icon="ZoomOut" @click="zoomOut">缩小</el-button>
|
|
|
|
|
- <el-button size="small" :icon="FullScreen" @click="resetView">重置视图</el-button>
|
|
|
|
|
- </el-button-group>
|
|
|
|
|
- <div class="current-stat">
|
|
|
|
|
- <span><el-icon><Position /></el-icon> 设备总数: {{ totalDevices }}</span>
|
|
|
|
|
- <span><el-icon><Monitor /></el-icon> 当前显示: {{ displayDevices.length }}</span>
|
|
|
|
|
- <span class="alert-summary">报警中: {{ totalActiveAlerts }}</span>
|
|
|
|
|
- </div>
|
|
|
|
|
- </div>
|
|
|
|
|
- <div class="map-container" ref="mapContainer" v-loading="listLoading">
|
|
|
|
|
- <div ref="baiduMapRef" class="baidu-map"></div>
|
|
|
|
|
- <div v-if="!listLoading && !displayDevices.length" class="empty-state">
|
|
|
|
|
- 当前筛选条件下暂无设备
|
|
|
|
|
- </div>
|
|
|
|
|
- <div v-if="mapError" class="map-error">
|
|
|
|
|
- {{ mapError }}
|
|
|
|
|
- </div>
|
|
|
|
|
- </div>
|
|
|
|
|
- <!-- 右下角报警聚合弹窗/详情 -->
|
|
|
|
|
- <el-drawer v-model="drawerVisible" :title="selectedDevice?.name + ' - 监测报警详情'" direction="rtl" size="420px" class="detail-drawer">
|
|
|
|
|
- <div v-if="selectedDevice" v-loading="detailLoading">
|
|
|
|
|
- <el-descriptions :column="1" border>
|
|
|
|
|
- <el-descriptions-item label="设备编号">{{ selectedDevice.deviceCode || '—' }}</el-descriptions-item>
|
|
|
|
|
- <el-descriptions-item label="地理位置">{{ selectedDevice.address }}</el-descriptions-item>
|
|
|
|
|
- <el-descriptions-item label="当前状态">
|
|
|
|
|
- <el-tag :type="getStatusTag(selectedDevice.status)">{{ selectedDevice.statusText }}</el-tag>
|
|
|
|
|
- </el-descriptions-item>
|
|
|
|
|
- <el-descriptions-item label="报警级别" v-if="selectedDevice.hasAlert">
|
|
|
|
|
- <el-tag :type="selectedDevice.level === 'critical' ? 'danger' : 'warning'">{{ selectedDevice.level === 'critical' ? '严重报警' : '一般报警' }}</el-tag>
|
|
|
|
|
- </el-descriptions-item>
|
|
|
|
|
- <el-descriptions-item label="报警类型">{{ selectedDevice.alertType || '无' }}</el-descriptions-item>
|
|
|
|
|
- <el-descriptions-item label="实时监测值">{{ selectedDevice.realTimeValue || '—' }}</el-descriptions-item>
|
|
|
|
|
- <el-descriptions-item label="报警阈值">{{ selectedDevice.threshold || '—' }}</el-descriptions-item>
|
|
|
|
|
- <el-descriptions-item label="最近通讯">{{ selectedDevice.lastComm }}</el-descriptions-item>
|
|
|
|
|
- </el-descriptions>
|
|
|
|
|
- <el-divider>处置建议</el-divider>
|
|
|
|
|
- <div class="advice-box">
|
|
|
|
|
- <p>{{ getAlertAdvice(selectedDevice) }}</p>
|
|
|
|
|
- <el-button type="primary" size="small" style="margin-top: 12px;" @click="dispatchWorkOrder">派发处置工单</el-button>
|
|
|
|
|
- </div>
|
|
|
|
|
- <el-divider v-if="selectedDevice.alertLogs?.length">报警记录</el-divider>
|
|
|
|
|
- <el-timeline>
|
|
|
|
|
- <el-timeline-item v-for="log in selectedDevice.alertLogs" :key="log.time" :timestamp="log.time" type="danger">
|
|
|
|
|
- {{ log.content }}
|
|
|
|
|
- </el-timeline-item>
|
|
|
|
|
- </el-timeline>
|
|
|
|
|
- </div>
|
|
|
|
|
- </el-drawer>
|
|
|
|
|
- </div>
|
|
|
|
|
- </div>
|
|
|
|
|
- </div>
|
|
|
|
|
-</template>
|
|
|
|
|
-
|
|
|
|
|
-<script setup>
|
|
|
|
|
-import {computed, nextTick, onMounted, onUnmounted, ref, watch} from 'vue'
|
|
|
|
|
-import {
|
|
|
|
|
- Connection,
|
|
|
|
|
- FullScreen,
|
|
|
|
|
- InfoFilled,
|
|
|
|
|
- Monitor,
|
|
|
|
|
- Position,
|
|
|
|
|
- RefreshRight,
|
|
|
|
|
- Search,
|
|
|
|
|
- Setting,
|
|
|
|
|
- WarningFilled,
|
|
|
|
|
- ZoomIn,
|
|
|
|
|
- ZoomOut
|
|
|
|
|
-} from '@element-plus/icons-vue'
|
|
|
|
|
-import {ElMessage} from 'element-plus'
|
|
|
|
|
-import {convertCoord} from '@/utils/coordTransform'
|
|
|
|
|
-import {getManholeAlarmDeviceDetail, getManholeAlarmDeviceList, getManholeLayerData} from '@/api/pipeNetwork/basic'
|
|
|
|
|
-
|
|
|
|
|
-// 树复选框默认勾选全部状态
|
|
|
|
|
-const DEFAULT_CHECKED_KEYS = ['current-1', 'current-3', 'alarm-1', 'alarm-0', 'online-1', 'online-0']
|
|
|
|
|
-const checkedKeys = ref([...DEFAULT_CHECKED_KEYS])
|
|
|
|
|
-const visibleLayers = ref(new Set(DEFAULT_CHECKED_KEYS))
|
|
|
|
|
-const layerTreeRef = ref(null)
|
|
|
|
|
-
|
|
|
|
|
-// 动态生成树数据(包括设备级子节点),每次 displayDevices 变化自动重算
|
|
|
|
|
-const layerTreeData = computed(() => {
|
|
|
|
|
- const devices = displayDevices.value
|
|
|
|
|
- const total = devices.length
|
|
|
|
|
- const critical = devices.filter(m => m.status === 'critical').length
|
|
|
|
|
- const warning = devices.filter(m => m.status === 'warning').length
|
|
|
|
|
- const normal = devices.filter(m => m.status === 'normal').length
|
|
|
|
|
- const offline = devices.filter(m => m.status === 'offline').length
|
|
|
|
|
- const inUse = devices.filter(item => Number(item.raw?.equipmentStatus?.currentStatus) === 1).length
|
|
|
|
|
- const maintenance = devices.filter(item => Number(item.raw?.equipmentStatus?.currentStatus) === 3).length
|
|
|
|
|
-
|
|
|
|
|
- function buildDeviceNode(device) {
|
|
|
|
|
- const typeId = Number(device.raw?.equipmentTypeId)
|
|
|
|
|
- const typeName = typeId === 5 ? '供水窨井' : typeId === 9 ? '排水窨井' : typeId === 10 ? '燃气窨井' : (device.equipmentTypeName || '未知类型')
|
|
|
|
|
- const code = device.deviceCode || device.id || ''
|
|
|
|
|
- return {
|
|
|
|
|
- id: `device-${device.equipmentId}`,
|
|
|
|
|
- label: `${typeName}-${code}`,
|
|
|
|
|
- disabled: true
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- return [
|
|
|
|
|
- {
|
|
|
|
|
- id: 'current-status',
|
|
|
|
|
- label: '运维状态筛选',
|
|
|
|
|
- icon: Setting,
|
|
|
|
|
- alertCount: inUse + maintenance,
|
|
|
|
|
- disabled: true,
|
|
|
|
|
- children: [
|
|
|
|
|
- { id: 'current-1', label: '在用设备', icon: Monitor, alertCount: inUse,
|
|
|
|
|
- children: devices.filter(d => Number(d.raw?.equipmentStatus?.currentStatus) === 1).map(buildDeviceNode) },
|
|
|
|
|
- { id: 'current-3', label: '维修设备', icon: Setting, alertCount: maintenance,
|
|
|
|
|
- children: devices.filter(d => Number(d.raw?.equipmentStatus?.currentStatus) === 3).map(buildDeviceNode) }
|
|
|
|
|
- ]
|
|
|
|
|
- },
|
|
|
|
|
- {
|
|
|
|
|
- id: 'alarm-status',
|
|
|
|
|
- label: '报警状态筛选',
|
|
|
|
|
- icon: WarningFilled,
|
|
|
|
|
- alertCount: critical + warning + normal,
|
|
|
|
|
- disabled: true,
|
|
|
|
|
- children: [
|
|
|
|
|
- { id: 'alarm-1', label: '报警设备', icon: WarningFilled, alertCount: critical + warning,
|
|
|
|
|
- children: devices.filter(d => d.status === 'critical' || d.status === 'warning').map(buildDeviceNode) },
|
|
|
|
|
- { id: 'alarm-0', label: '正常设备', icon: InfoFilled, alertCount: normal,
|
|
|
|
|
- children: devices.filter(d => d.status === 'normal').map(buildDeviceNode) }
|
|
|
|
|
- ]
|
|
|
|
|
- },
|
|
|
|
|
- {
|
|
|
|
|
- id: 'online-status',
|
|
|
|
|
- label: '在线状态筛选',
|
|
|
|
|
- icon: Connection,
|
|
|
|
|
- alertCount: total,
|
|
|
|
|
- disabled: true,
|
|
|
|
|
- children: [
|
|
|
|
|
- { id: 'online-1', label: '在线设备', icon: Connection, alertCount: total - offline,
|
|
|
|
|
- children: devices.filter(d => d.status !== 'offline').map(buildDeviceNode) },
|
|
|
|
|
- { id: 'online-0', label: '离线设备', icon: Connection, alertCount: offline,
|
|
|
|
|
- children: devices.filter(d => d.status === 'offline').map(buildDeviceNode) }
|
|
|
|
|
- ]
|
|
|
|
|
- }
|
|
|
|
|
- ]
|
|
|
|
|
-})
|
|
|
|
|
-const queryParams = ref({
|
|
|
|
|
- keyword: ''
|
|
|
|
|
-})
|
|
|
|
|
-const mockManholes = ref([])
|
|
|
|
|
-const listLoading = ref(false)
|
|
|
|
|
-const detailLoading = ref(false)
|
|
|
|
|
-const activeQuickFilter = ref('')
|
|
|
|
|
-const baiduMapRef = ref(null)
|
|
|
|
|
-const mapError = ref('')
|
|
|
|
|
-
|
|
|
|
|
-const BAIDU_MAP_AK = import.meta.env.VITE_BAIDU_MAP_AK || ''
|
|
|
|
|
-const DEFAULT_MAP_CENTER = { lng: 110.393, lat: 28.452 }
|
|
|
|
|
-
|
|
|
|
|
-let mapInstance = null
|
|
|
|
|
-let mapOverlays = []
|
|
|
|
|
-let baiduMapScriptPromise = null
|
|
|
|
|
-
|
|
|
|
|
-const quickFilterLabelMap = {
|
|
|
|
|
- critical: '严重报警',
|
|
|
|
|
- warning: '一般报警',
|
|
|
|
|
- normal: '正常井盖',
|
|
|
|
|
- offline: '离线设备'
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-const quickFilterLabel = computed(() => quickFilterLabelMap[activeQuickFilter.value] || '')
|
|
|
|
|
-
|
|
|
|
|
-function isBatteryLow(device) {
|
|
|
|
|
- const battery = toNumber(device.battery)
|
|
|
|
|
- return battery !== null && battery <= 20
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-function matchesQuickFilter(device) {
|
|
|
|
|
- if (!activeQuickFilter.value) return true
|
|
|
|
|
- return device.status === activeQuickFilter.value
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-/**
|
|
|
|
|
- * 图层树勾选过滤:根据 visibleLayers 判断设备是否应显示
|
|
|
|
|
- * - 无状态记录的设备视为离线(onlineStatus=0)
|
|
|
|
|
- * - 某分类全部勾选时等同于不过滤(兼容无状态记录的设备)
|
|
|
|
|
- */
|
|
|
|
|
-function matchesLayerFilter(device) {
|
|
|
|
|
- const keys = visibleLayers.value
|
|
|
|
|
- // 没有勾选任何状态时显示全部
|
|
|
|
|
- if (keys.size === 0) return true
|
|
|
|
|
-
|
|
|
|
|
- const st = device.raw?.equipmentStatus || {}
|
|
|
|
|
- const currentStatus = Number(st.currentStatus)
|
|
|
|
|
- const alarmStatus = Number(st.alarmStatus)
|
|
|
|
|
- const onlineStatus = Number(st.onlineStatus)
|
|
|
|
|
- const hasNoStatus = !device.raw?.equipmentStatus
|
|
|
|
|
-
|
|
|
|
|
- // OR 逻辑:匹配任意一个勾选状态即显示
|
|
|
|
|
- if (keys.has('current-1') && currentStatus === 1) return true
|
|
|
|
|
- if (keys.has('current-3') && currentStatus === 3) return true
|
|
|
|
|
- if (keys.has('alarm-1') && alarmStatus === 1) return true
|
|
|
|
|
- if (keys.has('alarm-0') && alarmStatus === 0) return true
|
|
|
|
|
- if (keys.has('online-1') && onlineStatus === 1) return true
|
|
|
|
|
- if (keys.has('online-0') && (onlineStatus === 0 || hasNoStatus)) return true
|
|
|
|
|
-
|
|
|
|
|
- return false
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-const displayDevices = computed(() => {
|
|
|
|
|
- return mockManholes.value.filter(device => {
|
|
|
|
|
- // 图层树过滤
|
|
|
|
|
- if (!matchesLayerFilter(device)) return false
|
|
|
|
|
- // 顶部统计栏快速过滤
|
|
|
|
|
- if (!matchesQuickFilter(device)) return false
|
|
|
|
|
- return true
|
|
|
|
|
- })
|
|
|
|
|
-})
|
|
|
|
|
-
|
|
|
|
|
-const criticalCount = computed(() => displayDevices.value.filter(m => m.status === 'critical').length)
|
|
|
|
|
-const warningCount = computed(() => displayDevices.value.filter(m => m.status === 'warning').length)
|
|
|
|
|
-const normalCount = computed(() => displayDevices.value.filter(m => m.status === 'normal').length)
|
|
|
|
|
-const offlineCount = computed(() => displayDevices.value.filter(m => m.status === 'offline').length)
|
|
|
|
|
-const totalDevices = computed(() => mockManholes.value.length)
|
|
|
|
|
-const totalActiveAlerts = computed(() => criticalCount.value + warningCount.value)
|
|
|
|
|
-
|
|
|
|
|
-const mapContainer = ref(null)
|
|
|
|
|
-const bounds = ref({ minLng: 121.45, maxLng: 121.55, minLat: 31.22, maxLat: 31.28 })
|
|
|
|
|
-
|
|
|
|
|
-function toNumber(value) {
|
|
|
|
|
- const num = Number(value)
|
|
|
|
|
- return Number.isFinite(num) ? num : null
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-function formatDateTime(value) {
|
|
|
|
|
- return value || '—'
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-function isValidCoordinate(lng, lat) {
|
|
|
|
|
- return lng !== null && lat !== null && lng >= 70 && lng <= 140 && lat >= 3 && lat <= 55
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-/**
|
|
|
|
|
- * 业务坐标(WGS84)转百度坐标点(BD-09)
|
|
|
|
|
- *
|
|
|
|
|
- * 设备台账的 longitude/latitude 存的是 WGS84,百度地图要 BD-09,
|
|
|
|
|
- * 直接传原值会整体偏移约 1.1 公里,必须转换后再构建 Point
|
|
|
|
|
- *
|
|
|
|
|
- * @param lng 业务经度
|
|
|
|
|
- * @param lat 业务纬度
|
|
|
|
|
- * @returns {BMapGL.Point} 百度坐标点
|
|
|
|
|
- */
|
|
|
|
|
-function toBdPoint(lng, lat) {
|
|
|
|
|
- const [bdLng, bdLat] = convertCoord(Number(lng), Number(lat))
|
|
|
|
|
- return new window.BMapGL.Point(bdLng, bdLat)
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-function getThresholdTextByCode(raw, warningCode) {
|
|
|
|
|
- const list = Array.isArray(raw?.warningThresholdList) ? raw.warningThresholdList : []
|
|
|
|
|
- const target = list.find(item => item.warningCode === warningCode)
|
|
|
|
|
- if (!target) return '—'
|
|
|
|
|
- const { minValue, maxValue } = target
|
|
|
|
|
- if (minValue !== null && minValue !== undefined && maxValue !== null && maxValue !== undefined) {
|
|
|
|
|
- return `${minValue} ~ ${maxValue}`
|
|
|
|
|
- }
|
|
|
|
|
- if (minValue !== null && minValue !== undefined) return `<= ${minValue}`
|
|
|
|
|
- if (maxValue !== null && maxValue !== undefined) return `>= ${maxValue}`
|
|
|
|
|
- return '—'
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-function buildAlertInfo(raw) {
|
|
|
|
|
- const status = raw?.equipmentStatus || {}
|
|
|
|
|
- const manholeData = raw?.manholeData || {}
|
|
|
|
|
- const battery = toNumber(manholeData.batteryLevel)
|
|
|
|
|
- const signal = toNumber(manholeData.signalStrength)
|
|
|
|
|
- const tiltAngle = manholeData?.tiltAngle
|
|
|
|
|
-
|
|
|
|
|
- if (Number(status.onlineStatus) !== 1) {
|
|
|
|
|
- return { status: 'offline', statusText: '离线', hasAlert: false, level: '', alertType: '', realTimeValue: '—', threshold: '—' }
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- if (`${manholeData.waterLevelAlarmStatus}` === '1') {
|
|
|
|
|
- return {
|
|
|
|
|
- status: 'critical',
|
|
|
|
|
- statusText: '严重报警',
|
|
|
|
|
- hasAlert: true,
|
|
|
|
|
- level: 'critical',
|
|
|
|
|
- alertType: '水位报警',
|
|
|
|
|
- realTimeValue: '报警',
|
|
|
|
|
- threshold: getThresholdTextByCode(raw, 'WARN-WATER-LEVEL')
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- if (`${manholeData.waterInfiltrationAlarmStatus}` === '1') {
|
|
|
|
|
- return {
|
|
|
|
|
- status: 'critical',
|
|
|
|
|
- statusText: '严重报警',
|
|
|
|
|
- hasAlert: true,
|
|
|
|
|
- level: 'critical',
|
|
|
|
|
- alertType: '水浸报警',
|
|
|
|
|
- realTimeValue: '报警',
|
|
|
|
|
- threshold: getThresholdTextByCode(raw, 'WARN-WATER-INFILTRATION')
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- if (Number(status.alarmStatus) === 1) {
|
|
|
|
|
- return {
|
|
|
|
|
- status: 'critical',
|
|
|
|
|
- statusText: '严重报警',
|
|
|
|
|
- hasAlert: true,
|
|
|
|
|
- level: 'critical',
|
|
|
|
|
- alertType: tiltAngle ? '倾斜报警' : '设备报警',
|
|
|
|
|
- realTimeValue: tiltAngle ? `${tiltAngle}°` : '报警',
|
|
|
|
|
- threshold: tiltAngle ? getThresholdTextByCode(raw, 'WARN-TILT') : getThresholdTextByCode(raw, 'WARN-DEVICE-ALARM')
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- if ((battery !== null && battery <= 20) || (signal !== null && signal <= 20)) {
|
|
|
|
|
- return {
|
|
|
|
|
- status: 'warning',
|
|
|
|
|
- statusText: '一般报警',
|
|
|
|
|
- hasAlert: true,
|
|
|
|
|
- level: 'warning',
|
|
|
|
|
- alertType: battery !== null && battery <= 20 ? '低电量预警' : '弱信号预警',
|
|
|
|
|
- realTimeValue: battery !== null && battery <= 20 ? `${battery}%` : `${signal}%`,
|
|
|
|
|
- threshold: '20'
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- return { status: 'normal', statusText: '正常', hasAlert: false, level: '', alertType: '', realTimeValue: '—', threshold: '—' }
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-function buildAlertLogs(raw, alertInfo) {
|
|
|
|
|
- if (!alertInfo?.hasAlert) return []
|
|
|
|
|
- const timestamp = raw?.equipmentStatus?.statusUpdateTime || raw?.updateTime || raw?.installTime || ''
|
|
|
|
|
- return [{
|
|
|
|
|
- time: formatDateTime(timestamp),
|
|
|
|
|
- content: `${alertInfo.alertType}${alertInfo.realTimeValue && alertInfo.realTimeValue !== '—' ? `:${alertInfo.realTimeValue}` : ''}${alertInfo.threshold && alertInfo.threshold !== '—' ? `,阈值 ${alertInfo.threshold}` : ''}`
|
|
|
|
|
- }]
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-function normalizeDevice(raw, _index = 0, baseDevice = {}) {
|
|
|
|
|
- const rawLng = toNumber(raw?.longitude)
|
|
|
|
|
- const rawLat = toNumber(raw?.latitude)
|
|
|
|
|
- const baseLng = toNumber(baseDevice.lng)
|
|
|
|
|
- const baseLat = toNumber(baseDevice.lat)
|
|
|
|
|
- const useRawCoordinate = isValidCoordinate(rawLng, rawLat)
|
|
|
|
|
- const useBaseCoordinate = isValidCoordinate(baseLng, baseLat)
|
|
|
|
|
- const lng = useRawCoordinate ? rawLng : (useBaseCoordinate ? baseLng : null)
|
|
|
|
|
- const lat = useRawCoordinate ? rawLat : (useBaseCoordinate ? baseLat : null)
|
|
|
|
|
- const alertInfo = buildAlertInfo(raw)
|
|
|
|
|
- const address = raw?.equipmentLocation || baseDevice.address || '暂无位置信息'
|
|
|
|
|
- return {
|
|
|
|
|
- equipmentId: raw?.equipmentId || baseDevice.equipmentId || '',
|
|
|
|
|
- id: raw?.equipmentCode || baseDevice.id || raw?.equipmentId || '',
|
|
|
|
|
- deviceCode: raw?.equipmentCode || baseDevice.deviceCode || '',
|
|
|
|
|
- name: raw?.equipmentName || baseDevice.name || raw?.equipmentCode || '未命名设备',
|
|
|
|
|
- lng,
|
|
|
|
|
- lat,
|
|
|
|
|
- address,
|
|
|
|
|
- status: alertInfo.status,
|
|
|
|
|
- statusText: alertInfo.statusText,
|
|
|
|
|
- hasAlert: alertInfo.hasAlert,
|
|
|
|
|
- level: alertInfo.level,
|
|
|
|
|
- alertType: alertInfo.alertType,
|
|
|
|
|
- realTimeValue: alertInfo.realTimeValue,
|
|
|
|
|
- threshold: alertInfo.threshold,
|
|
|
|
|
- lastComm: formatDateTime(raw?.equipmentStatus?.statusUpdateTime || raw?.updateTime || raw?.installTime),
|
|
|
|
|
- locationShort: address.length > 8 ? `${address.slice(0, 8)}...` : address,
|
|
|
|
|
- battery: raw?.manholeData?.batteryLevel || '—',
|
|
|
|
|
- alertLogs: buildAlertLogs(raw, alertInfo),
|
|
|
|
|
- equipmentTypeName: raw?.equipmentTypeName || '—',
|
|
|
|
|
- equipmentModel: raw?.equipmentModel || '—',
|
|
|
|
|
- manufacturer: raw?.manufacturer || '—',
|
|
|
|
|
- installTime: formatDateTime(raw?.installTime),
|
|
|
|
|
- maintainer: raw?.maintainer || '—',
|
|
|
|
|
- raw
|
|
|
|
|
- }
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-function refreshLayerTree() {
|
|
|
|
|
- // 已废弃:树数据由 layerTreeData computed 自动生成
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-function refreshBounds() {
|
|
|
|
|
- const validList = displayDevices.value.filter(item => Number.isFinite(item.lng) && Number.isFinite(item.lat))
|
|
|
|
|
- if (!validList.length) {
|
|
|
|
|
- bounds.value = { minLng: 110.35, maxLng: 110.43, minLat: 28.42, maxLat: 28.5 }
|
|
|
|
|
- return
|
|
|
|
|
- }
|
|
|
|
|
- const lngList = validList.map(item => item.lng)
|
|
|
|
|
- const latList = validList.map(item => item.lat)
|
|
|
|
|
- const minLng = Math.min(...lngList)
|
|
|
|
|
- const maxLng = Math.max(...lngList)
|
|
|
|
|
- const minLat = Math.min(...latList)
|
|
|
|
|
- const maxLat = Math.max(...latList)
|
|
|
|
|
- bounds.value = {
|
|
|
|
|
- minLng: minLng - 0.01,
|
|
|
|
|
- maxLng: maxLng + 0.01,
|
|
|
|
|
- minLat: minLat - 0.01,
|
|
|
|
|
- maxLat: maxLat + 0.01
|
|
|
|
|
- }
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-function getStatusColor(status) {
|
|
|
|
|
- if (status === 'critical') return '#f56c6c'
|
|
|
|
|
- if (status === 'warning') return '#e6a23c'
|
|
|
|
|
- if (status === 'offline') return '#909399'
|
|
|
|
|
- return '#67c23a'
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-function getMarkerHtml(device, isActive = false) {
|
|
|
|
|
- const color = getStatusColor(device.status)
|
|
|
|
|
- const pulse = device.status === 'critical'
|
|
|
|
|
- ? 'box-shadow: 0 0 0 8px rgba(245,108,108,0.18);'
|
|
|
|
|
- : ''
|
|
|
|
|
- const activeRing = isActive
|
|
|
|
|
- ? 'border: 3px solid #ffffff; transform: scale(1.08);'
|
|
|
|
|
- : 'border: 2px solid #ffffff;'
|
|
|
|
|
- return `
|
|
|
|
|
- <div style="display:flex;flex-direction:column;align-items:center;transform:translateY(-6px);cursor:pointer;">
|
|
|
|
|
- <div style="max-width:160px;padding:4px 8px;border-radius:999px;background:rgba(20,33,61,0.82);color:#fff;font-size:12px;line-height:1.2;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;margin-bottom:6px;">
|
|
|
|
|
- ${device.name}
|
|
|
|
|
- </div>
|
|
|
|
|
- <div style="width:18px;height:18px;border-radius:50%;background:${color};${activeRing}${pulse}"></div>
|
|
|
|
|
- </div>
|
|
|
|
|
- `
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-function clearMapOverlays() {
|
|
|
|
|
- if (!mapInstance) return
|
|
|
|
|
- mapOverlays.forEach((overlay) => {
|
|
|
|
|
- try {
|
|
|
|
|
- mapInstance.removeOverlay(overlay)
|
|
|
|
|
- } catch (_) {}
|
|
|
|
|
- })
|
|
|
|
|
- mapOverlays = []
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-function fitMapViewport() {
|
|
|
|
|
- if (!mapInstance || !window.BMapGL) return
|
|
|
|
|
- if (!displayDevices.value.length) {
|
|
|
|
|
- mapInstance.centerAndZoom(toBdPoint(DEFAULT_MAP_CENTER.lng, DEFAULT_MAP_CENTER.lat), 14)
|
|
|
|
|
- return
|
|
|
|
|
- }
|
|
|
|
|
- const points = displayDevices.value
|
|
|
|
|
- .filter(device => isValidCoordinate(device.lng, device.lat))
|
|
|
|
|
- .map(device => toBdPoint(device.lng, device.lat))
|
|
|
|
|
- if (!points.length) {
|
|
|
|
|
- mapInstance.centerAndZoom(toBdPoint(DEFAULT_MAP_CENTER.lng, DEFAULT_MAP_CENTER.lat), 14)
|
|
|
|
|
- return
|
|
|
|
|
- }
|
|
|
|
|
- mapInstance.setViewport(points)
|
|
|
|
|
- if (mapInstance.getZoom() > 16) {
|
|
|
|
|
- mapInstance.setZoom(16)
|
|
|
|
|
- }
|
|
|
|
|
- if (mapInstance.getZoom() < 12) {
|
|
|
|
|
- mapInstance.setZoom(12)
|
|
|
|
|
- }
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-function renderMapOverlays() {
|
|
|
|
|
- if (!mapInstance || !window.BMapGL) return
|
|
|
|
|
- console.log('[renderMapOverlays] 准备渲染的设备:', displayDevices.value.length)
|
|
|
|
|
- clearMapOverlays()
|
|
|
|
|
- const validDevices = displayDevices.value.filter(device => isValidCoordinate(device.lng, device.lat))
|
|
|
|
|
- console.log('[renderMapOverlays] 有效坐标设备:', validDevices.length)
|
|
|
|
|
- console.log('[renderMapOverlays] 无效坐标设备:', displayDevices.value.filter(d => !isValidCoordinate(d.lng, d.lat)).map(d => ({ id: d.id, lng: d.lng, lat: d.lat })))
|
|
|
|
|
-
|
|
|
|
|
- validDevices.forEach((device) => {
|
|
|
|
|
- console.log('[renderMapOverlays] 渲染点位:', device.name, 'lng:', device.lng, 'lat:', device.lat)
|
|
|
|
|
- const point = toBdPoint(device.lng, device.lat)
|
|
|
|
|
- const isActive = selectedDevice.value?.equipmentId === device.equipmentId
|
|
|
|
|
- const label = new window.BMapGL.Label(getMarkerHtml(device, isActive), {
|
|
|
|
|
- position: point,
|
|
|
|
|
- offset: new window.BMapGL.Size(-30, -52)
|
|
|
|
|
- })
|
|
|
|
|
- label.setStyle({
|
|
|
|
|
- border: 'none',
|
|
|
|
|
- background: 'transparent',
|
|
|
|
|
- padding: '0'
|
|
|
|
|
- })
|
|
|
|
|
- label.addEventListener('click', async (event) => {
|
|
|
|
|
- event?.domEvent?.stopPropagation?.()
|
|
|
|
|
- await openDeviceDetail(device)
|
|
|
|
|
- })
|
|
|
|
|
- mapInstance.addOverlay(label)
|
|
|
|
|
- mapOverlays.push(label)
|
|
|
|
|
- })
|
|
|
|
|
- fitMapViewport()
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-let _renderTimer = null
|
|
|
|
|
-function renderMap() {
|
|
|
|
|
- if (!mapInstance || !window.BMapGL) return
|
|
|
|
|
- if (_renderTimer) clearTimeout(_renderTimer)
|
|
|
|
|
- _renderTimer = setTimeout(() => {
|
|
|
|
|
- renderMapOverlays()
|
|
|
|
|
- _renderTimer = null
|
|
|
|
|
- }, 50)
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-function ensureBaiduMap() {
|
|
|
|
|
- if (window.BMapGL) {
|
|
|
|
|
- return Promise.resolve(window.BMapGL)
|
|
|
|
|
- }
|
|
|
|
|
- if (baiduMapScriptPromise) {
|
|
|
|
|
- return baiduMapScriptPromise
|
|
|
|
|
- }
|
|
|
|
|
- if (!BAIDU_MAP_AK) {
|
|
|
|
|
- return Promise.reject(new Error('未配置百度地图 AK,请在 .env.development 中配置 VITE_BAIDU_MAP_AK'))
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- baiduMapScriptPromise = new Promise((resolve, reject) => {
|
|
|
|
|
- const existing = document.querySelector('script[data-baidu-map="webgl"]')
|
|
|
|
|
- if (existing) {
|
|
|
|
|
- let attempts = 0
|
|
|
|
|
- const timer = window.setInterval(() => {
|
|
|
|
|
- attempts++
|
|
|
|
|
- if (window.BMapGL) {
|
|
|
|
|
- window.clearInterval(timer)
|
|
|
|
|
- resolve(window.BMapGL)
|
|
|
|
|
- } else if (attempts >= 50) {
|
|
|
|
|
- window.clearInterval(timer)
|
|
|
|
|
- reject(new Error('百度地图脚本加载超时'))
|
|
|
|
|
- }
|
|
|
|
|
- }, 200)
|
|
|
|
|
- return
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- const script = document.createElement('script')
|
|
|
|
|
- script.src = `https://api.map.baidu.com/api?type=webgl&v=1.0&ak=${BAIDU_MAP_AK}`
|
|
|
|
|
- script.async = true
|
|
|
|
|
- script.defer = true
|
|
|
|
|
- script.setAttribute('data-baidu-map', 'webgl')
|
|
|
|
|
- script.onload = () => {
|
|
|
|
|
- let attempts = 0
|
|
|
|
|
- const timer = window.setInterval(() => {
|
|
|
|
|
- attempts++
|
|
|
|
|
- if (window.BMapGL) {
|
|
|
|
|
- window.clearInterval(timer)
|
|
|
|
|
- resolve(window.BMapGL)
|
|
|
|
|
- } else if (attempts >= 50) {
|
|
|
|
|
- window.clearInterval(timer)
|
|
|
|
|
- reject(new Error('百度地图脚本初始化超时'))
|
|
|
|
|
- }
|
|
|
|
|
- }, 200)
|
|
|
|
|
- }
|
|
|
|
|
- script.onerror = () => reject(new Error('百度地图脚本加载失败'))
|
|
|
|
|
- document.head.appendChild(script)
|
|
|
|
|
- })
|
|
|
|
|
-
|
|
|
|
|
- return baiduMapScriptPromise
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-async function initMap() {
|
|
|
|
|
- if (!baiduMapRef.value) return
|
|
|
|
|
- try {
|
|
|
|
|
- await ensureBaiduMap()
|
|
|
|
|
- if (!mapInstance) {
|
|
|
|
|
- mapInstance = new window.BMapGL.Map(baiduMapRef.value, { enableMapClick: true })
|
|
|
|
|
- mapInstance.enableScrollWheelZoom(true)
|
|
|
|
|
- mapInstance.setMinZoom(11)
|
|
|
|
|
- mapInstance.setMaxZoom(18)
|
|
|
|
|
- mapInstance.centerAndZoom(toBdPoint(DEFAULT_MAP_CENTER.lng, DEFAULT_MAP_CENTER.lat), 14)
|
|
|
|
|
- mapInstance.addEventListener('click', () => {
|
|
|
|
|
- drawerVisible.value = false
|
|
|
|
|
- })
|
|
|
|
|
- }
|
|
|
|
|
- mapError.value = ''
|
|
|
|
|
- renderMap()
|
|
|
|
|
- } catch (error) {
|
|
|
|
|
- mapError.value = error?.message || '百度地图加载失败'
|
|
|
|
|
- }
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-async function loadDeviceList() {
|
|
|
|
|
- listLoading.value = true
|
|
|
|
|
- try {
|
|
|
|
|
- const keyword = queryParams.value.keyword?.trim()
|
|
|
|
|
-
|
|
|
|
|
- // 只传 keyword 和 isQueryManholeData,不传状态筛选
|
|
|
|
|
- // 树勾选过滤在 displayDevices computed 中做前端过滤
|
|
|
|
|
- const res = await getManholeLayerData({
|
|
|
|
|
- keyword: keyword || undefined,
|
|
|
|
|
- isQueryManholeData: true
|
|
|
|
|
- })
|
|
|
|
|
-
|
|
|
|
|
- const result = res.data || {}
|
|
|
|
|
- mockManholes.value = (result.devices || []).map((item, index) => normalizeDevice(item, index))
|
|
|
|
|
-
|
|
|
|
|
- // 同步统计信息
|
|
|
|
|
- const stats = result.statistics || {}
|
|
|
|
|
- console.log('[alarmSanalysis] 后端统计数据:', stats)
|
|
|
|
|
-
|
|
|
|
|
- // displayDevices computed 和 watch 自动触发树、地图刷新
|
|
|
|
|
- } catch (error) {
|
|
|
|
|
- mockManholes.value = []
|
|
|
|
|
- const msg = error?.message || error?.msg || '未知错误'
|
|
|
|
|
- console.error('[alarmSanalysis] 加载失败:', error)
|
|
|
|
|
- ElMessage.error('井盖设备数据加载失败: ' + msg)
|
|
|
|
|
- } finally {
|
|
|
|
|
- listLoading.value = false
|
|
|
|
|
- }
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-async function openDeviceDetail(device) {
|
|
|
|
|
- if (!device?.equipmentId) return
|
|
|
|
|
- selectedDevice.value = device
|
|
|
|
|
- drawerVisible.value = true
|
|
|
|
|
- detailLoading.value = true
|
|
|
|
|
- try {
|
|
|
|
|
- const res = await getManholeAlarmDeviceDetail(device.equipmentId)
|
|
|
|
|
- if (res?.data) {
|
|
|
|
|
- const index = mockManholes.value.findIndex(item => item.equipmentId === device.equipmentId)
|
|
|
|
|
- selectedDevice.value = normalizeDevice(res.data, index, device)
|
|
|
|
|
- if (mapInstance && window.BMapGL && isValidCoordinate(selectedDevice.value.lng, selectedDevice.value.lat)) {
|
|
|
|
|
- mapInstance.panTo(toBdPoint(selectedDevice.value.lng, selectedDevice.value.lat))
|
|
|
|
|
- }
|
|
|
|
|
- renderMap()
|
|
|
|
|
- }
|
|
|
|
|
- } catch (error) {
|
|
|
|
|
- ElMessage.error('设备详情加载失败')
|
|
|
|
|
- } finally {
|
|
|
|
|
- detailLoading.value = false
|
|
|
|
|
- }
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-function zoomIn() {
|
|
|
|
|
- if (!mapInstance) return
|
|
|
|
|
- mapInstance.setZoom(Math.min(mapInstance.getZoom() + 1, 18))
|
|
|
|
|
-}
|
|
|
|
|
-function zoomOut() {
|
|
|
|
|
- if (!mapInstance) return
|
|
|
|
|
- mapInstance.setZoom(Math.max(mapInstance.getZoom() - 1, 11))
|
|
|
|
|
-}
|
|
|
|
|
-function resetView() {
|
|
|
|
|
- if (!mapInstance || !window.BMapGL) return
|
|
|
|
|
- fitMapViewport()
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-const onLayerChange = (_data, checkedInfo) => {
|
|
|
|
|
- // check-strictly 模式下 checkedKeys 只含叶子节点,仅需排除 device-*
|
|
|
|
|
- const statusKeys = checkedInfo.checkedKeys.filter(key => !key.startsWith('device-'))
|
|
|
|
|
- const newSet = new Set(statusKeys)
|
|
|
|
|
- const oldSet = visibleLayers.value
|
|
|
|
|
- // 值未变时跳过,阻断 setCheckedKeys → onLayerChange → watch 循环
|
|
|
|
|
- if (newSet.size === oldSet.size && [...newSet].every(k => oldSet.has(k))) return
|
|
|
|
|
- visibleLayers.value = newSet
|
|
|
|
|
- checkedKeys.value = statusKeys
|
|
|
|
|
-}
|
|
|
|
|
-const focusLayer = async (type) => {
|
|
|
|
|
- activeQuickFilter.value = activeQuickFilter.value === type ? '' : type
|
|
|
|
|
- const target = displayDevices.value.find(item => item.status === type)
|
|
|
|
|
- if (!target) {
|
|
|
|
|
- if (activeQuickFilter.value) {
|
|
|
|
|
- ElMessage.info('当前筛选下无对应设备')
|
|
|
|
|
- }
|
|
|
|
|
- return
|
|
|
|
|
- }
|
|
|
|
|
- if (!activeQuickFilter.value) {
|
|
|
|
|
- ElMessage.info('当前无对应设备')
|
|
|
|
|
- return
|
|
|
|
|
- }
|
|
|
|
|
- await nextTick()
|
|
|
|
|
- renderMap()
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-const selectedDevice = ref(null)
|
|
|
|
|
-const drawerVisible = ref(false)
|
|
|
|
|
-const timeRange = ref([])
|
|
|
|
|
-const shortcuts = [{ text: '最近24小时', value: () => [new Date(Date.now() - 86400000), new Date()] }]
|
|
|
|
|
-
|
|
|
|
|
-const getStatusTag = (status) => {
|
|
|
|
|
- if (status === 'critical') return 'danger'
|
|
|
|
|
- if (status === 'warning') return 'warning'
|
|
|
|
|
- if (status === 'offline') return 'info'
|
|
|
|
|
- return 'success'
|
|
|
|
|
-}
|
|
|
|
|
-const getAlertAdvice = (device) => {
|
|
|
|
|
- if (device.alertType?.includes('倾斜')) return '建议立即安排检修人员现场确认井盖固定情况,如损坏需更换。'
|
|
|
|
|
- if (device.alertType?.includes('水位')) return '井内水位过高,需紧急排水,检查管道是否堵塞。'
|
|
|
|
|
- if (device.alertType?.includes('水浸')) return '建议核查井内积水和周边排水情况,必要时安排现场排查。'
|
|
|
|
|
- if (device.alertType?.includes('低电量')) return '建议尽快安排电池更换,避免设备掉线。'
|
|
|
|
|
- if (device.alertType?.includes('弱信号')) return '建议核查设备天线、网络覆盖和安装环境。'
|
|
|
|
|
- return '请根据现场情况迅速核实并处置。'
|
|
|
|
|
-}
|
|
|
|
|
-const dispatchWorkOrder = () => { ElMessage.success('已派发工单至维护班组') }
|
|
|
|
|
-
|
|
|
|
|
-function handleSearch() {
|
|
|
|
|
- activeQuickFilter.value = ''
|
|
|
|
|
- loadDeviceList()
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-function handleReset() {
|
|
|
|
|
- queryParams.value.keyword = ''
|
|
|
|
|
- activeQuickFilter.value = ''
|
|
|
|
|
- visibleLayers.value = new Set(DEFAULT_CHECKED_KEYS)
|
|
|
|
|
- checkedKeys.value = [...DEFAULT_CHECKED_KEYS]
|
|
|
|
|
- loadDeviceList()
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-watch(displayDevices, async () => {
|
|
|
|
|
- refreshBounds()
|
|
|
|
|
- await nextTick()
|
|
|
|
|
- // 树数据变化后 el-tree 内部状态被重置,用 setCheckedKeys 恢复勾选
|
|
|
|
|
- layerTreeRef.value?.setCheckedKeys(checkedKeys.value)
|
|
|
|
|
- renderMap()
|
|
|
|
|
-}, { deep: true })
|
|
|
|
|
-
|
|
|
|
|
-onMounted(async () => {
|
|
|
|
|
- await nextTick()
|
|
|
|
|
- await initMap()
|
|
|
|
|
- await loadDeviceList()
|
|
|
|
|
- // renderMap 由 watch(displayDevices) 自动触发,避免重复渲染
|
|
|
|
|
- window.addEventListener('resize', renderMap)
|
|
|
|
|
-})
|
|
|
|
|
-onUnmounted(() => {
|
|
|
|
|
- window.removeEventListener('resize', renderMap)
|
|
|
|
|
- clearMapOverlays()
|
|
|
|
|
- mapInstance = null
|
|
|
|
|
-})
|
|
|
|
|
-</script>
|
|
|
|
|
-
|
|
|
|
|
-<style scoped>
|
|
|
|
|
-.gis-alert-dashboard {
|
|
|
|
|
- background: #eef2f0;
|
|
|
|
|
- height: 100vh;
|
|
|
|
|
- display: flex;
|
|
|
|
|
- flex-direction: column;
|
|
|
|
|
- overflow: hidden;
|
|
|
|
|
-}
|
|
|
|
|
-.top-bar {
|
|
|
|
|
- background: white;
|
|
|
|
|
- padding: 10px 24px;
|
|
|
|
|
- display: flex;
|
|
|
|
|
- justify-content: space-between;
|
|
|
|
|
- align-items: center;
|
|
|
|
|
- box-shadow: 0 2px 10px rgba(0,0,0,0.05);
|
|
|
|
|
-}
|
|
|
|
|
-.logo-area { display: flex; align-items: center; gap: 14px; }
|
|
|
|
|
-.title { font-size: 1.5rem; font-weight: 700; background: linear-gradient(135deg, #1e5128, #409eff); -webkit-background-clip: text; background-clip: text; color: transparent; }
|
|
|
|
|
-.alert-stats { display: flex; gap: 24px; }
|
|
|
|
|
-.stat { text-align: center; cursor: pointer; padding: 4px 16px; border-radius: 40px; transition: 0.2s; }
|
|
|
|
|
-.stat:hover { background: #f0f2f5; }
|
|
|
|
|
-.stat-value { font-size: 26px; font-weight: bold; display: block; line-height: 1.2; }
|
|
|
|
|
-.stat-value.critical { color: #f56c6c; }
|
|
|
|
|
-.stat-value.warning { color: #e6a23c; }
|
|
|
|
|
-.stat-value.offline { color: #909399; }
|
|
|
|
|
-.stat-label { font-size: 12px; color: #606266; }
|
|
|
|
|
-.main-layout { display: flex; flex: 1; gap: 16px; padding: 16px; overflow: hidden; }
|
|
|
|
|
-.layer-control {
|
|
|
|
|
- width: 280px;
|
|
|
|
|
- background: white;
|
|
|
|
|
- border-radius: 20px;
|
|
|
|
|
- padding: 16px;
|
|
|
|
|
- display: flex;
|
|
|
|
|
- flex-direction: column;
|
|
|
|
|
- gap: 14px;
|
|
|
|
|
- overflow-y: auto;
|
|
|
|
|
-}
|
|
|
|
|
-.layer-header { font-weight: bold; display: flex; align-items: center; gap: 8px; margin-bottom: 12px; }
|
|
|
|
|
-.layer-filter-panel {
|
|
|
|
|
- display: flex;
|
|
|
|
|
- flex-direction: column;
|
|
|
|
|
- gap: 10px;
|
|
|
|
|
-}
|
|
|
|
|
-.filter-actions {
|
|
|
|
|
- display: flex;
|
|
|
|
|
- gap: 8px;
|
|
|
|
|
-}
|
|
|
|
|
-.filter-summary {
|
|
|
|
|
- display: flex;
|
|
|
|
|
- flex-wrap: wrap;
|
|
|
|
|
- gap: 6px;
|
|
|
|
|
-}
|
|
|
|
|
-.layer-tree { flex: 1; }
|
|
|
|
|
-.layer-badge { margin-left: auto; }
|
|
|
|
|
-.legend-panel { border-top: 1px solid #e9ecef; padding-top: 12px; margin-top: 8px; }
|
|
|
|
|
-.legend-title { margin-bottom: 8px; font-weight: 500; font-size: 13px; }
|
|
|
|
|
-.legend-marker { display: inline-block; width: 14px; height: 14px; border-radius: 14px; margin-right: 8px; vertical-align: middle; }
|
|
|
|
|
-.critical-dot { background: #f56c6c; box-shadow: 0 0 0 2px rgba(245,108,108,0.4); }
|
|
|
|
|
-.warning-dot { background: #e6a23c; }
|
|
|
|
|
-.normal-dot { background: #67c23a; }
|
|
|
|
|
-.offline-dot { background: #909399; }
|
|
|
|
|
-.alarm-pulse { width: 14px; height: 14px; background: red; border-radius: 50%; animation: pulse 1s infinite; display: inline-block; margin-right: 8px; }
|
|
|
|
|
-@keyframes pulse { 0% { opacity: 1; } 50% { opacity: 0.4; transform: scale(1.2); } 100% { opacity: 1; } }
|
|
|
|
|
-.gis-map-area {
|
|
|
|
|
- flex: 1;
|
|
|
|
|
- background: white;
|
|
|
|
|
- border-radius: 20px;
|
|
|
|
|
- display: flex;
|
|
|
|
|
- flex-direction: column;
|
|
|
|
|
- overflow: hidden;
|
|
|
|
|
-}
|
|
|
|
|
-.map-toolbar {
|
|
|
|
|
- padding: 12px 16px;
|
|
|
|
|
- border-bottom: 1px solid #edf2f0;
|
|
|
|
|
- display: flex;
|
|
|
|
|
- justify-content: space-between;
|
|
|
|
|
-}
|
|
|
|
|
-.current-stat { display: flex; gap: 20px; font-size: 13px; }
|
|
|
|
|
-.map-container {
|
|
|
|
|
- flex: 1;
|
|
|
|
|
- position: relative;
|
|
|
|
|
- overflow: hidden;
|
|
|
|
|
-}
|
|
|
|
|
-.baidu-map {
|
|
|
|
|
- width: 100%;
|
|
|
|
|
- height: 100%;
|
|
|
|
|
-}
|
|
|
|
|
-.empty-state {
|
|
|
|
|
- position: absolute;
|
|
|
|
|
- top: 50%;
|
|
|
|
|
- left: 50%;
|
|
|
|
|
- transform: translate(-50%, -50%);
|
|
|
|
|
- color: #909399;
|
|
|
|
|
- font-size: 14px;
|
|
|
|
|
- background: rgba(255, 255, 255, 0.92);
|
|
|
|
|
- padding: 10px 18px;
|
|
|
|
|
- border-radius: 12px;
|
|
|
|
|
- box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
|
|
|
|
|
- z-index: 2;
|
|
|
|
|
-}
|
|
|
|
|
-.map-error {
|
|
|
|
|
- position: absolute;
|
|
|
|
|
- top: 16px;
|
|
|
|
|
- right: 16px;
|
|
|
|
|
- max-width: 320px;
|
|
|
|
|
- color: #f56c6c;
|
|
|
|
|
- background: rgba(255, 255, 255, 0.95);
|
|
|
|
|
- border: 1px solid rgba(245, 108, 108, 0.25);
|
|
|
|
|
- border-radius: 12px;
|
|
|
|
|
- padding: 10px 12px;
|
|
|
|
|
- font-size: 12px;
|
|
|
|
|
- line-height: 1.6;
|
|
|
|
|
- z-index: 3;
|
|
|
|
|
-}
|
|
|
|
|
-.advice-box { background: #f8f9fa; padding: 12px; border-radius: 12px; }
|
|
|
|
|
-.panel-tip {
|
|
|
|
|
- font-size: 12px;
|
|
|
|
|
- color: #909399;
|
|
|
|
|
- line-height: 1.5;
|
|
|
|
|
-}
|
|
|
|
|
-</style>
|
|
|