Kaynağa Gözat

百度替换替换成天地图

zengy 4 gün önce
ebeveyn
işleme
7e1ec59d6e
53 değiştirilmiş dosya ile 1261 ekleme ve 895 silme
  1. 3 0
      .env.development
  2. 7 0
      .env.production
  3. 0 1
      index.html
  4. 4 32
      src/components/EquipmentEditorFields/index.vue
  5. 46 81
      src/components/GisMapCanvas.vue
  6. 4 0
      src/main.js
  7. 9 82
      src/utils/coordTransform.js
  8. 134 0
      src/utils/mapApi.js
  9. 454 0
      src/utils/tiandituMap.js
  10. 6 2
      src/views/subSystem/alarm/AlarmMap.vue
  11. 1 2
      src/views/subSystem/alarm/common/alarmGIS.vue
  12. 11 20
      src/views/subSystem/basic/Manhole.vue
  13. 28 35
      src/views/subSystem/basic/RepairRecord.vue
  14. 13 18
      src/views/subSystem/dispatch/disOnDuty/cphs.vue
  15. 11 24
      src/views/subSystem/dispatch/disOnDuty/jbgl.vue
  16. 1 1
      src/views/subSystem/dispatch/dishome/disHome.vue
  17. 11 11
      src/views/subSystem/drainage/jcsj/bzyxqk.vue
  18. 4 5
      src/views/subSystem/drainage/jcsj/gcssgl.vue
  19. 22 40
      src/views/subSystem/drainage/jcsj/gwyxqk.vue
  20. 11 157
      src/views/subSystem/drainage/jcsj/jsqc.vue
  21. 19 19
      src/views/subSystem/drainage/jcsj/psfqgl.vue
  22. 1 1
      src/views/subSystem/drainage/jcsj/psgwgl.vue
  23. 3 3
      src/views/subSystem/drainage/jcsj/sbyx.vue
  24. 2 3
      src/views/subSystem/drainage/jcsj/yjsdgl.vue
  25. 18 14
      src/views/subSystem/drainage/jcyj/dqyjGIS.vue
  26. 17 13
      src/views/subSystem/drainage/jcyj/lsyjGIS.vue
  27. 1 1
      src/views/subSystem/drainage/spjc/jcspGIS.vue
  28. 1 1
      src/views/subSystem/gas/gasBasicadata/gasPipelintNet.vue
  29. 1 1
      src/views/subSystem/gas/gasBasicadata/gasTestingeq.vue
  30. 1 1
      src/views/subSystem/gas/gasOperamon/czspjk.vue
  31. 1 1
      src/views/subSystem/gas/gasOperamon/gxlljc.vue
  32. 1 1
      src/views/subSystem/gas/gasOperamon/gxyljc.vue
  33. 27 34
      src/views/subSystem/gas/gasOperamon/ssjcyzt.vue
  34. 20 27
      src/views/subSystem/gas/gasOperamon/xldxkjkrqtndjc.vue
  35. 137 70
      src/views/subSystem/gas/gasfxpg/gwfxpg.vue
  36. 1 1
      src/views/subSystem/gas/gasfxpg/gwfxpgqd.vue
  37. 1 1
      src/views/subSystem/gas/gasfxpg/gwfxxq.vue
  38. 24 33
      src/views/subSystem/gas/gasfxpg/gxfxpgsst.vue
  39. 1 1
      src/views/subSystem/gas/gashiddendanger/gasApicture.vue
  40. 1 1
      src/views/subSystem/gas/gasjcyj/gxllyctx.vue
  41. 1 1
      src/views/subSystem/gas/gasjcyj/gxylyctx.vue
  42. 1 1
      src/views/subSystem/gas/gasjcyj/krqindbj.vue
  43. 1 1
      src/views/subSystem/gas/gasjcyj/xldxkjkrqindbj.vue
  44. 15 22
      src/views/subSystem/hidden/HazardMap.vue
  45. 1 1
      src/views/subSystem/manholeCover/device/statistics.vue
  46. 1 1
      src/views/subSystem/manholeCover/mRealTimemonit/mRealApicture.vue
  47. 1 1
      src/views/subSystem/manholeCover/operation/mOdataStatistics.vue
  48. 129 40
      src/views/subSystem/pipeNetwork/assetInfo/equipmentBase/index.vue
  49. 2 3
      src/views/subSystem/pipeNetwork/assetInfo/monitorPoint/index.vue
  50. 19 67
      src/views/subSystem/pipeNetwork/home/index.vue
  51. 6 2
      src/views/subSystem/risk/RiskMap.vue
  52. 18 17
      tests/gisMapCanvas.test.mjs
  53. 8 0
      vite.config.js

+ 3 - 0
.env.development

@@ -9,3 +9,6 @@ VITE_APP_BASE_API = '/dev-api'
 
 # MinIO文件访问基础地址
 VITE_APP_MINIO_BASE_URL = 'http://111.23.174.45:9000/pipe/'
+
+# 天地图tk(底图瓦片与地理编码共用)
+VITE_TIANDITU_TK = 167ae255a96f2cb7b60b56c5b6d304a1

+ 7 - 0
.env.production

@@ -12,3 +12,10 @@ VITE_BUILD_COMPRESS = gzip
 
 # MinIO 统一通过同源 Nginx 路径访问,不在前端绑定服务器 IP
 VITE_APP_MINIO_BASE_URL = '/minio/pipe/'
+
+# 天地图tk(底图瓦片与地理编码共用)
+VITE_TIANDITU_TK = 167ae255a96f2cb7b60b56c5b6d304a1
+
+# 部署备注:地理编码走同源 /tdtApi,生产需在 Nginx 配置
+#   location /tdtApi/ { proxy_pass https://api.tianditu.gov.cn/; }
+# 瓦片为图片直连 t{s}.tianditu.gov.cn,无需代理;tk 需在控制台配置域名/Referer 白名单

+ 0 - 1
index.html

@@ -209,7 +209,6 @@
       <div class="load_title">正在加载系统资源,请耐心等待</div>
     </div>
   </div>
-  <script type="text/javascript" src="https://api.map.baidu.com/api?type=webgl&v=1.0&ak=vMma3LYQQyrfhlFUm0CFzXFQhBrwxAmi"></script>
   <script type="module" src="/src/main.js"></script>
 </body>
 

+ 4 - 32
src/components/EquipmentEditorFields/index.vue

@@ -155,9 +155,9 @@
 </template>
 
 <script setup>
+import { lookupAddress } from '@/utils/mapApi'
 import { computed, ref } from 'vue'
 import GisMapCanvas from '@/components/GisMapCanvas.vue'
-import { bd09ToWgs84 } from '@/utils/coordTransform'
 const props = defineProps({ modelValue: { type: Object, required: true }, types: { type: Array, default: () => [] }, typeLocked: Boolean })
 const emit = defineEmits(['update:modelValue'])
 const form = computed({ get: () => props.modelValue, set: value => emit('update:modelValue', value) })
@@ -170,41 +170,13 @@ const rules = {
   equipmentTypeId: [{ required: true, message: '设备类型不能为空', trigger: 'change' }],
   district: [{ required: true, message: '所属片区不能为空', trigger: 'change' }]
 }
-// 逆地理编码:传入百度 BD-09 坐标,返回结构化地址文本,失败时返回空串
-function reverseGeocode(lng, lat) {
-  return new Promise((resolve) => {
-    const BMapGL = window.BMapGL;
-    if (!BMapGL || typeof BMapGL.Geocoder !== "function") {
-      resolve("");
-      return;
-    }
-    try {
-      const geocoder = new BMapGL.Geocoder();
-      geocoder.getLocation(new BMapGL.Point(lng, lat), (result) => {
-        resolve(extractAddress(result));
-      });
-    } catch (error) {
-      resolve("");
-    }
-  });
-}
+// 逆地理编码:传入 CGCS2000 坐标,返回地址文本,失败时返回空串
 
-function extractAddress(result) {
-  const content = result?.content || result || {};
-  const address = result?.address || content.address;
-  if (address) {
-    return String(address);
-  }
-  const components = result?.addressComponents || content.address_detail || {};
-  return [components.province, components.city, components.district, components.street, components.streetNumber]
-    .filter(Boolean)
-    .join("");
-}
 
 // 地图选点后按坐标回填安装位置,解析失败时保留原值
 async function fillLocationByPoint(lng, lat) {
   locationTip.value = "正在解析地址...";
-  const address = await reverseGeocode(lng, lat);
+  const address = await lookupAddress(lng, lat);
   if (address) {
     form.value.equipmentLocation = address;
     locationTip.value = "已按选点回填安装位置,可手工修改。";
@@ -228,7 +200,7 @@ function openMapPicker() {
 
 function handleMapClick(point) {
   if (!point) return;
-  const [longitude, latitude] = bd09ToWgs84(point.lng, point.lat);
+  const longitude = Number(point.lng), latitude = Number(point.lat);
   form.value.longitude = Number(longitude.toFixed(6));
   form.value.latitude = Number(latitude.toFixed(6));
   mapPoints.value = [{ lng: form.value.longitude, lat: form.value.latitude, name: form.value.equipmentName || "设备位置" }];

+ 46 - 81
src/components/GisMapCanvas.vue

@@ -3,6 +3,7 @@
 </template>
 
 <script setup>
+import { ensureTdtMap } from '@/utils/mapApi'
 import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'
 import { convertCoord } from '@/utils/coordTransform'
 import { getMapClusterIcon, getMonitoringPointIcon } from '@/utils/gisMapColors'
@@ -17,8 +18,7 @@ const props = defineProps({
   clusterRadius: { type: Number, default: 48 },
   enableClustering: { type: Boolean, default: true },
   clusterLabel: { type: String, default: '点位集合' },
-  showMarkerName: { type: Boolean, default: true },
-  mapStyleId: { type: String, default: 'a0f4e6e6f9e7a6c5d8e7f7e7f7e7f7e5' }
+  showMarkerName: { type: Boolean, default: true }
 })
 
 const emit = defineEmits(['point-click', 'line-click', 'cluster-click', 'map-click', 'ready', 'error'])
@@ -27,7 +27,6 @@ const mapContainer = ref(null)
 const mapError = ref('')
 let mapInstance = null
 let overlays = []
-let bmapLoadTimer = null
 let disposed = false
 
 const convertedCenter = computed(() => convertCoord(props.center.lng, props.center.lat))
@@ -41,47 +40,17 @@ function escapeHtml(value) {
     .replace(/'/g, '&#39;')
 }
 
-function waitForBMapGL() {
-  return new Promise((resolve, reject) => {
-    if (window.BMapGL) {
-      resolve(window.BMapGL)
-      return
-    }
-    if (disposed) {
-      reject(new Error('GIS map component disposed'))
-      return
-    }
-    let attempts = 0
-    bmapLoadTimer = setInterval(() => {
-      attempts += 1
-      if (disposed) {
-        clearInterval(bmapLoadTimer)
-        reject(new Error('GIS map component disposed'))
-        return
-      }
-      if (window.BMapGL) {
-        clearInterval(bmapLoadTimer)
-        resolve(window.BMapGL)
-        return
-      }
-      if (attempts >= 100) {
-        clearInterval(bmapLoadTimer)
-        reject(new Error('百度地图加载超时'))
-      }
-    }, 200)
-  })
-}
+
 
 async function initMap() {
   if (!mapContainer.value) return
   try {
-    const BMapGL = await waitForBMapGL()
+    const TdtMap = await ensureTdtMap()
     if (disposed || !mapContainer.value) return
-    mapInstance = new BMapGL.Map(mapContainer.value, { enableMapClick: true })
+    mapInstance = new TdtMap.Map(mapContainer.value, { enableMapClick: true })
     const [lng, lat] = convertedCenter.value
-    mapInstance.centerAndZoom(new BMapGL.Point(lng, lat), props.zoom)
+    mapInstance.centerAndZoom(new TdtMap.Point(lng, lat), props.zoom)
     mapInstance.enableScrollWheelZoom(true)
-    mapInstance.setMapStyleV2({ styleId: props.mapStyleId })
     mapInstance.addEventListener('click', (event) => {
       const point = event?.latLng || event?.latlng || event?.point || event?.target?.point
       const lng = typeof point?.lng === 'function' ? point.lng() : point?.lng
@@ -95,7 +64,7 @@ async function initMap() {
     renderScene()
     emit('ready', mapInstance)
   } catch (error) {
-    mapError.value = '百度地图初始化失败,请检查网络或地图服务配置。'
+    mapError.value = '天地图初始化失败,请检查网络或地图服务配置。'
     emit('error', mapError.value)
   }
 }
@@ -113,21 +82,21 @@ function clearScene() {
 }
 
 
-function createMarkerIcon(BMapGL, iconUrl, size) {
-  return new BMapGL.Icon(iconUrl, new BMapGL.Size(size, size), {
-    anchor: new BMapGL.Size(size / 2, size),
-    imageSize: new BMapGL.Size(size, size)
+function createMarkerIcon(TdtMap, iconUrl, size) {
+  return new TdtMap.Icon(iconUrl, new TdtMap.Size(size, size), {
+    anchor: new TdtMap.Size(size / 2, size),
+    imageSize: new TdtMap.Size(size, size)
   })
 }
 
-function addPointMarker(BMapGL, point) {
+function addPointMarker(TdtMap, point) {
   if (!Number.isFinite(Number(point.lng)) || !Number.isFinite(Number(point.lat))) return
   const [lng, lat] = convertCoord(point.lng, point.lat)
   const name = point.name || point.equipmentName || point.facilityName || point.label || ''
   const iconUrl = point.iconUrl || getMonitoringPointIcon(point)
-  const position = new BMapGL.Point(lng, lat)
-  const marker = new BMapGL.Marker(position, {
-    icon: createMarkerIcon(BMapGL, iconUrl, props.markerSize),
+  const position = new TdtMap.Point(lng, lat)
+  const marker = new TdtMap.Marker(position, {
+    icon: createMarkerIcon(TdtMap, iconUrl, props.markerSize),
     enableDragging: false
   })
   marker.addEventListener('click', (event) => {
@@ -138,9 +107,9 @@ function addPointMarker(BMapGL, point) {
   overlays.push(marker)
 
   if (props.showMarkerName && name) {
-    const label = new BMapGL.Label(`<div class="gis-marker-label-text">${escapeHtml(name)}</div>`, {
+    const label = new TdtMap.Label(`<div class="gis-marker-label-text">${escapeHtml(name)}</div>`, {
       position,
-      offset: new BMapGL.Size(-28, -props.markerSize - 18)
+      offset: new TdtMap.Size(-28, -props.markerSize - 18)
     })
     label.setStyle({ border: 'none', background: 'transparent', padding: '0' })
     label.addEventListener('click', (event) => {
@@ -152,11 +121,11 @@ function addPointMarker(BMapGL, point) {
   }
 }
 
-function addClusterMarker(BMapGL, cluster) {
+function addClusterMarker(TdtMap, cluster) {
   const [positionLng, positionLat] = convertCoord(cluster.position.lng, cluster.position.lat)
-  const position = new BMapGL.Point(positionLng, positionLat)
-  const marker = new BMapGL.Marker(position, {
-    icon: createMarkerIcon(BMapGL, getMapClusterIcon(cluster.points), props.markerSize),
+  const position = new TdtMap.Point(positionLng, positionLat)
+  const marker = new TdtMap.Marker(position, {
+    icon: createMarkerIcon(TdtMap, getMapClusterIcon(cluster.points), props.markerSize),
     enableDragging: false
   })
   marker.addEventListener('click', (event) => {
@@ -167,9 +136,9 @@ function addClusterMarker(BMapGL, cluster) {
   mapInstance.addOverlay(marker)
   overlays.push(marker)
 
-  const label = new BMapGL.Label(`<div class="gis-cluster-label-text">${escapeHtml(props.clusterLabel)}:${cluster.count}</div>`, {
+  const label = new TdtMap.Label(`<div class="gis-cluster-label-text">${escapeHtml(props.clusterLabel)}:${cluster.count}</div>`, {
     position,
-    offset: new BMapGL.Size(-28, -props.markerSize - 18)
+    offset: new TdtMap.Size(-28, -props.markerSize - 18)
   })
   label.setStyle({ border: 'none', background: 'transparent', padding: '0' })
   label.addEventListener('click', (event) => {
@@ -188,32 +157,32 @@ function isClusterUnexpandable(cluster) {
 }
 
 function projectMapPoint({ lng, lat }) {
-  const [bmapLng, bmapLat] = convertCoord(lng, lat)
-  const pixel = mapInstance.pointToPixel(new BMapGL.Point(bmapLng, bmapLat))
+  const [pixelLng, pixelLat] = convertCoord(lng, lat)
+  const pixel = mapInstance.pointToPixel(new TdtMap.Point(pixelLng, pixelLat))
   return { x: Number(pixel?.x), y: Number(pixel?.y) }
 }
 
 function focusCluster(cluster) {
-  if (!mapInstance || !window.BMapGL || !cluster?.points?.length) return
-  const bmapPoints = cluster.points.map(point => {
+  if (!mapInstance || !window.TdtMap || !cluster?.points?.length) return
+  const tdtPoints = cluster.points.map(point => {
     const [lng, lat] = convertCoord(point.lng, point.lat)
-    return new BMapGL.Point(lng, lat)
+    return new TdtMap.Point(lng, lat)
   })
-  if (bmapPoints.length === 1) {
+  if (tdtPoints.length === 1) {
     locatePoint(cluster.points[0])
     return
   }
-  mapInstance.setViewport(bmapPoints, { margins: [110, 110, 110, 110] })
+  mapInstance.setViewport(tdtPoints, { margins: [110, 110, 110, 110] })
 }
 
-function addLine(BMapGL, line) {
+function addLine(TdtMap, line) {
   const points = (line.points || []).filter((point) => Number.isFinite(Number(point.lng)) && Number.isFinite(Number(point.lat)))
   if (points.length < 2) return
-  const bmapPoints = points.map((point) => {
+  const tdtPoints = points.map((point) => {
     const [lng, lat] = convertCoord(point.lng, point.lat)
-    return new BMapGL.Point(lng, lat)
+    return new TdtMap.Point(lng, lat)
   })
-  const polyline = new BMapGL.Polyline(bmapPoints, {
+  const polyline = new TdtMap.Polyline(tdtPoints, {
     strokeColor: line.color || '#3498db',
     strokeWeight: line.strokeWeight || 6,
     strokeOpacity: line.strokeOpacity ?? 0.92,
@@ -227,38 +196,38 @@ function addLine(BMapGL, line) {
   overlays.push(polyline)
 }
 function renderScene() {
-  if (!mapInstance || !window.BMapGL) return
+  if (!mapInstance || !window.TdtMap) return
   clearScene()
-  const BMapGL = window.BMapGL
+  const TdtMap = window.TdtMap
   const renderPoints = props.points
   const renderedPoints = props.enableClustering
     ? mergeMapPoints(renderPoints, projectMapPoint, props.clusterRadius)
     : renderPoints.map(point => ({ type: 'point', point, position: { lng: point.lng, lat: point.lat } }))
-  props.lines.forEach((line) => addLine(BMapGL, line))
+  props.lines.forEach((line) => addLine(TdtMap, line))
   renderedPoints.forEach((item) => {
-    if (item.type === 'cluster') addClusterMarker(BMapGL, item)
-    else addPointMarker(BMapGL, item.point)
+    if (item.type === 'cluster') addClusterMarker(TdtMap, item)
+    else addPointMarker(TdtMap, item.point)
   })
 }
 
 function fitBounds() {
-  if (!mapInstance || !window.BMapGL) return
+  if (!mapInstance || !window.TdtMap) return
   const points = [
     ...props.points.map((point) => ({ lng: point.lng, lat: point.lat })),
     ...props.lines.flatMap((line) => line.points || [])
   ].filter((point) => Number.isFinite(Number(point.lng)) && Number.isFinite(Number(point.lat)))
   if (!points.length) return
-  const bmapPoints = points.map((point) => {
+  const tdtPoints = points.map((point) => {
     const [lng, lat] = convertCoord(point.lng, point.lat)
-    return new window.BMapGL.Point(lng, lat)
+    return new window.TdtMap.Point(lng, lat)
   })
-  mapInstance.setViewport(bmapPoints, { margins: [70, 70, 70, 70] })
+  mapInstance.setViewport(tdtPoints, { margins: [70, 70, 70, 70] })
 }
 
 function locatePoint(point) {
-  if (!mapInstance || !window.BMapGL || !Number.isFinite(Number(point?.lng)) || !Number.isFinite(Number(point?.lat))) return
+  if (!mapInstance || !window.TdtMap || !Number.isFinite(Number(point?.lng)) || !Number.isFinite(Number(point?.lat))) return
   const [lng, lat] = convertCoord(point.lng, point.lat)
-  mapInstance.centerAndZoom(new window.BMapGL.Point(lng, lat), 17)
+  mapInstance.centerAndZoom(new window.TdtMap.Point(lng, lat), 17)
 }
 
 function zoomIn() {
@@ -280,15 +249,11 @@ onMounted(async () => {
 
 onBeforeUnmount(() => {
   disposed = true
-  if (bmapLoadTimer) {
-    clearInterval(bmapLoadTimer)
-    bmapLoadTimer = null
-  }
   clearScene()
   try {
     mapInstance?.destroy()
   } catch (_) {
-    // ignore BMapGL cleanup errors
+    // ignore TdtMap cleanup errors
   }
   mapInstance = null
 })

+ 4 - 0
src/main.js

@@ -15,6 +15,10 @@ import directive from './directive' // directive
 // 注册指令
 import plugins from './plugins' // plugins
 import {download} from '@/utils/request'
+import {TdtMap} from '@/utils/tiandituMap'
+
+// 天地图适配层全局注册,供各 GIS 页面通过 window.TdtMap 使用
+window.TdtMap = TdtMap
 
 // svg图标
 import 'virtual:svg-icons-register'

+ 9 - 82
src/utils/coordTransform.js

@@ -1,94 +1,21 @@
 /**
- * WGS84 → GCJ-02 → BD-09 坐标转换工具
- * 百度地图需要使用 BD-09 坐标系
+ * 坐标工具类(天地图 / CGCS2000)
+ *
+ * 天地图使用 CGCS2000 坐标系,与业务库中的 WGS84 经纬度在厘米级一致,
+ * 业务坐标直通即可,无需任何偏移转换。
  */
 
-const PI = Math.PI
-const X_PI = (PI * 3000.0) / 180.0
-const A = 6378245.0
-const EE = 0.00669342162296594323
-
-function transformLat(lng, lat) {
-  let ret = -100.0 + 2.0 * lng + 3.0 * lat + 0.2 * lat * lat + 0.1 * lng * lat + 0.2 * Math.sqrt(Math.abs(lng))
-  ret += ((20.0 * Math.sin(6.0 * lng * PI) + 20.0 * Math.sin(2.0 * lng * PI)) * 2.0) / 3.0
-  ret += ((20.0 * Math.sin(lat * PI) + 40.0 * Math.sin((lat / 3.0) * PI)) * 2.0) / 3.0
-  ret += ((160.0 * Math.sin((lat / 12.0) * PI) + 320 * Math.sin((lat * PI) / 30.0)) * 2.0) / 3.0
-  return ret
-}
-
-function transformLng(lng, lat) {
-  let ret = 300.0 + lng + 2.0 * lat + 0.1 * lng * lng + 0.1 * lng * lat + 0.1 * Math.sqrt(Math.abs(lng))
-  ret += ((20.0 * Math.sin(6.0 * lng * PI) + 20.0 * Math.sin(2.0 * lng * PI)) * 2.0) / 3.0
-  ret += ((20.0 * Math.sin(lng * PI) + 40.0 * Math.sin((lng / 3.0) * PI)) * 2.0) / 3.0
-  ret += ((150.0 * Math.sin((lng / 12.0) * PI) + 300.0 * Math.sin((lng / 30.0) * PI)) * 2.0) / 3.0
-  return ret
-}
-
-/** WGS84 → GCJ-02 (火星坐标) */
-function wgs84ToGcj02(lng, lat) {
-  let dLat = transformLat(lng - 105.0, lat - 35.0)
-  let dLng = transformLng(lng - 105.0, lat - 35.0)
-  const radLat = (lat / 180.0) * PI
-  let magic = Math.sin(radLat)
-  magic = 1 - EE * magic * magic
-  const sqrtMagic = Math.sqrt(magic)
-  dLat = (dLat * 180.0) / (((A * (1 - EE)) / (magic * sqrtMagic)) * PI)
-  dLng = (dLng * 180.0) / ((A / sqrtMagic) * Math.cos(radLat) * PI)
-  return [lng + dLng, lat + dLat]
-}
-
-/** GCJ-02 → BD-09 (百度坐标) */
-function gcj02ToBd09(lng, lat) {
-  const z = Math.sqrt(lng * lng + lat * lat) + 0.00002 * Math.sin(lat * X_PI)
-  const theta = Math.atan2(lat, lng) + 0.000003 * Math.cos(lng * X_PI)
-  return [z * Math.cos(theta) + 0.0065, z * Math.sin(theta) + 0.006]
-}
-
-/** WGS84 → BD-09 一步转换 */
-export function wgs84ToBd09(lng, lat) {
-  const [gcjLng, gcjLat] = wgs84ToGcj02(lng, lat)
-  return gcj02ToBd09(gcjLng, gcjLat)
-}
-
-/** 百度 BD-09 → WGS84,用于地图选点回填业务坐标 */
-export function bd09ToWgs84(lng, lat) {
-  const x = Number(lng) - 0.0065
-  const y = Number(lat) - 0.006
-  const z = Math.sqrt(x * x + y * y) - 0.00002 * Math.sin(y * X_PI)
-  const theta = Math.atan2(y, x) - 0.000003 * Math.cos(x * X_PI)
-  const gcjLng = z * Math.cos(theta)
-  const gcjLat = z * Math.sin(theta)
-  const [wgsLng, wgsLat] = wgs84ToGcj02Inverse(gcjLng, gcjLat)
-  return [wgsLng, wgsLat]
-}
-
-function wgs84ToGcj02Inverse(lng, lat) {
-  const [forwardLng, forwardLat] = wgs84ToGcj02(lng, lat)
-  return [lng * 2 - forwardLng, lat * 2 - forwardLat]
-}
-
-/** 转换管线数据中的坐标数组 */
+/** 管线坐标保持原值 */
 export function convertPipeline(pipeline) {
-  return {
-    ...pipeline,
-    path: pipeline.path ? pipeline.path.map(([lng, lat]) => wgs84ToBd09(lng, lat)) : undefined,
-    points: pipeline.points
-      ? (Array.isArray(pipeline.points[0])
-          ? pipeline.points.map(([lng, lat]) => wgs84ToBd09(lng, lat))
-          : pipeline.points.map(p => ({ ...p, lng: wgs84ToBd09(p.lng, p.lat)[0], lat: wgs84ToBd09(p.lng, p.lat)[1] })))
-      : undefined
-  }
+  return { ...pipeline }
 }
 
 /** 转换单个坐标点 */
 export function convertCoord(lng, lat) {
-  return wgs84ToBd09(lng, lat)
+  return [Number(lng), Number(lat)]
 }
 
-/** 批量转换点数组中的 lng/lat */
+/** 批量规范化点数组中的 lng/lat */
 export function convertPoints(points) {
-  return points.map(p => {
-    const [lng, lat] = wgs84ToBd09(p.lng, p.lat)
-    return { ...p, lng, lat }
-  })
+  return points.map(p => ({ ...p, lng: Number(p.lng), lat: Number(p.lat) }))
 }

+ 134 - 0
src/utils/mapApi.js

@@ -0,0 +1,134 @@
+/**
+ * 地图业务向 API
+ *
+ * 底层能力(tk、WMTS 瓦片、Leaflet 适配、天地图 Web 服务)统一由 tiandituMap.js 提供。
+ * 本文件收拢页面里重复的「等待就绪 / 初始化 / 坐标构造 / 视野 / 销毁 / 地理编码」操作,
+ * 业务页面只负责把数据组装成覆盖物(Marker / Label / 管线 / 弹窗)。
+ */
+import { TdtMap, reverseGeocode, geocode, searchPlaces } from '@/utils/tiandituMap'
+
+/** 适配层随 main.js 同步注册;保留 Promise 形态以兼容既有 await 写法 */
+export function ensureTdtMap() {
+  return Promise.resolve(window.TdtMap || TdtMap)
+}
+
+/**
+ * 适配层就绪后执行初始化,替代各页面各自的 setInterval 轮询。
+ * @param {Function} initMap 初始化函数
+ * @param {Function} onTimeout 超时回调(可选)
+ * @returns {Function} 取消等待的清理函数
+ */
+export function whenMapReady(initMap, onTimeout) {
+  if (window.TdtMap) {
+    initMap()
+    return () => {}
+  }
+  let attempts = 0
+  const timer = setInterval(() => {
+    attempts += 1
+    if (window.TdtMap) {
+      clearInterval(timer)
+      initMap()
+    } else if (attempts >= 100) {
+      clearInterval(timer)
+      if (onTimeout) onTimeout()
+      else console.error('[mapApi] 地图适配层加载超时,请检查 main.js 是否已注册 window.TdtMap')
+    }
+  }, 200)
+  return () => clearInterval(timer)
+}
+
+/**
+ * 统一创建地图实例:底图图层、初始中心/层级、缩放范围、事件一次配置完成。
+ * @param {HTMLElement|string} element 容器元素或元素 id
+ */
+export function createMap(element, options = {}) {
+  const map = new TdtMap.Map(element)
+  if (options.center) {
+    map.centerAndZoom(new TdtMap.Point(options.center.lng, options.center.lat), options.zoom ?? 15)
+  }
+  map.enableScrollWheelZoom(options.scrollWheelZoom !== false)
+  if (options.minZoom != null) map.setMinZoom(options.minZoom)
+  if (options.maxZoom != null) map.setMaxZoom(options.maxZoom)
+  if (options.maxBounds) map.setMaxBounds(options.maxBounds)
+  Object.entries(options.events || {}).forEach(([type, handler]) => {
+    map.addEventListener(type, handler)
+  })
+  return map
+}
+
+export function toPoint(lng, lat) {
+  return new TdtMap.Point(lng, lat)
+}
+
+export function toPoints(points = []) {
+  return points.map(point => toPoint(point.lng, point.lat))
+}
+
+export function fitPoints(map, points = [], margins = [70, 70, 70, 70]) {
+  if (map && points.length) map.setViewport(toPoints(points), { margins })
+}
+
+export function clearMap(map) {
+  map?.clearOverlays?.()
+}
+
+export function destroyMap(map) {
+  if (!map) return null
+  try {
+    map.clearOverlays?.()
+    map.destroy?.()
+  } catch (_) {
+    // ignore cleanup errors
+  }
+  return null
+}
+
+/** 逆地理编码:坐标 -> 地址文本,失败返回空串 */
+export async function lookupAddress(lng, lat) {
+  try {
+    return await reverseGeocode(lng, lat)
+  } catch (_) {
+    return ''
+  }
+}
+
+/** 正地理编码:地址 -> { lng, lat },失败返回 null */
+export async function lookupCoordinate(address) {
+  try {
+    return await geocode(address)
+  } catch (_) {
+    return null
+  }
+}
+
+/** 地名搜索:关键词 -> [{ name, address, lng, lat }],服务异常时抛出以便页面区分「失败」与「无结果」 */
+export async function lookupPlaces(keyword) {
+  try {
+    return await searchPlaces(keyword)
+  } catch (error) {
+    console.error('[mapApi] 地名搜索失败:', error)
+    throw error
+  }
+}
+
+/** 从搜索结果中挑出与关键词最相关的一条,避免自动定位到同名乡镇等弱相关 POI */
+export function pickBestPlace(places = [], keyword = '') {
+  if (!places.length) return null
+  if (places.length === 1) return places[0]
+  const normalized = (text) => String(text || '').replace(/[\s·、,,()()]/g, '')
+  const target = normalized(keyword)
+  const score = (place) => {
+    const name = normalized(place.name)
+    const address = normalized(place.address)
+    let value = 0
+    if (name === target) value += 100
+    if (name.includes(target) || target.includes(name)) value += 50
+    for (const char of new Set(target)) {
+      if (name.includes(char)) value += 2
+      else if (address.includes(char)) value += 1
+    }
+    return value
+  }
+  return places.reduce((best, place) => (score(place) > score(best) ? place : best), places[0])
+}

+ 454 - 0
src/utils/tiandituMap.js

@@ -0,0 +1,454 @@
+/**
+ * 天地图统一接入层
+ *
+ * - 底图:天地图 WMTS(vec_w 矢量底图 + cva_w 中文注记),由 Leaflet 承载
+ * - 坐标:CGCS2000 / WGS84,业务经纬度直接使用,无需偏移转换
+ * - 密钥:VITE_TIANDITU_TK,统一在 .env.development / .env.production 配置
+ * - 地理编码:走 /tdtApi 代理调用天地图 Web 服务,避免跨域
+ *
+ * 对外暴露 TdtMap,保持原有地图 API 的调用签名(Point / Marker / addOverlay 等),
+ * 使各业务页面只切换底层地图服务,无需重写点位、管线、弹窗等交互逻辑。
+ */
+import * as L from 'leaflet'
+import 'leaflet/dist/leaflet.css'
+
+export const TIANDITU_TK = import.meta.env.VITE_TIANDITU_TK || ''
+export const TIANDITU_SUBDOMAINS = '01234567'
+
+/**
+ * 地理编码同源前缀(仅 Web 服务需要,瓦片是 <img> 直连,不走代理)。
+ * - 开发环境:vite.config.js 的 server.proxy['/tdtApi'] 转发到 https://api.tianditu.gov.cn
+ * - 生产环境:必须在 Nginx 配置同名 location(Vite 代理不随构建产物生效):
+ *     location /tdtApi/ {
+ *       proxy_pass https://api.tianditu.gov.cn/;
+ *       proxy_set_header Host api.tianditu.gov.cn;
+ *       proxy_ssl_server_name on;
+ *     }
+ *   proxy_pass 末尾的 / 会把 /tdtApi/geocoder 映射为 /geocoder,与 Vite rewrite 等效。
+ * - tk 由前端拼在 query 上,需在天地图控制台配置域名/Referer 白名单。
+ */
+export const TIANDITU_PROXY = '/tdtApi'
+
+/** 天地图 WMTS 瓦片地址(Web 墨卡托,与 Leaflet 默认 CRS 一致) */
+export function tiandituTileUrl(layer = 'vec') {
+  return `https://t{s}.tianditu.gov.cn/${layer}_w/wmts?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=${layer}&STYLE=default&TILEMATRIXSET=w&FORMAT=tiles&TILEMATRIX={z}&TILEROW={y}&TILECOL={x}&tk=${TIANDITU_TK}`
+}
+
+export function hasTiandituKey() {
+  return Boolean(TIANDITU_TK)
+}
+
+function addBaseLayers(map) {
+  L.tileLayer(tiandituTileUrl('vec'), {
+    subdomains: TIANDITU_SUBDOMAINS,
+    maxZoom: 18,
+    attribution: '&copy; 天地图'
+  }).addTo(map)
+  L.tileLayer(tiandituTileUrl('cva'), {
+    subdomains: TIANDITU_SUBDOMAINS,
+    maxZoom: 18
+  }).addTo(map)
+}
+
+function toLatLng(point) {
+  return [Number(point?.lat), Number(point?.lng)]
+}
+
+function leafletEvent(event) {
+  const latlng = event?.latlng || { lng: 0, lat: 0 }
+  return {
+    latlng,
+    latLng: latlng,
+    point: latlng,
+    pixel: event?.containerPoint,
+    domEvent: event?.originalEvent,
+    target: event?.target
+  }
+}
+
+function styleToCss(style = {}) {
+  return Object.entries(style)
+    .map(([key, value]) => `${key.replace(/[A-Z]/g, (m) => `-${m.toLowerCase()}`)}:${value}`)
+    .join(';')
+}
+
+class Point {
+  constructor(lng, lat) {
+    this.lng = Number(lng)
+    this.lat = Number(lat)
+  }
+}
+
+class Size {
+  constructor(width, height) {
+    this.width = Number(width)
+    this.height = Number(height)
+  }
+}
+
+class Bounds {
+  constructor(sw, ne) {
+    this.sw = sw
+    this.ne = ne
+    this._leaflet = L.latLngBounds(toLatLng(sw), toLatLng(ne))
+  }
+
+  getSouthWest() { return this.sw }
+  getNorthEast() { return this.ne }
+}
+
+class Icon {
+  constructor(url, size, options = {}) {
+    this._options = {
+      iconUrl: url,
+      iconSize: [size?.width || 28, size?.height || 28],
+      iconAnchor: options.anchor ? [options.anchor.width, options.anchor.height] : undefined
+    }
+  }
+
+  toLeaflet() {
+    return L.icon(this._options)
+  }
+}
+
+class Overlay {
+  constructor(layer) {
+    this._layer = layer
+    this._handlers = []
+  }
+
+  addTo(map) {
+    const target = map?._leaflet || map
+    this._layer.addTo(target)
+    if (map?._overlays) map._overlays.add(this._layer)
+    return this
+  }
+
+  addEventListener(type, callback) {
+    const handler = (event) => callback(leafletEvent(event))
+    this._layer.on(type, handler)
+    this._handlers.push({ type, callback, handler })
+    return this
+  }
+
+  removeEventListener(type, callback) {
+    this._handlers
+      .filter(item => item.type === type && (!callback || item.callback === callback))
+      .forEach(item => this._layer.off(item.type, item.handler))
+    return this
+  }
+
+  setPosition(point) {
+    if (this._layer.setLatLng) this._layer.setLatLng(toLatLng(point))
+    return this
+  }
+
+  setStyle() { return this }
+  setContent() { return this }
+}
+
+class Marker extends Overlay {
+  constructor(point, options = {}) {
+    super(L.marker(toLatLng(point), {
+      icon: options.icon?.toLeaflet?.(),
+      draggable: Boolean(options.enableDragging),
+      title: options.title || ''
+    }))
+  }
+
+  setIcon(icon) {
+    if (icon?.toLeaflet && this._layer.setIcon) this._layer.setIcon(icon.toLeaflet())
+    return this
+  }
+}
+
+class Label extends Overlay {
+  constructor(content, options = {}) {
+    super(L.marker(toLatLng(options.position || new Point(0, 0)), {
+      interactive: true,
+      keyboard: false,
+      icon: L.divIcon({ className: 'tdt-label', html: '', iconSize: [0, 0] })
+    }))
+    this._content = content
+    this._style = {}
+    this._offset = options.offset || new Size(0, 0)
+    this._refresh()
+  }
+
+  _refresh() {
+    const css = styleToCss(this._style)
+    this._layer.setIcon(L.divIcon({
+      className: 'tdt-label',
+      html: `<div style="${css}">${this._content}</div>`,
+      iconSize: [0, 0],
+      iconAnchor: [-this._offset.width, -this._offset.height]
+    }))
+  }
+
+  setStyle(style) {
+    this._style = { ...this._style, ...(style || {}) }
+    this._refresh()
+    return this
+  }
+
+  setContent(content) {
+    this._content = content
+    this._refresh()
+    return this
+  }
+
+  setOffset(size) {
+    this._offset = size || new Size(0, 0)
+    this._refresh()
+    return this
+  }
+}
+
+function strokeOptions(options = {}) {
+  return {
+    color: options.strokeColor,
+    weight: options.strokeWeight,
+    opacity: options.strokeOpacity,
+    dashArray: options.strokeStyle === 'dashed' ? '6,6' : undefined
+  }
+}
+
+class Polyline extends Overlay {
+  constructor(points, options = {}) {
+    super(L.polyline((points || []).map(toLatLng), strokeOptions(options)))
+  }
+}
+
+class Polygon extends Overlay {
+  constructor(points, options = {}) {
+    super(L.polygon((points || []).map(toLatLng), {
+      ...strokeOptions(options),
+      fillColor: options.fillColor,
+      fillOpacity: options.fillOpacity
+    }))
+  }
+}
+
+class Circle extends Overlay {
+  constructor(point, radius, options = {}) {
+    super(L.circle(toLatLng(point), {
+      ...strokeOptions(options),
+      radius: Number(radius) || 0,
+      fillColor: options.fillColor,
+      fillOpacity: options.fillOpacity
+    }))
+  }
+}
+
+class InfoWindow {
+  constructor(content, options = {}) {
+    this._content = content
+    this._popup = L.popup({
+      maxWidth: options.width || 300,
+      minWidth: options.width ? Math.min(options.width, 300) : 100,
+      autoPan: true,
+      closeButton: true,
+      className: 'tdt-info-window'
+    }).setContent(content)
+  }
+
+  setContent(content) {
+    this._content = content
+    this._popup.setContent(content)
+    return this
+  }
+}
+
+async function requestTianditu(path, params) {
+  const query = new URLSearchParams(params)
+  const response = await fetch(`${TIANDITU_PROXY}${path}?${query}`)
+  if (!response.ok) throw new Error(`天地图服务请求失败:${response.status}`)
+  return response.json()
+}
+
+/** 逆地理编码:坐标 -> 地址 */
+export async function reverseGeocode(lng, lat) {
+  const data = await requestTianditu('/geocoder', {
+    postStr: JSON.stringify({ lon: Number(lng), lat: Number(lat), ver: 1 }),
+    type: 'geocode',
+    tk: TIANDITU_TK
+  })
+  return data?.result?.formatted_address || ''
+}
+
+/** v2/search 的坐标是 lonlat: "经度,纬度",个别版本/代理会拆成 lon / lat */
+function parsePoiCoord(poi) {
+  const lonlat = typeof poi?.lonlat === 'string' ? poi.lonlat.split(',') : null
+  if (lonlat?.length >= 2) return { lng: Number(lonlat[0]), lat: Number(lonlat[1]) }
+  return {
+    lng: Number(poi?.lon ?? poi?.pointx ?? poi?.x),
+    lat: Number(poi?.lat ?? poi?.pointy ?? poi?.y)
+  }
+}
+
+/** 地名搜索:关键词 -> POI 列表 */
+export async function searchPlaces(keyword, count = 10) {
+  const data = await requestTianditu('/v2/search', {
+    postStr: JSON.stringify({
+      keyWord: String(keyword || ''),
+      level: 12,
+      mapBound: '-180,-90,180,90',
+      queryType: 1,
+      count,
+      start: 0
+    }),
+    type: 'query',
+    tk: TIANDITU_TK
+  })
+  // 正常响应 status 为 { infocode: 1000 } 或字符串 '0';无结果时 pois 为空但 infocode 仍是 1000
+  const status = data?.status
+  if (status && status !== '0' && Number(status?.infocode) !== 1000) {
+    throw new Error(`天地图搜索服务异常:${status?.cndesc || status}`)
+  }
+  // v2/search 的 POI 在顶层 pois,部分版本会包在 result.pois
+  const pois = Array.isArray(data?.pois) ? data.pois
+    : (Array.isArray(data?.result?.pois) ? data.result.pois : [])
+  return pois.map((poi) => {
+    const { lng, lat } = parsePoiCoord(poi)
+    return { name: poi.name || '', address: poi.address || '', lng, lat }
+  }).filter((poi) => Number.isFinite(poi.lng) && Number.isFinite(poi.lat))
+}
+
+/** 地名搜索:地址 -> 坐标(取首个结果) */
+export async function geocode(address) {
+  const [poi] = await searchPlaces(address, 1)
+  return poi ? { lng: poi.lng, lat: poi.lat } : null
+}
+
+class Geocoder {
+  getLocation(point, callback) {
+    reverseGeocode(point?.lng, point?.lat)
+      .then((address) => callback?.({ address, content: { address } }))
+      .catch(() => callback?.(null))
+  }
+
+  getPoint(address, callback) {
+    geocode(address)
+      .then((point) => callback?.(point))
+      .catch(() => callback?.(null))
+  }
+}
+
+class Map {
+  constructor(element) {
+    this._leaflet = L.map(element, {
+      zoomControl: false,
+      attributionControl: true,
+      scrollWheelZoom: true
+    })
+    this._overlays = new Set()
+    this._handlers = []
+    if (!hasTiandituKey()) {
+      console.warn('[tiandituMap] 未配置 VITE_TIANDITU_TK,天地图瓦片将无法加载')
+    }
+    addBaseLayers(this._leaflet)
+  }
+
+  centerAndZoom(point, zoom) {
+    this._leaflet.setView(toLatLng(point), Number(zoom) || 15)
+    return this
+  }
+
+  enableScrollWheelZoom(enabled = true) {
+    if (enabled) this._leaflet.scrollWheelZoom.enable()
+    else this._leaflet.scrollWheelZoom.disable()
+    return this
+  }
+
+  addEventListener(type, callback) {
+    const handler = (event) => callback(leafletEvent(event))
+    this._leaflet.on(type, handler)
+    this._handlers.push({ type, callback, handler })
+    return this
+  }
+
+  removeEventListener(type, callback) {
+    this._handlers
+      .filter(item => item.type === type && (!callback || item.callback === callback))
+      .forEach(item => this._leaflet.off(item.type, item.handler))
+    return this
+  }
+
+  addOverlay(overlay) {
+    const layer = overlay?._layer || overlay
+    if (!layer) return this
+    layer.addTo(this._leaflet)
+    this._overlays.add(layer)
+    return this
+  }
+
+  removeOverlay(overlay) {
+    const layer = overlay?._layer || overlay
+    if (layer?.remove) layer.remove()
+    this._overlays.delete(layer)
+    return this
+  }
+
+  clearOverlays() {
+    this._overlays.forEach((layer) => layer.remove())
+    this._overlays.clear()
+    return this
+  }
+
+  pointToPixel(point) {
+    const pixel = this._leaflet.latLngToContainerPoint(toLatLng(point))
+    return { x: Number(pixel?.x), y: Number(pixel?.y) }
+  }
+
+  setViewport(points, options = {}) {
+    const list = points || []
+    if (!list.length) return this
+    const bounds = L.latLngBounds(list.map(toLatLng))
+    const margins = options.margins || [50, 50, 50, 50]
+    this._leaflet.fitBounds(bounds, {
+      paddingTopLeft: [margins[3], margins[0]],
+      paddingBottomRight: [margins[1], margins[2]],
+      maxZoom: 17
+    })
+    return this
+  }
+
+  openInfoWindow(infoWindow, point) {
+    if (infoWindow?._popup && point) {
+      infoWindow._popup.setLatLng(toLatLng(point)).openOn(this._leaflet)
+    }
+    return this
+  }
+
+  zoomIn() { this._leaflet.zoomIn(); return this }
+  zoomOut() { this._leaflet.zoomOut(); return this }
+  setMinZoom(zoom) { this._leaflet.setMinZoom(zoom); return this }
+  setMaxZoom(zoom) { this._leaflet.setMaxZoom(zoom); return this }
+  setMaxBounds(bounds) { this._leaflet.setMaxBounds(bounds?._leaflet || bounds); return this }
+  resize() { this._leaflet.invalidateSize(); return this }
+  getZoom() { return this._leaflet.getZoom() }
+  setZoom(zoom) { this._leaflet.setZoom(zoom); return this }
+  panTo(point) { this._leaflet.panTo(toLatLng(point)); return this }
+  destroy() { this._leaflet.remove(); return this }
+}
+
+/** 兼容原有地图 API 签名的天地图适配对象,供各页面通过 window.TdtMap 使用 */
+export const TdtMap = {
+  Map,
+  Point,
+  Size,
+  Bounds,
+  Icon,
+  Marker,
+  Label,
+  Polyline,
+  Polygon,
+  Circle,
+  InfoWindow,
+  Geocoder
+}
+
+/** 兼容原 SDK 异步加载写法:适配层随应用同步注册,可直接 resolve */
+export function waitForTdtMap() {
+  return Promise.resolve(window.TdtMap || TdtMap)
+}

+ 6 - 2
src/views/subSystem/alarm/AlarmMap.vue

@@ -40,8 +40,12 @@ async function initMap() {
   try {
     const L = await import('leaflet')
     mapInstance = L.map('alarmMap').setView([34.77, 113.65], 12)
-    L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
-      attribution: '&copy; OpenStreetMap contributors'
+    const { tiandituTileUrl, TIANDITU_SUBDOMAINS } = await import('@/utils/tiandituMap')
+    L.tileLayer(tiandituTileUrl('vec'), {
+      subdomains: TIANDITU_SUBDOMAINS, maxZoom: 18, attribution: '&copy; 天地图'
+    }).addTo(mapInstance)
+    L.tileLayer(tiandituTileUrl('cva'), {
+      subdomains: TIANDITU_SUBDOMAINS, maxZoom: 18
     }).addTo(mapInstance)
 
     const res = await getActiveAlarmPoints()

+ 1 - 2
src/views/subSystem/alarm/common/alarmGIS.vue

@@ -2,7 +2,6 @@
   <div class="alarm-page alarm-gis-page">
     <header class="gis-page-heading">
       <div><h1>{{ route.meta.title || '报警GIS一张图' }}</h1><p>按设备查看报警分布,关联监测曲线与报警记录</p></div>
-      <span class="gis-view-label"><el-icon><MapLocation /></el-icon>{{ route.meta.title || '报警GIS一张图' }}</span>
     </header>
 
     <section class="gis-filter-card">
@@ -40,7 +39,7 @@
         </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 || []" />
+          <GisMapCanvas :key="mapKey" ref="mapRef" :points="points" :marker-size="36" @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>

+ 11 - 20
src/views/subSystem/basic/Manhole.vue

@@ -204,6 +204,7 @@
 </template>
 
 <script setup>
+import { ensureTdtMap } from '@/utils/mapApi'
 import { ref, reactive, computed, onMounted, nextTick, watch, onBeforeUnmount } from 'vue'
 import { ElMessage, ElMessageBox } from 'element-plus'
 import {
@@ -212,7 +213,7 @@ import {
   getEquipmentByTopLevelType, getPipeNetworkOptions
 } from '@/api/pipeNetwork/basic'
 import { getDicts } from '@/api/system/dict/data'
-// 台账存 WGS84,百度地图是 BD-09,不转换会偏约 1 公里
+// 台账存 WGS84,天地图使用 CGCS2000,两者一致无需转换
 import { convertCoord } from '@/utils/coordTransform'
 
 defineOptions({ name: 'Manhole' })
@@ -402,17 +403,7 @@ let mapInstance = null
 let mapInitialized = false
 const mapError = ref('')
 
-function waitForBMapGL() {
-  return new Promise((resolve, reject) => {
-    if (window.BMapGL) { resolve(window.BMapGL); return }
-    let attempts = 0
-    const timer = setInterval(() => {
-      attempts++
-      if (window.BMapGL) { clearInterval(timer); resolve(window.BMapGL); return }
-      if (attempts >= 80) { clearInterval(timer); reject(new Error('百度地图加载超时')) }
-    }, 200)
-  })
-}
+
 
 async function initMap() {
   await nextTick()
@@ -420,23 +411,23 @@ async function initMap() {
   if (!el || mapInitialized) return
 
   try {
-    const BMapGL = await waitForBMapGL()
-    mapInstance = new BMapGL.Map(el, { enableMapClick: true })
+    const TdtMap = await ensureTdtMap()
+    mapInstance = new TdtMap.Map(el, { enableMapClick: true })
     const center = tableData.value.length > 0 && tableData.value[0].longitude
       ? convertCoord(Number(tableData.value[0].longitude), Number(tableData.value[0].latitude))
       : convertCoord(110.3965, 28.4638)
-    mapInstance.centerAndZoom(new BMapGL.Point(center[0], center[1]), 15)
+    mapInstance.centerAndZoom(new TdtMap.Point(center[0], center[1]), 15)
     mapInstance.enableScrollWheelZoom(true)
     mapInitialized = true
     renderMarkers()
   } catch (e) {
     console.error(e)
-    mapError.value = '百度地图加载失败,请刷新页面重试'
+    mapError.value = '天地图加载失败,请刷新页面重试'
   }
 }
 
 function renderMarkers() {
-  if (!mapInstance || typeof BMapGL === 'undefined') return
+  if (!mapInstance || typeof TdtMap === 'undefined') return
   try { mapInstance.clearOverlays() } catch (_) {}
 
   const data = tableData.value.filter(m => m.longitude && m.latitude)
@@ -444,8 +435,8 @@ function renderMarkers() {
 
   data.forEach(m => {
     const [lng, lat] = convertCoord(Number(m.longitude), Number(m.latitude))
-    const pt = new BMapGL.Point(lng, lat)
-    const marker = new BMapGL.Marker(pt)
+    const pt = new TdtMap.Point(lng, lat)
+    const marker = new TdtMap.Marker(pt)
     mapInstance.addOverlay(marker)
 
     const shapeLabel = getDictLabel(manholeShapeDict.value, m.manholeShape)
@@ -461,7 +452,7 @@ function renderMarkers() {
     </div>`
 
     marker.addEventListener('click', () => {
-      const infoWindow = new BMapGL.InfoWindow(html, { width: 240 })
+      const infoWindow = new TdtMap.InfoWindow(html, { width: 240 })
       mapInstance.openInfoWindow(infoWindow, pt)
     })
   })

+ 28 - 35
src/views/subSystem/basic/RepairRecord.vue

@@ -1,6 +1,6 @@
 <template>
   <div class="repair-container">
-    <!-- 上半部分:百度地图(左) + 图表(右) -->
+    <!-- 上半部分:天地图(左) + 图表(右) -->
     <div class="top-section">
       <el-card class="topo-card">
         <template #header>
@@ -16,7 +16,7 @@
           </div>
         </template>
         <div class="map-wrapper">
-          <div id="repairBaiduMap" class="baidu-map"></div>
+          <div id="repairTdtMap" class="tdt-map"></div>
           <div v-if="mapError" class="map-error">{{ mapError }}</div>
         </div>
       </el-card>
@@ -163,6 +163,7 @@
 </template>
 
 <script setup>
+import { whenMapReady } from '@/utils/mapApi'
 import { computed, nextTick, onBeforeUnmount, onMounted, reactive, ref } from 'vue'
 import * as echarts from 'echarts'
 import { ElMessage, ElMessageBox } from 'element-plus'
@@ -197,7 +198,7 @@ function getDictLabel(dict, value) {
   return item ? item.dictLabel : value
 }
 
-// ==================== 百度地图配置 ====================
+// ==================== 天地图配置 ====================
 const centerPoint = { lng: 110.386, lat: 28.445 }
 const MAP_BOUNDS = { southWest: { lng: 110.355, lat: 28.428 }, northEast: { lng: 110.410, lat: 28.460 } }
 const MAP_MIN_ZOOM = 13
@@ -319,16 +320,8 @@ async function loadData() {
   } catch (e) { console.error(e) } finally { loading.value = false }
 }
 
-// ==================== 百度地图 ====================
-function waitForBMapGL() {
-  if (typeof BMapGL !== 'undefined') { initMap(); return }
-  let attempts = 0
-  const timer = setInterval(() => {
-    attempts++
-    if (typeof BMapGL !== 'undefined') { clearInterval(timer); initMap(); return }
-    if (attempts >= 50) { clearInterval(timer); mapError.value = '百度地图脚本加载超时,请刷新页面重试。' }
-  }, 200)
-}
+// ==================== 天地图 ====================
+
 
 function closeInfoWindow() {
   if (activeInfoWindow) { try { activeInfoWindow.close(); mapInstance.removeOverlay(activeInfoWindow) } catch (_) {}; activeInfoWindow = null }
@@ -341,22 +334,22 @@ function getMidPoint(points) {
 }
 
 function initMap() {
-  const el = document.getElementById('repairBaiduMap')
+  const el = document.getElementById('repairTdtMap')
   if (!el) { mapError.value = '地图容器未找到。'; return }
 
-  try { mapInstance = new BMapGL.Map('repairBaiduMap', { enableMapClick: true }) } catch (e) {
-    console.error('创建地图实例失败:', e); mapError.value = '百度地图初始化失败,请检查AK和网络连接。'; return
+  try { mapInstance = new TdtMap.Map('repairTdtMap', { enableMapClick: true }) } catch (e) {
+    console.error('创建地图实例失败:', e); mapError.value = '天地图初始化失败,请检查 VITE_TIANDITU_TK 与网络连接。'; return
   }
 
-  const [clng, clat] = convertCoord(centerPoint.lng, centerPoint.lat); try { mapInstance.centerAndZoom(new BMapGL.Point(clng, clat), MAP_DEFAULT_ZOOM); mapInstance.enableScrollWheelZoom(true) } catch (e) { console.error('设置中心失败:', e) }
+  const [clng, clat] = convertCoord(centerPoint.lng, centerPoint.lat); try { mapInstance.centerAndZoom(new TdtMap.Point(clng, clat), MAP_DEFAULT_ZOOM); mapInstance.enableScrollWheelZoom(true) } catch (e) { console.error('设置中心失败:', e) }
 
   try { if (typeof mapInstance.setMinZoom === 'function') mapInstance.setMinZoom(MAP_MIN_ZOOM); if (typeof mapInstance.setMaxZoom === 'function') mapInstance.setMaxZoom(MAP_MAX_ZOOM) } catch (e) {}
 
   try {
     if (typeof mapInstance.setMaxBounds === 'function') {
-      const sw = new BMapGL.Point(MAP_BOUNDS.southWest.lng, MAP_BOUNDS.southWest.lat)
-      const ne = new BMapGL.Point(MAP_BOUNDS.northEast.lng, MAP_BOUNDS.northEast.lat)
-      mapInstance.setMaxBounds(new BMapGL.Bounds(sw, ne))
+      const sw = new TdtMap.Point(MAP_BOUNDS.southWest.lng, MAP_BOUNDS.southWest.lat)
+      const ne = new TdtMap.Point(MAP_BOUNDS.northEast.lng, MAP_BOUNDS.northEast.lat)
+      mapInstance.setMaxBounds(new TdtMap.Bounds(sw, ne))
     }
   } catch (e) {}
 
@@ -369,7 +362,7 @@ function initMap() {
 }
 
 function renderMapScene() {
-  if (!mapInstance || typeof BMapGL === 'undefined') return
+  if (!mapInstance || typeof TdtMap === 'undefined') return
   closeInfoWindow()
   try { mapInstance.clearOverlays() } catch (_) {}
   mapOverlays = []
@@ -378,8 +371,8 @@ function renderMapScene() {
   pipelineData.forEach(pipe => {
     try {
       const hasRepair = repairedNetworkIds.value.has(pipe.id)
-      const pts = pipe.points.map(p => { const [lng, lat] = convertCoord(p.lng, p.lat); return new BMapGL.Point(lng, lat) })
-      const polyline = new BMapGL.Polyline(pts, {
+      const pts = pipe.points.map(p => { const [lng, lat] = convertCoord(p.lng, p.lat); return new TdtMap.Point(lng, lat) })
+      const polyline = new TdtMap.Polyline(pts, {
         strokeColor: hasRepair ? '#f59e0b' : pressureColors[pipe.pressure],
         strokeWeight: hasRepair ? 5 : 3.5,
         strokeOpacity: 0.9
@@ -399,8 +392,8 @@ function renderMapScene() {
           <span style="color:${pressureColors[pipe.pressure]};font-weight:bold;">${pipe.pressure === 'high' ? '高压' : pipe.pressure === 'medium' ? '中压' : '低压'}</span>
           &nbsp;累计维修 <b style="color:#e63946;">${count}</b> 次
           ${count > 0 ? '<br><span style="color:#909399;font-size:11px;">' + repairList + '</span>' : ''}</div>`
-        activeInfoWindow = new BMapGL.InfoWindow(html, { width: 260 })
-        mapInstance.openInfoWindow(activeInfoWindow, new BMapGL.Point(mid.lng, mid.lat))
+        activeInfoWindow = new TdtMap.InfoWindow(html, { width: 260 })
+        mapInstance.openInfoWindow(activeInfoWindow, new TdtMap.Point(mid.lng, mid.lat))
         // 筛选表格
         searchForm.networkId = pipe.id
         pageNum.value = 1
@@ -412,12 +405,12 @@ function renderMapScene() {
   pipePointData.forEach(point => {
     try {
       const hasRepair = repairedNetworkIds.value.has(point.pipelineId)
-      const [plng, plat] = convertCoord(point.lng, point.lat); const pt = new BMapGL.Point(plng, plat)
+      const [plng, plat] = convertCoord(point.lng, point.lat); const pt = new TdtMap.Point(plng, plat)
       const color = hasRepair ? '#f59e0b' : '#3b82f6'
       const size = hasRepair ? 10 : 7
-      const label = new BMapGL.Label(point.name, {
+      const label = new TdtMap.Label(point.name, {
         position: pt,
-        offset: new BMapGL.Size(-22, -22)
+        offset: new TdtMap.Size(-22, -22)
       })
       label.setStyle({
         color: '#fff', fontSize: '10px', padding: '2px 6px',
@@ -434,7 +427,7 @@ function renderMapScene() {
         const pipeName = getPipelineName(point.pipelineId)
         const count = tableData.value.filter(r => r.networkId === point.pipelineId).length
         const html = `<div style="font-size:13px;"><b>${point.name}</b><br>所属管线: ${point.pipelineId} ${pipeName}<br>维修记录: <b style="color:#e63946;">${count}</b> 次</div>`
-        activeInfoWindow = new BMapGL.InfoWindow(html, { width: 240 })
+        activeInfoWindow = new TdtMap.InfoWindow(html, { width: 240 })
         mapInstance.openInfoWindow(activeInfoWindow, pt)
         searchForm.networkId = point.pipelineId
         pageNum.value = 1
@@ -448,11 +441,11 @@ function renderMapScene() {
     try {
       const rawMid2 = getMidPoint(pipe.points)
       const mid2 = { lng: convertCoord(rawMid2.lng, rawMid2.lat)[0], lat: convertCoord(rawMid2.lng, rawMid2.lat)[1] }
-      const pt = new BMapGL.Point(mid2.lng, mid2.lat)
+      const pt = new TdtMap.Point(mid2.lng, mid2.lat)
       const count = tableData.value.filter(r => r.networkId === pipe.id).length
-      const marker = new BMapGL.Label(String(count), {
+      const marker = new TdtMap.Label(String(count), {
         position: pt,
-        offset: new BMapGL.Size(-12, -12)
+        offset: new TdtMap.Size(-12, -12)
       })
       marker.setStyle({
         color: '#fff', fontSize: '11px', fontWeight: 'bold',
@@ -564,7 +557,7 @@ onMounted(async () => {
   await nextTick()
   await loadNetworkOptions()
   await loadData()
-  waitForBMapGL()
+  whenMapReady(initMap)
   loadStats()
   window.addEventListener('resize', () => {
     clearTimeout(resizeTimer)
@@ -572,7 +565,7 @@ onMounted(async () => {
       resizeCharts()
       if (mapInstance) {
         try { mapInstance.resize() } catch (_) {}
-        try { mapInstance.setViewport([new BMapGL.Point(centerPoint.lng, centerPoint.lat)], { zoom: mapInstance.getZoom() }) } catch (_) {}
+        try { mapInstance.setViewport([new TdtMap.Point(centerPoint.lng, centerPoint.lat)], { zoom: mapInstance.getZoom() }) } catch (_) {}
       }
     }, 200)
   })
@@ -657,7 +650,7 @@ onBeforeUnmount(() => {
 .legend-dot.point { background: #3b82f6; border: 2px solid #fff; }
 
 .map-wrapper { position: relative; width: 100%; height: 100%; min-height: 460px; border-radius: 6px; overflow: hidden; }
-.baidu-map { width: 100%; height: 100%; min-height: 460px; }
+.tdt-map { width: 100%; height: 100%; min-height: 460px; }
 .map-error { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; background: #fafbfc; color: #e63946; font-size: 14px; font-weight: 600; }
 .topo-card { display: flex; flex-direction: column; }
 :deep(.topo-card .el-card__body) { flex: 1; display: flex; padding: 0; overflow: hidden; }

+ 13 - 18
src/views/subSystem/dispatch/disOnDuty/cphs.vue

@@ -167,7 +167,7 @@ import { ref, computed, nextTick, onMounted } from 'vue'
 import { ElMessage, ElMessageBox } from 'element-plus'
 import { useDict } from '@/utils/dict'
 import useUserStore from '@/store/modules/user'
-import { bd09ToWgs84 } from '@/utils/coordTransform'
+import { lookupCoordinate } from '@/utils/mapApi'
 import { gisPointStatusIcons } from '@/utils/gisMapColors'
 import GisMapCanvas from '@/components/GisMapCanvas.vue'
 import EventDetailDrawer from './EventDetailDrawer.vue'
@@ -312,8 +312,8 @@ async function loadData() {
 }
 
 // ==================== 事件坐标 GIS 展示区 ====================
-// 固定区域为沅陵县(GisMapCanvas 默认中心即沅陵):库内坐标为 WGS84,组件内自动转百度坐标打点。
-// 没录坐标的事件按「怀化市沅陵县 + 发生地点」用百度 Geocoder 解析后仅作展示定位,不回写数据库。
+// 固定区域为沅陵县(GisMapCanvas 默认中心即沅陵):库内坐标为 WGS84,与天地图 CGCS2000 一致,直接打点。
+// 没录坐标的事件按「怀化市沅陵县 + 发生地点」用天地图地名搜索解析后仅作展示定位,不回写数据库。
 const gisError = ref('')
 const geocodedPoints = ref([])
 const geoFailed = ref(0)
@@ -377,14 +377,11 @@ let geoSeq = 0
 function resolveMissingCoords() {
   geocodedPoints.value = []
   const seq = ++geoSeq
-  const BMapGL = window.BMapGL
-  if (!BMapGL || typeof BMapGL.Geocoder !== 'function') return
   const pending = tableData.value.filter(row => !hasCoord(row.longitude) || !hasCoord(row.latitude))
   if (!pending.length) {
     geoFailed.value = 0
     return
   }
-  const geocoder = new BMapGL.Geocoder()
   let idx = 0
   let failed = 0
 
@@ -395,8 +392,8 @@ function resolveMissingCoords() {
     }
   }
 
-  // 串行 + 间隔解析:并发请求会被百度 Geocoder 限流,部分结果会静默丢失
-  const next = () => {
+  // 串行 + 间隔解析:并发请求会被天地图服务限流,部分结果会静默丢失
+  const next = async () => {
     if (seq !== geoSeq || idx >= pending.length) {
       geoFailed.value = failed
       return
@@ -409,16 +406,14 @@ function resolveMissingCoords() {
       next()
       return
     }
-    geocoder.getPoint(address, (point) => {
-      if (point) {
-        const [lng, lat] = bd09ToWgs84(point.lng, point.lat)
-        geocodeCache.set(address, [lng, lat])
-        applyGeocode(row, lng, lat)
-      } else {
-        failed++
-      }
-      setTimeout(next, 300)
-    })
+    const point = await lookupCoordinate(address)
+    if (point) {
+      geocodeCache.set(address, [point.lng, point.lat])
+      applyGeocode(row, point.lng, point.lat)
+    } else {
+      failed++
+    }
+    setTimeout(next, 300)
   }
   next()
 }

+ 11 - 24
src/views/subSystem/dispatch/disOnDuty/jbgl.vue

@@ -113,7 +113,7 @@
 import { ref, computed, nextTick, onMounted } from 'vue'
 import { ElMessage, ElMessageBox } from 'element-plus'
 import { useDict } from '@/utils/dict'
-import { bd09ToWgs84 } from '@/utils/coordTransform'
+import { lookupCoordinate } from '@/utils/mapApi'
 import { getEventPage, saveEventReport, updateEventInfo, deleteEventInfo, getEventStatusOptions } from '@/api/dispatch'
 import { Search, Refresh } from '@element-plus/icons-vue'
 import EventDetailDrawer from './EventDetailDrawer.vue'
@@ -243,36 +243,23 @@ const geoText = computed(() => {
   return text === '-' ? '' : text
 })
 
-// 发生地点 → 坐标:百度 Geocoder 返回 BD-09,落库前转 WGS84
-function handleGeocode() {
+// 发生地点 → 坐标:统一走 mapApi.lookupCoordinate
+async function handleGeocode() {
   const address = (addForm.value.location || '').trim()
   if (!address) {
     ElMessage.warning('请先填写发生地点,再解析坐标')
     return
   }
-  const BMapGL = window.BMapGL
-  if (!BMapGL || typeof BMapGL.Geocoder !== 'function') {
-    ElMessage.warning('地图服务未就绪,请稍后重试')
-    return
-  }
   geoLoading.value = true
-  try {
-    const geocoder = new BMapGL.Geocoder()
-    geocoder.getPoint(address, (point) => {
-      geoLoading.value = false
-      if (!point) {
-        ElMessage.warning('地址解析失败,请把发生地点写得更具体后重试')
-        return
-      }
-      const [lng, lat] = bd09ToWgs84(point.lng, point.lat)
-      addForm.value.longitude = Number(lng.toFixed(6))
-      addForm.value.latitude = Number(lat.toFixed(6))
-      ElMessage.success('坐标解析成功:' + formatGeo(addForm.value.longitude, addForm.value.latitude))
-    })
-  } catch (error) {
-    geoLoading.value = false
-    ElMessage.warning('地址解析失败,请稍后重试')
+  const point = await lookupCoordinate(address)
+  geoLoading.value = false
+  if (!point) {
+    ElMessage.warning('地址解析失败,请把发生地点写得更具体后重试')
+    return
   }
+  addForm.value.longitude = Number(point.lng.toFixed(6))
+  addForm.value.latitude = Number(point.lat.toFixed(6))
+  ElMessage.success('坐标解析成功:' + formatGeo(addForm.value.longitude, addForm.value.latitude))
 }
 
 function handleAdd() {

+ 1 - 1
src/views/subSystem/dispatch/dishome/disHome.vue

@@ -202,7 +202,7 @@ const gisError = ref('')
 
 const hasCoord = (v) => v !== null && v !== undefined && v !== ''
 
-// GIS 点位:紧急红 / 较大黄 / 一般绿(库内 WGS84,组件内自动转百度坐标)
+// GIS 点位:紧急红 / 较大黄 / 一般绿(库内 WGS84,与天地图 CGCS2000 一致,直接使用)
 const mapPoints = computed(() => {
   const list = board.value.mapEvents || []
   return list

+ 11 - 11
src/views/subSystem/drainage/jcsj/bzyxqk.vue

@@ -354,21 +354,21 @@ let mapInstance = null
 
 function initMap() {
   const container = document.getElementById('bzyxqkMap')
-  if (!container || typeof BMapGL === 'undefined') {
-    console.warn('BMapGL 未加载或容器不存在')
+  if (!container || typeof TdtMap === 'undefined') {
+    console.warn('TdtMap 未加载或容器不存在')
     return
   }
 
   try {
-    mapInstance = new BMapGL.Map('bzyxqkMap')
-    const centerPoint = new BMapGL.Point(110.393, 28.452)
+    mapInstance = new TdtMap.Map('bzyxqkMap')
+    const centerPoint = new TdtMap.Point(110.393, 28.452)
     mapInstance.centerAndZoom(centerPoint, 15)
     mapInstance.enableScrollWheelZoom(true)
 
     // 渲染泵站标记(数据加载完成后由loadData调用)
     renderMapMarkers()
   } catch (e) {
-    console.error('百度地图初始化失败:', e)
+    console.error('天地图初始化失败:', e)
   }
 }
 
@@ -381,7 +381,7 @@ function renderMapMarkers() {
   pumpList.value.forEach(pump => {
     if (pump.lng && pump.lat) {
       addMapMarker(pump)
-      allPoints.push(new BMapGL.Point(pump.lng, pump.lat))
+      allPoints.push(new TdtMap.Point(pump.lng, pump.lat))
     }
   })
 
@@ -403,16 +403,16 @@ function addMapMarker(pump) {
     'fault': '故障'
   }
   const color = statusColor[pump.statusClass] || '#409eff'
-  const bPoint = new BMapGL.Point(pump.lng, pump.lat)
+  const bPoint = new TdtMap.Point(pump.lng, pump.lat)
 
   const html = `<div style="text-align:center;cursor:pointer;">
     <div style="color:#fff;background:${color};border-radius:4px;padding:2px 6px;font-size:11px;white-space:nowrap;display:inline-block;margin-bottom:2px;">${pump.name}</div>
     <div><img src="${locationIcon}" style="width:24px;height:24px;display:block;margin:0 auto;"/></div>
   </div>`
 
-  const label = new BMapGL.Label(html, {
+  const label = new TdtMap.Label(html, {
     position: bPoint,
-    offset: new BMapGL.Size(-30, -55)
+    offset: new TdtMap.Size(-30, -55)
   })
   label.setStyle({
     border: 'none',
@@ -432,7 +432,7 @@ function addMapMarker(pump) {
       <div>经度:${pump.lng}</div>
       <div>纬度:${pump.lat}</div>
     </div>`
-    const infoWindow = new BMapGL.InfoWindow(infoContent, {
+    const infoWindow = new TdtMap.InfoWindow(infoContent, {
       width: 220,
       title: ''
     })
@@ -555,7 +555,7 @@ window.addEventListener('resize', () => {
 /* ==================== 全局暗色大屏 ==================== */
 .bzyxqk-screen {
   width: 100%;
-  height: 100vh;
+  height: calc(100vh - 84px);
   background: #0a1929;
   color: #fff;
   font-family: 'Microsoft YaHei', sans-serif;

+ 4 - 5
src/views/subSystem/drainage/jcsj/gcssgl.vue

@@ -298,7 +298,6 @@ import { ElMessage, ElMessageBox } from 'element-plus'
 import { Aim, Refresh } from '@element-plus/icons-vue'
 import GisMapCanvas from '@/components/GisMapCanvas.vue'
 import { useDict } from '@/utils/dict'
-import { bd09ToWgs84 } from '@/utils/coordTransform'
 import {
   getFacilityPage,
   getFacilityMap,
@@ -496,7 +495,7 @@ function fitMap() {
   mapRef.value?.fitBounds()
 }
 
-// 地图实例就绪后再贴合视野:百度 SDK 异步加载,可能晚于接口数据返回
+// 地图实例就绪后再贴合视野:地图初始化可能晚于接口数据返回
 function onMapReady() {
   nextTick(() => fitMap())
 }
@@ -600,7 +599,7 @@ function isStorageType(value) {
   return String(value) === '1'
 }
 
-/** 表单里的坐标标记(内部已是 WGS84,组件会自动转百度坐标) */
+/** 表单里的坐标标记(内部已是 WGS84,与天地图坐标系一致) */
 const formPoints = computed(() => {
   const coord = toCoord(form.value.longitude, form.value.latitude)
   if (!coord) return []
@@ -662,12 +661,12 @@ function onFormMapReady() {
   })
 }
 
-/** 地图点选:百度坐标 → WGS84 后回填,与后端存储口径一致 */
+/** 地图点选:坐标直接回填,WGS84 与 CGCS2000 口径一致 */
 function handleMapPick(point) {
   const lng = Number(point?.lng)
   const lat = Number(point?.lat)
   if (!Number.isFinite(lng) || !Number.isFinite(lat)) return
-  const [wgsLng, wgsLat] = bd09ToWgs84(lng, lat)
+  const wgsLng = Number(lng), wgsLat = Number(lat)
   form.value.longitude = Number(wgsLng.toFixed(8))
   form.value.latitude = Number(wgsLat.toFixed(8))
 }

+ 22 - 40
src/views/subSystem/drainage/jcsj/gwyxqk.vue

@@ -182,6 +182,7 @@ import locationIcon from '@/assets/images/location.png'
 import { getPipeNetworkPage, getEquipmentPage, getAlarmDataPage, getRadarDataByCode, getRadarDataHistory } from '@/api/drainage'
 import { getGisPipeLines } from '@/api/pipeNetwork/basic'
 import { convertCoord } from '@/utils/coordTransform'
+import { whenMapReady } from '@/utils/mapApi'
 
 // ==================== 实时时间 ====================
 const currentDateTime = ref('')
@@ -509,17 +510,16 @@ function loadAlarmData(alarmRecords, equipmentList) {
 
 // ==================== 地图初始化 ====================
 let mapInstance = null
-let mapInitTimer = null
 
 function initMap() {
   const container = document.getElementById('gwyxqkMap')
-  if (!container || typeof BMapGL === 'undefined') {
-    console.warn('BMapGL 未加载或容器不存在')
+  if (!container || !window.TdtMap) {
+    console.warn('TdtMap 未加载或容器不存在')
     return
   }
 
   try {
-    mapInstance = new BMapGL.Map('gwyxqkMap')
+    mapInstance = new TdtMap.Map('gwyxqkMap')
     // 默认中心点,有数据时以第一个设备点为中心
     let centerLng = 110.393
     let centerLat = 28.452
@@ -528,7 +528,7 @@ function initMap() {
       centerLat = mapPoints.value[0].lat
     }
     const [mapCenterLng, mapCenterLat] = convertCoord(centerLng, centerLat)
-    const centerPoint = new BMapGL.Point(mapCenterLng, mapCenterLat)
+    const centerPoint = new TdtMap.Point(mapCenterLng, mapCenterLat)
     mapInstance.centerAndZoom(centerPoint, 14)
     mapInstance.enableScrollWheelZoom(true)
 
@@ -538,11 +538,11 @@ function initMap() {
     mapPoints.value.forEach(point => {
       addMapMarker(point)
       const [lng, lat] = convertCoord(point.lng, point.lat)
-      viewportPoints.push(new BMapGL.Point(lng, lat))
+      viewportPoints.push(new TdtMap.Point(lng, lat))
     })
     if (viewportPoints.length > 0) mapInstance.setViewport(viewportPoints)
   } catch (e) {
-    console.error('百度地图初始化失败:', e)
+    console.error('天地图初始化失败:', e)
   }
 }
 
@@ -565,11 +565,11 @@ function escapeMapText(value) {
 function drawGisPipelines() {
   const viewportPoints = []
   gisPipelines.value.forEach(line => {
-    const bmapPoints = line.points.map(point => {
+    const linePoints = line.points.map(point => {
       const [lng, lat] = convertCoord(point.lng, point.lat)
-      return new BMapGL.Point(lng, lat)
+      return new TdtMap.Point(lng, lat)
     })
-    const polyline = new BMapGL.Polyline(bmapPoints, {
+    const polyline = new TdtMap.Polyline(linePoints, {
       strokeColor: getGisPipelineColor(line.level),
       strokeWeight: 5,
       strokeOpacity: 0.9
@@ -585,10 +585,10 @@ function drawGisPipelines() {
         <div>材质:${escapeMapText(line.material || '-')}</div>
         <div>位置:${escapeMapText(line.location || '-')}</div>
       </div>`
-      mapInstance.openInfoWindow(new BMapGL.InfoWindow(content, { width: 280, enableMessage: false }), bmapPoints[0])
+      mapInstance.openInfoWindow(new TdtMap.InfoWindow(content, { width: 280, enableMessage: false }), linePoints[0])
     })
     mapInstance.addOverlay(polyline)
-    viewportPoints.push(...bmapPoints)
+    viewportPoints.push(...linePoints)
   })
   return viewportPoints
 }
@@ -596,7 +596,7 @@ function drawGisPipelines() {
 function addMapMarker(point) {
   const color = point.color || '#409eff'
   const [lng, lat] = convertCoord(point.lng, point.lat)
-  const bPoint = new BMapGL.Point(lng, lat)
+  const bPoint = new TdtMap.Point(lng, lat)
 
   const html = `<div style="text-align:center;cursor:pointer;">
     <div style="color:#fff;background:${color};border-radius:4px;padding:2px 6px;font-size:11px;white-space:nowrap;display:inline-block;margin-bottom:2px;">${point.name}</div>
@@ -604,9 +604,9 @@ function addMapMarker(point) {
     <div><img src="${locationIcon}" style="width:24px;height:24px;display:block;margin:0 auto;"/></div>
   </div>`
 
-  const label = new BMapGL.Label(html, {
+  const label = new TdtMap.Label(html, {
     position: bPoint,
-    offset: new BMapGL.Size(-30, -55)
+    offset: new TdtMap.Size(-30, -55)
   })
   label.setStyle({
     border: 'none',
@@ -630,7 +630,7 @@ function addMapMarker(point) {
       <div>经度:${point.lng}</div>
       <div>纬度:${point.lat}</div>
     </div>`
-    const infoWindow = new BMapGL.InfoWindow(infoContent, {
+    const infoWindow = new TdtMap.InfoWindow(infoContent, {
       width: 240,
       title: ''
     })
@@ -663,7 +663,7 @@ function addPipeNetworkMarker(network) {
   const networkName = network.name || network.networkName || '管网'
   points.forEach(p => {
     if (!p.lng || !p.lat) return
-    const bPoint = new BMapGL.Point(Number(p.lng), Number(p.lat))
+    const bPoint = new TdtMap.Point(Number(p.lng), Number(p.lat))
     const pointName = p.name || networkName
 
     const html = `<div style="text-align:center;cursor:pointer;">
@@ -672,9 +672,9 @@ function addPipeNetworkMarker(network) {
       <div><img src="${locationIcon}" style="width:20px;height:20px;display:block;margin:0 auto;"/></div>
     </div>`
 
-    const label = new BMapGL.Label(html, {
+    const label = new TdtMap.Label(html, {
       position: bPoint,
-      offset: new BMapGL.Size(-30, -50)
+      offset: new TdtMap.Size(-30, -50)
     })
     label.setStyle({
       border: 'none',
@@ -693,7 +693,7 @@ function addPipeNetworkMarker(network) {
         <div>经度:${p.lng}</div>
         <div>纬度:${p.lat}</div>
       </div>`
-      const infoWindow = new BMapGL.InfoWindow(infoContent, {
+      const infoWindow = new TdtMap.InfoWindow(infoContent, {
         width: 240,
         title: ''
       })
@@ -994,7 +994,7 @@ onMounted(async () => {
 
   // 延迟初始化确保 DOM 渲染完成
   setTimeout(() => {
-    waitForBMapAndInit()
+    whenMapReady(initMap, () => { pipelineMapError.value = '天地图加载超时,请检查网络或地图配置。' })
     initPieChart()
     // 加载默认设备的曲线历史数据
     if (selectedCurveDevice.value) {
@@ -1008,26 +1008,8 @@ onMounted(async () => {
   }, 300)
 })
 
-function waitForBMapAndInit() {
-  let attempts = 0
-  const tryInit = () => {
-    attempts += 1
-    if (typeof BMapGL !== 'undefined') {
-      initMap()
-      return
-    }
-    if (attempts >= 100) {
-      pipelineMapError.value = '百度地图加载超时,请检查网络或地图配置。'
-      return
-    }
-    mapInitTimer = window.setTimeout(tryInit, 200)
-  }
-  tryInit()
-}
-
 onUnmounted(() => {
   if (timer) clearInterval(timer)
-  if (mapInitTimer) clearTimeout(mapInitTimer)
   if (pieChart) pieChart.dispose()
   if (lineChart) lineChart.dispose()
   if (barChart) barChart.dispose()
@@ -1046,7 +1028,7 @@ window.addEventListener('resize', () => {
 /* ==================== 全局暗色大屏 ==================== */
 .gwyxqk-screen {
   width: 100%;
-  height: 100vh;
+  height: calc(100vh - 84px);
   background: #0a1929;
   color: #fff;
   font-family: 'Microsoft YaHei', sans-serif;

+ 11 - 157
src/views/subSystem/drainage/jcsj/jsqc.vue

@@ -16,22 +16,6 @@
     <div class="screen-body">
       <!-- ========== 左侧面板 ========== -->
       <div class="panel-left">
-        <!-- 天气预报 -->
-        <div class="panel-box">
-          <div class="panel-title">
-            <span class="title-icon">▶</span> 天气预报
-          </div>
-          <div class="weather-cards">
-            <div class="weather-card" v-for="(w, i) in weatherList" :key="i">
-              <div class="weather-date">{{ w.date }}</div>
-              <div class="weather-icon">{{ getWeatherIcon(w.text) }}</div>
-              <div class="weather-desc">{{ w.desc }}</div>
-              <div class="weather-temp">{{ w.tempRange }}</div>
-              <div class="weather-wind">{{ w.wind }}</div>
-            </div>
-          </div>
-        </div>
-
         <!-- 监测点概况 -->
         <div class="panel-box">
           <div class="panel-title">
@@ -151,89 +135,6 @@ function updateRefreshTime() {
   refreshTime.value = `${pad(now.getHours())}:${pad(now.getMinutes())}:${pad(now.getSeconds())}`
 }
 
-// ==================== 天气数据 ====================
-const weatherList = ref([])
-
-// 百度地图 AK(与 index.html 中加载百度地图 SDK 使用同一个)
-const BAIDU_MAP_AK = 'mvSNSN4CPN6WJs2Xi6sQFNmWwCLRgU6T'
-
-// 天气文字 -> emoji 图标映射
-function getWeatherIcon(text) {
-  if (!text) return '☀️'
-  const t = String(text)
-  if (t.includes('晴')) return '☀️'
-  if (t.includes('多云')) return '⛅'
-  if (t.includes('阴')) return '☁️'
-  if (t.includes('雨') && t.includes('雪')) return '🌨️'
-  if (t.includes('雪')) return '❄️'
-  if (t.includes('雷')) return '⛈️'
-  if (t.includes('雨')) return '🌧️'
-  if (t.includes('雾') || t.includes('霾')) return '🌫️'
-  return '☁️'
-}
-
-// 简易 JSONP 调用百度天气服务(服务端未开放 CORS,需用 JSONP)
-function jsonpRequest(url) {
-  return new Promise((resolve, reject) => {
-    const callbackName = 'jsqcWeatherCallback_' + Date.now()
-    const script = document.createElement('script')
-    script.src = url + (url.includes('?') ? '&' : '?') + 'callback=' + callbackName
-    script.onerror = () => {
-      document.head.removeChild(script)
-      delete window[callbackName]
-      reject(new Error('天气接口加载失败'))
-    }
-    window[callbackName] = (data) => {
-      document.head.removeChild(script)
-      delete window[callbackName]
-      resolve(data)
-    }
-    document.head.appendChild(script)
-  })
-}
-
-async function loadWeatherData() {
-  try {
-    const url = `/baidu-weather/weather/v1/?district_id=431222&data_type=all&ak=${BAIDU_MAP_AK}&output=json`
-    const response = await fetch(url)
-    const res = await response.json()
-    if (res && (res.status === 0 || res.status === '0') && res.result && Array.isArray(res.result.forecasts)) {
-      weatherList.value = res.result.forecasts.slice(0, 3).map(day => {
-        const dateStr = day.date || ''
-        const monthDay = dateStr.length >= 10 ? `${dateStr.slice(5, 7)}/${dateStr.slice(8, 10)}` : dateStr
-        return {
-          date: monthDay,
-          text: day.text_day || day.text_night || '多云',
-          desc: day.text_day || '多云',
-          tempRange: `${day.low || '--'}°C ~ ${day.high || '--'}°C`,
-          wind: `${day.wd_day || ''} ${day.wc_day || ''}`
-        }
-      })
-    } else {
-      console.warn('百度天气返回异常:', res)
-      useMockWeather()
-    }
-  } catch (e) {
-    console.error('加载天气数据失败:', e)
-    useMockWeather()
-  }
-}
-
-// 天气接口失败时使用模拟数据兜底
-function useMockWeather() {
-  const now = new Date()
-  const pad = n => String(n).padStart(2, '0')
-  const formatDate = (offset) => {
-    const d = new Date(now.getTime() + offset * 24 * 60 * 60 * 1000)
-    return `${pad(d.getMonth() + 1)}/${pad(d.getDate())}`
-  }
-  weatherList.value = [
-    { date: formatDate(0), text: '多云', desc: '多云', tempRange: '18°C ~ 26°C', wind: '东南风 2级' },
-    { date: formatDate(1), text: '小雨', desc: '小雨', tempRange: '16°C ~ 23°C', wind: '东风 3级' },
-    { date: formatDate(2), text: '阴', desc: '阴', tempRange: '15°C ~ 22°C', wind: '北风 2级' }
-  ]
-}
-
 // ==================== 监测点概况 ====================
 const pieData = ref([
   { name: '未积水', value: 0, color: '#67c23a' },
@@ -333,13 +234,13 @@ let mapInstance = null
 
 function initMap() {
   const container = document.getElementById('jsqcMap')
-  if (!container || typeof BMapGL === 'undefined') {
-    console.warn('BMapGL 未加载或容器不存在')
+  if (!container || typeof TdtMap === 'undefined') {
+    console.warn('TdtMap 未加载或容器不存在')
     return
   }
 
   try {
-    mapInstance = new BMapGL.Map('jsqcMap')
+    mapInstance = new TdtMap.Map('jsqcMap')
     mapInstance.enableScrollWheelZoom(true)
 
     // 添加标记点
@@ -349,17 +250,17 @@ function initMap() {
 
     // 根据所有点位自适应视野
     if (mapPoints.value.length > 0) {
-      const viewPoints = mapPoints.value.map(p => new BMapGL.Point(p.lng, p.lat))
+      const viewPoints = mapPoints.value.map(p => new TdtMap.Point(p.lng, p.lat))
       mapInstance.setViewport(viewPoints)
       // 确保缩放级别不低于14,避免地图显示太小
       if (mapInstance.getZoom() < 14) {
         mapInstance.setZoom(14)
       }
     } else {
-      mapInstance.centerAndZoom(new BMapGL.Point(110.393, 28.452), 14)
+      mapInstance.centerAndZoom(new TdtMap.Point(110.393, 28.452), 14)
     }
   } catch (e) {
-    console.error('百度地图初始化失败:', e)
+    console.error('天地图初始化失败:', e)
   }
 }
 
@@ -371,7 +272,7 @@ function addMapMarker(point) {
     '严重积水': '#f56c6c'
   }
   const color = levelColor[point.level] || '#409eff'
-  const bPoint = new BMapGL.Point(point.lng, point.lat)
+  const bPoint = new TdtMap.Point(point.lng, point.lat)
 
   const html = `<div style="text-align:center;cursor:pointer;">
     <div style="color:#fff;background:${color};border-radius:4px;padding:2px 6px;font-size:11px;white-space:nowrap;display:inline-block;margin-bottom:2px;">${point.name}</div>
@@ -379,9 +280,9 @@ function addMapMarker(point) {
     <div><img src="${locationIcon}" style="width:24px;height:24px;display:block;margin:0 auto;"/></div>
   </div>`
 
-  const label = new BMapGL.Label(html, {
+  const label = new TdtMap.Label(html, {
     position: bPoint,
-    offset: new BMapGL.Size(-30, -55)
+    offset: new TdtMap.Size(-30, -55)
   })
   label.setStyle({
     border: 'none',
@@ -595,10 +496,7 @@ onMounted(async () => {
   updateRefreshTime()
 
   // 先加载后端数据,再初始化地图和图表
-  await Promise.all([
-    loadWaterloggingData(),
-    loadWeatherData()
-  ])
+  await loadWaterloggingData()
 
   // 延迟初始化确保 DOM 渲染完成
   setTimeout(() => {
@@ -628,7 +526,7 @@ window.addEventListener('resize', () => {
 /* ==================== 全局暗色大屏 ==================== */
 .jsqc-screen {
   width: 100%;
-  height: 100vh;
+  height: calc(100vh - 84px);
   background: #0a1929;
   color: #fff;
   font-family: 'Microsoft YaHei', sans-serif;
@@ -775,50 +673,6 @@ window.addEventListener('resize', () => {
   font-weight: 400;
 }
 
-/* ==================== 天气预报 ==================== */
-.weather-cards {
-  display: flex;
-  gap: 8px;
-  flex-shrink: 0;
-}
-
-.weather-card {
-  flex: 1;
-  background: rgba(20, 60, 110, 0.5);
-  border: 1px solid rgba(64, 158, 255, 0.15);
-  border-radius: 6px;
-  padding: 10px 8px;
-  text-align: center;
-}
-
-.weather-date {
-  font-size: 12px;
-  color: #a0cfff;
-  margin-bottom: 4px;
-}
-
-.weather-icon {
-  font-size: 24px;
-  margin: 4px 0;
-}
-
-.weather-desc {
-  font-size: 12px;
-  color: #d0e8ff;
-  margin-bottom: 2px;
-}
-
-.weather-temp {
-  font-size: 11px;
-  color: #7ec8e3;
-}
-
-.weather-wind {
-  font-size: 10px;
-  color: #607080;
-  margin-top: 2px;
-}
-
 /* ==================== 监测点概况 ==================== */
 .pie-chart-wrapper {
   flex: 1;

+ 19 - 19
src/views/subSystem/drainage/jcsj/psfqgl.vue

@@ -364,9 +364,9 @@ function createMapMarker(map, bPoint, name, bgColor) {
     <div style="color:#fff;background:${color};border-radius:4px;padding:2px 6px;font-size:11px;white-space:nowrap;display:inline-block;margin-bottom:2px;">${name || ''}</div>
     <div><img src="${locationIcon}" style="width:28px;height:28px;display:block;margin:0 auto;"/></div>
   </div>`
-  const label = new BMapGL.Label(html, {
+  const label = new TdtMap.Label(html, {
     position: bPoint,
-    offset: new BMapGL.Size(-30, -50)
+    offset: new TdtMap.Size(-30, -50)
   })
   label.setStyle({
     border: 'none',
@@ -414,13 +414,13 @@ function initMainMap() {
   const container = document.getElementById('psfqglMainMap')
   if (!container) return
   try {
-    mainMapInstance = new BMapGL.Map('psfqglMainMap')
-    const centerPoint = new BMapGL.Point(110.393, 28.452)
+    mainMapInstance = new TdtMap.Map('psfqglMainMap')
+    const centerPoint = new TdtMap.Point(110.393, 28.452)
     mainMapInstance.centerAndZoom(centerPoint, 12)
     mainMapInstance.enableScrollWheelZoom(true)
     renderMainMapMarkers()
   } catch (e) {
-    console.error('顶部百度地图初始化失败:', e)
+    console.error('顶部天地图初始化失败:', e)
   }
 }
 
@@ -441,7 +441,7 @@ function renderMainMapMarkers() {
     const color = item.partitionType === '污水分区' ? '#e6a23c' : '#409eff'
     const points = parsePolygonPoints(item.points)
     if (points.length >= 3) {
-      const polygon = new BMapGL.Polygon(points.map(point => new BMapGL.Point(point.lng, point.lat)), {
+      const polygon = new TdtMap.Polygon(points.map(point => new TdtMap.Point(point.lng, point.lat)), {
         strokeColor: color,
         fillColor: color,
         strokeWeight: 2,
@@ -451,10 +451,10 @@ function renderMainMapMarkers() {
       mainMapInstance.addOverlay(polygon)
       mainMapMarkers.value.push(polygon)
       const center = getPartitionCenter(item)
-      const label = createMapMarker(mainMapInstance, new BMapGL.Point(center.lng, center.lat), item.name, color)
+      const label = createMapMarker(mainMapInstance, new TdtMap.Point(center.lng, center.lat), item.name, color)
       mainMapMarkers.value.push(label)
     } else {
-      const bPoint = new BMapGL.Point(item.longitude, item.latitude)
+      const bPoint = new TdtMap.Point(item.longitude, item.latitude)
       const label = createMapMarker(mainMapInstance, bPoint, item.name, color)
       mainMapMarkers.value.push(label)
     }
@@ -594,24 +594,24 @@ function initViewMap() {
   if (!container) return
 
   try {
-    viewMapInstance = new BMapGL.Map('psfqglViewMap')
+    viewMapInstance = new TdtMap.Map('psfqglViewMap')
     const center = getPartitionCenter(currentRow.value)
     const centerLng = center.lng || 110.393
     const centerLat = center.lat || 28.452
-    const centerPoint = new BMapGL.Point(centerLng, centerLat)
+    const centerPoint = new TdtMap.Point(centerLng, centerLat)
     viewMapInstance.centerAndZoom(centerPoint, 15)
     viewMapInstance.enableScrollWheelZoom(true)
 
     const points = parsePolygonPoints(currentRow.value.points)
     if (points.length >= 3) {
-      viewMapInstance.addOverlay(new BMapGL.Polygon(points.map(point => new BMapGL.Point(point.lng, point.lat)), {
+      viewMapInstance.addOverlay(new TdtMap.Polygon(points.map(point => new TdtMap.Point(point.lng, point.lat)), {
         strokeColor: '#409eff', fillColor: '#409eff', strokeWeight: 2, fillOpacity: 0.25
       }))
     } else if (currentRow.value.longitude && currentRow.value.latitude) {
-      createMapMarker(viewMapInstance, new BMapGL.Point(currentRow.value.longitude, currentRow.value.latitude), currentRow.value.name)
+      createMapMarker(viewMapInstance, new TdtMap.Point(currentRow.value.longitude, currentRow.value.latitude), currentRow.value.name)
     }
   } catch (e) {
-    console.error('百度地图初始化失败:', e)
+    console.error('天地图初始化失败:', e)
   }
 }
 
@@ -712,10 +712,10 @@ function initEditMap() {
   if (!container) return
 
   try {
-    editMapInstance = new BMapGL.Map('psfqglEditMap')
+    editMapInstance = new TdtMap.Map('psfqglEditMap')
     const centerLng = editForm.value.longitude || 110.393
     const centerLat = editForm.value.latitude || 28.452
-    const centerPoint = new BMapGL.Point(centerLng, centerLat)
+    const centerPoint = new TdtMap.Point(centerLng, centerLat)
     editMapInstance.centerAndZoom(centerPoint, 15)
     editMapInstance.enableScrollWheelZoom(true)
 
@@ -730,7 +730,7 @@ function initEditMap() {
       renderEditPolygon()
     })
   } catch (e) {
-    console.error('百度地图初始化失败:', e)
+    console.error('天地图初始化失败:', e)
   }
 }
 
@@ -760,7 +760,7 @@ function renderEditPolygon() {
   editPointOverlays.value.forEach(overlay => editMapInstance.removeOverlay(overlay))
   editPointOverlays.value = []
   editPolygonPoints.value.forEach(point => {
-    const overlay = new BMapGL.Circle(new BMapGL.Point(point.lng, point.lat), 5, {
+    const overlay = new TdtMap.Circle(new TdtMap.Point(point.lng, point.lat), 5, {
       strokeColor: '#ffffff',
       strokeWeight: 2,
       strokeOpacity: 1,
@@ -771,8 +771,8 @@ function renderEditPolygon() {
     editPointOverlays.value.push(overlay)
   })
   if (editPolygonPoints.value.length < 2) return
-  const points = editPolygonPoints.value.map(point => new BMapGL.Point(point.lng, point.lat))
-  const OverlayType = editPolygonPoints.value.length >= 3 ? BMapGL.Polygon : BMapGL.Polyline
+  const points = editPolygonPoints.value.map(point => new TdtMap.Point(point.lng, point.lat))
+  const OverlayType = editPolygonPoints.value.length >= 3 ? TdtMap.Polygon : TdtMap.Polyline
   editPolygonOverlay = new OverlayType(points, {
     strokeColor: '#409eff',
     strokeWeight: 2,

+ 1 - 1
src/views/subSystem/drainage/jcsj/psgwgl.vue

@@ -383,7 +383,7 @@ function fitMap() {
   mapRef.value?.fitBounds()
 }
 
-// 地图实例就绪后再贴合视野:百度 SDK 异步加载,可能晚于接口数据返回
+// 地图实例就绪后再贴合视野:地图初始化可能晚于接口数据返回
 function onMapReady() {
   nextTick(() => fitMap())
 }

+ 3 - 3
src/views/subSystem/drainage/jcsj/sbyx.vue

@@ -780,8 +780,8 @@ function initLocateMap() {
   if (!container) return
   const lng = parseFloat(locateRow.value.longitude)
   const lat = parseFloat(locateRow.value.latitude)
-  const center = lng && lat ? new BMapGL.Point(lng, lat) : new BMapGL.Point(110.393, 28.452)
-  locateMap = new BMapGL.Map('sbyxLocationMap')
+  const center = lng && lat ? new TdtMap.Point(lng, lat) : new TdtMap.Point(110.393, 28.452)
+  locateMap = new TdtMap.Map('sbyxLocationMap')
   locateMap.centerAndZoom(center, 15)
   locateMap.enableScrollWheelZoom(true)
   if (lng && lat) {
@@ -789,7 +789,7 @@ function initLocateMap() {
       <div style="color:#fff;background:#409eff;border-radius:4px;padding:2px 6px;font-size:11px;white-space:nowrap;display:inline-block;margin-bottom:2px;">${locateRow.value.equipmentName || ''}</div>
       <div><img src="${locationIcon}" style="width:28px;height:28px;display:block;margin:0 auto;"/></div>
     </div>`
-    const label = new BMapGL.Label(html, { position: new BMapGL.Point(lng, lat), offset: new BMapGL.Size(-30, -50) })
+    const label = new TdtMap.Label(html, { position: new TdtMap.Point(lng, lat), offset: new TdtMap.Size(-30, -50) })
     label.setStyle({ border: 'none', background: 'transparent', padding: '0' })
     locateMap.addOverlay(label)
   }

+ 2 - 3
src/views/subSystem/drainage/jcsj/yjsdgl.vue

@@ -382,7 +382,6 @@ import { Aim, Refresh } from '@element-plus/icons-vue'
 import GisMapCanvas from '@/components/GisMapCanvas.vue'
 import DrainageLiveTile from '@/views/subSystem/drainage/spjc/DrainageLiveTile.vue'
 import { useDict } from '@/utils/dict'
-import { bd09ToWgs84 } from '@/utils/coordTransform'
 import { getDrainageVideoMap } from '@/api/drainageVideo'
 import { deptTreeSelect, enabledAll } from '@/api/system/user'
 import {
@@ -974,12 +973,12 @@ function onFormMapReady() {
   })
 }
 
-/** 地图点选:百度坐标 → WGS84 后回填,与后端存储口径一致 */
+/** 地图点选:坐标直接回填,WGS84 与 CGCS2000 口径一致 */
 function handleMapPick(point) {
   const lng = Number(point?.lng)
   const lat = Number(point?.lat)
   if (!Number.isFinite(lng) || !Number.isFinite(lat)) return
-  const [wgsLng, wgsLat] = bd09ToWgs84(lng, lat)
+  const wgsLng = Number(lng), wgsLat = Number(lat)
   form.value.longitude = Number(wgsLng.toFixed(8))
   form.value.latitude = Number(wgsLat.toFixed(8))
 }

+ 18 - 14
src/views/subSystem/drainage/jcyj/dqyjGIS.vue

@@ -104,7 +104,7 @@ const rawList = ref([])
 /** 归一化后的全部预警(含无坐标的,清单里照样能看详情) */
 const warningList = ref([])
 
-/** 能上图的点位:百度的 Point 只接受有效经纬度 */
+/** 能上图的点位:地图 Point 只接受有效经纬度 */
 const mapPoints = computed(() => warningList.value.filter((p) => p.lng && p.lat))
 
 const levelMap = { 1: '严重', 2: '重要', 3: '一般' }
@@ -245,25 +245,25 @@ const markerMap = new Map()
 
 function initMap() {
   const container = document.getElementById('dqyjGISMap')
-  if (!container || typeof BMapGL === 'undefined') {
-    console.warn('BMapGL 未加载或容器不存在')
+  if (!container || typeof TdtMap === 'undefined') {
+    console.warn('TdtMap 未加载或容器不存在')
     return
   }
   try {
-    mapInstance = new BMapGL.Map('dqyjGISMap')
-    mapInstance.centerAndZoom(new BMapGL.Point(110.393, 28.452), 15)
+    mapInstance = new TdtMap.Map('dqyjGISMap')
+    mapInstance.centerAndZoom(new TdtMap.Point(110.393, 28.452), 15)
     mapInstance.enableScrollWheelZoom(true)
 
     mapPoints.value.forEach(addMapMarker)
     fitView()
   } catch (e) {
-    console.error('百度地图初始化失败:', e)
+    console.error('天地图初始化失败:', e)
   }
 }
 
 function addMapMarker(point) {
   const color = getLevelColor(point.level)
-  const bPoint = new BMapGL.Point(point.lng, point.lat)
+  const bPoint = new TdtMap.Point(point.lng, point.lat)
   const html = `<div class="gis-marker" style="text-align:center;cursor:pointer;">
     <div style="color:#fff;background:${color};border-radius:4px;padding:2px 8px;font-size:11px;white-space:nowrap;display:inline-block;margin-bottom:2px;box-shadow:0 1px 4px rgba(0,0,0,0.3);">
       ${point.name}
@@ -271,9 +271,9 @@ function addMapMarker(point) {
     <div><img src="${locationIcon}" style="width:28px;height:28px;display:block;margin:0 auto;"/></div>
   </div>`
 
-  const label = new BMapGL.Label(html, {
+  const label = new TdtMap.Label(html, {
     position: bPoint,
-    offset: new BMapGL.Size(-30, -50)
+    offset: new TdtMap.Size(-30, -50)
   })
   label.setStyle({ border: 'none', background: 'transparent', padding: '0', zIndex: '10' })
   mapInstance.addOverlay(label)
@@ -285,7 +285,7 @@ function addMapMarker(point) {
 /** 回到能看全所有点位的视野 */
 function fitView() {
   if (!mapInstance || !mapPoints.value.length) return
-  const points = mapPoints.value.map((p) => new BMapGL.Point(p.lng, p.lat))
+  const points = mapPoints.value.map((p) => new TdtMap.Point(p.lng, p.lat))
   if (points.length === 1) {
     mapInstance.centerAndZoom(points[0], 16)
     return
@@ -297,7 +297,7 @@ function fitView() {
 function locate(item) {
   if (!item) return
   if (mapInstance && item.lng && item.lat) {
-    mapInstance.centerAndZoom(new BMapGL.Point(item.lng, item.lat), 17)
+    mapInstance.centerAndZoom(new TdtMap.Point(item.lng, item.lat), 17)
   }
   if (!item.lng || !item.lat) {
     ElMessage.warning('该预警缺少经纬度坐标,无法在地图上定位')
@@ -326,11 +326,15 @@ onMounted(async () => {
 .gis-page {
   position: relative;
   width: 100%;
-  height: 100vh;
+  height: calc(100vh - 84px);
   overflow: hidden;
 }
 
 .map-container {
+  /* Leaflet 内部 pane 的 z-index 为 200~800,用独立堆叠上下文限制在容器内,
+     否则会盖住 z-index 更低的左侧面板和右上工具条 */
+  position: relative;
+  z-index: 0;
   width: 100%;
   height: 100%;
 }
@@ -347,7 +351,7 @@ onMounted(async () => {
   background: rgba(6, 30, 65, 0.88);
   border: 1px solid rgba(90, 160, 255, 0.18);
   border-radius: 10px;
-  z-index: 100;
+  z-index: 9999;
   box-shadow: 0 8px 30px rgba(0, 0, 0, 0.45);
   backdrop-filter: blur(6px);
   overflow: hidden;
@@ -614,7 +618,7 @@ onMounted(async () => {
   position: absolute;
   top: 16px;
   right: 16px;
-  z-index: 100;
+  z-index: 9999;
 }
 
 .map-tools :deep(.el-button) {

+ 17 - 13
src/views/subSystem/drainage/jcyj/lsyjGIS.vue

@@ -254,25 +254,25 @@ const markerMap = new Map()
 
 function initMap() {
   const container = document.getElementById('lsyjGISMap')
-  if (!container || typeof BMapGL === 'undefined') {
-    console.warn('BMapGL 未加载或容器不存在')
+  if (!container || typeof TdtMap === 'undefined') {
+    console.warn('TdtMap 未加载或容器不存在')
     return
   }
   try {
-    mapInstance = new BMapGL.Map('lsyjGISMap')
-    mapInstance.centerAndZoom(new BMapGL.Point(110.393, 28.452), 15)
+    mapInstance = new TdtMap.Map('lsyjGISMap')
+    mapInstance.centerAndZoom(new TdtMap.Point(110.393, 28.452), 15)
     mapInstance.enableScrollWheelZoom(true)
 
     mapPoints.value.forEach(addMapMarker)
     fitView()
   } catch (e) {
-    console.error('百度地图初始化失败:', e)
+    console.error('天地图初始化失败:', e)
   }
 }
 
 function addMapMarker(point) {
   const color = getLevelColor(point.level)
-  const bPoint = new BMapGL.Point(point.lng, point.lat)
+  const bPoint = new TdtMap.Point(point.lng, point.lat)
   const html = `<div style="text-align:center;cursor:pointer;">
     <div style="color:#fff;background:${color};border-radius:4px;padding:2px 8px;font-size:11px;white-space:nowrap;display:inline-block;margin-bottom:2px;box-shadow:0 1px 4px rgba(0,0,0,0.3);">
       ${point.name}
@@ -280,9 +280,9 @@ function addMapMarker(point) {
     <div><img src="${locationIcon}" style="width:28px;height:28px;display:block;margin:0 auto;"/></div>
   </div>`
 
-  const label = new BMapGL.Label(html, {
+  const label = new TdtMap.Label(html, {
     position: bPoint,
-    offset: new BMapGL.Size(-30, -50)
+    offset: new TdtMap.Size(-30, -50)
   })
   label.setStyle({ border: 'none', background: 'transparent', padding: '0', zIndex: '10' })
   mapInstance.addOverlay(label)
@@ -294,7 +294,7 @@ function addMapMarker(point) {
 /** 回到能看全所有点位的视野 */
 function fitView() {
   if (!mapInstance || !mapPoints.value.length) return
-  const points = mapPoints.value.map((p) => new BMapGL.Point(p.lng, p.lat))
+  const points = mapPoints.value.map((p) => new TdtMap.Point(p.lng, p.lat))
   if (points.length === 1) {
     mapInstance.centerAndZoom(points[0], 16)
     return
@@ -305,7 +305,7 @@ function fitView() {
 function locate(item) {
   if (!item) return
   if (mapInstance && item.lng && item.lat) {
-    mapInstance.centerAndZoom(new BMapGL.Point(item.lng, item.lat), 17)
+    mapInstance.centerAndZoom(new TdtMap.Point(item.lng, item.lat), 17)
   }
   if (!item.lng || !item.lat) {
     ElMessage.warning('该预警缺少经纬度坐标,无法在地图上定位')
@@ -333,11 +333,15 @@ onMounted(async () => {
 .gis-page {
   position: relative;
   width: 100%;
-  height: 100vh;
+  height: calc(100vh - 84px);
   overflow: hidden;
 }
 
 .map-container {
+  /* Leaflet 内部 pane 的 z-index 为 200~800,用独立堆叠上下文限制在容器内,
+     否则会盖住 z-index 更低的左侧面板和右上工具条 */
+  position: relative;
+  z-index: 0;
   width: 100%;
   height: 100%;
 }
@@ -354,7 +358,7 @@ onMounted(async () => {
   background: rgba(6, 30, 65, 0.88);
   border: 1px solid rgba(90, 160, 255, 0.18);
   border-radius: 10px;
-  z-index: 100;
+  z-index: 9999;
   box-shadow: 0 8px 30px rgba(0, 0, 0, 0.45);
   backdrop-filter: blur(6px);
   overflow: hidden;
@@ -646,7 +650,7 @@ onMounted(async () => {
   position: absolute;
   top: 16px;
   right: 16px;
-  z-index: 100;
+  z-index: 9999;
 }
 
 .map-tools :deep(.el-button) {

+ 1 - 1
src/views/subSystem/drainage/spjc/jcspGIS.vue

@@ -8,7 +8,7 @@
       <div v-for="row in points" :key="row.id" class="resource"><el-button link type="primary" @click="locate(row)">{{ row.deviceName }}</el-button><small>{{ row.districtName }} · {{ row.pointName || '未关联点位' }}</small><el-button v-hasPermi="['video:device:play']" link @click="open(row)">查看视频</el-button></div>
     </aside>
     <main>
-      <GisMapCanvas :key="mapKey" ref="map" :points="points" :marker-size="36" :map-style-id="''" @ready="ready" @error="mapError=$event" @point-click="open" @cluster-click="cluster=$event.points || []" />
+      <GisMapCanvas :key="mapKey" ref="map" :points="points" :marker-size="36" @ready="ready" @error="mapError=$event" @point-click="open" @cluster-click="cluster=$event.points || []" />
       <div v-if="mapError" class="map-error"><span>{{ mapError }}</span><el-button @click="retryMap">重试地图</el-button></div>
       <div v-if="cluster.length" class="cluster"><el-button link @click="cluster=[]">关闭重叠视频列表</el-button><el-button v-for="row in cluster" :key="row.videoId" link type="primary" @click="open(row)">{{ row.deviceName }}</el-button></div>
     </main>

+ 1 - 1
src/views/subSystem/gas/gasBasicadata/gasPipelintNet.vue

@@ -166,7 +166,7 @@ onMounted(loadPipelines)
 </script>
 
 <style scoped>
-.gas-gis-dashboard { background: #f0f2f6; height: 100vh; display: flex; flex-direction: column; overflow: hidden; }
+.gas-gis-dashboard { background: #f0f2f6; height: calc(100vh - 84px); display: flex; flex-direction: column; overflow: hidden; }
 .page-header { background: white; padding: 12px 24px; display: flex; justify-content: space-between; align-items: center; border-bottom: 1px solid #e9ecef; flex-shrink: 0; }
 .header-left, .header-right, .stats-badge { display: flex; align-items: center; gap: 16px; }.header-left .title { font-size: 1.3rem; font-weight: 600; color: #1f2d3d; }.stat-item { font-size: 13px; display: flex; align-items: center; gap: 6px; }.dot { width: 12px; height: 12px; border-radius: 2px; display: inline-block; }.pipeline-dot { background: #e34d59; }.point-dot { background: #409eff; border-radius: 50%; }
 .main-layout { display: flex; flex: 1; overflow: hidden; padding: 12px; gap: 12px; }.layer-panel { width: 260px; background: white; border-radius: 12px; padding: 16px; display: flex; flex-direction: column; gap: 16px; overflow-y: auto; flex-shrink: 0; }.layer-header { font-weight: 600; font-size: 16px; display: flex; align-items: center; gap: 8px; }.group-title, .legend-title, .info-title { font-weight: 600; margin-bottom: 8px; font-size: 13px; color: #606266; }.layer-group .el-checkbox { display: block; margin-left: 8px; margin-bottom: 8px; }.legend-panel { border-top: 1px solid #e9ecef; padding-top: 12px; line-height: 26px; font-size: 13px; }.legend-line { display: inline-block; width: 30px; height: 4px; margin-right: 8px; vertical-align: middle; }.legend-line.high { background: #e34d59; }.legend-line.medium { background: #e6a23c; }.legend-line.low { background: #32a852; }.legend-line.other { background: #409eff; }.info-stats { background: #f5f7fa; border-radius: 8px; padding: 12px; font-size: 13px; line-height: 26px; }

+ 1 - 1
src/views/subSystem/gas/gasBasicadata/gasTestingeq.vue

@@ -486,7 +486,7 @@ onUnmounted(() => window.removeEventListener('resize', renderMap))
 <style scoped>
 .monitor-device-gis {
   background: #f0f2f6;
-  height: 100vh;
+  height: calc(100vh - 84px);
   display: flex;
   flex-direction: column;
   overflow: hidden;

+ 1 - 1
src/views/subSystem/gas/gasOperamon/czspjk.vue

@@ -418,7 +418,7 @@ onMounted(() => {
 <style scoped>
 .gas-video-monitor {
   background: #f0f2f6;
-  height: 100vh;
+  height: calc(100vh - 84px);
   display: flex;
   flex-direction: column;
   overflow: hidden;

+ 1 - 1
src/views/subSystem/gas/gasOperamon/gxlljc.vue

@@ -451,7 +451,7 @@ onBeforeUnmount(() => {
 <style scoped>
 .flow-monitor {
   background: #f0f2f6;
-  height: 100vh;
+  height: calc(100vh - 84px);
   display: flex;
   flex-direction: column;
   overflow: hidden;

+ 1 - 1
src/views/subSystem/gas/gasOperamon/gxyljc.vue

@@ -373,7 +373,7 @@ onBeforeUnmount(() => {
 <style scoped>
 .pressure-monitor {
   background: #f0f2f6;
-  height: 100vh;
+  height: calc(100vh - 84px);
   display: flex;
   flex-direction: column;
   overflow: hidden;

+ 27 - 34
src/views/subSystem/gas/gasOperamon/ssjcyzt.vue

@@ -87,7 +87,7 @@
           <div class="coordinate-info">坐标: {{ currentLng.toFixed(4) }}, {{ currentLat.toFixed(4) }}</div>
         </div>
         <div class="map-wrapper">
-          <div id="gasSafetyBaiduMap" class="map-div"></div>
+          <div id="gasSafetyTdtMap" class="map-div"></div>
           <div v-if="mapError" class="map-error">{{ mapError }}</div>
         </div>
 
@@ -118,6 +118,7 @@
 </template>
 
 <script setup>
+import { whenMapReady } from '@/utils/mapApi'
 import { ref, computed, reactive, onMounted, onBeforeUnmount, nextTick } from 'vue'
 import { MapLocation, Search, Refresh, FullScreen, ZoomIn, ZoomOut, Location } from '@element-plus/icons-vue'
 import { convertPipeline, convertPoints, convertCoord } from '@/utils/coordTransform'
@@ -225,11 +226,11 @@ const filteredFeatures = computed(() => {
 })
 
 function onSearch() {
-  if (!mapInstance || typeof BMapGL === 'undefined') return
+  if (!mapInstance || typeof TdtMap === 'undefined') return
   const found = filteredFeatures.value[0]
   if (found) {
     const [sfLng, sfLat] = convertCoord(found.lng, found.lat)
-    const pt = new BMapGL.Point(sfLng, sfLat)
+    const pt = new TdtMap.Point(sfLng, sfLat)
     mapInstance.panTo(pt)
     openFeatureInfo(found)
   }
@@ -260,9 +261,9 @@ function zoomOut() {
   mapInstance.zoomOut()
 }
 function fitBounds() {
-  if (!mapInstance || typeof BMapGL === 'undefined') return
+  if (!mapInstance || typeof TdtMap === 'undefined') return
   const [fLng, fLat] = convertCoord(MAP_CENTER.lng, MAP_CENTER.lat)
-  mapInstance.centerAndZoom(new BMapGL.Point(fLng, fLat), MAP_DEFAULT_ZOOM)
+  mapInstance.centerAndZoom(new TdtMap.Point(fLng, fLat), MAP_DEFAULT_ZOOM)
   closeInfoWindow()
 }
 function refreshData() {
@@ -271,16 +272,8 @@ function refreshData() {
   renderAllOverlays()
 }
 
-// ==================== 百度地图初始化 ====================
-function waitForBMapGL() {
-  if (typeof BMapGL !== 'undefined') { initMap(); return }
-  let attempts = 0
-  const timer = setInterval(() => {
-    attempts++
-    if (typeof BMapGL !== 'undefined') { clearInterval(timer); initMap(); return }
-    if (attempts >= 50) { clearInterval(timer); mapError.value = '百度地图脚本加载超时,请刷新页面重试。' }
-  }, 200)
-}
+// ==================== 天地图初始化 ====================
+
 
 function closeInfoWindow() {
   if (activeInfoWindow) {
@@ -290,18 +283,18 @@ function closeInfoWindow() {
 }
 
 function initMap() {
-  const el = document.getElementById('gasSafetyBaiduMap')
+  const el = document.getElementById('gasSafetyTdtMap')
   if (!el) { mapError.value = '地图容器未找到。'; return }
 
   try {
-    mapInstance = new BMapGL.Map('gasSafetyBaiduMap', { enableMapClick: true })
+    mapInstance = new TdtMap.Map('gasSafetyTdtMap', { enableMapClick: true })
   } catch (e) {
-    mapError.value = '百度地图初始化失败,请检查AK和网络连接。'
+    mapError.value = '天地图初始化失败,请检查 VITE_TIANDITU_TK 与网络连接。'
     return
   }
 
   const [mcLng, mcLat] = convertCoord(MAP_CENTER.lng, MAP_CENTER.lat)
-  mapInstance.centerAndZoom(new BMapGL.Point(mcLng, mcLat), MAP_DEFAULT_ZOOM)
+  mapInstance.centerAndZoom(new TdtMap.Point(mcLng, mcLat), MAP_DEFAULT_ZOOM)
   mapInstance.enableScrollWheelZoom(true)
   mapInstance.setMinZoom(12)
   mapInstance.setMaxZoom(18)
@@ -338,13 +331,13 @@ function getLayerKey(category) {
 
 // 绘制管线
 function renderPipelines() {
-  if (!mapInstance || typeof BMapGL === 'undefined') return
+  if (!mapInstance || typeof TdtMap === 'undefined') return
   pipelines.forEach(pipe => {
     const pts = pipe.path.map(([lng, lat]) => {
       const [bdLng, bdLat] = convertCoord(lng, lat)
-      return new BMapGL.Point(bdLng, bdLat)
+      return new TdtMap.Point(bdLng, bdLat)
     })
-    const polyline = new BMapGL.Polyline(pts, {
+    const polyline = new TdtMap.Polyline(pts, {
       strokeColor: pipe.color,
       strokeWeight: pipe.width,
       strokeOpacity: 0.85
@@ -352,7 +345,7 @@ function renderPipelines() {
     polyline.addEventListener('click', () => {
       closeInfoWindow()
       const html = `<div style="font-size:13px;line-height:1.8;"><b>${pipe.name}</b><br/>压力等级:${pipe.pressure}</div>`
-      activeInfoWindow = new BMapGL.InfoWindow(html, { width: 200, height: 60 })
+      activeInfoWindow = new TdtMap.InfoWindow(html, { width: 200, height: 60 })
       const mid = Math.floor(pts.length / 2)
       mapInstance.openInfoWindow(activeInfoWindow, pts[mid])
     })
@@ -363,11 +356,11 @@ function renderPipelines() {
 
 // 绘制点位标记
 function drawPointMarkers(points, category, color, size = 10, shape = 'circle', iconText = '') {
-  if (!mapInstance || typeof BMapGL === 'undefined') return
+  if (!mapInstance || typeof TdtMap === 'undefined') return
   const layerKey = getLayerKey(category)
   points.forEach(p => {
     const [bdLng, bdLat] = convertCoord(p.lng, p.lat)
-    const pt = new BMapGL.Point(bdLng, bdLat)
+    const pt = new TdtMap.Point(bdLng, bdLat)
     let displayColor = color
     if (p.status === 'alert' || p.status === 'danger') displayColor = '#f56c6c'
     if (p.status === 'warning') displayColor = '#e6a23c'
@@ -376,9 +369,9 @@ function drawPointMarkers(points, category, color, size = 10, shape = 'circle',
     const r = size
     const html = `<div style="width:${r*2}px;height:${r*2}px;border-radius:${shape === 'circle' ? '50%' : shape === 'rect' ? '3px' : '0'};background:${displayColor};border:2px solid #fff;box-shadow:0 2px 6px rgba(0,0,0,0.3);cursor:pointer;display:flex;align-items:center;justify-content:center;color:#fff;font-size:10px;font-weight:bold;">${iconText}</div>`
 
-    const label = new BMapGL.Label(html, {
+    const label = new TdtMap.Label(html, {
       position: pt,
-      offset: new BMapGL.Size(-r, -r)
+      offset: new TdtMap.Size(-r, -r)
     })
     label.setStyle({ border: 'none', background: 'transparent', padding: '0' })
 
@@ -393,9 +386,9 @@ function drawPointMarkers(points, category, color, size = 10, shape = 'circle',
   /*  // 告警点额外显示脉冲圈
     if ((p.status === 'alert' || p.status === 'danger') && layers.alert) {
       const pulseHtml = `<div style="width:${r*3}px;height:${r*3}px;border-radius:50%;background:rgba(245,108,108,0.3);border:2px solid rgba(245,108,108,0.6);animation:pulse 1.5s infinite;"></div>`
-      const pulseLabel = new BMapGL.Label(pulseHtml, {
+      const pulseLabel = new TdtMap.Label(pulseHtml, {
         position: pt,
-        offset: new BMapGL.Size(-r * 1.5, -r * 1.5)
+        offset: new TdtMap.Size(-r * 1.5, -r * 1.5)
       })
       pulseLabel.setStyle({ border: 'none', background: 'transparent', padding: '0' })
       mapInstance.addOverlay(pulseLabel)
@@ -405,7 +398,7 @@ function drawPointMarkers(points, category, color, size = 10, shape = 'circle',
 }
 
 function renderAllOverlays() {
-  if (!mapInstance || typeof BMapGL === 'undefined') return
+  if (!mapInstance || typeof TdtMap === 'undefined') return
   clearAllOverlays()
 
   // 管线
@@ -456,7 +449,7 @@ function openFeatureInfo(feature) {
   detailDrawerVisible.value = true
 
   const [bfLng, bfLat] = convertCoord(feature.lng, feature.lat)
-  const pt = new BMapGL.Point(bfLng, bfLat)
+  const pt = new TdtMap.Point(bfLng, bfLat)
   let infoHtml = `<div style="font-size:13px;line-height:1.8;max-width:240px;"><b>${feature.name}</b><br/>类型:${feature.typeName}<br/>`
   if (feature.pressure) infoHtml += `压力:${feature.pressure} kPa<br/>`
   if (feature.flow) infoHtml += `流量:${feature.flow} Nm³/h<br/>`
@@ -465,14 +458,14 @@ function openFeatureInfo(feature) {
   if (feature.humidity) infoHtml += `湿度:${feature.humidity} %RH<br/>`
   infoHtml += `状态:${statusLabel(feature.status)}<br/>地址:${feature.address}</div>`
 
-  activeInfoWindow = new BMapGL.InfoWindow(infoHtml, { width: 240, height: 150 })
+  activeInfoWindow = new TdtMap.InfoWindow(infoHtml, { width: 240, height: 150 })
   mapInstance.openInfoWindow(activeInfoWindow, pt)
   mapInstance.panTo(pt)
 }
 
 onMounted(() => {
   nextTick(() => {
-    waitForBMapGL()
+    whenMapReady(initMap)
   })
 })
 
@@ -485,7 +478,7 @@ onBeforeUnmount(() => {
 <style scoped>
 .gas-safety-gis {
   background: #f0f2f6;
-  height: 100vh;
+  height: calc(100vh - 84px);
   display: flex;
   flex-direction: column;
   overflow: hidden;

+ 20 - 27
src/views/subSystem/gas/gasOperamon/xldxkjkrqtndjc.vue

@@ -101,7 +101,7 @@
           </div>
         </div>
         <div class="map-wrapper">
-          <div id="methaneBaiduMap" class="map-div"></div>
+          <div id="methaneTdtMap" class="map-div"></div>
           <div v-if="mapError" class="map-error">{{ mapError }}</div>
         </div>
 
@@ -179,6 +179,7 @@
 </template>
 
 <script setup>
+import { whenMapReady } from '@/utils/mapApi'
 import { ref, computed, onMounted, onBeforeUnmount, nextTick, watch } from 'vue'
 import { Warning, Refresh, Download, Search, ZoomIn, ZoomOut, Location } from '@element-plus/icons-vue'
 import * as echarts from 'echarts'
@@ -367,21 +368,13 @@ function renderTrendChart() {
   })
 }
 
-// ==================== 百度地图 ====================
+// ==================== 天地图 ====================
 const mapError = ref('')
 let mapInstance = null
 let activeInfoWindow = null
 let mapMarkers = []
 
-function waitForBMapGL() {
-  if (typeof BMapGL !== 'undefined') { initMap(); return }
-  let attempts = 0
-  const timer = setInterval(() => {
-    attempts++
-    if (typeof BMapGL !== 'undefined') { clearInterval(timer); initMap(); return }
-    if (attempts >= 50) { clearInterval(timer); mapError.value = '百度地图脚本加载超时,请刷新页面重试。' }
-  }, 200)
-}
+
 
 function closeInfoWindow() {
   if (activeInfoWindow) {
@@ -391,17 +384,17 @@ function closeInfoWindow() {
 }
 
 function initMap() {
-  const el = document.getElementById('methaneBaiduMap')
+  const el = document.getElementById('methaneTdtMap')
   if (!el) { mapError.value = '地图容器未找到。'; return }
 
   try {
-    mapInstance = new BMapGL.Map('methaneBaiduMap', { enableMapClick: true })
+    mapInstance = new TdtMap.Map('methaneTdtMap', { enableMapClick: true })
   } catch (e) {
-    mapError.value = '百度地图初始化失败,请检查AK和网络连接。'
+    mapError.value = '天地图初始化失败,请检查 VITE_TIANDITU_TK 与网络连接。'
     return
   }
 
-  mapInstance.centerAndZoom(new BMapGL.Point(MAP_CENTER.lng, MAP_CENTER.lat), MAP_DEFAULT_ZOOM)
+  mapInstance.centerAndZoom(new TdtMap.Point(MAP_CENTER.lng, MAP_CENTER.lat), MAP_DEFAULT_ZOOM)
   mapInstance.enableScrollWheelZoom(true)
   mapInstance.setMinZoom(12)
   mapInstance.setMaxZoom(18)
@@ -414,13 +407,13 @@ function initMap() {
 }
 
 function renderMapMarkers() {
-  if (!mapInstance || typeof BMapGL === 'undefined') return
+  if (!mapInstance || typeof TdtMap === 'undefined') return
   closeInfoWindow()
   mapMarkers.forEach(m => { try { mapInstance.removeOverlay(m) } catch (_) {} })
   mapMarkers = []
 
   mockPoints.value.forEach(p => {
-    const pt = new BMapGL.Point(p.lng, p.lat)
+    const pt = new TdtMap.Point(p.lng, p.lat)
     let color = '#67c23a'
     let size = 12
     if (p.alertLevel === 'critical') { color = '#f56c6c'; size = 16 }
@@ -428,9 +421,9 @@ function renderMapMarkers() {
     if (p.deviceStatus === 'offline') { color = '#909399'; size = 10 }
 
     const html = `<div style="width:${size*2}px;height:${size*2}px;border-radius:50%;background:${color};border:2px solid #fff;box-shadow:0 2px 6px rgba(0,0,0,0.3);cursor:pointer;display:flex;align-items:center;justify-content:center;color:#fff;font-size:9px;font-weight:bold;">CH4</div>`
-    const label = new BMapGL.Label(html, {
+    const label = new TdtMap.Label(html, {
       position: pt,
-      offset: new BMapGL.Size(-size, -size)
+      offset: new TdtMap.Size(-size, -size)
     })
     label.setStyle({ border: 'none', background: 'transparent', padding: '0' })
 
@@ -445,9 +438,9 @@ function renderMapMarkers() {
     // 严重告警脉冲圈
     if (p.alertLevel === 'critical') {
       const pulseHtml = `<div style="width:${size*3}px;height:${size*3}px;border-radius:50%;background:rgba(245,108,108,0.25);border:2px solid rgba(245,108,108,0.5);"></div>`
-      const pulseLabel = new BMapGL.Label(pulseHtml, {
+      const pulseLabel = new TdtMap.Label(pulseHtml, {
         position: pt,
-        offset: new BMapGL.Size(-size * 1.5, -size * 1.5)
+        offset: new TdtMap.Size(-size * 1.5, -size * 1.5)
       })
       pulseLabel.setStyle({ border: 'none', background: 'transparent', padding: '0' })
       mapInstance.addOverlay(pulseLabel)
@@ -459,10 +452,10 @@ function renderMapMarkers() {
 function openPointInfo(point) {
   selectPoint(point)
   closeInfoWindow()
-  const pt = new BMapGL.Point(point.lng, point.lat)
+  const pt = new TdtMap.Point(point.lng, point.lat)
   const levelText = point.alertLevel === 'critical' ? '严重告警' : point.alertLevel === 'warning' ? '预警' : '正常'
   const html = `<div style="font-size:13px;line-height:1.8;max-width:240px;"><b>${point.name}</b><br/>甲烷: <b style="color:${point.methane > 1 ? '#f56c6c' : point.methane > 0.5 ? '#e6a23c' : '#67c23a'}">${point.methane} %VOL</b><br/>状态: ${levelText}<br/>地址: ${point.address}</div>`
-  activeInfoWindow = new BMapGL.InfoWindow(html, { width: 240, height: 120 })
+  activeInfoWindow = new TdtMap.InfoWindow(html, { width: 240, height: 120 })
   mapInstance.openInfoWindow(activeInfoWindow, pt)
   mapInstance.panTo(pt)
 }
@@ -470,8 +463,8 @@ function openPointInfo(point) {
 function zoomIn() { mapInstance?.zoomIn() }
 function zoomOut() { mapInstance?.zoomOut() }
 function fitBounds() {
-  if (!mapInstance || typeof BMapGL === 'undefined') return
-  mapInstance.centerAndZoom(new BMapGL.Point(MAP_CENTER.lng, MAP_CENTER.lat), MAP_DEFAULT_ZOOM)
+  if (!mapInstance || typeof TdtMap === 'undefined') return
+  mapInstance.centerAndZoom(new TdtMap.Point(MAP_CENTER.lng, MAP_CENTER.lat), MAP_DEFAULT_ZOOM)
   closeInfoWindow()
 }
 
@@ -488,7 +481,7 @@ watch([searchKeyword, alertFilter], () => {
 
 onMounted(() => {
   nextTick(() => {
-    waitForBMapGL()
+    whenMapReady(initMap)
   })
   window.addEventListener('resize', () => { try { trendChartInstance?.resize() } catch {} })
 })
@@ -504,7 +497,7 @@ onBeforeUnmount(() => {
 <style scoped>
 .methane-monitor-dashboard {
   background: #f0f2f6;
-  height: 100vh;
+  height: calc(100vh - 84px);
   display: flex;
   flex-direction: column;
   overflow: hidden;

+ 137 - 70
src/views/subSystem/gas/gasfxpg/gwfxpg.vue

@@ -101,10 +101,10 @@
             <el-button size="small" :icon="ZoomOut" @click="zoomOut">缩小</el-button>
             <el-button size="small" :icon="Location" @click="fitBounds">重置</el-button>
           </el-button-group>
-          <div class="coordinate-info">坐标: {{ currentLng.toFixed(4) }}, {{ currentLat.toFixed(4) }}</div>
+          <div class="coordinate-info">坐标: {{ currentLng.toFixed(4) }}, {{ currentLat.toFixed(4) }}<span class="map-hint"> · 放大地图查看管段标签</span></div>
         </div>
         <div class="map-wrapper">
-          <div id="riskAssessmentBaiduMap" class="map-div"></div>
+          <div id="riskAssessmentTdtMap" class="map-div"></div>
           <div v-if="mapError" class="map-error">{{ mapError }}</div>
           <!-- 悬浮信息卡 -->
           <div v-if="hoverPipeline" class="hover-card" :style="{ top: hoverY + 'px', left: hoverX + 'px' }">
@@ -157,6 +157,7 @@
 </template>
 
 <script setup>
+import { ensureTdtMap } from '@/utils/mapApi'
 import { ref, computed, onMounted, onBeforeUnmount, nextTick, watch } from 'vue'
 import { MapLocation, Search, Refresh, FullScreen, Location, ZoomIn, ZoomOut } from '@element-plus/icons-vue'
 import { ElMessage } from 'element-plus'
@@ -165,7 +166,7 @@ import { convertPipeline, convertPoints, convertCoord } from '@/utils/coordTrans
 import { getGisPipeLines } from '@/api/pipeNetwork/basic'
 import { getGasRiskAssessments } from '@/api/pipeNetwork/hazard'
 
-// ==================== 百度地图初始化 ====================
+// ==================== 天地图初始化 ====================
 const MAP_CENTER = { lng: 110.386, lat: 28.445 }
 const MAP_DEFAULT_ZOOM = 14
 
@@ -178,30 +179,32 @@ const overlayStore = {
   high: [], medium: [], low: [], hazard: [], target: []
 }
 
-function waitForBMapGL() {
-  return new Promise((resolve, reject) => {
-    let attempts = 0
-    const timer = setInterval(() => {
-      attempts++
-      if (window.BMapGL) { clearInterval(timer); resolve(window.BMapGL) }
-      if (attempts > 150) { clearInterval(timer); reject(new Error('百度地图API加载超时')) }
-    }, 200)
-  })
-}
+/** 管段标签独立成层:低缩放/密集区域不逐条渲染,按风险等级优先做屏幕网格抽稀 */
+const RISK_COLOR = { high: '#f56c6c', medium: '#e6a23c', low: '#67c23a' }
+const RISK_TEXT_COLOR = { high: '#b91c1c', medium: '#b45309', low: '#15803d' }
+const RISK_PRIORITY = { high: 0, medium: 1, low: 2 }
+const LABEL_FONT = "'Microsoft YaHei', 'PingFang SC', 'Helvetica Neue', Arial, sans-serif"
+const LABEL_MIN_ZOOM = 15
+const LABEL_CELL = { x: 120, y: 44 }
+const pipelineLabels = []
+let labelTimer = null
+
+
 
 function initMap() {
-  const BMapGL = window.BMapGL
-  const el = document.getElementById('riskAssessmentBaiduMap')
+  const TdtMap = window.TdtMap
+  const el = document.getElementById('riskAssessmentTdtMap')
   if (!el) return
-  mapInstance = new BMapGL.Map(el, { enableMapClick: true })
+  mapInstance = new TdtMap.Map(el, { enableMapClick: true })
   const [clng, clat] = convertCoord(MAP_CENTER.lng, MAP_CENTER.lat)
-  mapInstance.centerAndZoom(new BMapGL.Point(clng, clat), MAP_DEFAULT_ZOOM)
+  mapInstance.centerAndZoom(new TdtMap.Point(clng, clat), MAP_DEFAULT_ZOOM)
   mapInstance.enableScrollWheelZoom(true)
-  mapInstance.setMapStyleV2({ styleId: 'a0f4e6e6f9e7a6c5d8e7f7e7f7e7f7e5' })
   mapInstance.addEventListener('mousemove', (e) => {
     currentLng.value = e.latlng.lng
     currentLat.value = e.latlng.lat
   })
+  mapInstance.addEventListener('zoomend', scheduleLabels)
+  mapInstance.addEventListener('moveend', scheduleLabels)
   renderAllOverlays()
   // 风险接口和地图接口是并行加载的,若数据先返回,这里补一次按真实管线范围定位。
   fitBounds()
@@ -367,6 +370,7 @@ async function loadRiskData() {
 
 // ==================== 地图覆盖物渲染 ====================
 function clearOverlays() {
+  clearPipelineLabels()
   Object.values(overlayStore).forEach(arr => {
     arr.forEach(o => { try { mapInstance.removeOverlay(o) } catch {} })
     arr.length = 0
@@ -378,14 +382,14 @@ function addToStore(layer, overlay) {
 }
 
 function renderPipelines() {
-  const BMapGL = window.BMapGL
+  const TdtMap = window.TdtMap
   filteredPipelines.value.forEach(p => {
     if (!Array.isArray(p.path) || p.path.length < 2) return
     const layerKey = p.riskLevel
-    const points = p.path.map(c => { const [lng, lat] = convertCoord(c[0], c[1]); return new BMapGL.Point(lng, lat) })
+    const points = p.path.map(c => { const [lng, lat] = convertCoord(c[0], c[1]); return new TdtMap.Point(lng, lat) })
     const color = p.riskLevel === 'high' ? '#f56c6c' : p.riskLevel === 'medium' ? '#e6a23c' : '#67c23a'
     const weight = p.riskLevel === 'high' ? 6 : p.riskLevel === 'medium' ? 5 : 4
-    const polyline = new BMapGL.Polyline(points, {
+    const polyline = new TdtMap.Polyline(points, {
       strokeColor: color, strokeWeight: weight, strokeOpacity: 0.8
     })
     mapInstance.addOverlay(polyline)
@@ -405,49 +409,29 @@ function renderPipelines() {
       hoverPipeline.value = null
     })
 
-    // 管线名称标签
-    const mid = Math.floor(points.length / 2)
-    const scoreLabel = new BMapGL.Label(p.riskScore + '分', {
-      position: points[mid],
-      offset: new BMapGL.Size(0, -16)
-    })
-    scoreLabel.setStyle({
-      color: '#fff', fontSize: '11px', fontWeight: 'bold', padding: '2px 8px',
-      backgroundColor: color, borderRadius: '10px', border: 'none', whiteSpace: 'nowrap'
-    })
-    mapInstance.addOverlay(scoreLabel)
-    addToStore(layerKey, scoreLabel)
-
-    const nameLabel = new BMapGL.Label(p.name, {
-      position: points[mid],
-      offset: new BMapGL.Size(0, 4)
-    })
-    nameLabel.setStyle({
-      color: '#333', fontSize: '10px', padding: '1px 4px',
-      backgroundColor: 'rgba(255,255,255,0.85)', borderRadius: '2px', border: 'none', whiteSpace: 'nowrap'
-    })
-    mapInstance.addOverlay(nameLabel)
-    addToStore(layerKey, nameLabel)
   })
 }
 
 function renderHazardPoints() {
-  const BMapGL = window.BMapGL
+  const TdtMap = window.TdtMap
   hazardPoints.value.forEach(h => {
     const [hlng, hlat] = convertCoord(h.lng, h.lat)
-    const pt = new BMapGL.Point(hlng, hlat)
-    const label = new BMapGL.Label(h.name, {
+    const pt = new TdtMap.Point(hlng, hlat)
+    const label = new TdtMap.Label(h.name, {
       position: pt,
-      offset: new BMapGL.Size(0, -18)
+      offset: new TdtMap.Size(0, -18)
     })
     label.setStyle({
-      color: '#fff', fontSize: '10px', padding: '3px 8px',
-      backgroundColor: '#9b59b6', borderRadius: '3px', border: 'none', whiteSpace: 'nowrap'
+      fontFamily: LABEL_FONT, fontSize: '12px', fontWeight: 600,
+      color: '#fff', padding: '3px 9px', textShadow: '0 1px 1px rgba(0,0,0,.4)',
+      backgroundColor: '#8b46a8', borderRadius: '4px',
+      border: '1px solid rgba(255,255,255,.85)',
+      boxShadow: '0 2px 6px rgba(15,23,42,.3)', whiteSpace: 'nowrap'
     })
     mapInstance.addOverlay(label)
     addToStore('hazard', label)
 
-    const marker = new BMapGL.Marker(pt)
+    const marker = new TdtMap.Marker(pt)
     mapInstance.addOverlay(marker)
     addToStore('hazard', marker)
 
@@ -466,22 +450,25 @@ function renderHazardPoints() {
 }
 
 function renderTargetPoints() {
-  const BMapGL = window.BMapGL
+  const TdtMap = window.TdtMap
   targetPoints.value.forEach(t => {
     const [tlng, tlat] = convertCoord(t.lng, t.lat)
-    const pt = new BMapGL.Point(tlng, tlat)
-    const label = new BMapGL.Label(t.name, {
+    const pt = new TdtMap.Point(tlng, tlat)
+    const label = new TdtMap.Label(t.name, {
       position: pt,
-      offset: new BMapGL.Size(0, -18)
+      offset: new TdtMap.Size(0, -18)
     })
     label.setStyle({
-      color: '#fff', fontSize: '10px', padding: '3px 8px',
-      backgroundColor: '#409eff', borderRadius: '3px', border: 'none', whiteSpace: 'nowrap'
+      fontFamily: LABEL_FONT, fontSize: '12px', fontWeight: 600,
+      color: '#fff', padding: '3px 9px', textShadow: '0 1px 1px rgba(0,0,0,.4)',
+      backgroundColor: '#1d6fd0', borderRadius: '4px',
+      border: '1px solid rgba(255,255,255,.85)',
+      boxShadow: '0 2px 6px rgba(15,23,42,.3)', whiteSpace: 'nowrap'
     })
     mapInstance.addOverlay(label)
     addToStore('target', label)
 
-    const marker = new BMapGL.Marker(pt)
+    const marker = new TdtMap.Marker(pt)
     mapInstance.addOverlay(marker)
     addToStore('target', marker)
 
@@ -499,6 +486,80 @@ function renderTargetPoints() {
   })
 }
 
+function escapeLabel(value) {
+  return String(value ?? '').replace(/[&<>"']/g, ch => ({
+    '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;'
+  }[ch]))
+}
+
+function pipelineLabelText(pipeline, count) {
+  const name = escapeLabel(pipeline.name || pipeline.code || '管段')
+  const score = Number.isFinite(Number(pipeline.riskScore)) ? pipeline.riskScore + '分' : '—'
+  const scoreColor = RISK_TEXT_COLOR[pipeline.riskLevel] || RISK_TEXT_COLOR.low
+  const more = count > 1
+    ? '<span style="margin-left:6px;padding:0 5px;border-radius:8px;background:#e2e8f0;color:#334155;font-size:11px">+' + (count - 1) + '</span>'
+    : ''
+  return '<span style="color:#111827">' + name + '</span>'
+    + '<span style="margin-left:6px;color:' + scoreColor + '">' + score + '</span>'
+    + more
+}
+
+function clearPipelineLabels() {
+  pipelineLabels.forEach(label => { try { mapInstance.removeOverlay(label) } catch {} })
+  pipelineLabels.length = 0
+}
+
+/** 只在放大后或高/中风险时显示标签;同一屏幕网格只保留一个,其余合并为 +N */
+function renderPipelineLabels() {
+  if (!mapInstance || !window.TdtMap) return
+  const TdtMap = window.TdtMap
+  clearPipelineLabels()
+  const zoom = Number(mapInstance.getZoom?.() || 0)
+  const visible = new Set(visibleLayers.value)
+  const candidates = filteredPipelines.value
+    .filter(p => visible.has(p.riskLevel) && Array.isArray(p.path) && p.path.length >= 2)
+    .filter(p => p.riskLevel !== 'low' || zoom >= LABEL_MIN_ZOOM)
+    .sort((a, b) => (RISK_PRIORITY[a.riskLevel] ?? 9) - (RISK_PRIORITY[b.riskLevel] ?? 9))
+
+  const cells = new Map()
+  candidates.forEach(p => {
+    const mid = p.path[Math.floor(p.path.length / 2)]
+    const [lng, lat] = convertCoord(Number(mid?.[0]), Number(mid?.[1]))
+    if (!Number.isFinite(lng) || !Number.isFinite(lat)) return
+    const point = new TdtMap.Point(lng, lat)
+    const pixel = mapInstance.pointToPixel(point)
+    if (!Number.isFinite(pixel?.x) || !Number.isFinite(pixel?.y)) return
+    const key = Math.floor(pixel.x / LABEL_CELL.x) + '_' + Math.floor(pixel.y / LABEL_CELL.y)
+    const hit = cells.get(key)
+    if (hit) {
+      hit.count += 1
+      hit.label.setContent(pipelineLabelText(hit.pipeline, hit.count))
+      return
+    }
+    const label = new TdtMap.Label(pipelineLabelText(p, 1), {
+      position: point,
+      offset: new TdtMap.Size(0, -18)
+    })
+    label.setStyle({
+      fontFamily: LABEL_FONT, fontSize: '12px', fontWeight: 600, lineHeight: '18px',
+      color: '#111827', padding: '3px 9px',
+      backgroundColor: 'rgba(255,255,255,0.97)',
+      border: '1px solid #cbd5e1',
+      borderLeft: '3px solid ' + (RISK_COLOR[p.riskLevel] || RISK_COLOR.low),
+      borderRadius: '4px', boxShadow: '0 2px 6px rgba(15,23,42,.28)',
+      whiteSpace: 'nowrap', pointerEvents: 'none'
+    })
+    mapInstance.addOverlay(label)
+    pipelineLabels.push(label)
+    cells.set(key, { label, pipeline: p, count: 1 })
+  })
+}
+
+function scheduleLabels() {
+  clearTimeout(labelTimer)
+  labelTimer = setTimeout(renderPipelineLabels, 120)
+}
+
 function renderAllOverlays() {
   clearOverlays()
   renderPipelines()
@@ -517,6 +578,7 @@ function onLayerChange() {
       } catch {}
     })
   })
+  scheduleLabels()
 }
 
 function onScoreFilter() {
@@ -534,25 +596,25 @@ function zoomOut() {
 
 function fitBounds() {
   if (mapInstance) {
-    const BMapGL = window.BMapGL
+    const TdtMap = window.TdtMap
     const coordinates = filteredPipelines.value
       .flatMap(p => Array.isArray(p.path) ? p.path : [])
       .filter(c => Number.isFinite(Number(c?.[0])) && Number.isFinite(Number(c?.[1])))
     if (coordinates.length >= 2) {
       const points = coordinates.map(c => {
         const [lng, lat] = convertCoord(Number(c[0]), Number(c[1]))
-        return new BMapGL.Point(lng, lat)
+        return new TdtMap.Point(lng, lat)
       })
       try {
         mapInstance.setViewport(points, { margins: [40, 40, 40, 40], zoomFactor: -1 })
       } catch {
-        // 兼容旧版百度地图 GL 不支持 viewportOptions 的情况。
+        // 兼容部分地图视口参数不支持的情况。
         mapInstance.setViewport(points)
       }
       return
     }
     const [clng, clat] = convertCoord(MAP_CENTER.lng, MAP_CENTER.lat)
-    mapInstance.centerAndZoom(new BMapGL.Point(clng, clat), MAP_DEFAULT_ZOOM)
+    mapInstance.centerAndZoom(new TdtMap.Point(clng, clat), MAP_DEFAULT_ZOOM)
   }
 }
 
@@ -569,9 +631,9 @@ function locateArea() {
   }
   const area = areas[quickLocation.value]
   if (area && mapInstance) {
-    const BMapGL = window.BMapGL
+    const TdtMap = window.TdtMap
     const [alng, alat] = convertCoord(area.lng, area.lat)
-    mapInstance.centerAndZoom(new BMapGL.Point(alng, alat), area.zoom)
+    mapInstance.centerAndZoom(new TdtMap.Point(alng, alat), area.zoom)
     ElMessage.success('已定位到: ' + quickLocation.value)
   }
 }
@@ -582,10 +644,10 @@ function locateSearch() {
     p.name.includes(searchKeyword.value) || p.code.includes(searchKeyword.value)
   )
   if (found && mapInstance) {
-    const BMapGL = window.BMapGL
+    const TdtMap = window.TdtMap
     const mid = found.path[Math.floor(found.path.length / 2)]
     const [mlng, mlat] = convertCoord(mid[0], mid[1])
-    mapInstance.centerAndZoom(new BMapGL.Point(mlng, mlat), 16)
+    mapInstance.centerAndZoom(new TdtMap.Point(mlng, mlat), 16)
     currentPipeline.value = found
     detailDrawerVisible.value = true
     ElMessage.success(`已定位到: ${found.name}`)
@@ -596,7 +658,7 @@ function locateSearch() {
 
 function locatePipeline() {
   if (!mapInstance || !currentPipeline.value) return
-  const BMapGL = window.BMapGL
+  const TdtMap = window.TdtMap
   const item = currentPipeline.value
   let lng, lat
   if (item.path) {
@@ -608,7 +670,7 @@ function locatePipeline() {
     return
   }
   const [llng, llat] = convertCoord(lng, lat)
-  mapInstance.centerAndZoom(new BMapGL.Point(llng, llat), 16)
+  mapInstance.centerAndZoom(new TdtMap.Point(llng, llat), 16)
   detailDrawerVisible.value = false
 }
 
@@ -671,14 +733,15 @@ onMounted(async () => {
   window.addEventListener('mousemove', onWindowMouseMove)
   loadRiskData()
   try {
-    await waitForBMapGL()
+    await ensureTdtMap()
     nextTick(() => initMap())
   } catch {
-    mapError.value = '百度地图加载失败,请刷新页面重试'
+    mapError.value = '天地图加载失败,请刷新页面重试'
   }
 })
 
 onBeforeUnmount(() => {
+  clearTimeout(labelTimer)
   window.removeEventListener('mousemove', onWindowMouseMove)
   if (chartInstance) {
     chartInstance.dispose()
@@ -887,6 +950,10 @@ function onWindowMouseMove(e) {
   color: #909399;
 }
 
+.map-hint {
+  color: #b0b6c2;
+}
+
 .map-wrapper {
   flex: 1;
   position: relative;

+ 1 - 1
src/views/subSystem/gas/gasfxpg/gwfxpgqd.vue

@@ -313,7 +313,7 @@ onMounted(loadRiskData)
 <style scoped>
 .pipeline-risk-assessment {
   background: #f0f2f6;
-  min-height: 100vh;
+  min-height: calc(100vh - 84px);
   padding: 20px;
 }
 .page-header {

+ 1 - 1
src/views/subSystem/gas/gasfxpg/gwfxxq.vue

@@ -527,7 +527,7 @@ onBeforeUnmount(() => {
 <style scoped>
 .pipeline-risk-analysis {
   background: #f0f2f6;
-  min-height: 100vh;
+  min-height: calc(100vh - 84px);
   padding: 20px;
 }
 .page-header {

+ 24 - 33
src/views/subSystem/gas/gasfxpg/gxfxpgsst.vue

@@ -120,7 +120,7 @@
           </div>
         </div>
         <div class="map-wrapper">
-          <div id="riskFourColorBaiduMap" class="map-div"></div>
+          <div id="riskFourColorTdtMap" class="map-div"></div>
           <div v-if="mapError" class="map-error">{{ mapError }}</div>
           <!-- 悬浮信息卡 -->
           <div v-if="hoverPipeline" class="hover-card" :style="{ top: hoverY + 'px', left: hoverX + 'px' }">
@@ -168,6 +168,7 @@
 </template>
 
 <script setup>
+import { ensureTdtMap } from '@/utils/mapApi'
 import { ref, computed, onMounted, onBeforeUnmount, nextTick } from 'vue'
 import { MapLocation, Search, Refresh, FullScreen, Location, ZoomIn, ZoomOut, InfoFilled } from '@element-plus/icons-vue'
 import { ElMessage } from 'element-plus'
@@ -175,7 +176,7 @@ import { convertCoord } from '@/utils/coordTransform'
 import { getGisPipeLines } from '@/api/pipeNetwork/basic'
 import { getGasRiskAssessments } from '@/api/pipeNetwork/hazard'
 
-// ==================== 百度地图初始化 ====================
+// ==================== 天地图初始化 ====================
 const MAP_CENTER = { lng: 110.386, lat: 28.445 }
 const MAP_DEFAULT_ZOOM = 14
 
@@ -188,26 +189,16 @@ const overlayStore = {
   major: [], larger: [], general: [], low: []
 }
 
-function waitForBMapGL() {
-  return new Promise((resolve, reject) => {
-    let attempts = 0
-    const timer = setInterval(() => {
-      attempts++
-      if (window.BMapGL) { clearInterval(timer); resolve(window.BMapGL) }
-      if (attempts > 150) { clearInterval(timer); reject(new Error('百度地图API加载超时')) }
-    }, 200)
-  })
-}
+
 
 function initMap() {
-  const BMapGL = window.BMapGL
-  const el = document.getElementById('riskFourColorBaiduMap')
+  const TdtMap = window.TdtMap
+  const el = document.getElementById('riskFourColorTdtMap')
   if (!el) return
-  mapInstance = new BMapGL.Map(el, { enableMapClick: true })
+  mapInstance = new TdtMap.Map(el, { enableMapClick: true })
   const [clng, clat] = convertCoord(MAP_CENTER.lng, MAP_CENTER.lat)
-  mapInstance.centerAndZoom(new BMapGL.Point(clng, clat), MAP_DEFAULT_ZOOM)
+  mapInstance.centerAndZoom(new TdtMap.Point(clng, clat), MAP_DEFAULT_ZOOM)
   mapInstance.enableScrollWheelZoom(true)
-  mapInstance.setMapStyleV2({ styleId: 'a0f4e6e6f9e7a6c5d8e7f7e7f7e7f7e5' })
   mapInstance.addEventListener('mousemove', (e) => {
     currentLng.value = e.latlng.lng
     currentLat.value = e.latlng.lat
@@ -359,12 +350,12 @@ function addToStore(layer, overlay) {
 }
 
 function renderPipelines() {
-  const BMapGL = window.BMapGL
+  const TdtMap = window.TdtMap
   pipelines.value.forEach(p => {
     if (!visibleRisks.value.includes(p.riskLevel)) return
     if (!Array.isArray(p.path) || p.path.length < 2) return
-    const points = p.path.map(c => { const [lng, lat] = convertCoord(c[0], c[1]); return new BMapGL.Point(lng, lat) })
-    const polyline = new BMapGL.Polyline(points, {
+    const points = p.path.map(c => { const [lng, lat] = convertCoord(c[0], c[1]); return new TdtMap.Point(lng, lat) })
+    const polyline = new TdtMap.Polyline(points, {
       strokeColor: p.color, strokeWeight: p.lineWidth, strokeOpacity: 0.82
     })
     mapInstance.addOverlay(polyline)
@@ -379,9 +370,9 @@ function renderPipelines() {
 
     const mid = Math.floor(points.length / 2)
     // 风险评分标签
-    const scoreLabel = new BMapGL.Label(p.riskScore + '分', {
+    const scoreLabel = new TdtMap.Label(p.riskScore + '分', {
       position: points[mid],
-      offset: new BMapGL.Size(0, -16)
+      offset: new TdtMap.Size(0, -16)
     })
     scoreLabel.setStyle({
       color: '#fff', fontSize: '11px', fontWeight: 'bold', padding: '2px 8px',
@@ -391,9 +382,9 @@ function renderPipelines() {
     addToStore(p.riskLevel, scoreLabel)
 
     // 管线名称标签
-    const nameLabel = new BMapGL.Label(p.name, {
+    const nameLabel = new TdtMap.Label(p.name, {
       position: points[mid],
-      offset: new BMapGL.Size(0, 4)
+      offset: new TdtMap.Size(0, 4)
     })
     nameLabel.setStyle({
       color: '#333', fontSize: '10px', padding: '1px 4px',
@@ -431,19 +422,19 @@ function zoomOut() {
 
 function fitBounds() {
   if (mapInstance) {
-    const BMapGL = window.BMapGL
+    const TdtMap = window.TdtMap
     const coordinates = filteredPipelines.value.flatMap(p => Array.isArray(p.path) ? p.path : [])
     if (coordinates.length >= 2) {
       const points = coordinates.map(c => {
         const [lng, lat] = convertCoord(Number(c[0]), Number(c[1]))
-        return new BMapGL.Point(lng, lat)
+        return new TdtMap.Point(lng, lat)
       })
       try { mapInstance.setViewport(points, { margins: [40, 40, 40, 40], zoomFactor: -1 }) }
       catch { mapInstance.setViewport(points) }
       return
     }
     const [clng, clat] = convertCoord(MAP_CENTER.lng, MAP_CENTER.lat)
-    mapInstance.centerAndZoom(new BMapGL.Point(clng, clat), MAP_DEFAULT_ZOOM)
+    mapInstance.centerAndZoom(new TdtMap.Point(clng, clat), MAP_DEFAULT_ZOOM)
   }
 }
 
@@ -457,10 +448,10 @@ function locateSearch() {
     p.name.includes(searchKeyword.value) || p.code.includes(searchKeyword.value)
   )
   if (found && mapInstance) {
-    const BMapGL = window.BMapGL
+    const TdtMap = window.TdtMap
     const mid = found.path[Math.floor(found.path.length / 2)]
     const [midlng, midlat] = convertCoord(mid[0], mid[1])
-    mapInstance.centerAndZoom(new BMapGL.Point(midlng, midlat), 16)
+    mapInstance.centerAndZoom(new TdtMap.Point(midlng, midlat), 16)
     currentPipeline.value = found
     detailDrawerVisible.value = true
     ElMessage.success(`已定位到: ${found.name}`)
@@ -471,10 +462,10 @@ function locateSearch() {
 
 function locatePipeline() {
   if (!currentPipeline.value || !mapInstance) return
-  const BMapGL = window.BMapGL
+  const TdtMap = window.TdtMap
   const mid = currentPipeline.value.path[Math.floor(currentPipeline.value.path.length / 2)]
   const [midlng, midlat] = convertCoord(mid[0], mid[1])
-  mapInstance.centerAndZoom(new BMapGL.Point(midlng, midlat), 16)
+  mapInstance.centerAndZoom(new TdtMap.Point(midlng, midlat), 16)
   detailDrawerVisible.value = false
 }
 
@@ -495,10 +486,10 @@ onMounted(async () => {
   window.addEventListener('mousemove', onWindowMouseMove)
   loadRiskData()
   try {
-    await waitForBMapGL()
+    await ensureTdtMap()
     nextTick(() => initMap())
   } catch {
-    mapError.value = '百度地图加载失败,请刷新页面重试'
+    mapError.value = '天地图加载失败,请刷新页面重试'
   }
 })
 

+ 1 - 1
src/views/subSystem/gas/gashiddendanger/gasApicture.vue

@@ -531,7 +531,7 @@ onUnmounted(() => window.removeEventListener('resize', renderMap))
 <style scoped>
 .hazard-gis-dashboard {
   background: #f0f2f6;
-  height: 100vh;
+  height: calc(100vh - 84px);
   display: flex;
   flex-direction: column;
   overflow: hidden;

+ 1 - 1
src/views/subSystem/gas/gasjcyj/gxllyctx.vue

@@ -358,7 +358,7 @@ onBeforeUnmount(() => {
 <style scoped>
 .pressure-alert-center {
   background: #f0f2f6;
-  min-height: 100vh;
+  min-height: calc(100vh - 84px);
   padding: 20px;
 }
 .page-header {

+ 1 - 1
src/views/subSystem/gas/gasjcyj/gxylyctx.vue

@@ -362,7 +362,7 @@ onBeforeUnmount(() => { try { chartInstance?.dispose() } catch {}; chartInstance
 <style scoped>
 .pressure-alert-center {
   background: #f0f2f6;
-  min-height: 100vh;
+  min-height: calc(100vh - 84px);
   padding: 20px;
 }
 .page-header {

+ 1 - 1
src/views/subSystem/gas/gasjcyj/krqindbj.vue

@@ -276,7 +276,7 @@ onMounted(async () => {
 <style scoped>
 .methane-alert-center {
   background: #f0f2f6;
-  min-height: 100vh;
+  min-height: calc(100vh - 84px);
   padding: 20px;
 }
 .page-header {

+ 1 - 1
src/views/subSystem/gas/gasjcyj/xldxkjkrqindbj.vue

@@ -456,7 +456,7 @@ onBeforeUnmount(() => {
 <style scoped>
 .gas-alert-dashboard {
   background: #f0f2f6;
-  min-height: 100vh;
+  min-height: calc(100vh - 84px);
   padding: 20px;
 }
 .page-header {

+ 15 - 22
src/views/subSystem/hidden/HazardMap.vue

@@ -28,7 +28,7 @@
     </div>
 
     <!-- 地图容器 -->
-    <div id="hazardBaiduMap" class="map-container"></div>
+    <div id="hazardTdtMap" class="map-container"></div>
     <div v-if="mapError" class="map-error">{{ mapError }}</div>
 
     <!-- 左侧隐患列表 -->
@@ -110,6 +110,7 @@
 </template>
 
 <script setup>
+import { whenMapReady } from '@/utils/mapApi'
 import { ref, computed, onMounted, onBeforeUnmount, nextTick } from 'vue'
 import { getDicts } from '@/api/system/dict/data'
 import { getHazardMapPoints } from '@/api/pipeNetwork/hazard'
@@ -177,19 +178,11 @@ const levelLegend = computed(() =>
   }))
 )
 
-// ==================== 百度地图 ====================
+// ==================== 天地图 ====================
 const MAP_CENTER = { lng: 110.3965, lat: 28.4638 }
 const MAP_DEFAULT_ZOOM = 14
 
-function waitForBMapGL() {
-  if (typeof BMapGL !== 'undefined') { initMap(); return }
-  let attempts = 0
-  const timer = setInterval(() => {
-    attempts++
-    if (typeof BMapGL !== 'undefined') { clearInterval(timer); initMap(); return }
-    if (attempts >= 50) { clearInterval(timer); mapError.value = '百度地图脚本加载超时,请刷新页面重试。' }
-  }, 200)
-}
+
 
 function closeInfoWindow() {
   if (activeInfoWindow) {
@@ -199,19 +192,19 @@ function closeInfoWindow() {
 }
 
 function initMap() {
-  const el = document.getElementById('hazardBaiduMap')
+  const el = document.getElementById('hazardTdtMap')
   if (!el) { mapError.value = '地图容器未找到。'; return }
 
   try {
-    mapInstance = new BMapGL.Map('hazardBaiduMap', { enableMapClick: true })
+    mapInstance = new TdtMap.Map('hazardTdtMap', { enableMapClick: true })
   } catch (e) {
     console.error('创建地图实例失败:', e)
-    mapError.value = '百度地图初始化失败,请检查AK和网络连接。'
+    mapError.value = '天地图初始化失败,请检查 VITE_TIANDITU_TK 与网络连接。'
     return
   }
 
   try {
-    mapInstance.centerAndZoom(new BMapGL.Point(MAP_CENTER.lng, MAP_CENTER.lat), MAP_DEFAULT_ZOOM)
+    mapInstance.centerAndZoom(new TdtMap.Point(MAP_CENTER.lng, MAP_CENTER.lat), MAP_DEFAULT_ZOOM)
     mapInstance.enableScrollWheelZoom(true)
     mapInstance.setMinZoom(12)
     mapInstance.setMaxZoom(18)
@@ -228,20 +221,20 @@ function initMap() {
 }
 
 function renderMapMarkers() {
-  if (!mapInstance || typeof BMapGL === 'undefined') return
+  if (!mapInstance || typeof TdtMap === 'undefined') return
   closeInfoWindow()
   mapMarkers.forEach(m => { try { mapInstance.removeOverlay(m) } catch (_) {} })
   mapMarkers = []
 
   filteredHazards.value.forEach(h => {
-    const pt = new BMapGL.Point(h.longitude, h.latitude)
+    const pt = new TdtMap.Point(h.longitude, h.latitude)
     const color = levelColors[h.hazardLevel] || '#999'
     const isActive = activeHazardId.value === h.hazardId
     const size = isActive ? 16 : 12
 
-    const label = new BMapGL.Label('', {
+    const label = new TdtMap.Label('', {
       position: pt,
-      offset: new BMapGL.Size(-size, -size)
+      offset: new TdtMap.Size(-size, -size)
     })
     label.setStyle({
       color: '#fff',
@@ -275,7 +268,7 @@ function openHazardInfo(h) {
   activeHazardId.value = h.hazardId
   detailVisible.value = true
 
-  const pt = new BMapGL.Point(h.longitude, h.latitude)
+  const pt = new TdtMap.Point(h.longitude, h.latitude)
   const color = levelColors[h.hazardLevel] || '#999'
   const levelLabel = getDictLabel(hazardLevelDict.value, h.hazardLevel)
   const typeLabel = getDictLabel(hazardTypeDict.value, h.hazardType)
@@ -289,7 +282,7 @@ function openHazardInfo(h) {
     上报单位:${h.reportUnit}<br/>
     地址:${h.address}
   </div>`
-  activeInfoWindow = new BMapGL.InfoWindow(html, { width: 260, height: 150 })
+  activeInfoWindow = new TdtMap.InfoWindow(html, { width: 260, height: 150 })
   mapInstance.openInfoWindow(activeInfoWindow, pt)
   mapInstance.panTo(pt)
 
@@ -371,7 +364,7 @@ onMounted(async () => {
   await loadDicts()
   await loadHazards()
   await nextTick()
-  waitForBMapGL()
+  whenMapReady(initMap)
 })
 
 onBeforeUnmount(() => {

+ 1 - 1
src/views/subSystem/manholeCover/device/statistics.vue

@@ -288,7 +288,7 @@ onBeforeUnmount(() => {
 .device-statistics-dashboard {
   padding: 20px 24px;
   background: #f5f7fb;
-  min-height: 100vh;
+  min-height: calc(100vh - 84px);
   box-sizing: border-box;
   font-family: 'Segoe UI', 'PingFang SC', Roboto, Helvetica, Arial, sans-serif;
 }

+ 1 - 1
src/views/subSystem/manholeCover/mRealTimemonit/mRealApicture.vue

@@ -132,7 +132,7 @@ onMounted(loadList)
 .monitor-page {
   padding: 20px;
   background: #f0f2f6;
-  min-height: 100vh;
+  min-height: calc(100vh - 84px);
 }
 .alarm-filter-card {
   margin-bottom: 16px;

+ 1 - 1
src/views/subSystem/manholeCover/operation/mOdataStatistics.vue

@@ -263,7 +263,7 @@ onBeforeUnmount(() => {
 
 <style scoped>
 .alarm-dashboard {
-  min-height: 100vh;
+  min-height: calc(100vh - 84px);
   padding: 18px 22px 28px;
   background: #f0f2f6;
   color: #303133;

+ 129 - 40
src/views/subSystem/pipeNetwork/assetInfo/equipmentBase/index.vue

@@ -382,9 +382,35 @@
 
     <!-- 地图选点弹窗 -->
     <el-dialog v-model="mapOpen" title="地图选点" width="760px" append-to-body>
+      <div class="map-picker-search">
+        <el-input
+          v-model="mapSearchKeyword"
+          placeholder="搜索地名或地址,如:沅陵县太常片区"
+          clearable
+          @keyup.enter="handleMapSearch"
+        />
+        <el-button type="primary" :loading="mapSearchLoading" @click="handleMapSearch">搜索</el-button>
+        <el-select
+          v-if="mapSearchResults.length"
+          v-model="mapSearchSelected"
+          placeholder="选择搜索结果"
+          style="width: 260px"
+          @change="handlePlaceSelect"
+        >
+          <el-option
+            v-for="(place, index) in mapSearchResults"
+            :key="index"
+            :label="place.name + (place.address ? ' · ' + place.address : '')"
+            :value="index"
+          />
+        </el-select>
+      </div>
+      <div class="map-picker-search-tip">{{ mapSearchTip }}</div>
       <div class="map-picker">
         <GisMapCanvas
+          ref="mapCanvasRef"
           :center="mapCenter"
+          :zoom="mapZoom"
           :points="mapPoints"
           :enable-clustering="false"
           :show-marker-name="false"
@@ -406,9 +432,9 @@
 </template>
 
 <script setup>
-import { computed, getCurrentInstance, onBeforeUnmount, onMounted, reactive, ref, toRefs } from "vue";
+import { lookupAddress, lookupPlaces, pickBestPlace } from '@/utils/mapApi'
+import { computed, getCurrentInstance, nextTick, onBeforeUnmount, onMounted, reactive, ref, toRefs } from "vue";
 import GisMapCanvas from "@/components/GisMapCanvas.vue";
-import { bd09ToWgs84 } from "@/utils/coordTransform";
 import { useRoute } from "vue-router";
 import {
   addEquipment,
@@ -436,10 +462,20 @@ const selectedCategoryId = ref();
 const loading = ref(false);
 const open = ref(false);
 const detailOpen = ref(false);
+// 无有效坐标时的默认定位中心点:东经 110°22′3.44055″,北纬 28°26′32.02881″
+// 度分秒换算:110 + 22/60 + 3.44055/3600,28 + 26/60 + 32.02881/3600
+const DEFAULT_MAP_CENTER = { lng: 110.367622375, lat: 28.442230225 };
 const mapOpen = ref(false);
 const alarmOnly = ref(false);
-const mapCenter = ref({ lng: 110.3965, lat: 28.4638 });
+const mapCenter = ref({ ...DEFAULT_MAP_CENTER });
 const mapPoints = ref([]);
+const mapZoom = ref(15);
+const mapCanvasRef = ref(null);
+const mapSearchKeyword = ref("");
+const mapSearchResults = ref([]);
+const mapSearchSelected = ref(null);
+const mapSearchLoading = ref(false);
+const mapSearchTip = ref("");
 const total = ref(0);
 const title = ref("");
 const selectedDetail = ref({});
@@ -763,41 +799,13 @@ function reset() {
   proxy.resetForm("equipmentRef");
 }
 
-// 逆地理编码:传入百度 BD-09 坐标,返回结构化地址文本,失败时返回空串
-function reverseGeocode(lng, lat) {
-  return new Promise((resolve) => {
-    const BMapGL = window.BMapGL;
-    if (!BMapGL || typeof BMapGL.Geocoder !== "function") {
-      resolve("");
-      return;
-    }
-    try {
-      const geocoder = new BMapGL.Geocoder();
-      geocoder.getLocation(new BMapGL.Point(lng, lat), (result) => {
-        resolve(extractAddress(result));
-      });
-    } catch (error) {
-      resolve("");
-    }
-  });
-}
+// 逆地理编码:传入 CGCS2000 坐标,返回地址文本,失败时返回空串
 
-function extractAddress(result) {
-  const content = result?.content || result || {};
-  const address = result?.address || content.address;
-  if (address) {
-    return String(address);
-  }
-  const components = result?.addressComponents || content.address_detail || {};
-  return [components.province, components.city, components.district, components.street, components.streetNumber]
-    .filter(Boolean)
-    .join("");
-}
 
 // 地图选点后按坐标回填安装位置,解析失败时保留原值
 async function fillLocationByPoint(lng, lat) {
   locationTip.value = "正在解析地址...";
-  const address = await reverseGeocode(lng, lat);
+  const address = await lookupAddress(lng, lat);
   if (address) {
     form.value.equipmentLocation = address;
     locationTip.value = "已按选点回填安装位置,可手工修改。";
@@ -807,21 +815,85 @@ async function fillLocationByPoint(lng, lat) {
 }
 
 function openMapPicker() {
-  const longitude = Number(form.value.longitude);
-  const latitude = Number(form.value.latitude);
-  mapCenter.value = Number.isFinite(longitude) && Number.isFinite(latitude)
-    ? { lng: longitude, lat: latitude }
-    : { lng: 110.3965, lat: 28.4638 };
-  mapPoints.value = Number.isFinite(longitude) && Number.isFinite(latitude)
+  const rawLng = form.value.longitude;
+  const rawLat = form.value.latitude;
+  const longitude = Number(rawLng);
+  const latitude = Number(rawLat);
+  // el-input-number 空值可能是 null / '',Number(null) 与 Number('') 都是 0,不能当作有效坐标
+  const hasCoordinate = rawLng !== null && rawLng !== undefined && rawLng !== ""
+    && rawLat !== null && rawLat !== undefined && rawLat !== ""
+    && Number.isFinite(longitude) && Number.isFinite(latitude);
+
+  mapCenter.value = hasCoordinate ? { lng: longitude, lat: latitude } : { ...DEFAULT_MAP_CENTER };
+  mapPoints.value = hasCoordinate
     ? [{ lng: longitude, lat: latitude, name: form.value.equipmentName || "设备位置" }]
     : [];
+  mapZoom.value = hasCoordinate ? 16 : 13;
+  mapSearchKeyword.value = "";
+  mapSearchResults.value = [];
+  mapSearchSelected.value = null;
+  mapSearchTip.value = hasCoordinate
+    ? "已定位到当前坐标,可搜索地名或在地图上重新选点。"
+    : "默认定位到系统中心点,可搜索地名或在地图上选点。";
   locationTip.value = "";
   mapOpen.value = true;
 }
 
+async function handleMapSearch() {
+  const keyword = mapSearchKeyword.value.trim();
+  if (!keyword) {
+    mapSearchResults.value = [];
+    mapSearchSelected.value = null;
+    mapSearchTip.value = "请输入地名或地址后再搜索。";
+    return;
+  }
+  mapSearchLoading.value = true;
+  mapSearchTip.value = "正在搜索...";
+  try {
+    const places = await lookupPlaces(keyword);
+    mapSearchResults.value = places;
+    if (!places.length) {
+      mapSearchSelected.value = null;
+      mapSearchTip.value = "未搜索到相关地点,请换个关键词重试。";
+      return;
+    }
+    const best = pickBestPlace(places, keyword);
+    mapSearchSelected.value = Math.max(0, places.indexOf(best));
+    mapSearchTip.value = `找到 ${places.length} 个结果,已定位到「${best?.name || "第一个"}」,可下拉切换。`;
+    applyMapPlace(best);
+  } catch (error) {
+    mapSearchResults.value = [];
+    mapSearchSelected.value = null;
+    mapSearchTip.value = "搜索服务调用失败,请检查 /tdtApi 代理与天地图 tk 配置。";
+  } finally {
+    mapSearchLoading.value = false;
+  }
+}
+
+function handlePlaceSelect(index) {
+  const place = mapSearchResults.value[index];
+  if (place) applyMapPlace(place);
+}
+
+function applyMapPlace(place) {
+  const point = { lng: place.lng, lat: place.lat };
+  mapCenter.value = point;
+  mapZoom.value = 16;
+  mapPoints.value = [{ ...point, name: place.name || "搜索结果" }];
+  nextTick(() => mapCanvasRef.value?.locatePoint(point));
+  form.value.longitude = Number(place.lng.toFixed(6));
+  form.value.latitude = Number(place.lat.toFixed(6));
+  if (place.address) {
+    form.value.equipmentLocation = place.address;
+    locationTip.value = "已按搜索结果回填安装位置,可手工修改。";
+  } else {
+    fillLocationByPoint(point.lng, point.lat);
+  }
+}
+
 function handleMapClick(point) {
   if (!point) return;
-  const [longitude, latitude] = bd09ToWgs84(point.lng, point.lat);
+  const longitude = Number(point.lng), latitude = Number(point.lat);
   form.value.longitude = Number(longitude.toFixed(6));
   form.value.latitude = Number(latitude.toFixed(6));
   mapPoints.value = [{ lng: form.value.longitude, lat: form.value.latitude, name: form.value.equipmentName || "设备位置" }];
@@ -1443,6 +1515,23 @@ onBeforeUnmount(() => {
   font-size: 12px;
 }
 
+.map-picker-search {
+  display: flex;
+  align-items: center;
+  gap: 10px;
+  margin-bottom: 6px;
+}
+
+.map-picker-search :deep(.el-input) {
+  flex: 1;
+}
+
+.map-picker-search-tip {
+  margin-bottom: 10px;
+  color: #9ca3af;
+  font-size: 12px;
+}
+
 .map-picker {
   height: 420px;
   overflow: hidden;

+ 2 - 3
src/views/subSystem/pipeNetwork/assetInfo/monitorPoint/index.vue

@@ -138,10 +138,10 @@
 </template>
 
 <script setup>
+import { lookupAddress } from '@/utils/mapApi'
 import { computed, getCurrentInstance, onMounted, reactive, ref, watch } from "vue";
 import { useRoute } from "vue-router";
 import GisMapCanvas from "@/components/GisMapCanvas.vue";
-import { bd09ToWgs84 } from "@/utils/coordTransform";
 import { addMonitorPoint, bindPointEquipment, delMonitorPoint, getMonitorPoint, getMonitorPointDetail, getMonitorPointStatistics, listEquipmentByPoint, listMonitorPoint, listMonitorPointAvailableDevices, unbindPointEquipment, updateMonitorPoint } from "@/api/pipeNetwork/pdeviceInform";
 
 const { proxy } = getCurrentInstance();
@@ -197,9 +197,8 @@ function removePoint(row) { proxy.$modal.confirm(`确认删除“${row.pointName
 function openAssociation(row) { associationPoint.value = row; selectedEquipment.value = []; equipmentKeyword.value = ""; Promise.all([getMonitorPointDetail(row.pointId), listMonitorPointAvailableDevices(row.pointId)]).then(([detailRes, equipmentRes]) => { associationEquipment.value = detailRes.data?.equipmentList || []; allEquipment.value = equipmentRes.data || []; associationVisible.value = true; }); }
 function bind() { if (!selectedEquipment.value.length) { proxy.$modal.msgWarning("请选择要关联的设备"); return; } bindPointEquipment(associationPoint.value.pointId, selectedEquipment.value.map((item) => item.equipmentId)).then(() => { proxy.$modal.msgSuccess("关联成功"); associationVisible.value = false; return Promise.all([loadPoints(), loadStatistics(), detailVisible.value ? refreshDetail(associationPoint.value.pointId) : Promise.resolve()]); }); }
 function unbind(row) { const pointId = associationPoint.value.pointId || detail.value.pointInfo.pointId; proxy.$modal.confirm(`确认解除“${row.equipmentName || row.equipmentCode}”的关联?`).then(() => unbindPointEquipment(pointId, [row.equipmentId])).then(() => { proxy.$modal.msgSuccess("解绑成功"); return Promise.all([loadPoints(), loadStatistics(), refreshDetail(pointId)]); }); }
-function reverseGeocode(lng, lat) { return new Promise((resolve) => { const BMapGL = window.BMapGL; if (!BMapGL?.Geocoder) return resolve(""); try { new BMapGL.Geocoder().getLocation(new BMapGL.Point(lng, lat), (result) => resolve(result?.address || result?.content?.address || "")); } catch (_) { resolve(""); } }); }
 function openMapPicker() { const lng = Number(form.longitude); const lat = Number(form.latitude); mapCenter.value = Number.isFinite(lng) && Number.isFinite(lat) ? { lng, lat } : { lng: 110.3965, lat: 28.4638 }; mapPoints.value = Number.isFinite(lng) && Number.isFinite(lat) ? [{ lng, lat, name: form.pointName || "监测点" }] : []; mapVisible.value = true; }
-async function handleMapClick(point) { if (!point) return; const [lng, lat] = bd09ToWgs84(point.lng, point.lat); form.longitude = Number(lng.toFixed(6)); form.latitude = Number(lat.toFixed(6)); mapPoints.value = [{ lng: form.longitude, lat: form.latitude, name: form.pointName || "监测点" }]; locationTip.value = "正在解析地址..."; const address = await reverseGeocode(point.lng, point.lat); if (address) { form.location = address; locationTip.value = "已按地图选点回填地址,可手工修改。"; } else locationTip.value = "地址解析失败,请手动填写位置。"; }
+async function handleMapClick(point) { if (!point) return; const lng = Number(point.lng), lat = Number(point.lat); form.longitude = Number(lng.toFixed(6)); form.latitude = Number(lat.toFixed(6)); mapPoints.value = [{ lng: form.longitude, lat: form.latitude, name: form.pointName || "监测点" }]; locationTip.value = "正在解析地址..."; const address = await lookupAddress(point.lng, point.lat); if (address) { form.location = address; locationTip.value = "已按地图选点回填地址,可手工修改。"; } else locationTip.value = "地址解析失败,请手动填写位置。"; }
 watch(rootTypeId, (value) => { query.rootTypeId = value; query.pageNum = 1; Promise.all([loadPoints(), loadStatistics()]); });
 onMounted(() => { Promise.all([loadPoints(), loadStatistics()]); });
 </script>

+ 19 - 67
src/views/subSystem/pipeNetwork/home/index.vue

@@ -102,7 +102,7 @@
                 <i class="legend-dot offline"></i>离线
               </button>
               <button type="button" class="legend-row" :class="{ 'is-off': !legendVisible.alarm }" @click="toggleLegend('alarm')">
-                <i class="legend-dot alarm"></i>报警(在线/离线时叠加)
+                <i class="legend-dot alarm"></i>报警(在线/离线均优先显示)
               </button>
               <button type="button" class="legend-row" :class="{ 'is-off': !pipelineVisible }" @click="pipelineVisible = !pipelineVisible">
                 <i class="legend-line"></i>管网({{ pipeFeatures.length }} 段)
@@ -315,13 +315,13 @@ const ALARM_RANGE_DAYS = 15
 const tableHeaderStyle = { background: '#f7f9fc', color: '#5e6d82', borderColor: '#e3e9f2' }
 const priorityText = (l) => ({ 0: '紧急', 1: '一般', 2: '低' }[l] || '-')
 const priorityClass = (l) => ({ 0: 'high', 1: 'medium', 2: 'low' }[l] || 'low')
-const statusText = (s) => ({ normal: '在线', alert: '告警', offline: '离线' }[s] || s)
+const statusText = (s) => ({ normal: '在线', alert: '报警', offline: '离线' }[s] || s)
 function districtLabel(code) {
   return code == null ? '-' : (DISTRICT_MAP[code] ?? String(code))
 }
 /** 报警类型展示:优先报警内容,其次监测指标名称,绝不回退到编码 */
 function alarmTypeText(record) {
-  return record.alarmContent || indicatorName(record, '其他告警')
+  return record.alarmContent || indicatorName(record, '其他报警')
 }
 /** 报警等级配色/名称:与系统 alarm_level 字典一致(1红 2橙 3蓝 4绿) */
 function levelColor(level) {
@@ -427,64 +427,20 @@ const mapLines = computed(() => {
   }).filter(line => line.points.length >= 2)
 })
 
-/** 标记图预加载:在线=绿、离线=灰,报警时在其右上角叠加红色标记 */
-const iconsReady = ref(false)
-const markerImages = {
-  green: typeof Image !== 'undefined' ? new Image() : null,
-  grey: typeof Image !== 'undefined' ? new Image() : null,
-  red: typeof Image !== 'undefined' ? new Image() : null
-}
-const iconCache = new Map()
-
-function loadMarkerImages() {
-  const entries = [
-    ['green', gisPointStatusIcons.normal],
-    ['grey', gisPointStatusIcons.offline],
-    ['red', gisPointStatusIcons.alarm]
-  ]
-  let pending = entries.length
-  if (!pending) { iconsReady.value = true; return }
-  const done = () => {
-    pending -= 1
-    if (pending <= 0) iconsReady.value = true
-  }
-  entries.forEach(([key, url]) => {
-    const img = markerImages[key]
-    if (!img) return done()
-    img.onload = done
-    img.onerror = done
-    img.src = url
-  })
-}
-
-function composeAlarmIcon(online) {
-  const base = online ? markerImages.green : markerImages.grey
-  const red = markerImages.red
-  if (!base || !red || !base.complete || !red.complete) {
-    return online ? gisPointStatusIcons.normal : gisPointStatusIcons.offline
-  }
-  const size = 32
-  const canvas = document.createElement('canvas')
-  canvas.width = size
-  canvas.height = size
-  const ctx = canvas.getContext('2d')
-  ctx.drawImage(base, 0, 0, size, size)
-  const badge = Math.round(size * 0.5)
-  ctx.drawImage(red, size - badge + 2, 1, badge, badge)
-  return canvas.toDataURL()
-}
-
+/**
+ * 设备点位图标:报警优先于在线/离线状态。
+ * 在线+报警、离线+报警统一显示红色报警图标,不再叠加角标,也不会因图片未加载回退成绿/灰。
+ */
 function getDeviceIcon(device) {
-  const online = Number(device.onlineStatus) === 1
-  const alarm = Number(device.alarmStatus) === 1
-  if (!alarm) return online ? gisPointStatusIcons.normal : gisPointStatusIcons.offline
-  if (!iconsReady.value) return online ? gisPointStatusIcons.normal : gisPointStatusIcons.offline
-  const key = online ? 'online' : 'offline'
-  if (!iconCache.has(key)) iconCache.set(key, composeAlarmIcon(online))
-  return iconCache.get(key)
+  if (Number(device.alarmStatus) === 1) return gisPointStatusIcons.alarm
+  return Number(device.onlineStatus) === 1 ? gisPointStatusIcons.normal : gisPointStatusIcons.offline
 }
 
-/** 图例状态筛选:隐藏后对应状态的设备在地图上不显示 */
+/**
+ * 图例筛选:在线/离线按设备状态归类,报警开关额外把所有报警设备纳入(并集关系)。
+ * 这样报警设备不会因为「报警」开关关闭就从在线/离线结果里消失,
+ * 也不会因为对应的在线/离线开关关闭而漏掉;图标颜色由 getDeviceIcon 保证红色优先。
+ */
 const legendVisible = reactive({ online: true, offline: true, alarm: true })
 function toggleLegend(key) {
   legendVisible[key] = !legendVisible[key]
@@ -492,15 +448,12 @@ function toggleLegend(key) {
 function isDeviceVisible(device) {
   const online = Number(device.onlineStatus) === 1
   const alarm = Number(device.alarmStatus) === 1
-  if (online && !legendVisible.online) return false
-  if (!online && !legendVisible.offline) return false
-  if (alarm && !legendVisible.alarm) return false
-  return true
+  const byStatus = online ? legendVisible.online : legendVisible.offline
+  return byStatus || (alarm && legendVisible.alarm)
 }
 
 const mapPoints = computed(() => {
-  // 依赖 iconsReady 与图例筛选状态,图片就绪或切换筛选后重算
-  void iconsReady.value
+  // 依赖图例筛选状态,切换筛选后重算
   void legendVisible.online
   void legendVisible.offline
   void legendVisible.alarm
@@ -671,7 +624,7 @@ async function loadAlarmData() {
     recentAlarms.value = records.filter(r => !r.createTime || new Date(String(r.createTime).replace(/-/g, '/')) >= rangeStart)
   } catch (e) {
     console.error(e)
-    ElMessage.error('告警数据加载失败')
+    ElMessage.error('报警数据加载失败')
     recentAlarms.value = []
   } finally {
     loading.alarm = false
@@ -681,7 +634,6 @@ async function loadAlarmData() {
 // ==================== 生命周期 ====================
 onMounted(() => {
   loadIndicatorNames()
-  loadMarkerImages()
   loadAll()
 })
 // 同一组件被四个菜单复用:切换子系统菜单(标签页/keep-alive)后按新的 rootTypeId 重载
@@ -958,7 +910,7 @@ watch(rootTypeId, () => {
   .hb-items { gap: 12px; }
 }
 @media (max-width: 1024px) {
-  .monitor-container { height: auto; min-height: 100vh; overflow: auto; }
+  .monitor-container { height: auto; min-height: calc(100vh - 84px); overflow: auto; }
   .main-grid { display: flex; flex-direction: column; }
   .col { min-height: auto; }
   .panel { min-height: 240px; }

+ 6 - 2
src/views/subSystem/risk/RiskMap.vue

@@ -67,8 +67,12 @@ async function initMap() {
   try {
     const L = await import('leaflet')
     mapInstance = L.map('riskMap').setView([34.77, 113.65], 12)
-    L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
-      attribution: '&copy; OpenStreetMap contributors'
+    const { tiandituTileUrl, TIANDITU_SUBDOMAINS } = await import('@/utils/tiandituMap')
+    L.tileLayer(tiandituTileUrl('vec'), {
+      subdomains: TIANDITU_SUBDOMAINS, maxZoom: 18, attribution: '&copy; 天地图'
+    }).addTo(mapInstance)
+    L.tileLayer(tiandituTileUrl('cva'), {
+      subdomains: TIANDITU_SUBDOMAINS, maxZoom: 18
     }).addTo(mapInstance)
 
     const res = await getFourColorMap()

+ 18 - 17
tests/gisMapCanvas.test.mjs

@@ -4,31 +4,31 @@ import { readFile } from 'node:fs/promises'
 import { getMapClusterIcon, getMonitoringPointColor, getMonitoringPointIcon, getMonitoringPointLabel, getMonitoringPointStatus, getMonitoringPointTagType } from '../src/utils/gisMapColors.js'
 import { getFacilityStatusIcon, getFacilityStatusMeta } from '../src/utils/gisMapColors.js'
 
-test('shared gis map canvas converts WGS84 coordinates before BMapGL rendering', async () => {
+test('shared gis map canvas keeps WGS84 coordinates for TdtMap rendering', async () => {
   const source = await readFile('src/components/GisMapCanvas.vue', 'utf8')
 
   assert.match(source, /import \{ convertCoord \} from '@\/utils\/coordTransform'/u)
   assert.match(source, /const \[lng, lat\] = convertCoord\(point\.lng, point\.lat\)/u)
-  assert.match(source, /new BMapGL\.Point\(lng, lat\)/u)
-  assert.match(source, /mapInstance\.setViewport\(bmapPoints/u)
-  assert.doesNotMatch(source, /new BMapGL\.Point\(point\.lng, point\.lat\)/u)
+  assert.match(source, /new TdtMap\.Point\(lng, lat\)/u)
+  assert.match(source, /mapInstance\.setViewport\(tdtPoints/u)
+  assert.doesNotMatch(source, /new TdtMap\.Point\(point\.lng, point\.lat\)/u)
 })
 
 test('shared gis map canvas renders pipelines without endpoint markers', async () => {
   const source = await readFile('src/components/GisMapCanvas.vue', 'utf8')
-  const addLine = source.match(/function addLine\(BMapGL, line\) \{[\s\S]*?\n\}/u)?.[0] || ''
+  const addLine = source.match(/function addLine\(TdtMap, line\) \{[\s\S]*?\n\}/u)?.[0] || ''
 
-  assert.match(addLine, /new BMapGL\.Polyline\(bmapPoints/u)
+  assert.match(addLine, /new TdtMap\.Polyline\(tdtPoints/u)
   assert.match(addLine, /emit\('line-click', line\)/u)
-  assert.doesNotMatch(addLine, /new BMapGL\.Marker/u)
-  assert.doesNotMatch(addLine, /new BMapGL\.Label/u)
+  assert.doesNotMatch(addLine, /new TdtMap\.Marker/u)
+  assert.doesNotMatch(addLine, /new TdtMap\.Label/u)
   assert.doesNotMatch(addLine, /起点/u)
   assert.doesNotMatch(addLine, /终点/u)
 })
 
 test('shared gis map canvas renders contextual clusters with colorful mixed icons', async () => {
   const source = await readFile('src/components/GisMapCanvas.vue', 'utf8')
-  const addClusterMarker = source.match(/function addClusterMarker\(BMapGL, cluster\) \{[\s\S]*?\n\}/u)?.[0] || ''
+  const addClusterMarker = source.match(/function addClusterMarker\(TdtMap, cluster\) \{[\s\S]*?\n\}/u)?.[0] || ''
 
   assert.match(source, /import \{ mergeMapPoints \} from '@\/utils\/gisMapClustering'/u)
   assert.match(source, /clusterRadius: \{ type: Number, default: 48 \}/u)
@@ -49,19 +49,20 @@ test('shared gis map canvas renders every point as an official icon with a label
   const source = await readFile('src/components/GisMapCanvas.vue', 'utf8')
 
   assert.match(source, /function escapeHtml\(value\)/u)
-  assert.match(source, /let bmapLoadTimer = null/u)
+  assert.match(source, /import \{ ensureTdtMap \} from '@\/utils\/mapApi'/u)
   assert.match(source, /let disposed = false/u)
-  assert.match(source, /if \(disposed\) \{[\s\S]*?clearInterval\(bmapLoadTimer\)/u)
+  assert.match(source, /const TdtMap = await ensureTdtMap\(\)/u)
+  assert.doesNotMatch(source, /setInterval/u)
   assert.match(source, /if \(disposed \|\| !mapContainer\.value\) return/u)
   assert.match(source, /onBeforeUnmount\(\(\) => \{[\s\S]*?disposed = true/u)
-  assert.match(source, /clearInterval\(bmapLoadTimer\)/u)
+  assert.match(source, /clearScene\(\)/u)
   assert.match(source, /markerSize: \{ type: Number, default: 28 \}/u)
   assert.match(source, /const iconUrl = point\.iconUrl \|\| getMonitoringPointIcon\(point\)/u)
-  assert.match(source, /new BMapGL\.Icon\(iconUrl, new BMapGL\.Size\(size, size\), \{/u)
-  assert.match(source, /anchor: new BMapGL\.Size\(size \/ 2, size\)/u)
-  assert.match(source, /const marker = new BMapGL\.Marker\(position, \{/u)
-  assert.match(source, /new BMapGL\.Label\(`<div class="gis-marker-label-text">/u)
-  assert.match(source, /offset: new BMapGL\.Size\(-28, -props\.markerSize - 18\)/u)
+  assert.match(source, /new TdtMap\.Icon\(iconUrl, new TdtMap\.Size\(size, size\), \{/u)
+  assert.match(source, /anchor: new TdtMap\.Size\(size \/ 2, size\)/u)
+  assert.match(source, /const marker = new TdtMap\.Marker\(position, \{/u)
+  assert.match(source, /new TdtMap\.Label\(`<div class="gis-marker-label-text">/u)
+  assert.match(source, /offset: new TdtMap\.Size\(-28, -props\.markerSize - 18\)/u)
   assert.match(source, /width: 56px/u)
   assert.match(source, /overflow-wrap: anywhere/u)
   assert.doesNotMatch(source, /white-space: nowrap/u)

+ 8 - 0
vite.config.js

@@ -35,6 +35,14 @@ export default defineConfig(({ mode, command }) => {
           // target: 'http://111.23.174.45:8301',
           changeOrigin: true,
           rewrite: (p) => p.replace(/^\/dev-api/, '')
+        },
+        // 天地图 Web 服务(逆地理编码 / 地名搜索),仅开发环境生效
+        // 生产环境需在 Nginx 配置 location /tdtApi/ -> https://api.tianditu.gov.cn/
+        // 详见 src/utils/tiandituMap.js 中 TIANDITU_PROXY 的部署备注
+        '/tdtApi': {
+          target: 'https://api.tianditu.gov.cn',
+          changeOrigin: true,
+          rewrite: (p) => p.replace(/^\/tdtApi/, '')
         }
       }
     },