| | |
| | | <template> |
| | | <view class="weizan-live-container"> |
| | | <!-- 子 webview 由 plus.webview.create 命令式创建并浮在该 view 之上,无需在 template 中渲染 --> |
| | | <view class="container"> |
| | | <web-view v-if="webViewUrl" class="webview-content" :src="webViewUrl"></web-view> |
| | | <view v-else class="empty-box"><text class="empty-text">链接地址无效</text></view> |
| | | </view> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { ref } from 'vue' |
| | | import { onLoad, onUnload, onShow, onHide } from '@dcloudio/uni-app' |
| | | import { onLoad, onUnload } from '@dcloudio/uni-app' |
| | | import { clearWebViewData } from '@/utils/webview-cache.js' |
| | | import { registerWeizanPayBridge, unregisterWeizanPayBridge } from '@/utils/weizan-pay-bridge.js' |
| | | |
| | | const WEIZAN_WEBVIEW_ID = 'weizanLiveWebview' |
| | | // 微赞识别易宝支付通道用的 UA 后缀(接入方案文档约定)。 |
| | | // 必须完整携带以下标识,微赞 H5 才会把当前环境判定为 isVzanApp / 成功注册 AppChannel, |
| | | // 进而回调 window.android.jumpToMiniProgramPay: |
| | | // webchatPay/wx... → 易宝支付通道 |
| | | // InAppPay Vzan/miniAppPayment → 微赞 APP 内小程序支付 |
| | | // vzanApp / zhiboApp → 微赞 APP / 直播 APP 标识 |
| | | const WEIZAN_UA_SUFFIX = 'webchatPay/wx074c1f0cf5a0ecf4 InAppPay Vzan/miniAppPayment vzanApp navigator.userAgent zhiboApp' |
| | | const webViewUrl = ref('') |
| | | const WEIXIN_APP_ID = 'wx074c1f0cf5a0ecf4' |
| | | let originalUserAgent = '' |
| | | |
| | | let subWebview = null |
| | | let currentUrl = '' |
| | | let currentTitle = '' |
| | | const loadError = ref('') |
| | | |
| | | /** |
| | | * 解码 query 参数。 |
| | | */ |
| | | function decodeQueryValue(value) { |
| | | if (!value) { |
| | | return '' |
| | | } |
| | | try { |
| | | return decodeURIComponent(value) |
| | | } catch (e) { |
| | | return value || '' |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 页面加载:读取 URL/Title,进入前清缓存,创建子 webview 并注册支付桥。 |
| | | */ |
| | | onLoad((options) => { |
| | | currentUrl = decodeQueryValue(options?.url) |
| | | currentTitle = decodeQueryValue(options?.title) |
| | | console.log('[weizanLive] onLoad, url=', currentUrl, 'title=', currentTitle) |
| | | |
| | | if (currentTitle) { |
| | | uni.setNavigationBarTitle({ title: currentTitle }) |
| | | } |
| | | |
| | | const url = decodeValue(options?.url) |
| | | const title = decodeValue(options?.title) |
| | | if (title) uni.setNavigationBarTitle({ title }) |
| | | // #ifdef APP-PLUS |
| | | // 进入前先清理上次缓存、cookie、登录态 |
| | | clearWebViewData() |
| | | // 创建子 webview(注入 UA + 注册支付桥 + 加载 URL) |
| | | createWeizanWebview(currentUrl) |
| | | injectUserAgent() |
| | | console.log('[weizanLive] 跳转前完整 UA:', plus.navigator.getUserAgent() || '') |
| | | // #endif |
| | | webViewUrl.value = url |
| | | }) |
| | | |
| | | /** |
| | | * 页面卸载:反注册支付桥并关闭子 webview,避免残留。 |
| | | */ |
| | | onUnload(() => { |
| | | console.log('[weizanLive] onUnload') |
| | | // #ifdef APP-PLUS |
| | | if (subWebview) { |
| | | try { |
| | | unregisterWeizanPayBridge(subWebview) |
| | | } catch (e) { |
| | | console.warn('[weizanLive] unregisterWeizanPayBridge fail:', e) |
| | | } |
| | | try { |
| | | subWebview.close() |
| | | } catch (e) { |
| | | console.warn('[weizanLive] subWebview.close fail:', e) |
| | | } |
| | | subWebview = null |
| | | if (originalUserAgent) { |
| | | try { plus.navigator.setUserAgent(originalUserAgent, false) } catch (e) {} |
| | | originalUserAgent = '' |
| | | } |
| | | // #endif |
| | | }) |
| | | |
| | | onShow(() => { |
| | | function injectUserAgent() { |
| | | // #ifdef APP-PLUS |
| | | if (subWebview) { |
| | | try { |
| | | subWebview.show() |
| | | } catch (e) {} |
| | | const baseUA = plus.navigator.getUserAgent() || '' |
| | | if (baseUA.indexOf('webchatPay/') >= 0) { |
| | | originalUserAgent = baseUA.replace(/\s*webchatPay\/\S+/g, '') |
| | | return |
| | | } |
| | | // #endif |
| | | }) |
| | | |
| | | onHide(() => { |
| | | // #ifdef APP-PLUS |
| | | if (subWebview) { |
| | | try { |
| | | subWebview.hide() |
| | | } catch (e) {} |
| | | } |
| | | // #endif |
| | | }) |
| | | |
| | | /** |
| | | * 创建子 webview:先创建空白实例 → 注入 UA → 注册支付桥 → 加载真实 URL → show。 |
| | | * 这个顺序保证 H5 加载完成时 UA 已带上微赞标识、window.android.jumpToMiniProgramPay 已可用。 |
| | | */ |
| | | function createWeizanWebview(targetUrl) { |
| | | // #ifdef APP-PLUS |
| | | const platform = String(uni.getSystemInfoSync().platform || '').toLowerCase() |
| | | console.log('[weizanLive] createWeizanWebview, platform=', platform, 'url=', targetUrl) |
| | | |
| | | try { |
| | | // 1. 创建空白 webview(不立即加载 URL) |
| | | subWebview = plus.webview.create('', WEIZAN_WEBVIEW_ID, { |
| | | top: '0px', |
| | | bottom: '0px', |
| | | background: '#FFFFFF', |
| | | // 让子 webview 的 progress 进度条颜色与 APP 一致 |
| | | progress: { |
| | | color: '#FF3333' |
| | | } |
| | | }) |
| | | console.log('[weizanLive] plus.webview.create 完成, subWebview=', subWebview ? '[Object]' : 'null') |
| | | |
| | | // 2. 注入 UA(让微赞 H5 识别为易宝支付通道) |
| | | injectUserAgent(subWebview, platform) |
| | | |
| | | // 3. 注册支付 JSBridge(在 loadURL 之前注册,保证 H5 加载完成时钩子已可用) |
| | | registerWeizanPayBridge(subWebview) |
| | | |
| | | // 4. 加载真实 URL |
| | | if (targetUrl) { |
| | | subWebview.loadURL(targetUrl) |
| | | console.log('[weizanLive] loadURL 调用完成:', targetUrl) |
| | | } else { |
| | | loadError.value = '直播间地址为空' |
| | | } |
| | | |
| | | // 5. 显示子 webview |
| | | subWebview.show() |
| | | console.log('[weizanLive] subWebview.show() 完成') |
| | | } catch (e) { |
| | | console.error('[weizanLive] createWeizanWebview 失败:', e) |
| | | loadError.value = '直播间加载失败' |
| | | } |
| | | originalUserAgent = baseUA |
| | | plus.navigator.setUserAgent(`${baseUA} webchatPay/${WEIXIN_APP_ID}`.trim(), false) |
| | | } catch (e) { console.error('[weizanLive] UA 注入失败:', e) } |
| | | // #endif |
| | | } |
| | | |
| | | /** |
| | | * 给子 webview 注入微赞支付 UA 后缀。 |
| | | * - Android:取底层原生 WebView 后反射 getSettings().setUserAgentString |
| | | * - iOS:反射 WKWebView setValue:forKey:@"customUserAgent" |
| | | */ |
| | | function injectUserAgent(webview, platform) { |
| | | // #ifdef APP-PLUS |
| | | if (platform === 'android') { |
| | | try { |
| | | // plus.webview.create 返回的是 JS 包装对象,不能直接反射, |
| | | // 必须先取底层原生 WebView(android.webkit.WebView)再取 settings。 |
| | | const nativeWebView = getAndroidNativeWebView(webview) |
| | | const settings = safeAndroidInvoke(nativeWebView, 'getSettings') |
| | | if (settings) { |
| | | const originalUA = safeAndroidInvoke(settings, 'getUserAgentString') |
| | | const customUA = `${originalUA} ${WEIZAN_UA_SUFFIX}` |
| | | safeAndroidInvoke(settings, 'setUserAgentString', customUA) |
| | | console.log('[weizanLive] Android UA 注入完成:', customUA) |
| | | } else { |
| | | console.warn('[weizanLive] Android 未取到原生 WebView 或 getSettings 返回空,UA 未注入') |
| | | } |
| | | } catch (e) { |
| | | console.error('[weizanLive] Android UA 注入失败:', e) |
| | | } |
| | | } else if (platform === 'ios') { |
| | | try { |
| | | // plus.webview 实例 → valueForKey:@"webView" 拿底层 WKWebView |
| | | const wkWebView = safeIosInvoke(webview, 'valueForKey:', 'webView') || webview |
| | | // 读取原 UA |
| | | const originalUA = safeIosInvoke(wkWebView, 'valueForKey:', 'userAgent') || '' |
| | | const customUA = `${originalUA} ${WEIZAN_UA_SUFFIX}` |
| | | // WKWebView.customUserAgent 是可写属性,反射设置 |
| | | safeIosInvoke(wkWebView, 'setValue:forKey:', customUA, 'customUserAgent') |
| | | console.log('[weizanLive] iOS UA 注入完成:', customUA) |
| | | } catch (e) { |
| | | console.error('[weizanLive] iOS UA 注入失败:', e) |
| | | } |
| | | } |
| | | // #endif |
| | | } |
| | | |
| | | /** |
| | | * 取 plus.webview 实例底层的原生 android.webkit.WebView。 |
| | | * 优先用官方 nativeInstanceObject(),失败再退回 obtainWebview() 等兼容方式。 |
| | | */ |
| | | function getAndroidNativeWebView(webview) { |
| | | if (!webview) { |
| | | return null |
| | | } |
| | | // 1. 官方 API:WebviewObject.nativeInstanceObject() 返回原生 WebView |
| | | if (typeof webview.nativeInstanceObject === 'function') { |
| | | try { |
| | | const nativeObj = webview.nativeInstanceObject() |
| | | if (nativeObj) { |
| | | return nativeObj |
| | | } |
| | | } catch (e) { |
| | | console.warn('[weizanLive] nativeInstanceObject 调用失败:', e && e.message) |
| | | } |
| | | } |
| | | // 2. 兼容:部分运行时通过 obtainWebview 取原生 WebView |
| | | if (typeof webview.obtainWebview === 'function') { |
| | | try { |
| | | const obtained = webview.obtainWebview() |
| | | if (obtained) { |
| | | return obtained |
| | | } |
| | | } catch (e) {} |
| | | } |
| | | return null |
| | | } |
| | | |
| | | /** |
| | | * 安全调用 plus.android.invoke,失败返回 null。 |
| | | */ |
| | | function safeAndroidInvoke(obj, methodName, ...args) { |
| | | try { |
| | | return plus.android.invoke(obj, methodName, ...args) |
| | | } catch (e) { |
| | | console.warn('[weizanLive] android invoke fail:', methodName, e && e.message) |
| | | return null |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 安全调用 plus.ios.invoke,失败返回 null。 |
| | | */ |
| | | function safeIosInvoke(obj, selector, ...args) { |
| | | try { |
| | | return plus.ios.invoke(obj, selector, ...args) |
| | | } catch (e) { |
| | | console.warn('[weizanLive] ios invoke fail:', selector, e && e.message) |
| | | return null |
| | | } |
| | | function decodeValue(value) { |
| | | if (typeof value !== 'string' || !value.trim()) return '' |
| | | try { return decodeURIComponent(value) } catch (e) { return value.trim() } |
| | | } |
| | | </script> |
| | | |
| | | <style> |
| | | .weizan-live-container { |
| | | width: 100%; |
| | | height: 100vh; |
| | | background-color: #FFFFFF; |
| | | } |
| | | <style lang="scss" scoped> |
| | | .container { width: 100vw; min-height: 100vh; background: #fff; } |
| | | .webview-content { width: 100%; height: 100vh; } |
| | | .empty-box { min-height: 100vh; display: flex; align-items: center; justify-content: center; } |
| | | .empty-text { font-size: 28rpx; color: #999; } |
| | | </style> |