diff --git a/src/layout/MapLayer.vue b/src/layout/MapLayer.vue index b6de6d0..e3e7a4a 100644 --- a/src/layout/MapLayer.vue +++ b/src/layout/MapLayer.vue @@ -7,13 +7,31 @@ import mapHelper from '@/core/mapHelper' import 'mapbox-gl/dist/mapbox-gl.css' 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() { const tk = mapConfig.token if (!tk) { console.error('[MapLayer] 缺少 APP_MAPBOX_TOKEN,跳过地图初始化') commonRefs.setRef('map', null) + mapHelper.setMap(null) return } @@ -28,32 +46,33 @@ function initMap() { projection: 'globe', }) + if (map.isStyleLoaded()) ensureGlobe(map) + else map.once('style.load', () => ensureGlobe(map)) + commonRefs.setRef('map', map) mapHelper.setMap(map) } onMounted(() => { - initTimer = setTimeout(() => { - initTimer = null - try { - initMap() - } catch (error) { - console.error('[MapLayer] 地图初始化失败:', error) - commonRefs.setRef('map', null) - } - }) + try { + initMap() + } catch (error) { + console.error('[MapLayer] 地图初始化失败:', error) + commonRefs.setRef('map', null) + mapHelper.setMap(null) + } }) onUnmounted(() => { - if (initTimer != null) { - clearTimeout(initTimer) - initTimer = null - } if (map) { - map.remove() - commonRefs.removeRef('map') - mapHelper.setMap(null) + const dying = map map = null + try { + dying.remove() + } catch (_) { + /* ignore */ + } + clearMapRef(dying) } }) diff --git a/src/utils/commonRefs.js b/src/utils/commonRefs.js index d85f4c5..2c13b71 100644 --- a/src/utils/commonRefs.js +++ b/src/utils/commonRefs.js @@ -18,21 +18,27 @@ class CommonRefs { } 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] ;(resolves || []).forEach((resolve, index) => { if (!resolve) return resolve(ref) resolves[index] = undefined // 只作废,不删除 }) + this._pendingList[key] = [] } getRef(key) { return new Promise((resolve) => { - if (key in this._refs) { + if (key in this._refs && this._refs[key] != null) { resolve(this._refs[key]) return } diff --git a/src/views/MonitorView/MonitorView.vue b/src/views/MonitorView/MonitorView.vue index fb76634..4f8dd64 100644 --- a/src/views/MonitorView/MonitorView.vue +++ b/src/views/MonitorView/MonitorView.vue @@ -659,6 +659,7 @@ function cancelPendingMapHandlers() { } if (pendingLoadHandler) { mapInstance.off('load', pendingLoadHandler) + mapInstance.off('style.load', pendingLoadHandler) pendingLoadHandler = null } if (pendingStyleHandler) { @@ -670,6 +671,11 @@ function cancelPendingMapHandlers() { async function rebuildAfterStyle() { pendingStyleHandler = null if (!mapInstance) return + try { + mapInstance.setProjection('globe') + } catch (_) { + /* ignore */ + } layersReady = false await ensureMonitorLayers() syncMarkers() @@ -695,16 +701,15 @@ function setMapMode(mode) { pendingStyleHandler = rebuildAfterStyle mapInstance.once('style.load', pendingStyleHandler) } - function resetOverviewCamera() { if (!mapInstance) return - mapInstance.easeTo({ + mapInstance.jumpTo({ center: MAP_OVERVIEW.center, zoom: MAP_OVERVIEW.zoom, pitch: 0, bearing: 0, - duration: 450, }) + updateMapScale() } function fitAll() { @@ -722,39 +727,14 @@ function flyToSelected() { const lon = Number(asset.longitude) const lat = Number(asset.latitude) 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], - 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() { @@ -765,8 +745,12 @@ async function setupMap() { } mapInstance = map mapHelper.toggleMapMode({ is2D: true }) + updateMapScale() + let readyStarted = false const onReady = async () => { + if (readyStarted) return + readyStarted = true pendingLoadHandler = null if (!mapInstance) return await ensureMonitorLayers() @@ -777,10 +761,23 @@ async function setupMap() { } cancelPendingMapHandlers() - if (map.isStyleLoaded()) onReady() - else { + if (map.isStyleLoaded()) { + await onReady() + } else { pendingLoadHandler = onReady 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) @@ -790,6 +787,37 @@ async function setupMap() { 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() { cancelPendingMapHandlers() if (mapInstance) { @@ -812,10 +840,14 @@ onMounted(async () => { if (route.query.deviceId && devices.record(route.query.deviceId)) selectedId.value = String(route.query.deviceId) const rec = selectedId.value ? devices.record(selectedId.value) : null controlExpanded.value = route.query.control === '1' && rec?.kind === 'dock' - await setupMap() } catch (e) { ui.toast(e.message || '加载设备失败') } + try { + await setupMap() + } catch (e) { + ui.toast(e.message || '地图初始化失败') + } }) onUnmounted(() => {