修复 H5 来馆第三方导航构建边界
Some checks failed
CI / verify (push) Has been cancelled

This commit is contained in:
lyf
2026-07-12 00:30:18 +08:00
parent e99d713dd0
commit e006333c0a
13 changed files with 560 additions and 523 deletions

View File

@@ -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>

View File

@@ -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
View File

@@ -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'

View File

@@ -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",

View File

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