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 {}