fix(map): 修正定位落到零坐标、旧详情页路由白屏与跳转期间地图可拖动

三个问题,前两个各有独立根因。

1) 首次打开定位到几内亚湾、底图变成 HERE

   regionchange 处理里读的是 `event.detail.longitude`,但微信的 regionchange
   载荷没有这个字段,`Number(undefined)` 恒为 NaN,于是每次都落到异步的
   getCenterLocation。冷启动时地图 SDK 还没拿到有效中心点,它会回传 (0, 0),
   而两个 store 的坐标校验只查 Number.isFinite,(0, 0) 直接通过。

   (0, 0) 落在几内亚湾(Null Island),微信 <map> 一到境外坐标就切 HERE 底图。
   全程不需要用户点过定位按钮。

   数据集校验器本来就在拒绝零坐标,只是运行时 store 没套用同一条规则。现把它
   提成共享守卫 isTrustworthyCoordinate,validateCoordinates 改为复用,并铺到
   map store、location store 和打卡定位服务。regionchange 一侧改为优先读微信
   真正给的 detail.centerLocation,回落路径校验后才写入,且 mapReady 为 false
   时不接受任何回写。

2) 冷启动白屏、左上角只剩回首页的小房子

   f8ad5e1 把详情页从 pages/poi/detail 移进 pages-poi 分包,源码调用点都改了,
   但旧路由本身从 app.json 消失。任何还攥着旧路径的入口 —— 开发者工具模拟器上次
   停留的路由、预览启动页、已发出的分享卡片或二维码 —— 冷启动会落到微信解析不出
   的路由,渲染成白屏;因为不是 tabBar 页且栈深为 1,左上角只剩小房子。

   旧路径补一个只做重定向的跳板页,带 poiId 就 redirectTo 新路径,不带就 reLaunch
   回地图。用 redirectTo 是为了让跳板不留在页面栈里。主包 +1 KB。等所有入口都刷新
   过可以删掉。

3) 跳转详情页时地图仍可拖动几毫秒

   <map> 是渲染在 webview 之上的原生组件,销毁是异步的,页面转场开始了它还在。
   navigating 之前只用于防重复点击,没接到地图上。现在接上 enable-scroll /
   enable-zoom,并给 selectMarker、openCheckInRecords 补同一个守卫(后者原先完全
   没有防护),regionchange 在跳转期间也不再回写 viewport。onShow 里直接复位
   navigating,返回时立刻恢复交互,不等 500ms 兜底定时器。

   原生组件的异步销毁消不掉,但那几毫秒里地图不再响应拖动和缩放。

验证:测试 100 passed(map store 新增零坐标、越界、resetViewport 兜底用例),
type-check 干净。build 主包 1187 KB / 分包 1841 KB,dev 1527 KB / 1847 KB,
均过 verify:mp-weixin。
This commit is contained in:
周瑞哲
2026-08-03 10:45:28 +08:00
parent f8ad5e1faf
commit 1bcdd40729
10 changed files with 201 additions and 31 deletions
+51 -6
View File
@@ -1,11 +1,12 @@
<script setup lang="ts">
import type { PoiCategory, PoiDatasetMeta, PoiSummary } from '@/domain/poi'
import type { GeoPoint, PoiCategory, PoiDatasetMeta, PoiSummary } from '@/domain/poi'
import type { PlanResponse } from '@/domain/travel'
import type { PoiMapMarker } from '@/services/map'
import MapFilterHeader from '@/components/map/MapFilterHeader.vue'
import PoiSummaryCard from '@/components/map/PoiSummaryCard.vue'
import PageState from '@/components/poi/PageState.vue'
import { getPoiRepository } from '@/data/poi'
import { isTrustworthyCoordinate } from '@/domain/poi'
import { buildMarkerIdMap, createPoiMarkers } from '@/services/map'
import { getSessionPlanningOrigin, loadPlan } from '@/services/travel-assistant'
import { useLocationStore, useMapStore } from '@/stores'
@@ -243,6 +244,9 @@ function selectCategory(categoryCode: string | null) {
}
function selectMarker(event: { detail: { markerId: number } }) {
// 跳转过程中原生地图还在,别再受理点选。
if (navigating.value)
return
lastMarkerTapAt.value = Date.now()
const poiId = markerIdToPoiId.value.get(Number(event.detail.markerId))
if (!poiId)
@@ -267,22 +271,49 @@ function clearSelection() {
mapStore.selectPoi(null)
}
/**
* 尽量直接从 regionchange 事件里取中心点。
* 微信把中心点放在 `detail.centerLocation`,老基础库才退化到 `detail.longitude`。
* 两者都拿不到时才回落到异步的 getCenterLocation。
*/
function readEventCenter(detail: Record<string, unknown>): GeoPoint | null {
const center = detail.centerLocation
if (center && typeof center === 'object') {
const { longitude, latitude } = center as Record<string, unknown>
if (isTrustworthyCoordinate(Number(longitude), Number(latitude)))
return { longitude: Number(longitude), latitude: Number(latitude) }
}
const longitude = Number(detail.longitude)
const latitude = Number(detail.latitude)
if (isTrustworthyCoordinate(longitude, latitude))
return { longitude, latitude }
return null
}
function updateViewport(event: { type: string, detail: Record<string, unknown> }) {
const changeType = String(event.detail.type ?? event.type ?? '')
if (changeType !== 'end')
return
// 地图上下文还没建立、或正在跳转详情页时,地图回传的中心点不可信,直接丢弃。
if (!mapReady.value || navigating.value)
return
const longitude = Number(event.detail.longitude)
const latitude = Number(event.detail.latitude)
const eventScale = Number(event.detail.scale)
const fallbackScale = Number.isFinite(eventScale) ? eventScale : viewport.value.scale
if (Number.isFinite(longitude) && Number.isFinite(latitude)) {
updateViewportWithScale(longitude, latitude, fallbackScale)
const center = readEventCenter(event.detail)
if (center) {
updateViewportWithScale(center.longitude, center.latitude, fallbackScale)
return
}
mapContext.value?.getCenterLocation({
success: (location: { longitude: number, latitude: number }) => {
// 地图 SDK 还没拿到有效中心点时会回传 (0, 0),写进 viewport 会把地图甩到几内亚湾。
if (!isTrustworthyCoordinate(location.longitude, location.latitude))
return
updateViewportWithScale(location.longitude, location.latitude, fallbackScale)
},
})
@@ -485,7 +516,17 @@ function openDetail(poiId: string) {
}
function openCheckInRecords() {
uni.navigateTo({ url: '/pages/check-in/records' })
if (navigating.value)
return
navigating.value = true
uni.navigateTo({
url: '/pages/check-in/records',
complete: () => {
setTimeout(() => {
navigating.value = false
}, 500)
},
})
}
function initializeMap() {
@@ -527,6 +568,8 @@ onReady(() => {
})
onShow(() => {
// 从详情页返回后立刻恢复地图交互,不等 navigateTo 的 500ms 兜底定时器。
navigating.value = false
loadActivePlan()
if (activePlan.value)
includePlannedRoute()
@@ -597,6 +640,8 @@ onUnload(stopForegroundLocationUpdates)
:enable-rotate="false"
:enable-overlooking="false"
:show-compass="false"
:enable-scroll="!navigating"
:enable-zoom="!navigating"
@markertap="selectMarker"
@tap="clearSelection"
@regionchange="updateViewport"
+62
View File
@@ -0,0 +1,62 @@
<script setup lang="ts">
import PageState from '@/components/poi/PageState.vue'
/**
* 旧详情页路由的兼容跳板。
*
* 详情页已从 `pages/poi/detail` 移入 `pages-poi` 分包(见 f8ad5e1)。
* 旧路径一旦从 app.json 消失,任何还攥着它的入口 —— 开发者工具模拟器上次停留的
* 路由、预览时的启动页、已经发出去的分享卡片或二维码 —— 冷启动都会落到一个
* 微信解析不出来的路由上,渲染成白屏,左上角只剩一个回首页的小房子。
*
* 这里保留一个只做重定向的空页面把流量接回新路径。等所有入口都刷新过之后可以删掉。
*/
const REDIRECT_FAILURE_TOAST = '点位详情已更新,正在返回地图'
function fallbackToMap() {
uni.reLaunch({ url: '/pages/map/index' })
}
onLoad((query) => {
const rawPoiId = typeof query?.poiId === 'string' ? query.poiId : ''
if (!rawPoiId.trim()) {
fallbackToMap()
return
}
// redirectTo 而非 navigateTo:跳板本身不该留在页面栈里。
uni.redirectTo({
url: `/pages-poi/detail?poiId=${encodeURIComponent(rawPoiId)}`,
fail: () => {
uni.showToast({ title: REDIRECT_FAILURE_TOAST, icon: 'none' })
fallbackToMap()
},
})
})
</script>
<template>
<view class="poi-detail-redirect">
<PageState state="loading" title="正在打开点位详情…" />
</view>
</template>
<route lang="yaml">
layout: map
style:
navigationBarTitleText: 点位详情
navigationBarBackgroundColor: '#ffffff'
navigationBarTextStyle: black
backgroundColor: '#f5f7f6'
</route>
<style scoped lang="scss">
.poi-detail-redirect {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
min-height: 100vh;
background: #f5f7f6;
}
</style>