| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849 |
- <!--监测报警一张图-->
- <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="layerTree"
- show-checkbox
- node-key="id"
- :default-checked-keys="defaultCheckedLayers"
- @check="onLayerChange"
- class="layer-tree"
- >
- <template #default="{ node, data }">
- <span class="layer-node">
- <el-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">
- <div v-if="selectedDevice" v-loading="detailLoading">
- <el-descriptions :column="1" border>
- <el-descriptions-item label="设备编号">{{ selectedDevice.id }}</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 {getManholeAlarmDeviceDetail, getManholeAlarmDeviceList} from '@/api/pipeNetwork/basic'
- const layerTree = ref([
- {
- id: 'current-status',
- label: '运维状态筛选',
- icon: Setting,
- alertCount: 0,
- children: [
- { id: 'current-1', label: '在用设备', icon: Monitor, alertCount: 0 },
- { id: 'current-3', label: '维修设备', icon: Setting, alertCount: 0 }
- ]
- },
- {
- id: 'alarm-status',
- label: '报警状态筛选',
- icon: WarningFilled,
- alertCount: 0,
- children: [
- { id: 'alarm-1', label: '报警设备', icon: WarningFilled, alertCount: 0 },
- { id: 'alarm-0', label: '正常设备', icon: InfoFilled, alertCount: 0 }
- ]
- },
- {
- id: 'online-status',
- label: '在线状态筛选',
- icon: Connection,
- alertCount: 0,
- children: [
- { id: 'online-1', label: '在线设备', icon: Connection, alertCount: 0 },
- { id: 'online-0', label: '离线设备', icon: Connection, alertCount: 0 }
- ]
- }
- ])
- const defaultCheckedLayers = ['current-1', 'current-3', 'alarm-1', 'alarm-0', 'online-1', 'online-0']
- const visibleLayers = ref(new Set(defaultCheckedLayers))
- const layerTreeRef = ref(null)
- 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
- }
- function getSingleCheckedValue(checkedKeys, prefix) {
- const matched = [...checkedKeys].filter(key => key.startsWith(prefix))
- if (matched.length !== 1) return undefined
- const value = Number(matched[0].slice(prefix.length))
- return Number.isFinite(value) ? value : undefined
- }
- const displayDevices = computed(() => {
- return mockManholes.value.filter(matchesQuickFilter)
- })
- const criticalCount = computed(() => mockManholes.value.filter(m => m.status === 'critical').length)
- const warningCount = computed(() => mockManholes.value.filter(m => m.status === 'warning').length)
- const normalCount = computed(() => mockManholes.value.filter(m => m.status === 'normal').length)
- const offlineCount = computed(() => mockManholes.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
- }
- 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 || '',
- 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() {
- const sourceDevices = mockManholes.value
- const critical = criticalCount.value
- const warning = warningCount.value
- const normal = normalCount.value
- const offline = offlineCount.value
- const inUse = sourceDevices.filter(item => Number(item.raw?.equipmentStatus?.currentStatus) === 1).length
- const maintenance = sourceDevices.filter(item => Number(item.raw?.equipmentStatus?.currentStatus) === 3).length
- layerTree.value[0].alertCount = inUse + maintenance
- layerTree.value[0].children[0].alertCount = inUse
- layerTree.value[0].children[1].alertCount = maintenance
- layerTree.value[1].alertCount = critical + warning + normal
- layerTree.value[1].children[0].alertCount = critical + warning
- layerTree.value[1].children[1].alertCount = normal
- layerTree.value[2].alertCount = totalDevices.value
- layerTree.value[2].children[0].alertCount = totalDevices.value - offline
- layerTree.value[2].children[1].alertCount = offline
- }
- 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(new window.BMapGL.Point(DEFAULT_MAP_CENTER.lng, DEFAULT_MAP_CENTER.lat), 14)
- return
- }
- const points = displayDevices.value
- .filter(device => isValidCoordinate(device.lng, device.lat))
- .map(device => new window.BMapGL.Point(device.lng, device.lat))
- if (!points.length) {
- mapInstance.centerAndZoom(new window.BMapGL.Point(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
- clearMapOverlays()
- displayDevices.value
- .filter(device => isValidCoordinate(device.lng, device.lat))
- .forEach((device) => {
- const point = new window.BMapGL.Point(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()
- }
- function renderMap() {
- if (!mapInstance || !window.BMapGL) return
- renderMapOverlays()
- }
- 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(new window.BMapGL.Point(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()
- const checkedKeys = visibleLayers.value
- const currentStatus = getSingleCheckedValue(checkedKeys, 'current-')
- const alarmStatus = getSingleCheckedValue(checkedKeys, 'alarm-')
- const onlineStatus = getSingleCheckedValue(checkedKeys, 'online-')
- const res = await getManholeAlarmDeviceList({
- keyword: keyword || undefined,
- currentStatus,
- alarmStatus,
- onlineStatus,
- isQueryManholeData: true
- })
- const list = Array.isArray(res?.data) ? res.data : []
- mockManholes.value = list.map((item, index) => normalizeDevice(item, index))
- refreshLayerTree()
- refreshBounds()
- await nextTick()
- renderMap()
- } catch (error) {
- mockManholes.value = []
- ElMessage.error('井盖设备数据加载失败')
- } 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(new window.BMapGL.Point(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) => {
- visibleLayers.value = new Set(checkedInfo.checkedKeys)
- loadDeviceList()
- }
- 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(defaultCheckedLayers)
- layerTreeRef.value?.setCheckedKeys(defaultCheckedLayers)
- loadDeviceList()
- }
- watch(displayDevices, async () => {
- refreshBounds()
- await nextTick()
- renderMap()
- }, { deep: true })
- onMounted(async () => {
- await nextTick()
- await initMap()
- await loadDeviceList()
- nextTick(() => {
- renderMap()
- 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>
|