GisMapCanvas.vue 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295
  1. <template>
  2. <div ref="mapContainer" class="gis-map-canvas"></div>
  3. </template>
  4. <script setup>
  5. import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'
  6. import { convertCoord } from '@/utils/coordTransform'
  7. import { getMonitoringPointIcon } from '@/utils/gisMapColors'
  8. const props = defineProps({
  9. points: { type: Array, default: () => [] },
  10. lines: { type: Array, default: () => [] },
  11. center: { type: Object, default: () => ({ lng: 110.3965, lat: 28.4638 }) },
  12. zoom: { type: Number, default: 15 },
  13. markerSize: { type: Number, default: 28 },
  14. showMarkerName: { type: Boolean, default: true },
  15. mapStyleId: { type: String, default: 'a0f4e6e6f9e7a6c5d8e7f7e7f7e7f7e5' }
  16. })
  17. const emit = defineEmits(['point-click', 'line-click', 'map-click', 'ready', 'error'])
  18. const mapContainer = ref(null)
  19. const mapError = ref('')
  20. let mapInstance = null
  21. let overlays = []
  22. let bmapLoadTimer = null
  23. let disposed = false
  24. const convertedCenter = computed(() => convertCoord(props.center.lng, props.center.lat))
  25. function escapeHtml(value) {
  26. return String(value ?? '')
  27. .replace(/&/g, '&amp;')
  28. .replace(/</g, '&lt;')
  29. .replace(/>/g, '&gt;')
  30. .replace(/"/g, '&quot;')
  31. .replace(/'/g, '&#39;')
  32. }
  33. function waitForBMapGL() {
  34. return new Promise((resolve, reject) => {
  35. if (window.BMapGL) {
  36. resolve(window.BMapGL)
  37. return
  38. }
  39. if (disposed) {
  40. reject(new Error('GIS map component disposed'))
  41. return
  42. }
  43. let attempts = 0
  44. bmapLoadTimer = setInterval(() => {
  45. attempts += 1
  46. if (disposed) {
  47. clearInterval(bmapLoadTimer)
  48. reject(new Error('GIS map component disposed'))
  49. return
  50. }
  51. if (window.BMapGL) {
  52. clearInterval(bmapLoadTimer)
  53. resolve(window.BMapGL)
  54. return
  55. }
  56. if (attempts >= 100) {
  57. clearInterval(bmapLoadTimer)
  58. reject(new Error('百度地图加载超时'))
  59. }
  60. }, 200)
  61. })
  62. }
  63. async function initMap() {
  64. if (!mapContainer.value) return
  65. try {
  66. const BMapGL = await waitForBMapGL()
  67. if (disposed || !mapContainer.value) return
  68. mapInstance = new BMapGL.Map(mapContainer.value, { enableMapClick: true })
  69. const [lng, lat] = convertedCenter.value
  70. mapInstance.centerAndZoom(new BMapGL.Point(lng, lat), props.zoom)
  71. mapInstance.enableScrollWheelZoom(true)
  72. mapInstance.setMapStyleV2({ styleId: props.mapStyleId })
  73. mapInstance.addEventListener('click', () => emit('map-click'))
  74. renderScene()
  75. emit('ready', mapInstance)
  76. } catch (error) {
  77. mapError.value = '百度地图初始化失败,请检查网络或地图服务配置。'
  78. emit('error', mapError.value)
  79. }
  80. }
  81. function clearScene() {
  82. if (!mapInstance) return
  83. overlays.forEach((overlay) => {
  84. try {
  85. mapInstance.removeOverlay(overlay)
  86. } catch (_) {
  87. // ignore cleanup errors
  88. }
  89. })
  90. overlays = []
  91. }
  92. function createMarkerIcon(BMapGL, iconUrl, size) {
  93. return new BMapGL.Icon(iconUrl, new BMapGL.Size(size, size), {
  94. anchor: new BMapGL.Size(size / 2, size),
  95. imageSize: new BMapGL.Size(size, size)
  96. })
  97. }
  98. function addPointMarker(BMapGL, point) {
  99. if (!Number.isFinite(Number(point.lng)) || !Number.isFinite(Number(point.lat))) return
  100. const [lng, lat] = convertCoord(point.lng, point.lat)
  101. const name = point.name || point.equipmentName || point.facilityName || point.label || ''
  102. const iconUrl = point.iconUrl || getMonitoringPointIcon(point)
  103. const position = new BMapGL.Point(lng, lat)
  104. const marker = new BMapGL.Marker(position, {
  105. icon: createMarkerIcon(BMapGL, iconUrl, props.markerSize),
  106. enableDragging: false
  107. })
  108. marker.addEventListener('click', (event) => {
  109. event?.domEvent?.stopPropagation?.()
  110. emit('point-click', point)
  111. })
  112. mapInstance.addOverlay(marker)
  113. overlays.push(marker)
  114. if (props.showMarkerName && name) {
  115. const label = new BMapGL.Label(`<div class="gis-marker-label-text">${escapeHtml(name)}</div>`, {
  116. position,
  117. offset: new BMapGL.Size(-28, -props.markerSize - 24)
  118. })
  119. label.setStyle({ border: 'none', background: 'transparent', padding: '0' })
  120. label.addEventListener('click', (event) => {
  121. event?.domEvent?.stopPropagation?.()
  122. emit('point-click', point)
  123. })
  124. mapInstance.addOverlay(label)
  125. overlays.push(label)
  126. }
  127. }
  128. function addLine(BMapGL, line) {
  129. const points = (line.points || []).filter((point) => Number.isFinite(Number(point.lng)) && Number.isFinite(Number(point.lat)))
  130. if (points.length < 2) return
  131. const bmapPoints = points.map((point) => {
  132. const [lng, lat] = convertCoord(point.lng, point.lat)
  133. return new BMapGL.Point(lng, lat)
  134. })
  135. const polyline = new BMapGL.Polyline(bmapPoints, {
  136. strokeColor: line.color || '#3498db',
  137. strokeWeight: line.strokeWeight || 6,
  138. strokeOpacity: line.strokeOpacity ?? 0.92,
  139. strokeStyle: 'solid'
  140. })
  141. polyline.addEventListener('click', (event) => {
  142. event?.domEvent?.stopPropagation?.()
  143. emit('line-click', line)
  144. })
  145. mapInstance.addOverlay(polyline)
  146. overlays.push(polyline)
  147. ;[points[0], points[points.length - 1]].forEach((point, index) => {
  148. const [lng, lat] = convertCoord(point.lng, point.lat)
  149. const position = new BMapGL.Point(lng, lat)
  150. const iconUrl = line.iconUrl || getMonitoringPointIcon(point)
  151. const name = `${line.name || '供水管网'} ${index === 0 ? '起点' : '终点'}`
  152. const marker = new BMapGL.Marker(position, {
  153. icon: createMarkerIcon(BMapGL, iconUrl, props.markerSize),
  154. enableDragging: false
  155. })
  156. marker.addEventListener('click', (event) => {
  157. event?.domEvent?.stopPropagation?.()
  158. emit('line-click', line)
  159. })
  160. mapInstance.addOverlay(marker)
  161. overlays.push(marker)
  162. if (!props.showMarkerName) return
  163. const label = new BMapGL.Label(`<div class="gis-marker-label-text">${escapeHtml(name)}</div>`, {
  164. position,
  165. offset: new BMapGL.Size(-28, -props.markerSize - 24)
  166. })
  167. label.setStyle({ border: 'none', background: 'transparent', padding: '0' })
  168. label.addEventListener('click', (event) => {
  169. event?.domEvent?.stopPropagation?.()
  170. emit('line-click', line)
  171. })
  172. mapInstance.addOverlay(label)
  173. overlays.push(label)
  174. })
  175. }
  176. function renderScene() {
  177. if (!mapInstance || !window.BMapGL) return
  178. clearScene()
  179. const BMapGL = window.BMapGL
  180. props.lines.forEach((line) => addLine(BMapGL, line))
  181. props.points.forEach((point) => addPointMarker(BMapGL, point))
  182. }
  183. function fitBounds() {
  184. if (!mapInstance || !window.BMapGL) return
  185. const points = [
  186. ...props.points.map((point) => ({ lng: point.lng, lat: point.lat })),
  187. ...props.lines.flatMap((line) => line.points || [])
  188. ].filter((point) => Number.isFinite(Number(point.lng)) && Number.isFinite(Number(point.lat)))
  189. if (!points.length) return
  190. const bmapPoints = points.map((point) => {
  191. const [lng, lat] = convertCoord(point.lng, point.lat)
  192. return new window.BMapGL.Point(lng, lat)
  193. })
  194. mapInstance.setViewport(bmapPoints, { margins: [70, 70, 70, 70] })
  195. }
  196. function locatePoint(point) {
  197. if (!mapInstance || !window.BMapGL || !Number.isFinite(Number(point?.lng)) || !Number.isFinite(Number(point?.lat))) return
  198. const [lng, lat] = convertCoord(point.lng, point.lat)
  199. mapInstance.centerAndZoom(new window.BMapGL.Point(lng, lat), 17)
  200. }
  201. function zoomIn() {
  202. if (mapInstance) mapInstance.zoomIn()
  203. }
  204. function zoomOut() {
  205. if (mapInstance) mapInstance.zoomOut()
  206. }
  207. watch(() => [props.points, props.lines], () => {
  208. nextTick(renderScene)
  209. }, { deep: true })
  210. onMounted(async () => {
  211. await nextTick()
  212. await initMap()
  213. })
  214. onBeforeUnmount(() => {
  215. disposed = true
  216. if (bmapLoadTimer) {
  217. clearInterval(bmapLoadTimer)
  218. bmapLoadTimer = null
  219. }
  220. clearScene()
  221. try {
  222. mapInstance?.destroy()
  223. } catch (_) {
  224. // ignore BMapGL cleanup errors
  225. }
  226. mapInstance = null
  227. })
  228. defineExpose({
  229. fitBounds,
  230. locatePoint,
  231. zoomIn,
  232. zoomOut,
  233. mapError
  234. })
  235. </script>
  236. <style scoped>
  237. .gis-map-canvas {
  238. width: 100%;
  239. height: 100%;
  240. min-height: 420px;
  241. background: #e9f0e5;
  242. }
  243. </style>
  244. <style>
  245. .gis-marker-label {
  246. border: none !important;
  247. background: transparent !important;
  248. padding: 0 !important;
  249. }
  250. .gis-marker-label-text {
  251. box-sizing: border-box;
  252. width: 56px;
  253. margin-bottom: 4px;
  254. padding: 2px 1px;
  255. overflow: hidden;
  256. color: #22405c;
  257. background: rgba(255, 255, 255, 0.92);
  258. border: 1px solid rgba(118, 151, 181, 0.32);
  259. border-radius: 4px;
  260. box-shadow: 0 2px 6px rgba(31, 74, 121, 0.12);
  261. font-size: 9px;
  262. line-height: 12px;
  263. font-weight: 600;
  264. text-align: center;
  265. overflow-wrap: anywhere;
  266. }
  267. </style>