Browse Source

fix: 稳住监控 globe 投影与地图引用生命周期

MapLayer 在 style.load 后强制 setProjection(globe);commonRefs 允许覆盖且不把 null 当终态;Monitor 设备加载失败仍 setupMap,并用 jumpTo + style 就绪轮询保证选中/总览相机与比例尺。
main
xiaosi 3 weeks ago
parent
commit
2f7a564e80
  1. 41
      src/layout/MapLayer.vue
  2. 14
      src/utils/commonRefs.js
  3. 106
      src/views/MonitorView/MonitorView.vue

41
src/layout/MapLayer.vue

@ -7,13 +7,31 @@ import mapHelper from '@/core/mapHelper'
import 'mapbox-gl/dist/mapbox-gl.css' import 'mapbox-gl/dist/mapbox-gl.css'
let map = null let map = null
let initTimer = null
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')
}
}
function initMap() { function initMap() {
const tk = mapConfig.token const tk = mapConfig.token
if (!tk) { if (!tk) {
console.error('[MapLayer] 缺少 APP_MAPBOX_TOKEN,跳过地图初始化') console.error('[MapLayer] 缺少 APP_MAPBOX_TOKEN,跳过地图初始化')
commonRefs.setRef('map', null) commonRefs.setRef('map', null)
mapHelper.setMap(null)
return return
} }
@ -28,32 +46,33 @@ function initMap() {
projection: 'globe', projection: 'globe',
}) })
if (map.isStyleLoaded()) ensureGlobe(map)
else map.once('style.load', () => ensureGlobe(map))
commonRefs.setRef('map', map) commonRefs.setRef('map', map)
mapHelper.setMap(map) mapHelper.setMap(map)
} }
onMounted(() => { onMounted(() => {
initTimer = setTimeout(() => {
initTimer = null
try { try {
initMap() initMap()
} catch (error) { } catch (error) {
console.error('[MapLayer] 地图初始化失败:', error) console.error('[MapLayer] 地图初始化失败:', error)
commonRefs.setRef('map', null) commonRefs.setRef('map', null)
mapHelper.setMap(null)
} }
}) })
})
onUnmounted(() => { onUnmounted(() => {
if (initTimer != null) {
clearTimeout(initTimer)
initTimer = null
}
if (map) { if (map) {
map.remove()
commonRefs.removeRef('map')
mapHelper.setMap(null)
const dying = map
map = null map = null
try {
dying.remove()
} catch (_) {
/* ignore */
}
clearMapRef(dying)
} }
}) })
</script> </script>

14
src/utils/commonRefs.js

@ -18,21 +18,27 @@ class CommonRefs {
} }
setRef(key, ref) { setRef(key, ref) {
// 允许显式 set null 唤醒 pending(无 token 等失败路径)
if (key in this._refs && this._refs[key] != null) return
// 允许覆盖:MapLayer HMR/重挂时需要替换旧实例。
// set(null) 只用于唤醒当前 pending(失败路径),不把 null 持久化成终态,
// 否则后续成功 init 前的 getRef 会立刻拿到 null 并放弃。
if (ref == null) {
delete this._refs[key]
} else {
this._refs[key] = ref this._refs[key] = ref
}
const resolves = this._pendingList[key] const resolves = this._pendingList[key]
;(resolves || []).forEach((resolve, index) => { ;(resolves || []).forEach((resolve, index) => {
if (!resolve) return if (!resolve) return
resolve(ref) resolve(ref)
resolves[index] = undefined // 只作废,不删除 resolves[index] = undefined // 只作废,不删除
}) })
this._pendingList[key] = []
} }
getRef(key) { getRef(key) {
return new Promise((resolve) => { return new Promise((resolve) => {
if (key in this._refs) {
if (key in this._refs && this._refs[key] != null) {
resolve(this._refs[key]) resolve(this._refs[key])
return return
} }

106
src/views/MonitorView/MonitorView.vue

@ -659,6 +659,7 @@ function cancelPendingMapHandlers() {
} }
if (pendingLoadHandler) { if (pendingLoadHandler) {
mapInstance.off('load', pendingLoadHandler) mapInstance.off('load', pendingLoadHandler)
mapInstance.off('style.load', pendingLoadHandler)
pendingLoadHandler = null pendingLoadHandler = null
} }
if (pendingStyleHandler) { if (pendingStyleHandler) {
@ -670,6 +671,11 @@ function cancelPendingMapHandlers() {
async function rebuildAfterStyle() { async function rebuildAfterStyle() {
pendingStyleHandler = null pendingStyleHandler = null
if (!mapInstance) return if (!mapInstance) return
try {
mapInstance.setProjection('globe')
} catch (_) {
/* ignore */
}
layersReady = false layersReady = false
await ensureMonitorLayers() await ensureMonitorLayers()
syncMarkers() syncMarkers()
@ -695,16 +701,15 @@ function setMapMode(mode) {
pendingStyleHandler = rebuildAfterStyle pendingStyleHandler = rebuildAfterStyle
mapInstance.once('style.load', pendingStyleHandler) mapInstance.once('style.load', pendingStyleHandler)
} }
function resetOverviewCamera() { function resetOverviewCamera() {
if (!mapInstance) return if (!mapInstance) return
mapInstance.easeTo({
mapInstance.jumpTo({
center: MAP_OVERVIEW.center, center: MAP_OVERVIEW.center,
zoom: MAP_OVERVIEW.zoom, zoom: MAP_OVERVIEW.zoom,
pitch: 0, pitch: 0,
bearing: 0, bearing: 0,
duration: 450,
}) })
updateMapScale()
} }
function fitAll() { function fitAll() {
@ -722,39 +727,14 @@ function flyToSelected() {
const lon = Number(asset.longitude) const lon = Number(asset.longitude)
const lat = Number(asset.latitude) const lat = Number(asset.latitude)
if (!Number.isFinite(lon) || !Number.isFinite(lat)) return if (!Number.isFinite(lon) || !Number.isFinite(lat)) return
mapInstance.easeTo({
const nextZoom = Math.max(Number(mapInstance.getZoom()) || 0, MAP_VIEW.detailZoom)
mapInstance.jumpTo({
center: [lon, lat], center: [lon, lat],
zoom: Math.max(mapInstance.getZoom(), MAP_VIEW.detailZoom),
duration: 450,
})
}
function onMapClick(e) {
if (suppressMapClick) return
if (!layersReady) return
const layers = [DEVICE_SYMBOL, DEVICE_LABEL].filter((id) => mapInstance.getLayer(id))
if (!layers.length) return
const feats = mapInstance.queryRenderedFeatures(e.point, {
layers,
zoom: nextZoom,
pitch: 0,
bearing: 0,
}) })
const id = feats[0]?.properties?.assetId
if (id) {
suppressMapClick = true
select(String(id))
setTimeout(() => { suppressMapClick = false }, 0)
return
}
clearSelection()
}
function onSymbolEnter() {
if (!mapInstance) return
mapInstance.getCanvas().style.cursor = 'pointer'
}
function onSymbolLeave() {
if (!mapInstance) return
mapInstance.getCanvas().style.cursor = ''
updateMapScale()
} }
async function setupMap() { async function setupMap() {
@ -765,8 +745,12 @@ async function setupMap() {
} }
mapInstance = map mapInstance = map
mapHelper.toggleMapMode({ is2D: true }) mapHelper.toggleMapMode({ is2D: true })
updateMapScale()
let readyStarted = false
const onReady = async () => { const onReady = async () => {
if (readyStarted) return
readyStarted = true
pendingLoadHandler = null pendingLoadHandler = null
if (!mapInstance) return if (!mapInstance) return
await ensureMonitorLayers() await ensureMonitorLayers()
@ -777,10 +761,23 @@ async function setupMap() {
} }
cancelPendingMapHandlers() cancelPendingMapHandlers()
if (map.isStyleLoaded()) onReady()
else {
if (map.isStyleLoaded()) {
await onReady()
} else {
pendingLoadHandler = onReady pendingLoadHandler = onReady
map.once('load', pendingLoadHandler) map.once('load', pendingLoadHandler)
map.once('style.load', pendingLoadHandler)
//
const startedAt = Date.now()
const poll = () => {
if (readyStarted || mapInstance !== map) return
if (map.isStyleLoaded()) {
onReady()
return
}
if (Date.now() - startedAt < 5000) setTimeout(poll, 50)
}
setTimeout(poll, 0)
} }
map.on('click', onMapClick) map.on('click', onMapClick)
@ -790,6 +787,37 @@ async function setupMap() {
map.on('move', updateMapScale) map.on('move', updateMapScale)
} }
function onMapClick(e) {
if (suppressMapClick) return
if (!layersReady) return
const layers = [DEVICE_SYMBOL, DEVICE_LABEL].filter((id) => mapInstance.getLayer(id))
if (!layers.length) return
const feats = mapInstance.queryRenderedFeatures(e.point, {
layers,
})
const id = feats[0]?.properties?.assetId
if (id) {
suppressMapClick = true
select(String(id))
setTimeout(() => { suppressMapClick = false }, 0)
return
}
clearSelection()
}
function onSymbolEnter() {
if (!mapInstance) return
mapInstance.getCanvas().style.cursor = 'pointer'
}
function onSymbolLeave() {
if (!mapInstance) return
mapInstance.getCanvas().style.cursor = ''
}
function teardownMap() { function teardownMap() {
cancelPendingMapHandlers() cancelPendingMapHandlers()
if (mapInstance) { if (mapInstance) {
@ -812,10 +840,14 @@ onMounted(async () => {
if (route.query.deviceId && devices.record(route.query.deviceId)) selectedId.value = String(route.query.deviceId) if (route.query.deviceId && devices.record(route.query.deviceId)) selectedId.value = String(route.query.deviceId)
const rec = selectedId.value ? devices.record(selectedId.value) : null const rec = selectedId.value ? devices.record(selectedId.value) : null
controlExpanded.value = route.query.control === '1' && rec?.kind === 'dock' controlExpanded.value = route.query.control === '1' && rec?.kind === 'dock'
await setupMap()
} catch (e) { } catch (e) {
ui.toast(e.message || '加载设备失败') ui.toast(e.message || '加载设备失败')
} }
try {
await setupMap()
} catch (e) {
ui.toast(e.message || '地图初始化失败')
}
}) })
onUnmounted(() => { onUnmounted(() => {

Loading…
Cancel
Save