2 Commitit c4941073db ... 46d606626a

Tekijä SHA1 Viesti Päivämäärä
  nahida 46d606626a Merge remote-tracking branch 'origin/master' 1 viikko sitten
  nahida 786ba82b26 feat(manholeCover): 集成百度地图显示报警位置 1 viikko sitten
1 muutettua tiedostoa jossa 231 lisäystä ja 7 poistoa
  1. 231 7
      src/views/subSystem/manholeCover/alarm/alarmList.vue

+ 231 - 7
src/views/subSystem/manholeCover/alarm/alarmList.vue

@@ -111,19 +111,24 @@
     </div>
 
     <!-- 地图弹窗 -->
-    <el-dialog v-model="mapDialogVisible" title="报警位置地图" width="600px">
-      <div id="simpleMap" style="height: 400px; background: #e9f5e9; border-radius: 12px; display: flex; align-items: center; justify-content: center; flex-direction: column;">
-        <el-icon :size="48" color="#f56c6c"><Location /></el-icon>
-        <div><strong>{{ selectedAlert?.location }}</strong></div>
-        <div>经度: {{ selectedAlert?.lng || '暂无数据' }}, 纬度: {{ selectedAlert?.lat || '暂无数据' }}</div>
-        <div style="margin-top: 20px; font-size: 12px; color: gray">当前仅展示接口返回的真实坐标信息</div>
+    <el-dialog v-model="mapDialogVisible" title="报警位置地图" width="720px" @opened="handleMapDialogOpened">
+      <div class="dialog-map-wrapper">
+        <div ref="dialogMapRef" class="dialog-baidu-map"></div>
+        <div v-if="dialogMapError" class="dialog-map-error">{{ dialogMapError }}</div>
+        <div v-if="!hasAlertCoordinate" class="dialog-map-empty">
+          当前报警缺少真实坐标,已展示默认地图视野
+        </div>
+        <div class="dialog-map-footer">
+          <div><strong>{{ selectedAlert?.location || '暂无位置' }}</strong></div>
+          <div>经度: {{ selectedAlert?.lng || '暂无数据' }}, 纬度: {{ selectedAlert?.lat || '暂无数据' }}</div>
+        </div>
       </div>
     </el-dialog>
   </div>
 </template>
 
 <script setup>
-import {computed, onMounted, ref} from 'vue'
+import {computed, nextTick, onMounted, onUnmounted, ref, watch} from 'vue'
 import {CircleCheck, InfoFilled, Location, Warning, WarningFilled} from '@element-plus/icons-vue'
 import {ElMessage} from 'element-plus'
 import {getManholeAlarmDeviceDetail, getManholeAlarmDeviceList} from '@/api/pipeNetwork/basic'
@@ -135,12 +140,25 @@ const selectedAlert = ref(null)
 const mapDialogVisible = ref(false)
 const listLoading = ref(false)
 const detailLoading = ref(false)
+const dialogMapRef = ref(null)
+const dialogMapError = ref('')
+
+const BAIDU_MAP_AK = import.meta.env.VITE_BAIDU_MAP_AK || ''
+const DEFAULT_MAP_CENTER = { lng: 110.393, lat: 28.452 }
+
+let dialogMapInstance = null
+let dialogMapOverlays = []
+let baiduMapScriptPromise = null
 
 function toNumber(value) {
   const num = Number(value)
   return Number.isFinite(num) ? num : null
 }
 
+function isValidCoordinate(lng, lat) {
+  return lng !== null && lat !== null && lng >= 70 && lng <= 140 && lat >= 3 && lat <= 55
+}
+
 function formatDateTime(value) {
   if (!value) return '—'
   return String(value).replace('T', ' ')
@@ -337,6 +355,7 @@ const totalAlerts = computed(() => alerts.value.length)
 const criticalCount = computed(() => alerts.value.filter(a => a.level === 'critical').length)
 const warningCount = computed(() => alerts.value.filter(a => a.level === 'warning').length)
 const unhandledCount = computed(() => alerts.value.length)
+const hasAlertCoordinate = computed(() => isValidCoordinate(toNumber(selectedAlert.value?.lng), toNumber(selectedAlert.value?.lat)))
 
 const filterAlerts = (type) => {
   filterType.value = type
@@ -351,9 +370,173 @@ const viewOnMap = () => {
   mapDialogVisible.value = true
 }
 
+function clearDialogMapMarker() {
+  if (!dialogMapInstance || !dialogMapOverlays.length) return
+  dialogMapOverlays.forEach((overlay) => {
+    try {
+      dialogMapInstance.removeOverlay(overlay)
+    } catch (_) {}
+  })
+  dialogMapOverlays = []
+}
+
+function getDialogInfoWindowHtml() {
+  const levelTagColor = selectedAlert.value?.level === 'critical' ? '#f56c6c' : '#e6a23c'
+  const levelText = selectedAlert.value?.levelText || '报警'
+  return `
+    <div style="min-width:220px;max-width:280px;padding:4px 2px;line-height:1.7;color:#303133;">
+      <div style="font-size:14px;font-weight:700;color:#1f2937;margin-bottom:6px;">
+        ${selectedAlert.value?.deviceName || '报警设备'}
+      </div>
+      <div style="margin-bottom:4px;">
+        <span style="display:inline-block;padding:1px 8px;border-radius:999px;background:${levelTagColor};color:#fff;font-size:12px;">
+          ${levelText}
+        </span>
+      </div>
+      <div style="font-size:12px;"><span style="color:#909399;">报警类型:</span>${selectedAlert.value?.alertType || '—'}</div>
+      <div style="font-size:12px;"><span style="color:#909399;">报警时间:</span>${selectedAlert.value?.time || '—'}</div>
+      <div style="font-size:12px;"><span style="color:#909399;">实时数据:</span>${selectedAlert.value?.realData || selectedAlert.value?.exceedValue || '—'}</div>
+      <div style="font-size:12px;"><span style="color:#909399;">阈值:</span>${selectedAlert.value?.threshold || '—'}</div>
+      <div style="font-size:12px;"><span style="color:#909399;">报警位置:</span>${selectedAlert.value?.location || '—'}</div>
+    </div>
+  `
+}
+
+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
+}
+
+function renderDialogMapMarker() {
+  if (!dialogMapInstance || !window.BMapGL) return
+  clearDialogMapMarker()
+
+  const lng = toNumber(selectedAlert.value?.lng)
+  const lat = toNumber(selectedAlert.value?.lat)
+  if (!isValidCoordinate(lng, lat)) {
+    dialogMapInstance.centerAndZoom(new window.BMapGL.Point(DEFAULT_MAP_CENTER.lng, DEFAULT_MAP_CENTER.lat), 13)
+    return
+  }
+
+  const point = new window.BMapGL.Point(lng, lat)
+  dialogMapInstance.centerAndZoom(point, 15)
+  const marker = new window.BMapGL.Marker(point)
+  const infoWindow = new window.BMapGL.InfoWindow(getDialogInfoWindowHtml(), {
+    width: 260,
+    title: '报警信息'
+  })
+  dialogMapInstance.addOverlay(marker)
+  dialogMapOverlays.push(marker)
+  marker.addEventListener('click', () => {
+    dialogMapInstance.openInfoWindow(infoWindow, point)
+  })
+
+  const label = new window.BMapGL.Label(
+    `<div style="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;">${selectedAlert.value?.deviceName || '报警设备'}</div>`,
+    {
+      position: point,
+      offset: new window.BMapGL.Size(-30, -40)
+    }
+  )
+  label.setStyle({
+    border: 'none',
+    background: 'transparent',
+    padding: '0'
+  })
+  label.addEventListener('click', () => {
+    dialogMapInstance.openInfoWindow(infoWindow, point)
+  })
+  dialogMapInstance.addOverlay(label)
+  dialogMapOverlays.push(label)
+  dialogMapInstance.openInfoWindow(infoWindow, point)
+}
+
+async function initDialogMap() {
+  if (!dialogMapRef.value) return
+  try {
+    await ensureBaiduMap()
+    if (!dialogMapInstance) {
+      dialogMapInstance = new window.BMapGL.Map(dialogMapRef.value, { enableMapClick: true })
+      dialogMapInstance.enableScrollWheelZoom(true)
+      dialogMapInstance.centerAndZoom(new window.BMapGL.Point(DEFAULT_MAP_CENTER.lng, DEFAULT_MAP_CENTER.lat), 13)
+    }
+    dialogMapError.value = ''
+    renderDialogMapMarker()
+  } catch (error) {
+    dialogMapError.value = error?.message || '百度地图加载失败'
+  }
+}
+
+async function handleMapDialogOpened() {
+  await nextTick()
+  await initDialogMap()
+}
+
+watch(
+  () => selectedAlert.value?.id,
+  async () => {
+    if (!mapDialogVisible.value) return
+    await nextTick()
+    renderDialogMapMarker()
+  }
+)
+
 onMounted(async () => {
   await loadAlerts()
 })
+
+onUnmounted(() => {
+  clearDialogMapMarker()
+  dialogMapInstance = null
+})
 </script>
 
 <style scoped>
@@ -447,4 +630,45 @@ onMounted(async () => {
 .warning-value { color: #f56c6c; font-size: 18px; margin: 0 4px; }
 .action-buttons { display: flex; gap: 8px; }
 .empty-detail { flex: 1; display: flex; align-items: center; justify-content: center; background: white; border-radius: 20px; }
+.dialog-map-wrapper {
+  position: relative;
+}
+.dialog-baidu-map {
+  height: 420px;
+  border-radius: 12px;
+  overflow: hidden;
+  background: #eef2f0;
+}
+.dialog-map-footer {
+  padding-top: 12px;
+  font-size: 13px;
+  color: #606266;
+  line-height: 1.8;
+}
+.dialog-map-empty {
+  position: absolute;
+  top: 16px;
+  left: 16px;
+  background: rgba(255, 255, 255, 0.94);
+  color: #909399;
+  font-size: 12px;
+  padding: 8px 12px;
+  border-radius: 10px;
+  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
+  z-index: 2;
+}
+.dialog-map-error {
+  position: absolute;
+  top: 16px;
+  right: 16px;
+  max-width: 280px;
+  background: rgba(255, 255, 255, 0.95);
+  color: #f56c6c;
+  border: 1px solid rgba(245, 108, 108, 0.25);
+  border-radius: 10px;
+  padding: 8px 12px;
+  font-size: 12px;
+  line-height: 1.6;
+  z-index: 2;
+}
 </style>