@@ -69,7 +69,7 @@
|
||||
|
||||
<view
|
||||
class="arrival-primary"
|
||||
:class="{ disabled: !selectedTarget }"
|
||||
:class="{ disabled: !selectedTarget || isOpeningNavigation }"
|
||||
@tap="handleNavigateTap"
|
||||
>
|
||||
<text class="arrival-primary-text">{{ primaryActionText }}</text>
|
||||
@@ -124,10 +124,6 @@ import {
|
||||
THIRD_PARTY_MAP_PROVIDERS,
|
||||
type ThirdPartyMapProviderOption
|
||||
} from '@/services/ThirdPartyMapSearchService'
|
||||
import {
|
||||
isNativeWechatMiniProgram,
|
||||
isWechatMiniProgramRuntime
|
||||
} from '@/utils/hostEnvironment'
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
visible?: boolean
|
||||
@@ -151,13 +147,13 @@ const emit = defineEmits<{
|
||||
|
||||
const typeOptions = computed(() => ARRIVAL_TARGET_TYPES)
|
||||
const mapProviders = computed(() => THIRD_PARTY_MAP_PROVIDERS)
|
||||
const shouldUseMiniProgramLocation = computed(() => isWechatMiniProgramRuntime())
|
||||
const primaryActionText = computed(() => (
|
||||
shouldUseMiniProgramLocation.value ? '打开地图导航' : '第三方导航'
|
||||
isOpeningNavigation.value ? '正在打开...' : '打开地图导航'
|
||||
))
|
||||
const arrivalPanelRef = ref<unknown>(null)
|
||||
const isCollapsed = ref(false)
|
||||
const providerSheetVisible = ref(false)
|
||||
const isOpeningNavigation = ref(false)
|
||||
let resizeObserver: ResizeObserver | null = null
|
||||
|
||||
const activeTypeLabel = computed(() => (
|
||||
@@ -243,6 +239,7 @@ const startLayoutObserver = () => {
|
||||
}
|
||||
|
||||
const handleNavigateTap = async () => {
|
||||
if (isOpeningNavigation.value) return
|
||||
if (!props.selectedTarget) {
|
||||
uni.showToast({
|
||||
title: '请先选择点位',
|
||||
@@ -257,14 +254,15 @@ const handleNavigateTap = async () => {
|
||||
name: props.selectedTarget.title,
|
||||
address: props.selectedTarget.subtitle
|
||||
}
|
||||
const isNativeMiniProgram = isNativeWechatMiniProgram()
|
||||
const openedInMiniProgram = await openWechatMiniProgramLocation(target)
|
||||
|
||||
if (openedInMiniProgram || isNativeMiniProgram || isWechatMiniProgramRuntime()) {
|
||||
return
|
||||
isOpeningNavigation.value = true
|
||||
try {
|
||||
const result = await openWechatMiniProgramLocation(target)
|
||||
if (result.status === 'not-embedded') {
|
||||
providerSheetVisible.value = true
|
||||
}
|
||||
} finally {
|
||||
isOpeningNavigation.value = false
|
||||
}
|
||||
|
||||
providerSheetVisible.value = true
|
||||
}
|
||||
|
||||
const handleProviderSelect = (provider: ThirdPartyMapProviderOption) => {
|
||||
|
||||
@@ -16,15 +16,13 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onLoad } from '@dcloudio/uni-app'
|
||||
import { onLoad, onUnload } from '@dcloudio/uni-app'
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
interface OpenLocationOptions {
|
||||
latitude?: string
|
||||
longitude?: string
|
||||
name?: string
|
||||
address?: string
|
||||
}
|
||||
import { openWechatHostLocation } from '@/services/WechatOpenLocationService'
|
||||
import {
|
||||
parseWechatOpenLocationPageOptions,
|
||||
type WechatOpenLocationPageOptions
|
||||
} from '@/utils/wechatOpenLocationProtocol'
|
||||
|
||||
const latitude = ref<number | null>(null)
|
||||
const longitude = ref<number | null>(null)
|
||||
@@ -32,6 +30,10 @@ const locationName = ref('')
|
||||
const locationAddress = ref('')
|
||||
const hasOpened = ref(false)
|
||||
const hasError = ref(false)
|
||||
const isOpening = ref(false)
|
||||
const errorMessage = ref('')
|
||||
let isPageActive = true
|
||||
let autoBackTimer: ReturnType<typeof setTimeout> | null = null
|
||||
|
||||
const title = computed(() => {
|
||||
if (hasError.value) return '地图打开失败'
|
||||
@@ -40,22 +42,18 @@ const title = computed(() => {
|
||||
})
|
||||
|
||||
const description = computed(() => {
|
||||
if (hasError.value) return '请返回导览后重试,或检查小程序位置权限。'
|
||||
if (hasError.value) return errorMessage.value || '请返回导览后重试,或检查小程序位置权限。'
|
||||
if (locationName.value) return locationName.value
|
||||
return '正在为你唤起地图位置页。'
|
||||
})
|
||||
|
||||
const safeDecode = (value?: string) => {
|
||||
if (!value) return ''
|
||||
|
||||
try {
|
||||
return decodeURIComponent(value)
|
||||
} catch {
|
||||
return value
|
||||
}
|
||||
}
|
||||
|
||||
const goBack = () => {
|
||||
if (!isPageActive) return
|
||||
isPageActive = false
|
||||
if (autoBackTimer) {
|
||||
clearTimeout(autoBackTimer)
|
||||
autoBackTimer = null
|
||||
}
|
||||
uni.navigateBack({
|
||||
delta: 1,
|
||||
fail: () => {
|
||||
@@ -66,9 +64,11 @@ const goBack = () => {
|
||||
})
|
||||
}
|
||||
|
||||
const openLocation = () => {
|
||||
const openLocation = async () => {
|
||||
if (isOpening.value) return
|
||||
if (latitude.value === null || longitude.value === null) {
|
||||
hasError.value = true
|
||||
errorMessage.value = '地图参数无效,请返回导览后重试。'
|
||||
uni.showToast({
|
||||
title: '缺少地图坐标',
|
||||
icon: 'none'
|
||||
@@ -76,42 +76,57 @@ const openLocation = () => {
|
||||
return
|
||||
}
|
||||
|
||||
uni.openLocation({
|
||||
isOpening.value = true
|
||||
const result = await openWechatHostLocation({
|
||||
latitude: latitude.value,
|
||||
longitude: longitude.value,
|
||||
name: locationName.value,
|
||||
address: locationAddress.value,
|
||||
scale: 18,
|
||||
success: () => {
|
||||
hasOpened.value = true
|
||||
hasError.value = false
|
||||
setTimeout(goBack, 300)
|
||||
},
|
||||
fail: (error) => {
|
||||
console.error('打开地图位置失败:', error)
|
||||
hasError.value = true
|
||||
uni.showToast({
|
||||
title: '无法打开地图',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
address: locationAddress.value
|
||||
})
|
||||
}
|
||||
if (!isPageActive) return
|
||||
isOpening.value = false
|
||||
|
||||
onLoad((options?: OpenLocationOptions) => {
|
||||
latitude.value = Number(options?.latitude)
|
||||
longitude.value = Number(options?.longitude)
|
||||
locationName.value = safeDecode(options?.name) || '深圳自然博物馆'
|
||||
locationAddress.value = safeDecode(options?.address)
|
||||
|
||||
if (!Number.isFinite(latitude.value) || !Number.isFinite(longitude.value)) {
|
||||
latitude.value = null
|
||||
longitude.value = null
|
||||
hasError.value = true
|
||||
if (result.status === 'opened') {
|
||||
hasOpened.value = true
|
||||
hasError.value = false
|
||||
autoBackTimer = setTimeout(goBack, 300)
|
||||
return
|
||||
}
|
||||
|
||||
openLocation()
|
||||
hasError.value = true
|
||||
errorMessage.value = result.reason === 'timeout'
|
||||
? '微信地图响应超时,请重试或返回导览。'
|
||||
: '微信地图能力调用失败,请检查权限后重试。'
|
||||
uni.showToast({
|
||||
title: result.reason === 'timeout' ? '微信地图响应超时' : '无法打开地图',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
|
||||
onLoad((options?: WechatOpenLocationPageOptions) => {
|
||||
isPageActive = true
|
||||
const result = parseWechatOpenLocationPageOptions(options)
|
||||
if (!result.ok) {
|
||||
hasError.value = true
|
||||
errorMessage.value = result.message
|
||||
uni.showToast({
|
||||
title: result.message,
|
||||
icon: 'none'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
latitude.value = result.value.latitude
|
||||
longitude.value = result.value.longitude
|
||||
locationName.value = result.value.name
|
||||
locationAddress.value = result.value.address || ''
|
||||
void openLocation()
|
||||
})
|
||||
|
||||
onUnload(() => {
|
||||
isPageActive = false
|
||||
if (autoBackTimer) clearTimeout(autoBackTimer)
|
||||
autoBackTimer = null
|
||||
})
|
||||
</script>
|
||||
|
||||
|
||||
@@ -3,6 +3,11 @@ import {
|
||||
isEmbeddedInWechatMiniProgram,
|
||||
isNativeWechatMiniProgram
|
||||
} from '@/utils/hostEnvironment'
|
||||
import {
|
||||
buildWechatOpenLocationPageUrl,
|
||||
validateWechatOpenLocationTarget,
|
||||
type WechatOpenLocationTarget
|
||||
} from '@/utils/wechatOpenLocationProtocol'
|
||||
|
||||
export type ThirdPartyMapProvider = 'amap' | 'baidu' | 'tencent'
|
||||
|
||||
@@ -11,12 +16,7 @@ export interface ThirdPartyMapSearchTarget {
|
||||
region: string
|
||||
}
|
||||
|
||||
export interface WechatMiniProgramLocationTarget {
|
||||
latitude: number
|
||||
longitude: number
|
||||
name: string
|
||||
address?: string
|
||||
}
|
||||
export type WechatMiniProgramLocationTarget = WechatOpenLocationTarget
|
||||
|
||||
interface WechatMiniProgramBridge {
|
||||
getEnv?: (callback: (result: { miniprogram?: boolean }) => void) => void
|
||||
@@ -27,7 +27,17 @@ interface WechatMiniProgramBridge {
|
||||
}) => void
|
||||
}
|
||||
|
||||
let wechatJsSdkLoadPromise: Promise<boolean> | null = null
|
||||
export type WechatMiniProgramLocationOpenResult =
|
||||
| { status: 'opened' }
|
||||
| { status: 'not-embedded' }
|
||||
| { status: 'failed'; reason: 'invalid-target' | 'bridge-unavailable' | 'not-in-mini-program' | 'navigation-failed' }
|
||||
|
||||
const BRIDGE_WAIT_TIMEOUT = 2500
|
||||
const BRIDGE_POLL_INTERVAL = 50
|
||||
const SDK_SCRIPT_LOAD_TIMEOUT = 10000
|
||||
const GET_ENV_TIMEOUT = 1500
|
||||
const NAVIGATION_TIMEOUT = 3000
|
||||
let miniProgramNavigationPromise: Promise<WechatMiniProgramLocationOpenResult> | null = null
|
||||
|
||||
export interface ThirdPartyMapProviderOption {
|
||||
provider: ThirdPartyMapProvider
|
||||
@@ -82,18 +92,7 @@ export const openThirdPartyMapSearch = (
|
||||
|
||||
export const buildWechatMiniProgramOpenLocationUrl = (
|
||||
target: WechatMiniProgramLocationTarget
|
||||
) => {
|
||||
const params = [
|
||||
['latitude', String(target.latitude)],
|
||||
['longitude', String(target.longitude)],
|
||||
['name', target.name],
|
||||
['address', target.address || '']
|
||||
]
|
||||
.map(([key, value]) => `${key}=${encode(value)}`)
|
||||
.join('&')
|
||||
|
||||
return `/pages/open-location/index?${params}`
|
||||
}
|
||||
) => buildWechatOpenLocationPageUrl(target)
|
||||
|
||||
const getWechatMiniProgramBridge = (): WechatMiniProgramBridge | null => {
|
||||
if (typeof window === 'undefined') return null
|
||||
@@ -105,29 +104,35 @@ const getWechatMiniProgramBridge = (): WechatMiniProgramBridge | null => {
|
||||
}).wx?.miniProgram || null
|
||||
}
|
||||
|
||||
const loadWechatJsSdk = () => {
|
||||
if (typeof document === 'undefined') return Promise.resolve(false)
|
||||
if (getWechatMiniProgramBridge()) return Promise.resolve(true)
|
||||
if (wechatJsSdkLoadPromise) return wechatJsSdkLoadPromise
|
||||
const ensureWechatJsSdkScript = () => {
|
||||
if (typeof document === 'undefined' || getWechatMiniProgramBridge()) return
|
||||
const existingScript = document.querySelector<HTMLScriptElement>('script[data-wechat-js-sdk="true"]')
|
||||
if (existingScript) {
|
||||
const startedAt = Number(existingScript.dataset.wechatJsSdkStartedAt || 0)
|
||||
const isExpiredLoading = existingScript.dataset.wechatJsSdkState === 'loading'
|
||||
&& Date.now() - startedAt > SDK_SCRIPT_LOAD_TIMEOUT
|
||||
if (existingScript.dataset.wechatJsSdkState !== 'error' && !isExpiredLoading) return
|
||||
existingScript.remove()
|
||||
}
|
||||
|
||||
wechatJsSdkLoadPromise = new Promise<boolean>((resolve) => {
|
||||
const existingScript = document.querySelector<HTMLScriptElement>('script[data-wechat-js-sdk="true"]')
|
||||
if (existingScript) {
|
||||
existingScript.addEventListener('load', () => resolve(Boolean(getWechatMiniProgramBridge())), { once: true })
|
||||
existingScript.addEventListener('error', () => resolve(false), { once: true })
|
||||
return
|
||||
}
|
||||
|
||||
const script = document.createElement('script')
|
||||
script.src = 'https://res.wx.qq.com/open/js/jweixin-1.6.0.js'
|
||||
script.async = true
|
||||
script.dataset.wechatJsSdk = 'true'
|
||||
script.onload = () => resolve(Boolean(getWechatMiniProgramBridge()))
|
||||
script.onerror = () => resolve(false)
|
||||
const script = document.createElement('script')
|
||||
script.src = 'https://res.wx.qq.com/open/js/jweixin-1.6.0.js'
|
||||
script.async = true
|
||||
script.dataset.wechatJsSdk = 'true'
|
||||
script.dataset.wechatJsSdkState = 'loading'
|
||||
script.dataset.wechatJsSdkStartedAt = String(Date.now())
|
||||
script.onload = () => {
|
||||
script.dataset.wechatJsSdkState = 'loaded'
|
||||
}
|
||||
script.onerror = () => {
|
||||
script.dataset.wechatJsSdkState = 'error'
|
||||
script.remove()
|
||||
}
|
||||
try {
|
||||
document.head.appendChild(script)
|
||||
})
|
||||
|
||||
return wechatJsSdkLoadPromise
|
||||
} catch (error) {
|
||||
console.error('加载微信 JS SDK 失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const isWechatBrowser = () => (
|
||||
@@ -135,6 +140,25 @@ const isWechatBrowser = () => (
|
||||
&& /MicroMessenger/i.test(window.navigator?.userAgent || '')
|
||||
)
|
||||
|
||||
const wait = (duration: number) => new Promise<void>((resolve) => {
|
||||
setTimeout(resolve, duration)
|
||||
})
|
||||
|
||||
const waitForWechatMiniProgramBridge = async () => {
|
||||
const immediateBridge = getWechatMiniProgramBridge()
|
||||
if (immediateBridge) return immediateBridge
|
||||
|
||||
ensureWechatJsSdkScript()
|
||||
const startedAt = Date.now()
|
||||
while (Date.now() - startedAt < BRIDGE_WAIT_TIMEOUT) {
|
||||
await wait(BRIDGE_POLL_INTERVAL)
|
||||
const bridge = getWechatMiniProgramBridge()
|
||||
if (bridge) return bridge
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
const resolveMiniProgramWebViewEnvironment = (
|
||||
miniProgram: WechatMiniProgramBridge
|
||||
) => {
|
||||
@@ -150,13 +174,13 @@ const resolveMiniProgramWebViewEnvironment = (
|
||||
clearTimeout(timeout)
|
||||
resolve(isMiniProgram)
|
||||
}
|
||||
const timeout = setTimeout(() => finish(false), 1500)
|
||||
const timeout = setTimeout(() => finish(isEmbeddedInWechatMiniProgram()), GET_ENV_TIMEOUT)
|
||||
|
||||
try {
|
||||
getEnv((result) => finish(Boolean(result.miniprogram)))
|
||||
getEnv((result) => finish(Boolean(result?.miniprogram) || isEmbeddedInWechatMiniProgram()))
|
||||
} catch (error) {
|
||||
console.error('识别小程序 web-view 环境失败:', error)
|
||||
finish(false)
|
||||
// 保留超时重检,让延迟注入的宿主环境标识仍有机会生效。
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -164,50 +188,127 @@ const resolveMiniProgramWebViewEnvironment = (
|
||||
const navigateToMiniProgramLocationPage = (
|
||||
miniProgram: WechatMiniProgramBridge,
|
||||
target: WechatMiniProgramLocationTarget
|
||||
) => new Promise<boolean>((resolve) => {
|
||||
) => new Promise<WechatMiniProgramLocationOpenResult>((resolve) => {
|
||||
let settled = false
|
||||
const finish = (result: WechatMiniProgramLocationOpenResult) => {
|
||||
if (settled) return
|
||||
settled = true
|
||||
clearTimeout(timeout)
|
||||
resolve(result)
|
||||
}
|
||||
const timeout = setTimeout(() => {
|
||||
uni.showToast({
|
||||
title: '宿主地图页响应超时',
|
||||
icon: 'none'
|
||||
})
|
||||
finish({ status: 'failed', reason: 'navigation-failed' })
|
||||
}, NAVIGATION_TIMEOUT)
|
||||
|
||||
if (!miniProgram.navigateTo) {
|
||||
resolve(false)
|
||||
uni.showToast({
|
||||
title: '宿主地图桥接不可用',
|
||||
icon: 'none'
|
||||
})
|
||||
finish({ status: 'failed', reason: 'bridge-unavailable' })
|
||||
return
|
||||
}
|
||||
|
||||
miniProgram.navigateTo({
|
||||
url: buildWechatMiniProgramOpenLocationUrl(target),
|
||||
success: () => resolve(true),
|
||||
fail: (error) => {
|
||||
console.error('跳转小程序地图页失败:', error)
|
||||
uni.showToast({
|
||||
title: '无法打开地图导航',
|
||||
icon: 'none'
|
||||
})
|
||||
resolve(false)
|
||||
}
|
||||
})
|
||||
try {
|
||||
miniProgram.navigateTo({
|
||||
url: buildWechatMiniProgramOpenLocationUrl(target),
|
||||
success: () => finish({ status: 'opened' }),
|
||||
fail: (error) => {
|
||||
if (settled) return
|
||||
console.error('跳转小程序地图页失败:', error)
|
||||
const errorMessage = typeof error === 'object' && error && 'errMsg' in error
|
||||
? String(error.errMsg)
|
||||
: ''
|
||||
uni.showToast({
|
||||
title: /page.*not found|not found.*page/i.test(errorMessage)
|
||||
? '宿主小程序未配置地图页'
|
||||
: '宿主地图页跳转失败',
|
||||
icon: 'none'
|
||||
})
|
||||
finish({ status: 'failed', reason: 'navigation-failed' })
|
||||
}
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('调用小程序地图桥接失败:', error)
|
||||
uni.showToast({
|
||||
title: '宿主地图桥接调用失败',
|
||||
icon: 'none'
|
||||
})
|
||||
finish({ status: 'failed', reason: 'navigation-failed' })
|
||||
}
|
||||
})
|
||||
|
||||
export const openWechatMiniProgramLocation = async (
|
||||
const openWechatMiniProgramLocationOnce = async (
|
||||
target: WechatMiniProgramLocationTarget
|
||||
) => {
|
||||
if (isNativeWechatMiniProgram()) {
|
||||
return openExternalNavigation(target)
|
||||
): Promise<WechatMiniProgramLocationOpenResult> => {
|
||||
const validation = validateWechatOpenLocationTarget(target)
|
||||
if (!validation.ok) {
|
||||
uni.showToast({
|
||||
title: validation.message,
|
||||
icon: 'none'
|
||||
})
|
||||
return { status: 'failed', reason: 'invalid-target' }
|
||||
}
|
||||
|
||||
const synchronouslyEmbedded = isEmbeddedInWechatMiniProgram()
|
||||
if (!synchronouslyEmbedded && !isWechatBrowser()) return false
|
||||
|
||||
await loadWechatJsSdk()
|
||||
const miniProgram = getWechatMiniProgramBridge()
|
||||
if (!miniProgram?.navigateTo) {
|
||||
if (synchronouslyEmbedded) {
|
||||
if (isNativeWechatMiniProgram()) {
|
||||
try {
|
||||
const opened = await openExternalNavigation(validation.value)
|
||||
return opened
|
||||
? { status: 'opened' }
|
||||
: { status: 'failed', reason: 'navigation-failed' }
|
||||
} catch (error) {
|
||||
console.error('原生小程序地图能力调用失败:', error)
|
||||
uni.showToast({
|
||||
title: '请在小程序内打开地图',
|
||||
title: '无法打开微信地图',
|
||||
icon: 'none'
|
||||
})
|
||||
return { status: 'failed', reason: 'navigation-failed' }
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
if (!isEmbeddedInWechatMiniProgram() && !isWechatBrowser()) {
|
||||
return { status: 'not-embedded' }
|
||||
}
|
||||
|
||||
const miniProgram = await waitForWechatMiniProgramBridge()
|
||||
if (!miniProgram?.navigateTo) {
|
||||
if (isEmbeddedInWechatMiniProgram()) {
|
||||
uni.showToast({
|
||||
title: '小程序地图桥接加载失败',
|
||||
icon: 'none'
|
||||
})
|
||||
return { status: 'failed', reason: 'bridge-unavailable' }
|
||||
}
|
||||
return { status: 'not-embedded' }
|
||||
}
|
||||
|
||||
const isMiniProgramWebView = await resolveMiniProgramWebViewEnvironment(miniProgram)
|
||||
if (!isMiniProgramWebView) return false
|
||||
if (!isMiniProgramWebView) {
|
||||
if (isEmbeddedInWechatMiniProgram()) {
|
||||
uni.showToast({
|
||||
title: '当前页面未连接宿主小程序',
|
||||
icon: 'none'
|
||||
})
|
||||
return { status: 'failed', reason: 'not-in-mini-program' }
|
||||
}
|
||||
return { status: 'not-embedded' }
|
||||
}
|
||||
|
||||
return navigateToMiniProgramLocationPage(miniProgram, target)
|
||||
return navigateToMiniProgramLocationPage(miniProgram, validation.value)
|
||||
}
|
||||
|
||||
export const openWechatMiniProgramLocation = (
|
||||
target: WechatMiniProgramLocationTarget
|
||||
) => {
|
||||
if (miniProgramNavigationPromise) return miniProgramNavigationPromise
|
||||
|
||||
miniProgramNavigationPromise = openWechatMiniProgramLocationOnce(target)
|
||||
.finally(() => {
|
||||
miniProgramNavigationPromise = null
|
||||
})
|
||||
return miniProgramNavigationPromise
|
||||
}
|
||||
|
||||
40
src/services/WechatOpenLocationService.ts
Normal file
40
src/services/WechatOpenLocationService.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import type { WechatOpenLocationTarget } from '@/utils/wechatOpenLocationProtocol'
|
||||
|
||||
export type WechatOpenLocationResult =
|
||||
| { status: 'opened' }
|
||||
| { status: 'failed'; reason: 'api-failed' | 'timeout' }
|
||||
|
||||
const OPEN_LOCATION_TIMEOUT = 5000
|
||||
|
||||
export const openWechatHostLocation = (
|
||||
target: WechatOpenLocationTarget
|
||||
) => new Promise<WechatOpenLocationResult>((resolve) => {
|
||||
let settled = false
|
||||
const finish = (result: WechatOpenLocationResult) => {
|
||||
if (settled) return
|
||||
settled = true
|
||||
clearTimeout(timeout)
|
||||
resolve(result)
|
||||
}
|
||||
const timeout = setTimeout(() => {
|
||||
finish({ status: 'failed', reason: 'timeout' })
|
||||
}, OPEN_LOCATION_TIMEOUT)
|
||||
|
||||
try {
|
||||
uni.openLocation({
|
||||
latitude: target.latitude,
|
||||
longitude: target.longitude,
|
||||
name: target.name,
|
||||
address: target.address || '',
|
||||
scale: 18,
|
||||
success: () => finish({ status: 'opened' }),
|
||||
fail: (error) => {
|
||||
console.error('打开地图位置失败:', error)
|
||||
finish({ status: 'failed', reason: 'api-failed' })
|
||||
}
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('调用微信地图能力失败:', error)
|
||||
finish({ status: 'failed', reason: 'api-failed' })
|
||||
}
|
||||
})
|
||||
93
src/utils/wechatOpenLocationProtocol.ts
Normal file
93
src/utils/wechatOpenLocationProtocol.ts
Normal file
@@ -0,0 +1,93 @@
|
||||
export const WECHAT_OPEN_LOCATION_PAGE_PATH = '/pages/open-location/index'
|
||||
|
||||
export interface WechatOpenLocationTarget {
|
||||
latitude: number
|
||||
longitude: number
|
||||
name: string
|
||||
address?: string
|
||||
}
|
||||
|
||||
export interface WechatOpenLocationPageOptions {
|
||||
latitude?: unknown
|
||||
longitude?: unknown
|
||||
name?: unknown
|
||||
address?: unknown
|
||||
}
|
||||
|
||||
export type WechatOpenLocationValidationResult =
|
||||
| { ok: true; value: WechatOpenLocationTarget }
|
||||
| { ok: false; message: string }
|
||||
|
||||
const MAX_NAME_LENGTH = 100
|
||||
const MAX_ADDRESS_LENGTH = 300
|
||||
|
||||
const firstValue = (value: unknown) => (
|
||||
Array.isArray(value) ? value[0] : value
|
||||
)
|
||||
|
||||
const normalizeText = (value: unknown) => {
|
||||
const first = firstValue(value)
|
||||
return typeof first === 'string' ? first.trim() : ''
|
||||
}
|
||||
|
||||
const normalizeCoordinate = (value: unknown) => {
|
||||
const first = firstValue(value)
|
||||
if (typeof first === 'string' && !first.trim()) return null
|
||||
if (typeof first !== 'string' && typeof first !== 'number') return null
|
||||
|
||||
const coordinate = Number(first)
|
||||
return Number.isFinite(coordinate) ? coordinate : null
|
||||
}
|
||||
|
||||
export const validateWechatOpenLocationTarget = (
|
||||
target: WechatOpenLocationPageOptions
|
||||
): WechatOpenLocationValidationResult => {
|
||||
const latitude = normalizeCoordinate(target.latitude)
|
||||
const longitude = normalizeCoordinate(target.longitude)
|
||||
const name = normalizeText(target.name)
|
||||
const address = normalizeText(target.address)
|
||||
|
||||
if (latitude === null || longitude === null) {
|
||||
return { ok: false, message: '地图坐标缺失或格式错误' }
|
||||
}
|
||||
if (latitude < -90 || latitude > 90 || longitude < -180 || longitude > 180) {
|
||||
return { ok: false, message: '地图坐标超出有效范围' }
|
||||
}
|
||||
if (!name) {
|
||||
return { ok: false, message: '目的地名称不能为空' }
|
||||
}
|
||||
if (name.length > MAX_NAME_LENGTH || address.length > MAX_ADDRESS_LENGTH) {
|
||||
return { ok: false, message: '目的地名称或地址过长' }
|
||||
}
|
||||
|
||||
return {
|
||||
ok: true,
|
||||
value: {
|
||||
latitude,
|
||||
longitude,
|
||||
name,
|
||||
address
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const buildWechatOpenLocationPageUrl = (
|
||||
target: WechatOpenLocationPageOptions
|
||||
) => {
|
||||
const result = validateWechatOpenLocationTarget(target)
|
||||
if (!result.ok) throw new TypeError(result.message)
|
||||
|
||||
const params = new URLSearchParams()
|
||||
params.set('latitude', String(result.value.latitude))
|
||||
params.set('longitude', String(result.value.longitude))
|
||||
params.set('name', result.value.name)
|
||||
params.set('address', result.value.address || '')
|
||||
|
||||
// 微信页面路由对加号的解码行为不统一,空格固定使用百分号编码。
|
||||
const query = params.toString().replace(/\+/g, '%20')
|
||||
return `${WECHAT_OPEN_LOCATION_PAGE_PATH}?${query}`
|
||||
}
|
||||
|
||||
export const parseWechatOpenLocationPageOptions = (
|
||||
options: WechatOpenLocationPageOptions = {}
|
||||
) => validateWechatOpenLocationTarget(options)
|
||||
Reference in New Issue
Block a user