This commit is contained in:
@@ -77,6 +77,7 @@
|
||||
</template>
|
||||
</view>
|
||||
|
||||
<!-- #ifndef MP-WEIXIN -->
|
||||
<view
|
||||
v-if="providerSheetVisible && selectedTarget"
|
||||
class="provider-scrim"
|
||||
@@ -109,6 +110,24 @@
|
||||
<text class="provider-action">打开</text>
|
||||
</view>
|
||||
</view>
|
||||
<!-- #endif -->
|
||||
|
||||
<!-- #ifdef H5 -->
|
||||
<view
|
||||
v-if="buildDiagnostics"
|
||||
class="arrival-build-diagnostics"
|
||||
data-testid="arrival-build-diagnostics"
|
||||
>
|
||||
<text class="arrival-build-diagnostics-title">H5 构建诊断</text>
|
||||
<text class="arrival-build-diagnostics-line">构建 ID:{{ buildDiagnostics.buildId }}</text>
|
||||
<text class="arrival-build-diagnostics-line">Git commit:{{ buildDiagnostics.gitCommit }}</text>
|
||||
<text class="arrival-build-diagnostics-line">构建平台:{{ buildDiagnostics.platform }}</text>
|
||||
<text class="arrival-build-diagnostics-line">navigationMode:{{ buildDiagnostics.navigationMode }}</text>
|
||||
<text class="arrival-build-diagnostics-line">页面 chunk:{{ buildDiagnostics.chunkFileName }}</text>
|
||||
<text class="arrival-build-diagnostics-line">location.href:{{ buildDiagnostics.href }}</text>
|
||||
<text class="arrival-build-diagnostics-line">UA:{{ buildDiagnostics.userAgent }}</text>
|
||||
</view>
|
||||
<!-- #endif -->
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
@@ -118,17 +137,19 @@ import {
|
||||
type ArrivalSearchTarget,
|
||||
type ArrivalTargetType
|
||||
} from '@/data/arrivalSearchTargets'
|
||||
// #ifndef MP-WEIXIN
|
||||
import {
|
||||
openThirdPartyMapSearch,
|
||||
openWechatMiniProgramLocation,
|
||||
THIRD_PARTY_MAP_PROVIDERS,
|
||||
type ThirdPartyMapProviderOption
|
||||
} from '@/services/ThirdPartyMapSearchService'
|
||||
import { isNativeWechatMiniProgram } from '@/utils/hostEnvironment'
|
||||
import {
|
||||
getArrivalNavigationActionText,
|
||||
resolveArrivalNavigationMode
|
||||
} from '@/components/navigation/arrivalNavigationPolicy'
|
||||
// #endif
|
||||
// #ifdef MP-WEIXIN
|
||||
import { openWechatHostLocation } from '@/services/WechatOpenLocationService'
|
||||
import { validateWechatOpenLocationTarget } from '@/utils/wechatOpenLocationProtocol'
|
||||
// #endif
|
||||
|
||||
type ArrivalNavigationMode = 'third-party-providers' | 'native-location'
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
visible?: boolean
|
||||
@@ -151,17 +172,64 @@ const emit = defineEmits<{
|
||||
}>()
|
||||
|
||||
const typeOptions = computed(() => ARRIVAL_TARGET_TYPES)
|
||||
// #ifndef MP-WEIXIN
|
||||
const mapProviders = computed(() => THIRD_PARTY_MAP_PROVIDERS)
|
||||
const navigationMode = resolveArrivalNavigationMode(isNativeWechatMiniProgram())
|
||||
const primaryActionText = computed(() => (
|
||||
getArrivalNavigationActionText(navigationMode, isOpeningNavigation.value)
|
||||
))
|
||||
// #endif
|
||||
const navigationMode: ArrivalNavigationMode = __APP_BUILD_PLATFORM__ === 'mp-weixin'
|
||||
? 'native-location'
|
||||
: 'third-party-providers'
|
||||
const arrivalPanelRef = ref<unknown>(null)
|
||||
const isCollapsed = ref(false)
|
||||
const providerSheetVisible = ref(false)
|
||||
const isOpeningNavigation = ref(false)
|
||||
let resizeObserver: ResizeObserver | null = null
|
||||
|
||||
const primaryActionText = computed(() => {
|
||||
// #ifdef MP-WEIXIN
|
||||
if (navigationMode === 'native-location') {
|
||||
return isOpeningNavigation.value ? '正在打开...' : '打开地图导航'
|
||||
}
|
||||
// #endif
|
||||
|
||||
return '第三方导航'
|
||||
})
|
||||
|
||||
// #ifdef H5
|
||||
const hasDebugBuildFlag = () => {
|
||||
if (typeof window === 'undefined') return false
|
||||
|
||||
const searchParams = new URLSearchParams(window.location.search)
|
||||
if (searchParams.get('debug-build') === '1') return true
|
||||
|
||||
const hash = window.location.hash || ''
|
||||
const hashQueryStart = hash.indexOf('?')
|
||||
if (hashQueryStart < 0) return false
|
||||
|
||||
return new URLSearchParams(hash.slice(hashQueryStart + 1)).get('debug-build') === '1'
|
||||
}
|
||||
|
||||
const resolveCurrentChunkFileName = () => {
|
||||
try {
|
||||
const pathname = new URL(import.meta.url).pathname
|
||||
return decodeURIComponent(pathname.split('/').pop() || pathname)
|
||||
} catch {
|
||||
return import.meta.url
|
||||
}
|
||||
}
|
||||
|
||||
const buildDiagnostics = hasDebugBuildFlag()
|
||||
? {
|
||||
buildId: __APP_BUILD_ID__,
|
||||
gitCommit: __APP_GIT_COMMIT__,
|
||||
platform: __APP_BUILD_PLATFORM__,
|
||||
navigationMode,
|
||||
chunkFileName: resolveCurrentChunkFileName(),
|
||||
href: window.location.href,
|
||||
userAgent: window.navigator.userAgent
|
||||
}
|
||||
: null
|
||||
// #endif
|
||||
|
||||
const activeTypeLabel = computed(() => (
|
||||
typeOptions.value.find((option) => option.type === props.activeType)?.label || '来馆'
|
||||
))
|
||||
@@ -246,7 +314,8 @@ const startLayoutObserver = () => {
|
||||
|
||||
const handleNavigateTap = async () => {
|
||||
if (isOpeningNavigation.value) return
|
||||
if (!props.selectedTarget) {
|
||||
const selectedTarget = props.selectedTarget
|
||||
if (!selectedTarget) {
|
||||
uni.showToast({
|
||||
title: '请先选择点位',
|
||||
icon: 'none'
|
||||
@@ -254,26 +323,44 @@ const handleNavigateTap = async () => {
|
||||
return
|
||||
}
|
||||
|
||||
// H5(含小程序 web-view)统一由用户选择第三方地图。
|
||||
if (navigationMode === 'third-party-providers') {
|
||||
// #ifndef MP-WEIXIN
|
||||
if (__APP_BUILD_PLATFORM__ !== 'mp-weixin') {
|
||||
providerSheetVisible.value = true
|
||||
return
|
||||
}
|
||||
// #endif
|
||||
|
||||
const target = {
|
||||
latitude: props.selectedTarget.latitude,
|
||||
longitude: props.selectedTarget.longitude,
|
||||
name: props.selectedTarget.title,
|
||||
address: props.selectedTarget.subtitle
|
||||
// #ifdef MP-WEIXIN
|
||||
const validation = validateWechatOpenLocationTarget({
|
||||
latitude: selectedTarget.latitude,
|
||||
longitude: selectedTarget.longitude,
|
||||
name: selectedTarget.title,
|
||||
address: selectedTarget.subtitle
|
||||
})
|
||||
if (!validation.ok) {
|
||||
uni.showToast({
|
||||
title: validation.message,
|
||||
icon: 'none'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
isOpeningNavigation.value = true
|
||||
try {
|
||||
await openWechatMiniProgramLocation(target)
|
||||
const result = await openWechatHostLocation(validation.value)
|
||||
if (result.status !== 'opened') {
|
||||
uni.showToast({
|
||||
title: result.reason === 'timeout' ? '微信地图响应超时' : '无法打开微信地图',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
} finally {
|
||||
isOpeningNavigation.value = false
|
||||
}
|
||||
// #endif
|
||||
}
|
||||
|
||||
// #ifndef MP-WEIXIN
|
||||
const handleProviderSelect = (provider: ThirdPartyMapProviderOption) => {
|
||||
const target = props.selectedTarget
|
||||
if (!target) return
|
||||
@@ -283,6 +370,7 @@ const handleProviderSelect = (provider: ThirdPartyMapProviderOption) => {
|
||||
region: target.region
|
||||
})
|
||||
}
|
||||
// #endif
|
||||
|
||||
watch(
|
||||
() => props.visible,
|
||||
@@ -722,4 +810,36 @@ defineExpose({
|
||||
font-weight: 600;
|
||||
color: #696962;
|
||||
}
|
||||
|
||||
.arrival-build-diagnostics {
|
||||
position: fixed;
|
||||
top: calc(env(safe-area-inset-top) + 8px);
|
||||
left: 8px;
|
||||
right: 8px;
|
||||
z-index: 9999;
|
||||
max-height: 42vh;
|
||||
padding: 10px 12px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 3px;
|
||||
overflow: auto;
|
||||
box-sizing: border-box;
|
||||
border: 1px solid rgba(224, 225, 0, 0.75);
|
||||
border-radius: 6px;
|
||||
background: rgba(20, 22, 19, 0.94);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.arrival-build-diagnostics-title,
|
||||
.arrival-build-diagnostics-line {
|
||||
overflow-wrap: anywhere;
|
||||
font-size: 11px;
|
||||
line-height: 16px;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.arrival-build-diagnostics-title {
|
||||
font-weight: 700;
|
||||
color: #e0e100;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
export type ArrivalNavigationMode = 'third-party-providers' | 'native-location'
|
||||
|
||||
export const resolveArrivalNavigationMode = (
|
||||
isNativeWechatMiniProgram: boolean
|
||||
): ArrivalNavigationMode => (
|
||||
isNativeWechatMiniProgram ? 'native-location' : 'third-party-providers'
|
||||
)
|
||||
|
||||
export const getArrivalNavigationActionText = (
|
||||
mode: ArrivalNavigationMode,
|
||||
isOpening: boolean
|
||||
) => {
|
||||
if (isOpening) return '正在打开...'
|
||||
return mode === 'native-location' ? '打开地图导航' : '第三方导航'
|
||||
}
|
||||
6
src/env.d.ts
vendored
6
src/env.d.ts
vendored
@@ -1,7 +1,13 @@
|
||||
/// <reference types="vite/client" />
|
||||
|
||||
declare const __APP_BUILD_ID__: string
|
||||
declare const __APP_GIT_COMMIT__: string
|
||||
declare const __APP_BUILD_PLATFORM__: 'h5' | 'mp-weixin'
|
||||
|
||||
interface ImportMetaEnv {
|
||||
readonly VITE_APP_PUBLIC_BASE?: string
|
||||
readonly VITE_APP_BUILD_ID?: string
|
||||
readonly VITE_APP_GIT_COMMIT?: string
|
||||
readonly VITE_GUIDE_DATA_SOURCE_MODE?: 'static' | 'api' | 'sdk'
|
||||
readonly VITE_EXPLAIN_CONTENT_SOURCE_MODE?: 'static' | 'remote' | 'mock'
|
||||
readonly VITE_DATA_SOURCE_MODE?: 'static' | 'api' | 'sdk'
|
||||
|
||||
@@ -69,7 +69,9 @@
|
||||
"navigationBarBackgroundColor": "#FFFFFF",
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
},
|
||||
}
|
||||
// #ifdef MP-WEIXIN
|
||||
,
|
||||
{
|
||||
"path": "pages/open-location/index",
|
||||
"style": {
|
||||
@@ -77,6 +79,7 @@
|
||||
"navigationBarBackgroundColor": "#FFFFFF"
|
||||
}
|
||||
}
|
||||
// #endif
|
||||
],
|
||||
"globalStyle": {
|
||||
"navigationBarTextStyle": "black",
|
||||
|
||||
@@ -1,14 +1,3 @@
|
||||
import { openExternalNavigation } from '@/services/MapNavigationService'
|
||||
import {
|
||||
isEmbeddedInWechatMiniProgram,
|
||||
isNativeWechatMiniProgram
|
||||
} from '@/utils/hostEnvironment'
|
||||
import {
|
||||
buildWechatOpenLocationPageUrl,
|
||||
validateWechatOpenLocationTarget,
|
||||
type WechatOpenLocationTarget
|
||||
} from '@/utils/wechatOpenLocationProtocol'
|
||||
|
||||
export type ThirdPartyMapProvider = 'amap' | 'baidu' | 'tencent'
|
||||
|
||||
export interface ThirdPartyMapSearchTarget {
|
||||
@@ -16,29 +5,6 @@ export interface ThirdPartyMapSearchTarget {
|
||||
region: string
|
||||
}
|
||||
|
||||
export type WechatMiniProgramLocationTarget = WechatOpenLocationTarget
|
||||
|
||||
interface WechatMiniProgramBridge {
|
||||
getEnv?: (callback: (result: { miniprogram?: boolean }) => void) => void
|
||||
navigateTo?: (options: {
|
||||
url: string
|
||||
success?: () => void
|
||||
fail?: (error: unknown) => void
|
||||
}) => void
|
||||
}
|
||||
|
||||
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
|
||||
label: string
|
||||
@@ -89,226 +55,3 @@ export const openThirdPartyMapSearch = (
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
|
||||
export const buildWechatMiniProgramOpenLocationUrl = (
|
||||
target: WechatMiniProgramLocationTarget
|
||||
) => buildWechatOpenLocationPageUrl(target)
|
||||
|
||||
const getWechatMiniProgramBridge = (): WechatMiniProgramBridge | null => {
|
||||
if (typeof window === 'undefined') return null
|
||||
|
||||
return (window as Window & {
|
||||
wx?: {
|
||||
miniProgram?: WechatMiniProgramBridge
|
||||
}
|
||||
}).wx?.miniProgram || null
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
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)
|
||||
} catch (error) {
|
||||
console.error('加载微信 JS SDK 失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const isWechatBrowser = () => (
|
||||
typeof window !== 'undefined'
|
||||
&& /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
|
||||
) => {
|
||||
if (isEmbeddedInWechatMiniProgram()) return Promise.resolve(true)
|
||||
const getEnv = miniProgram.getEnv
|
||||
if (!getEnv) return Promise.resolve(false)
|
||||
|
||||
return new Promise<boolean>((resolve) => {
|
||||
let settled = false
|
||||
const finish = (isMiniProgram: boolean) => {
|
||||
if (settled) return
|
||||
settled = true
|
||||
clearTimeout(timeout)
|
||||
resolve(isMiniProgram)
|
||||
}
|
||||
const timeout = setTimeout(() => finish(isEmbeddedInWechatMiniProgram()), GET_ENV_TIMEOUT)
|
||||
|
||||
try {
|
||||
getEnv((result) => finish(Boolean(result?.miniprogram) || isEmbeddedInWechatMiniProgram()))
|
||||
} catch (error) {
|
||||
console.error('识别小程序 web-view 环境失败:', error)
|
||||
// 保留超时重检,让延迟注入的宿主环境标识仍有机会生效。
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const navigateToMiniProgramLocationPage = (
|
||||
miniProgram: WechatMiniProgramBridge,
|
||||
target: WechatMiniProgramLocationTarget
|
||||
) => 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) {
|
||||
uni.showToast({
|
||||
title: '宿主地图桥接不可用',
|
||||
icon: 'none'
|
||||
})
|
||||
finish({ status: 'failed', reason: 'bridge-unavailable' })
|
||||
return
|
||||
}
|
||||
|
||||
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' })
|
||||
}
|
||||
})
|
||||
|
||||
const openWechatMiniProgramLocationOnce = async (
|
||||
target: WechatMiniProgramLocationTarget
|
||||
): Promise<WechatMiniProgramLocationOpenResult> => {
|
||||
const validation = validateWechatOpenLocationTarget(target)
|
||||
if (!validation.ok) {
|
||||
uni.showToast({
|
||||
title: validation.message,
|
||||
icon: 'none'
|
||||
})
|
||||
return { status: 'failed', reason: 'invalid-target' }
|
||||
}
|
||||
|
||||
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: '无法打开微信地图',
|
||||
icon: 'none'
|
||||
})
|
||||
return { status: 'failed', reason: 'navigation-failed' }
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
if (isEmbeddedInWechatMiniProgram()) {
|
||||
uni.showToast({
|
||||
title: '当前页面未连接宿主小程序',
|
||||
icon: 'none'
|
||||
})
|
||||
return { status: 'failed', reason: 'not-in-mini-program' }
|
||||
}
|
||||
return { status: 'not-embedded' }
|
||||
}
|
||||
|
||||
return navigateToMiniProgramLocationPage(miniProgram, validation.value)
|
||||
}
|
||||
|
||||
export const openWechatMiniProgramLocation = (
|
||||
target: WechatMiniProgramLocationTarget
|
||||
) => {
|
||||
if (miniProgramNavigationPromise) return miniProgramNavigationPromise
|
||||
|
||||
miniProgramNavigationPromise = openWechatMiniProgramLocationOnce(target)
|
||||
.finally(() => {
|
||||
miniProgramNavigationPromise = null
|
||||
})
|
||||
return miniProgramNavigationPromise
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user