From 1bcdd40729e6a57ef025f57de061b903ca4afbfd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=91=A8=E7=91=9E=E5=93=B2?= Date: Mon, 3 Aug 2026 10:45:28 +0800 Subject: [PATCH] =?UTF-8?q?fix(map):=20=E4=BF=AE=E6=AD=A3=E5=AE=9A?= =?UTF-8?q?=E4=BD=8D=E8=90=BD=E5=88=B0=E9=9B=B6=E5=9D=90=E6=A0=87=E3=80=81?= =?UTF-8?q?=E6=97=A7=E8=AF=A6=E6=83=85=E9=A1=B5=E8=B7=AF=E7=94=B1=E7=99=BD?= =?UTF-8?q?=E5=B1=8F=E4=B8=8E=E8=B7=B3=E8=BD=AC=E6=9C=9F=E9=97=B4=E5=9C=B0?= =?UTF-8?q?=E5=9B=BE=E5=8F=AF=E6=8B=96=E5=8A=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 三个问题,前两个各有独立根因。 1) 首次打开定位到几内亚湾、底图变成 HERE regionchange 处理里读的是 `event.detail.longitude`,但微信的 regionchange 载荷没有这个字段,`Number(undefined)` 恒为 NaN,于是每次都落到异步的 getCenterLocation。冷启动时地图 SDK 还没拿到有效中心点,它会回传 (0, 0), 而两个 store 的坐标校验只查 Number.isFinite,(0, 0) 直接通过。 (0, 0) 落在几内亚湾(Null Island),微信 一到境外坐标就切 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) 跳转详情页时地图仍可拖动几毫秒 是渲染在 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。 --- scripts/verify-weixin-output.mjs | 3 ++ src/domain/poi/validation.ts | 30 ++++++++--- src/pages.json | 11 ++++ src/pages/map/index.vue | 57 ++++++++++++++++++--- src/pages/poi/detail.vue | 62 +++++++++++++++++++++++ src/services/location/current-location.ts | 3 +- src/stores/modules/location.ts | 12 +---- src/stores/modules/map.ts | 19 ++++--- test/map-store.test.ts | 34 +++++++++++++ uni-pages.d.ts | 1 + 10 files changed, 201 insertions(+), 31 deletions(-) create mode 100644 src/pages/poi/detail.vue diff --git a/scripts/verify-weixin-output.mjs b/scripts/verify-weixin-output.mjs index d437cfb..06b771b 100644 --- a/scripts/verify-weixin-output.mjs +++ b/scripts/verify-weixin-output.mjs @@ -12,6 +12,9 @@ const expectedPages = [ 'pages/planner/index', 'pages/check-in/index', 'pages/check-in/records', + // 旧详情页路由的兼容跳板,只做重定向。缺了它,还攥着旧路径的入口 + // (模拟器上次停留的路由、预览启动页、已发出的分享卡片)会白屏。 + 'pages/poi/detail', ] // 点位详情页放在 pages-poi 分包里,连带 800×600 大图一起按需下载, // 否则 30 张大图会把主包顶过 2 MB 上限。 diff --git a/src/domain/poi/validation.ts b/src/domain/poi/validation.ts index 4f5c655..4c50093 100644 --- a/src/domain/poi/validation.ts +++ b/src/domain/poi/validation.ts @@ -28,6 +28,27 @@ function isRecord(value: unknown): value is Record { return typeof value === 'object' && value !== null && !Array.isArray(value) } +/** + * 运行时坐标可信判断,用于点位数据校验与地图/定位状态写入。 + * + * 除了有限性和全球范围,还必须拒绝经纬度为 0 的坐标: + * 地图 SDK 尚未拿到定位、或定位接口降级时会回传 (0, 0), + * 它落在几内亚湾(Null Island)。微信 `` 在境外坐标下会切到 HERE 底图, + * 视觉上就是「地图跑到非洲的海里」,还会因为境外瓦片加载失败而留下大片空白。 + */ +export function isTrustworthyCoordinate(longitude: unknown, latitude: unknown): boolean { + return typeof longitude === 'number' + && typeof latitude === 'number' + && Number.isFinite(longitude) + && Number.isFinite(latitude) + && longitude !== 0 + && latitude !== 0 + && longitude >= -180 + && longitude <= 180 + && latitude >= -90 + && latitude <= 90 +} + function isNonEmptyString(value: unknown): value is string { return typeof value === 'string' && value.trim().length > 0 } @@ -194,14 +215,7 @@ function validateOpeningHours(openingHours: PoiOpeningHours, poiId: string, issu function validateCoordinates(poi: Poi, issues: PoiValidationIssue[]): void { const { longitude, latitude } = poi const id = isNonEmptyString(poi.id) ? poi.id : 'invalid_poi' - if (!Number.isFinite(longitude) - || !Number.isFinite(latitude) - || longitude < -180 - || longitude > 180 - || latitude < -90 - || latitude > 90 - || longitude === 0 - || latitude === 0) { + if (!isTrustworthyCoordinate(longitude, latitude)) { addIssue(issues, 'COORDINATE_INVALID', `pois.${id}.coordinates`, '经纬度必须为非零的有限合法数值', id) return } diff --git a/src/pages.json b/src/pages.json index 9824590..c0b1eb6 100644 --- a/src/pages.json +++ b/src/pages.json @@ -83,6 +83,17 @@ "navigationBarTextStyle": "black", "backgroundColor": "#f4f8f6" } + }, + { + "path": "pages/poi/detail", + "type": "page", + "layout": "map", + "style": { + "navigationBarTitleText": "点位详情", + "navigationBarBackgroundColor": "#ffffff", + "navigationBarTextStyle": "black", + "backgroundColor": "#f5f7f6" + } } ], "globalStyle": { diff --git a/src/pages/map/index.vue b/src/pages/map/index.vue index dfc663c..77271ff 100644 --- a/src/pages/map/index.vue +++ b/src/pages/map/index.vue @@ -1,11 +1,12 @@ + + + + +layout: map +style: + navigationBarTitleText: 点位详情 + navigationBarBackgroundColor: '#ffffff' + navigationBarTextStyle: black + backgroundColor: '#f5f7f6' + + + diff --git a/src/services/location/current-location.ts b/src/services/location/current-location.ts index 37359a4..bf8e999 100644 --- a/src/services/location/current-location.ts +++ b/src/services/location/current-location.ts @@ -1,4 +1,5 @@ import type { CheckInLocationSnapshot } from '@/domain/check-in' +import { isTrustworthyCoordinate } from '@/domain/poi' interface CurrentLocationOptions { highAccuracyExpireTime?: number @@ -63,7 +64,7 @@ export function getCurrentGcj02Location(options: CurrentLocationOptions = {}): P const longitude = Number(result.longitude) const latitude = Number(result.latitude) const accuracy = Number(result.accuracy ?? result.horizontalAccuracy) - if (!Number.isFinite(longitude) || !Number.isFinite(latitude)) { + if (!isTrustworthyCoordinate(longitude, latitude)) { finish(() => reject(new Error('定位结果无效,请稍后重试'))) return } diff --git a/src/stores/modules/location.ts b/src/stores/modules/location.ts index 200aa5e..6bb9ea4 100644 --- a/src/stores/modules/location.ts +++ b/src/stores/modules/location.ts @@ -1,5 +1,6 @@ import type { GeoPoint } from '@/domain/poi' import { defineStore } from 'pinia' +import { isTrustworthyCoordinate } from '@/domain/poi' export type UserLocationStatus = 'idle' | 'locating' | 'ready' | 'denied' | 'unavailable' | 'error' @@ -15,15 +16,6 @@ interface UserLocationState { errorMessage: string } -function isValidCoordinate(longitude: number, latitude: number): boolean { - return Number.isFinite(longitude) - && Number.isFinite(latitude) - && longitude >= -180 - && longitude <= 180 - && latitude >= -90 - && latitude <= 90 -} - export const useLocationStore = defineStore('user-location-session', { state: (): UserLocationState => ({ status: 'idle', @@ -36,7 +28,7 @@ export const useLocationStore = defineStore('user-location-session', { this.errorMessage = '' }, updateLocation(longitude: number, latitude: number, accuracy?: number | null) { - if (!isValidCoordinate(longitude, latitude)) + if (!isTrustworthyCoordinate(longitude, latitude)) return false this.snapshot = { diff --git a/src/stores/modules/map.ts b/src/stores/modules/map.ts index 04274ca..3f02b07 100644 --- a/src/stores/modules/map.ts +++ b/src/stores/modules/map.ts @@ -1,5 +1,6 @@ -import type { GeoPoint, MapViewport } from '@/domain/poi' +import type { MapViewport } from '@/domain/poi' import { defineStore } from 'pinia' +import { isTrustworthyCoordinate } from '@/domain/poi' export const DEFAULT_GUANGMING_VIEWPORT: MapViewport = { longitude: 113.935, @@ -14,8 +15,10 @@ interface MapSessionState { plannedRouteVisible: boolean } -function isFinitePoint(point: GeoPoint): boolean { - return Number.isFinite(point.longitude) && Number.isFinite(point.latitude) +function isUsableViewport(viewport: MapViewport): boolean { + return isTrustworthyCoordinate(viewport.longitude, viewport.latitude) + && Number.isFinite(viewport.scale) + && viewport.scale > 0 } export const useMapStore = defineStore('map-session', { @@ -33,13 +36,15 @@ export const useMapStore = defineStore('map-session', { this.selectedPoiId = poiId }, updateViewport(viewport: MapViewport) { - if (!isFinitePoint(viewport) || !Number.isFinite(viewport.scale) || viewport.scale <= 0) + if (!isUsableViewport(viewport)) return this.viewport = { ...viewport } }, resetViewport(viewport: MapViewport = DEFAULT_GUANGMING_VIEWPORT) { - this.viewport = { ...viewport } + this.viewport = isUsableViewport(viewport) + ? { ...viewport } + : { ...DEFAULT_GUANGMING_VIEWPORT } }, showPlannedRoute() { this.plannedRouteVisible = true @@ -50,7 +55,9 @@ export const useMapStore = defineStore('map-session', { resetSession(viewport: MapViewport = DEFAULT_GUANGMING_VIEWPORT) { this.categoryCode = null this.selectedPoiId = null - this.viewport = { ...viewport } + this.viewport = isUsableViewport(viewport) + ? { ...viewport } + : { ...DEFAULT_GUANGMING_VIEWPORT } this.plannedRouteVisible = false }, }, diff --git a/test/map-store.test.ts b/test/map-store.test.ts index c8ab961..a18b274 100644 --- a/test/map-store.test.ts +++ b/test/map-store.test.ts @@ -53,4 +53,38 @@ describe('map session store', () => { expect(store.updateLocation(181, 22.76, 10)).toBe(false) expect(store.snapshot).toBeNull() }) + + // 定位接口降级 / 地图 SDK 还没拿到定位时会回传 (0, 0), + // 它落在几内亚湾,会把地图甩到非洲外海并触发境外底图。 + it('rejects null-island coordinates for the map viewport', () => { + const store = useMapStore() + store.updateViewport({ longitude: 0, latitude: 0, scale: 15 }) + expect(store.viewport).toEqual(DEFAULT_GUANGMING_VIEWPORT) + + store.updateViewport({ longitude: 113.94, latitude: 0, scale: 15 }) + expect(store.viewport).toEqual(DEFAULT_GUANGMING_VIEWPORT) + + store.updateViewport({ longitude: 0, latitude: 22.76, scale: 15 }) + expect(store.viewport).toEqual(DEFAULT_GUANGMING_VIEWPORT) + }) + + it('rejects null-island coordinates for the user location', () => { + const store = useLocationStore() + expect(store.updateLocation(0, 0, 10)).toBe(false) + expect(store.updateLocation(113.94, 0, 10)).toBe(false) + expect(store.updateLocation(0, 22.76, 10)).toBe(false) + expect(store.snapshot).toBeNull() + expect(store.status).toBe('idle') + }) + + it('falls back to the default viewport when reset is given an unusable one', () => { + const store = useMapStore() + store.updateViewport({ longitude: 113.94, latitude: 22.76, scale: 13 }) + + store.resetViewport({ longitude: 0, latitude: 0, scale: 11 }) + expect(store.viewport).toEqual(DEFAULT_GUANGMING_VIEWPORT) + + store.resetSession({ longitude: 0, latitude: 0, scale: 11 }) + expect(store.viewport).toEqual(DEFAULT_GUANGMING_VIEWPORT) + }) }) diff --git a/uni-pages.d.ts b/uni-pages.d.ts index d10c805..2c0f814 100644 --- a/uni-pages.d.ts +++ b/uni-pages.d.ts @@ -10,6 +10,7 @@ interface NavigateToOptions { "/pages/check-in/records" | "/pages/itinerary/index" | "/pages/planner/index" | + "/pages/poi/detail" | "/pages-poi/detail"; } interface RedirectToOptions extends NavigateToOptions {}