| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352 |
- <template>
- <div ref="mapContainer" class="gis-map-canvas"></div>
- </template>
- <script setup>
- import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'
- import { convertCoord } from '@/utils/coordTransform'
- import { getMapClusterIcon, getMonitoringPointIcon } from '@/utils/gisMapColors'
- import { mergeMapPoints } from '@/utils/gisMapClustering'
- const props = defineProps({
- points: { type: Array, default: () => [] },
- lines: { type: Array, default: () => [] },
- center: { type: Object, default: () => ({ lng: 110.3965, lat: 28.4638 }) },
- zoom: { type: Number, default: 15 },
- markerSize: { type: Number, default: 28 },
- clusterRadius: { type: Number, default: 48 },
- enableClustering: { type: Boolean, default: true },
- clusterLabel: { type: String, default: '点位集合' },
- showMarkerName: { type: Boolean, default: true },
- mapStyleId: { type: String, default: 'a0f4e6e6f9e7a6c5d8e7f7e7f7e7f7e5' }
- })
- const emit = defineEmits(['point-click', 'line-click', 'cluster-click', 'map-click', 'ready', 'error'])
- 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))
- function escapeHtml(value) {
- return String(value ?? '')
- .replace(/&/g, '&')
- .replace(/</g, '<')
- .replace(/>/g, '>')
- .replace(/"/g, '"')
- .replace(/'/g, ''')
- }
- 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()
- if (disposed || !mapContainer.value) return
- mapInstance = new BMapGL.Map(mapContainer.value, { enableMapClick: true })
- const [lng, lat] = convertedCenter.value
- mapInstance.centerAndZoom(new BMapGL.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
- const lat = typeof point?.lat === 'function' ? point.lat() : point?.lat
- if (point && Number.isFinite(Number(lng)) && Number.isFinite(Number(lat))) {
- emit('map-click', { lng: Number(lng), lat: Number(lat) })
- }
- })
- mapInstance.addEventListener('moveend', renderScene)
- mapInstance.addEventListener('zoomend', renderScene)
- renderScene()
- emit('ready', mapInstance)
- } catch (error) {
- mapError.value = '百度地图初始化失败,请检查网络或地图服务配置。'
- emit('error', mapError.value)
- }
- }
- function clearScene() {
- if (!mapInstance) return
- overlays.forEach((overlay) => {
- try {
- mapInstance.removeOverlay(overlay)
- } catch (_) {
- // ignore cleanup errors
- }
- })
- overlays = []
- }
- 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 addPointMarker(BMapGL, 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),
- enableDragging: false
- })
- marker.addEventListener('click', (event) => {
- event?.domEvent?.stopPropagation?.()
- emit('point-click', point)
- })
- mapInstance.addOverlay(marker)
- overlays.push(marker)
- if (props.showMarkerName && name) {
- const label = new BMapGL.Label(`<div class="gis-marker-label-text">${escapeHtml(name)}</div>`, {
- position,
- offset: new BMapGL.Size(-28, -props.markerSize - 18)
- })
- label.setStyle({ border: 'none', background: 'transparent', padding: '0' })
- label.addEventListener('click', (event) => {
- event?.domEvent?.stopPropagation?.()
- emit('point-click', point)
- })
- mapInstance.addOverlay(label)
- overlays.push(label)
- }
- }
- function addClusterMarker(BMapGL, 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),
- enableDragging: false
- })
- marker.addEventListener('click', (event) => {
- event?.domEvent?.stopPropagation?.()
- focusCluster(cluster)
- if (isClusterUnexpandable(cluster)) emit('cluster-click', cluster)
- })
- mapInstance.addOverlay(marker)
- overlays.push(marker)
- const label = new BMapGL.Label(`<div class="gis-cluster-label-text">${escapeHtml(props.clusterLabel)}:${cluster.count}</div>`, {
- position,
- offset: new BMapGL.Size(-28, -props.markerSize - 18)
- })
- label.setStyle({ border: 'none', background: 'transparent', padding: '0' })
- label.addEventListener('click', (event) => {
- event?.domEvent?.stopPropagation?.()
- focusCluster(cluster)
- if (isClusterUnexpandable(cluster)) emit('cluster-click', cluster)
- })
- mapInstance.addOverlay(label)
- overlays.push(label)
- }
- function isClusterUnexpandable(cluster) {
- const bounds = cluster?.bounds
- if (!bounds) return false
- return bounds.minLng === bounds.maxLng && bounds.minLat === bounds.maxLat
- }
- function projectMapPoint({ lng, lat }) {
- const [bmapLng, bmapLat] = convertCoord(lng, lat)
- const pixel = mapInstance.pointToPixel(new BMapGL.Point(bmapLng, bmapLat))
- 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 => {
- const [lng, lat] = convertCoord(point.lng, point.lat)
- return new BMapGL.Point(lng, lat)
- })
- if (bmapPoints.length === 1) {
- locatePoint(cluster.points[0])
- return
- }
- mapInstance.setViewport(bmapPoints, { margins: [110, 110, 110, 110] })
- }
- function addLine(BMapGL, 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 [lng, lat] = convertCoord(point.lng, point.lat)
- return new BMapGL.Point(lng, lat)
- })
- const polyline = new BMapGL.Polyline(bmapPoints, {
- strokeColor: line.color || '#3498db',
- strokeWeight: line.strokeWeight || 6,
- strokeOpacity: line.strokeOpacity ?? 0.92,
- strokeStyle: 'solid'
- })
- polyline.addEventListener('click', (event) => {
- event?.domEvent?.stopPropagation?.()
- emit('line-click', line)
- })
- mapInstance.addOverlay(polyline)
- overlays.push(polyline)
- }
- function renderScene() {
- if (!mapInstance || !window.BMapGL) return
- clearScene()
- const BMapGL = window.BMapGL
- 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))
- renderedPoints.forEach((item) => {
- if (item.type === 'cluster') addClusterMarker(BMapGL, item)
- else addPointMarker(BMapGL, item.point)
- })
- }
- function fitBounds() {
- if (!mapInstance || !window.BMapGL) 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 [lng, lat] = convertCoord(point.lng, point.lat)
- return new window.BMapGL.Point(lng, lat)
- })
- mapInstance.setViewport(bmapPoints, { margins: [70, 70, 70, 70] })
- }
- function locatePoint(point) {
- if (!mapInstance || !window.BMapGL || !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)
- }
- function zoomIn() {
- if (mapInstance) mapInstance.zoomIn()
- }
- function zoomOut() {
- if (mapInstance) mapInstance.zoomOut()
- }
- watch(() => [props.points, props.lines], () => {
- nextTick(renderScene)
- }, { deep: true })
- onMounted(async () => {
- await nextTick()
- await initMap()
- })
- onBeforeUnmount(() => {
- disposed = true
- if (bmapLoadTimer) {
- clearInterval(bmapLoadTimer)
- bmapLoadTimer = null
- }
- clearScene()
- try {
- mapInstance?.destroy()
- } catch (_) {
- // ignore BMapGL cleanup errors
- }
- mapInstance = null
- })
- defineExpose({
- fitBounds,
- locatePoint,
- focusCluster,
- zoomIn,
- zoomOut,
- mapError
- })
- </script>
- <style scoped>
- .gis-map-canvas {
- width: 100%;
- height: 100%;
- min-height: 420px;
- background: #e9f0e5;
- }
- </style>
- <style>
- .gis-marker-label {
- border: none !important;
- background: transparent !important;
- padding: 0 !important;
- }
- .gis-marker-label-text,
- .gis-cluster-label-text {
- box-sizing: border-box;
- width: 56px;
- margin-bottom: 4px;
- padding: 2px 1px;
- overflow: hidden;
- color: #22405c;
- background: rgba(255, 255, 255, 0.92);
- border: 1px solid rgba(118, 151, 181, 0.32);
- border-radius: 4px;
- box-shadow: 0 2px 6px rgba(31, 74, 121, 0.12);
- text-align: center;
- }
- .gis-marker-label-text {
- font-size: 9px;
- line-height: 12px;
- font-weight: 600;
- overflow-wrap: anywhere;
- }
- .gis-cluster-label-text {
- width: auto;
- min-width: 56px;
- font-size: 11px;
- line-height: 16px;
- font-weight: 700;
- }
- </style>
|