You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
222 lines
5.5 KiB
222 lines
5.5 KiB
<script setup>
|
|
import { onMounted, onUnmounted } from 'vue'
|
|
import mapboxgl from 'mapbox-gl'
|
|
import mapConfig, { MAP_STYLES, MAP_OVERVIEW, MAP_VIEW } from '@/config/map'
|
|
import commonRefs from '@/utils/commonRefs'
|
|
import mapHelper from '@/core/mapHelper'
|
|
import { applyChineseLabels } from '@/utils/mapLabels'
|
|
import 'mapbox-gl/dist/mapbox-gl.css'
|
|
|
|
// 保证任意路径创建的 Map 都挂到 DOM/window,避免 HMR/异常吞错后 Monitor 丢实例
|
|
if (!mapboxgl.Map.__laicPatched) {
|
|
const OriginalMap = mapboxgl.Map
|
|
class LaicMap extends OriginalMap {
|
|
constructor(options) {
|
|
super(options)
|
|
try {
|
|
const el = this.getContainer?.()
|
|
if (el) el.__laicMap = this
|
|
if (typeof window !== 'undefined') {
|
|
window.__LAIC_MAPBOX_MAP__ = this
|
|
window.dispatchEvent(new CustomEvent('laic-map-ready', { detail: this }))
|
|
}
|
|
} catch (_) {
|
|
/* ignore */
|
|
}
|
|
}
|
|
}
|
|
LaicMap.__laicPatched = true
|
|
mapboxgl.Map = LaicMap
|
|
}
|
|
|
|
let map = null
|
|
let resizeObserver = null
|
|
|
|
function resizeMap(target = map) {
|
|
if (!target) return
|
|
try {
|
|
target.resize()
|
|
} catch (_) {
|
|
/* ignore */
|
|
}
|
|
}
|
|
|
|
function bindResizeObserver(target) {
|
|
if (resizeObserver) {
|
|
resizeObserver.disconnect()
|
|
resizeObserver = null
|
|
}
|
|
const el = target?.getContainer?.()
|
|
if (!el || typeof ResizeObserver === 'undefined') return
|
|
resizeObserver = new ResizeObserver(() => {
|
|
resizeMap(target)
|
|
})
|
|
resizeObserver.observe(el)
|
|
}
|
|
|
|
function ensureGlobe(target) {
|
|
if (!target) return
|
|
try {
|
|
target.setProjection('globe')
|
|
} catch (error) {
|
|
console.warn('[MapLayer] setProjection(globe) failed:', error)
|
|
}
|
|
}
|
|
|
|
function clearMapRef(target) {
|
|
if (!target) return
|
|
// 仅清理属于当前实例的引用,避免 HMR 下旧 onUnmounted 误删新 map
|
|
if (mapHelper.getMap() === target) {
|
|
mapHelper.setMap(null)
|
|
commonRefs.removeRef('map')
|
|
}
|
|
if (typeof window !== 'undefined' && window.__LAIC_MAPBOX_MAP__ === target) {
|
|
delete window.__LAIC_MAPBOX_MAP__
|
|
}
|
|
try {
|
|
if (target.getContainer?.().__laicMap === target) delete target.getContainer().__laicMap
|
|
} catch (_) {
|
|
/* ignore */
|
|
}
|
|
}
|
|
|
|
function exposeMap(target) {
|
|
if (!target) return
|
|
const el =
|
|
(typeof document !== 'undefined' ? document.getElementById('map') : null) ||
|
|
target.getContainer?.()
|
|
if (el) {
|
|
try {
|
|
Object.defineProperty(el, '__laicMap', { value: target, writable: true, configurable: true })
|
|
} catch (_) {
|
|
el.__laicMap = target
|
|
}
|
|
}
|
|
if (typeof window !== 'undefined') {
|
|
try {
|
|
Object.defineProperty(window, '__LAIC_MAPBOX_MAP__', {
|
|
value: target,
|
|
writable: true,
|
|
configurable: true,
|
|
})
|
|
} catch (_) {
|
|
window.__LAIC_MAPBOX_MAP__ = target
|
|
}
|
|
window.dispatchEvent(new CustomEvent('laic-map-ready', { detail: target }))
|
|
}
|
|
commonRefs.setRef('map', target)
|
|
mapHelper.setMap(target)
|
|
}
|
|
|
|
function destroyOrphanMap(container) {
|
|
if (!container) return
|
|
// HMR / 重复挂载时容器上常残留旧 Mapbox 实例,继续 new Map 会直接抛错
|
|
const orphan = container.__laicMap
|
|
if (orphan && typeof orphan.remove === 'function') {
|
|
try {
|
|
orphan.remove()
|
|
} catch (_) {
|
|
/* ignore */
|
|
}
|
|
}
|
|
delete container.__laicMap
|
|
// Mapbox 会给容器加 mapboxgl-map;清掉子节点避免二次初始化失败
|
|
if (container.classList?.contains('mapboxgl-map')) {
|
|
container.innerHTML = ''
|
|
container.classList.remove('mapboxgl-map')
|
|
container.removeAttribute('style')
|
|
}
|
|
}
|
|
|
|
function initMap() {
|
|
const tk = mapConfig.token
|
|
if (!tk) {
|
|
console.error('[MapLayer] 缺少 APP_MAPBOX_TOKEN,跳过地图初始化')
|
|
commonRefs.setRef('map', null)
|
|
mapHelper.setMap(null)
|
|
return
|
|
}
|
|
|
|
const container =
|
|
(typeof document !== 'undefined' ? document.getElementById('map') : null) || 'map'
|
|
if (typeof container !== 'string') destroyOrphanMap(container)
|
|
|
|
mapboxgl.accessToken = tk
|
|
map = new mapboxgl.Map({
|
|
container,
|
|
style: MAP_STYLES.satellite,
|
|
center: MAP_OVERVIEW.center,
|
|
zoom: MAP_OVERVIEW.zoom,
|
|
minZoom: MAP_VIEW.minZoom,
|
|
attributionControl: false,
|
|
projection: 'globe',
|
|
})
|
|
|
|
exposeMap(map)
|
|
console.info('[MapLayer] map ready', {
|
|
hasDom: !!(typeof document !== 'undefined' && document.getElementById('map')?.__laicMap),
|
|
hasWin: !!(typeof window !== 'undefined' && window.__LAIC_MAPBOX_MAP__),
|
|
})
|
|
|
|
if (map.isStyleLoaded()) {
|
|
ensureGlobe(map)
|
|
applyChineseLabels(map)
|
|
} else {
|
|
map.once('style.load', () => {
|
|
ensureGlobe(map)
|
|
applyChineseLabels(map)
|
|
})
|
|
}
|
|
|
|
// 布局未稳定时 Mapbox 落到错误高度(常见 ~300px),初始化后强制校正
|
|
map.once('load', () => resizeMap(map))
|
|
requestAnimationFrame(() => {
|
|
resizeMap(map)
|
|
requestAnimationFrame(() => resizeMap(map))
|
|
})
|
|
bindResizeObserver(map)
|
|
}
|
|
|
|
onMounted(() => {
|
|
try {
|
|
initMap()
|
|
} catch (error) {
|
|
console.error('[MapLayer] 地图初始化失败:', error)
|
|
commonRefs.setRef('map', null)
|
|
mapHelper.setMap(null)
|
|
}
|
|
})
|
|
|
|
onUnmounted(() => {
|
|
if (resizeObserver) {
|
|
resizeObserver.disconnect()
|
|
resizeObserver = null
|
|
}
|
|
if (map) {
|
|
const dying = map
|
|
map = null
|
|
try {
|
|
dying.remove()
|
|
} catch (_) {
|
|
/* ignore */
|
|
}
|
|
clearMapRef(dying)
|
|
}
|
|
})
|
|
</script>
|
|
|
|
<template>
|
|
<div id="map" :class="s.root" />
|
|
</template>
|
|
|
|
<style lang="less" module="s">
|
|
.root {
|
|
position: absolute;
|
|
left: 0;
|
|
top: 0;
|
|
right: 0;
|
|
bottom: 0;
|
|
height: 100%;
|
|
z-index: 0;
|
|
}
|
|
</style>
|
|
|