pay-webview.vue 9.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369
  1. <!-- 支付跳转中间页(完整兼容版) -->
  2. <template>
  3. <view>
  4. <!-- 自定义导航栏 -->
  5. <view class="pay-webview-nav" :style="{ height: (statusBarH + 44) + 'px' }">
  6. <view class="nav-bar" :style="{ height: (statusBarH + 44) + 'px', paddingTop: statusBarH + 'px' }">
  7. <!-- 返回按钮 -->
  8. <view class="nav-action" @tap="goBack">
  9. <text class="wlIcon wlIcon-fanhui1"></text>
  10. </view>
  11. <!-- 标题 -->
  12. <view class="nav-title">支付中</view>
  13. <!-- 首页按钮 -->
  14. <view class="nav-action" @tap="goHome">
  15. <text class="wlIcon wlIcon-zhuye"></text>
  16. <text class="nav-home-text">88live</text>
  17. </view>
  18. </view>
  19. </view>
  20. <view class="edgeInsetTop"></view>
  21. <!--
  22. TNG 等收银台:App 内 WebView 可能因 UA/存储与系统浏览器不同出现白屏。
  23. 本页在 App 端对 tngdigital 域名会在赋值 payUrl 前临时设置更接近 Chrome/Safari 的 UA,并在 onUnload 尝试恢复。
  24. -->
  25. <web-view v-if="payUrl" :webview-styles="webviewStyles" :src="payUrl" @message="onMessage" @error="onWebViewError"
  26. @load="onWebViewLoad"></web-view>
  27. <view v-if="payUrl" class="open-browser-bar">
  28. <view class="open-browser-btn" @tap="openInBrowser">
  29. <text class="open-browser-text">用瀏覽器打開</text>
  30. </view>
  31. </view>
  32. </view>
  33. </template>
  34. <script>
  35. export default {
  36. data() {
  37. return {
  38. payUrl: '',
  39. order_id: '',
  40. order_type: '',
  41. successUrl: '',
  42. hasClosedByPayPush: false,
  43. leavingToSuccess: false,
  44. statusBarH: 0,
  45. navTop: 0,
  46. webViewHeight: 600,
  47. /** App 端为 TNG 临时改 UA 前备份 */
  48. _beforePayWebviewUA: '',
  49. _tngUaReplaced: false,
  50. webviewStyles: {
  51. progress: {
  52. color: '#ff6b35'
  53. }
  54. }
  55. }
  56. },
  57. onLoad(option) {
  58. const sys = uni.getSystemInfoSync()
  59. this.statusBarH = sys.statusBarHeight || 0
  60. const navBarInner = uni.upx2px(88)
  61. this.navTop = this.statusBarH + navBarInner
  62. this.webViewHeight = (sys.windowHeight || 667) - this.navTop
  63. const rawUrl = option.url || ''
  64. // #ifdef APP-PLUS
  65. if (rawUrl && this.isTngCashierUrl(rawUrl)) {
  66. this.applyTngAppUserAgent()
  67. }
  68. // #endif
  69. this.payUrl = rawUrl
  70. if (option.order_id) this.order_id = option.order_id
  71. if (option.order_type) this.order_type = option.order_type
  72. if (option.success_url) this.successUrl = decodeURIComponent(option.success_url)
  73. this.webviewStyles = {
  74. progress: {
  75. color: '#ff6b35'
  76. },
  77. top: `${this.navTop}px`,
  78. height: `${this.webViewHeight}px`
  79. }
  80. uni.$on('onPayeMessage', this.onPayeMessage)
  81. },
  82. onReady() {
  83. // #ifdef APP-PLUS
  84. if (this.payUrl) {
  85. this.adjustAppWebviewLayout()
  86. if (this.isTngCashierUrl(this.payUrl)) {
  87. this.patchSubWebviewForTng()
  88. }
  89. }
  90. // #endif
  91. },
  92. onBackPress() {
  93. if (!this.leavingToSuccess) {
  94. uni.setStorageSync('pay_webview_return', JSON.stringify({
  95. type: 'back',
  96. ts: Date.now(),
  97. order_id: this.order_id,
  98. order_type: this.order_type
  99. }))
  100. }
  101. return false
  102. },
  103. onUnload() {
  104. uni.$off('onPayeMessage', this.onPayeMessage)
  105. // #ifdef APP-PLUS
  106. this.restoreAppUserAgent()
  107. // #endif
  108. },
  109. onShow() {
  110. this.checkPaymentStatus()
  111. },
  112. methods: {
  113. isTngCashierUrl(url) {
  114. if (!url || typeof url !== 'string') return false
  115. const u = url.toLowerCase()
  116. return u.indexOf('tngdigital.com') >= 0
  117. },
  118. /** App:整页级 UA(影响当前页内子 web-view 首包请求) */
  119. applyTngAppUserAgent() {
  120. try {
  121. if (typeof plus === 'undefined' || !plus.navigator) return
  122. if (typeof plus.navigator.getUserAgent === 'function') {
  123. this._beforePayWebviewUA = plus.navigator.getUserAgent()
  124. }
  125. const sys = uni.getSystemInfoSync()
  126. const androidChrome =
  127. 'Mozilla/5.0 (Linux; Android 12) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.6099.144 Mobile Safari/537.36'
  128. const iosSafari =
  129. 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.2 Mobile/15E148 Safari/604.1'
  130. plus.navigator.setUserAgent(sys.platform === 'ios' ? iosSafari : androidChrome)
  131. this._tngUaReplaced = true
  132. } catch (e) {
  133. console.warn('[pay-webview] applyTngAppUserAgent', e)
  134. }
  135. },
  136. restoreAppUserAgent() {
  137. if (!this._tngUaReplaced) return
  138. try {
  139. if (typeof plus !== 'undefined' && plus.navigator && this._beforePayWebviewUA) {
  140. plus.navigator.setUserAgent(this._beforePayWebviewUA)
  141. }
  142. } catch (e) {
  143. console.warn('[pay-webview] restoreAppUserAgent', e)
  144. }
  145. this._tngUaReplaced = false
  146. this._beforePayWebviewUA = ''
  147. },
  148. /** 延迟获取子 web-view,尝试 setStyle(兼容性因基座版本而异) */
  149. patchSubWebviewForTng() {
  150. setTimeout(() => {
  151. try {
  152. const pages = getCurrentPages()
  153. const page = pages[pages.length - 1]
  154. if (!page || typeof page.$getAppWebview !== 'function') return
  155. const cw = page.$getAppWebview()
  156. const children = cw.children && cw.children()
  157. if (!children || !children.length) return
  158. const wv = children[0]
  159. if (wv && typeof wv.setStyle === 'function') {
  160. wv.setStyle({ hardwareAccelerated: true })
  161. }
  162. const sys = uni.getSystemInfoSync()
  163. const androidChrome =
  164. 'Mozilla/5.0 (Linux; Android 12) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.6099.144 Mobile Safari/537.36'
  165. const iosSafari =
  166. 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.2 Mobile/15E148 Safari/604.1'
  167. const ua = sys.platform === 'ios' ? iosSafari : androidChrome
  168. if (wv && typeof wv.setUserAgent === 'function') {
  169. wv.setUserAgent(ua)
  170. } else if (wv && typeof wv.setStyle === 'function') {
  171. wv.setStyle({ userAgent: ua })
  172. }
  173. } catch (e) {
  174. console.warn('[pay-webview] patchSubWebviewForTng', e)
  175. }
  176. }, 300)
  177. },
  178. /** 调整 App 端原生 webview 布局,使其不遮挡自定义导航栏 */
  179. adjustAppWebviewLayout(retry = 0) {
  180. // #ifdef APP-PLUS
  181. const pages = getCurrentPages()
  182. const page = pages[pages.length - 1]
  183. if (!page || typeof page.$getAppWebview !== 'function') return
  184. const apply = () => {
  185. const currentWebview = page.$getAppWebview()
  186. const children = currentWebview.children()
  187. if (!children || !children.length) {
  188. if (retry < 10) {
  189. setTimeout(() => this.adjustAppWebviewLayout(retry + 1), 200)
  190. }
  191. return
  192. }
  193. const wv = children[0]
  194. wv.setStyle({
  195. top: `${this.navTop}px`,
  196. height: `${this.webViewHeight}px`,
  197. bottom: 'auto'
  198. })
  199. }
  200. setTimeout(apply, retry === 0 ? 300 : 0)
  201. // #endif
  202. },
  203. onPayeMessage(msg) {
  204. const isPay =
  205. (msg && msg.type === 'pay') ||
  206. (msg && msg.message && msg.message.type === 'pay')
  207. if (!isPay) return
  208. if (this.hasClosedByPayPush) return
  209. this.hasClosedByPayPush = true
  210. this.paySuccess()
  211. },
  212. onMessage(e) {
  213. console.log('[pay-webview] 收到 webview 消息:', e.detail.data)
  214. if (e.detail.data && e.detail.data[0] && e.detail.data[0].type === 'payment_success') {
  215. this.paySuccess()
  216. }
  217. },
  218. onWebViewError(e) {
  219. console.warn('[pay-webview] webview 加载失败:', e)
  220. uni.showModal({
  221. title: '加載失敗',
  222. content: '支付頁面無法在應用內加載,是否用瀏覽器打開?',
  223. confirmText: '用瀏覽器打開',
  224. cancelText: '取消',
  225. success: (res) => {
  226. if (res.confirm) this.openInBrowser()
  227. }
  228. })
  229. },
  230. onWebViewLoad() { },
  231. openInBrowser() {
  232. if (!this.payUrl) return
  233. // #ifdef H5
  234. window.open(this.payUrl, '_blank')
  235. // #endif
  236. // #ifdef APP-PLUS
  237. plus.runtime.openURL(this.payUrl, (err) => {
  238. if (err) {
  239. uni.showToast({ title: '打開失敗', icon: 'none' })
  240. }
  241. })
  242. // #endif
  243. // #ifndef H5 || APP-PLUS
  244. if (typeof plus !== 'undefined' && plus.runtime && plus.runtime.openURL) {
  245. plus.runtime.openURL(this.payUrl)
  246. } else {
  247. uni.showModal({
  248. content: '請復製鏈接到瀏覽器打開:\n' + this.payUrl,
  249. showCancel: false
  250. })
  251. }
  252. // #endif
  253. },
  254. checkPaymentStatus() { },
  255. paySuccess() {
  256. this.leavingToSuccess = true
  257. uni.showToast({ title: '支付成功', icon: 'success' })
  258. setTimeout(() => {
  259. uni.navigateBack()
  260. }, 1500)
  261. },
  262. // 返回上一页
  263. goBack() {
  264. uni.navigateBack()
  265. },
  266. // 返回首页
  267. goHome() {
  268. uni.switchTab({ url: '/pages/index/index' })
  269. }
  270. }
  271. }
  272. </script>
  273. <style>
  274. page {
  275. background-color: #fff;
  276. }
  277. /* 自定义导航栏 */
  278. .pay-webview-nav {
  279. position: relative;
  280. z-index: 999;
  281. background-color: #fff;
  282. border-bottom: 1rpx solid #eee;
  283. }
  284. .nav-bar {
  285. display: flex;
  286. align-items: center;
  287. justify-content: space-between;
  288. padding: 0 20rpx;
  289. box-sizing: border-box;
  290. position: relative;
  291. }
  292. .nav-action {
  293. width: 100rpx;
  294. height: 44px;
  295. display: flex;
  296. align-items: center;
  297. justify-content: center;
  298. font-size: 48rpx;
  299. color: #333;
  300. }
  301. .nav-home {
  302. width: auto;
  303. padding: 0 10rpx;
  304. }
  305. .nav-home-text {
  306. font-size: 28rpx;
  307. color: #333;
  308. margin-left: 8rpx;
  309. font-weight: 500;
  310. }
  311. .nav-title {
  312. flex: 1;
  313. text-align: center;
  314. font-size: 32rpx;
  315. font-weight: 600;
  316. color: #333;
  317. }
  318. .edgeInsetTop {
  319. height: var(--status-bar-height);
  320. }
  321. .open-browser-bar {
  322. position: fixed;
  323. bottom: 0;
  324. left: 0;
  325. right: 0;
  326. padding: 20rpx 30rpx;
  327. padding-bottom: calc(20rpx + env(safe-area-inset-bottom));
  328. background: linear-gradient(to top, rgba(255, 255, 255, 0.98), transparent);
  329. z-index: 999;
  330. }
  331. .open-browser-btn {
  332. background: #ff6b35;
  333. color: #fff;
  334. text-align: center;
  335. padding: 24rpx;
  336. border-radius: 12rpx;
  337. }
  338. .open-browser-text {
  339. font-size: 28rpx;
  340. color: #fff;
  341. }
  342. </style>