quling
2026-08-24 676f4f81dd1b24b12b21f68ed3f2c381b520c0bf
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
<template>
    <view class="weizan-live-container">
        <!-- 子 webview 由 plus.webview.create 命令式创建并浮在该 view 之上,无需在 template 中渲染 -->
    </view>
</template>
 
<script setup>
import { ref } from 'vue'
import { onLoad, onUnload, onShow, onHide } 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'
 
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 })
    }
 
    // #ifdef APP-PLUS
    // 进入前先清理上次缓存、cookie、登录态
    clearWebViewData()
    // 创建子 webview(注入 UA + 注册支付桥 + 加载 URL)
    createWeizanWebview(currentUrl)
    // #endif
})
 
/**
 * 页面卸载:反注册支付桥并关闭子 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
    }
    // #endif
})
 
onShow(() => {
    // #ifdef APP-PLUS
    if (subWebview) {
        try {
            subWebview.show()
        } catch (e) {}
    }
    // #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 = '直播间加载失败'
    }
    // #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
    }
}
</script>
 
<style>
.weizan-live-container {
    width: 100%;
    height: 100vh;
    background-color: #FFFFFF;
}
</style>