import { isEmbeddedInWechatMiniProgram } from '@/utils/hostEnvironment' import { buildWechatOpenLocationPageUrl, validateWechatOpenLocationTarget, type WechatOpenLocationTarget } from '@/utils/wechatOpenLocationProtocol' interface MiniProgramBridge { getEnv?: (callback: (result: { miniprogram?: boolean }) => void) => void navigateTo?: (options: { url: string; success?: () => void; fail?: (error: { errMsg?: string }) => void }) => void navigateBack?: (options: { delta: number; success?: () => void; fail?: (error: { errMsg?: string }) => void }) => void postMessage?: (options: { data: { type: string } }) => void } export type WechatMiniProgramBridgeResult = | { status: 'opened' } | { status: 'not-embedded' } | { status: 'failed'; reason: 'invalid-target' | 'bridge-unavailable' | 'not-in-mini-program' | 'navigation-failed' | 'page-not-found' | 'timeout' } export type WechatMiniProgramReturnResult = | { status: 'returned' } | { status: 'not-embedded' } | { status: 'failed'; reason: 'bridge-unavailable' | 'not-in-mini-program' | 'navigation-failed' | 'timeout' } const SDK_URL = 'https://res.wx.qq.com/open/js/jweixin-1.3.2.js' export const MINI_PROGRAM_RETURN_MESSAGE = 'museum-guide:return' const BRIDGE_WAIT_TIMEOUT = 2500 const BRIDGE_POLL_INTERVAL = 50 const GET_ENV_TIMEOUT = 1500 const NAVIGATION_TIMEOUT = 3000 let pendingNavigation: Promise | null = null let pendingReturn: Promise | null = null let sdkLoadPromise: Promise | null = null const getBridge = (): MiniProgramBridge | null => { if (typeof window === 'undefined') return null return (window as Window & { wx?: { miniProgram?: MiniProgramBridge } }).wx?.miniProgram || null } const wait = (duration: number) => new Promise((resolve) => setTimeout(resolve, duration)) const loadWechatJsSdk = () => { if (typeof document === 'undefined' || getBridge()) return Promise.resolve() if (sdkLoadPromise) return sdkLoadPromise sdkLoadPromise = new Promise((resolve) => { const existing = document.querySelector('script[data-wechat-js-sdk="true"]') if (existing) { existing.addEventListener('load', () => resolve(), { once: true }) existing.addEventListener('error', () => resolve(), { once: true }) return } const script = document.createElement('script') script.src = SDK_URL script.async = true script.dataset.wechatJsSdk = 'true' script.onload = () => resolve() script.onerror = () => { script.remove(); resolve() } document.head.appendChild(script) }).finally(() => { sdkLoadPromise = null }) return sdkLoadPromise } const waitForBridge = async (): Promise => { const immediate = getBridge() if (immediate) return immediate void loadWechatJsSdk() const deadline = Date.now() + BRIDGE_WAIT_TIMEOUT while (Date.now() < deadline) { await wait(BRIDGE_POLL_INTERVAL) const bridge = getBridge() if (bridge) return bridge } return null } const isMiniProgramWebView = (bridge: MiniProgramBridge) => new Promise((resolve) => { if (!bridge.getEnv) return resolve(isEmbeddedInWechatMiniProgram()) let settled = false const finish = (value: boolean) => { if (settled) return settled = true clearTimeout(timeout) resolve(value) } const timeout = setTimeout(() => finish(isEmbeddedInWechatMiniProgram()), GET_ENV_TIMEOUT) try { bridge.getEnv((result) => finish(Boolean(result?.miniprogram))) } catch (error) { console.error('微信小程序环境识别失败:', error) finish(false) } }) const navigateToHostLocationPage = (bridge: MiniProgramBridge, target: WechatOpenLocationTarget) => new Promise((resolve) => { if (!bridge.navigateTo) return resolve({ status: 'failed', reason: 'bridge-unavailable' }) let settled = false const finish = (result: WechatMiniProgramBridgeResult) => { if (settled) return settled = true clearTimeout(timeout) resolve(result) } const timeout = setTimeout(() => finish({ status: 'failed', reason: 'timeout' }), NAVIGATION_TIMEOUT) try { bridge.navigateTo({ url: buildWechatOpenLocationPageUrl(target), success: () => finish({ status: 'opened' }), fail: (error) => { console.error('跳转宿主地图页失败:', error) finish({ status: 'failed', reason: /page.*not found|not found.*page/i.test(error?.errMsg || '') ? 'page-not-found' : 'navigation-failed' }) } }) } catch (error) { console.error('调用微信小程序导航桥接失败:', error) finish({ status: 'failed', reason: 'navigation-failed' }) } }) const requestHostReturnFallback = (bridge: MiniProgramBridge) => { if (!bridge.postMessage) return false try { bridge.postMessage({ data: { type: MINI_PROGRAM_RETURN_MESSAGE } }) return true } catch (error) { console.error('请求宿主小程序安全返回失败:', error) return false } } const navigateBackToHost = (bridge: MiniProgramBridge) => new Promise((resolve) => { if (!bridge.navigateBack) return resolve({ status: 'failed', reason: 'bridge-unavailable' }) let settled = false const finish = (result: WechatMiniProgramReturnResult) => { if (settled) return settled = true clearTimeout(timeout) resolve(result) } const timeout = setTimeout(() => finish({ status: 'failed', reason: 'timeout' }), NAVIGATION_TIMEOUT) try { bridge.navigateBack({ delta: 1, success: () => finish({ status: 'returned' }), fail: (error) => { console.error('返回宿主小程序失败:', error) finish({ status: 'failed', reason: 'navigation-failed' }) } }) } catch (error) { console.error('调用宿主小程序返回桥接失败:', error) finish({ status: 'failed', reason: 'navigation-failed' }) } }) const openOnce = async (target: WechatOpenLocationTarget): Promise => { const validation = validateWechatOpenLocationTarget(target) if (!validation.ok) return { status: 'failed', reason: 'invalid-target' } if (!isEmbeddedInWechatMiniProgram()) return { status: 'not-embedded' } const bridge = await waitForBridge() if (!bridge) return { status: 'failed', reason: 'bridge-unavailable' } if (!await isMiniProgramWebView(bridge)) return { status: 'failed', reason: 'not-in-mini-program' } return navigateToHostLocationPage(bridge, validation.value) } export const openWechatMiniProgramLocation = (target: WechatOpenLocationTarget) => { if (pendingNavigation) return pendingNavigation pendingNavigation = openOnce(target).finally(() => { pendingNavigation = null }) return pendingNavigation } const returnOnce = async (): Promise => { if (!isEmbeddedInWechatMiniProgram()) return { status: 'not-embedded' } const bridge = await waitForBridge() if (!bridge) return { status: 'failed', reason: 'bridge-unavailable' } if (!await isMiniProgramWebView(bridge)) return { status: 'failed', reason: 'not-in-mini-program' } const result = await navigateBackToHost(bridge) if (result.status === 'returned') return result // 宿主页可能由分享或冷启动直接打开,页面栈没有上一页时改由宿主执行安全回退。 if (requestHostReturnFallback(bridge)) return { status: 'returned' } return result } export const returnToWechatMiniProgram = () => { if (pendingReturn) return pendingReturn pendingReturn = returnOnce().finally(() => { pendingReturn = null }) return pendingReturn }