pay.vue 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878
  1. <!-- 支付页 -->
  2. <template>
  3. <view class="pay-page">
  4. <view class="edgeInsetTop"></view>
  5. <view class="wanl-pay">
  6. <view class="price-box">
  7. <text class="text-df">{{ order_no }}</text>
  8. <text class="wanl-pip text-price margin-top-sm">{{ price }}</text>
  9. </view>
  10. <view class="payment-method-title">
  11. <text class="payment-method-text">支付方式:</text>
  12. </view>
  13. <view class="list-box">
  14. <view class="item" v-for="(item, index) in payList" :key="index">
  15. <image v-if="item.type == 'fiuu'" class="fiuu-pay-icon" src="/static/images/payment/fiuu-logo.png"
  16. mode="aspectFit"></image>
  17. <image v-else-if="item.type == 'google' || item.type == 'googlepay'" class="fiuu-pay-icon"
  18. src="/static/images/payment/google-pay-logo.svg" mode="aspectFit"></image>
  19. <image v-else-if="item.type == 'balance'" class="fiuu-pay-icon"
  20. src="/static/images/payment/balance-pay-logo.svg" mode="aspectFit"></image>
  21. <!-- <image v-else-if="item.type == 'payz'" class="fiuu-pay-icon" src="/static/images/payment/payz-pay-mark.svg" mode="aspectFit"></image> -->
  22. <image v-else-if="item.type == 'stripe'" class="fiuu-pay-icon" src="/static/images/payment/Tripe_800.svg"
  23. mode="aspectFit"></image>
  24. <image v-else-if="item.type == 'offline'" class="fiuu-pay-icon"
  25. src="/static/images/payment/icon-offline-payment.svg" mode="aspectFit"></image>
  26. <text v-else :class="'wlIcon-' + item.type + '-pay'"></text>
  27. <view class="action">
  28. <text class="title wanl-pip">{{ item.name }}</text>
  29. <view v-if="item.type == 'balance'">
  30. <text v-if="isbalance">可用餘額 HKD{{ user.money }}</text>
  31. <text v-else>餘額不足 HKD{{ user.money }}</text>
  32. </view>
  33. <view v-else>{{ item.describe }}</view>
  34. </view>
  35. <view class="radio text-xxl" v-if="item.state" @tap="onSelect(index)">
  36. <text v-if="item.select" class="wlIcon-xuanze-danxuan wanl-orange"></text>
  37. <text v-else class="wlIcon-xuanze wanl-gray-light"></text>
  38. </view>
  39. <view class="radio text-xxl" v-else>
  40. <text class="wlIcon-xuanze-danxuan wanl-gray-light"></text>
  41. </view>
  42. </view>
  43. <!-- 线下支付选中时展开 -->
  44. <view v-if="isOfflineSelected && offlineSubListVisible" class="offline-sub-list">
  45. <view v-if="offlinePaymentInfo.reminder" class="offline-reminder">{{ offlinePaymentInfo.reminder }}</view>
  46. <view v-if="offlineQrFullUrl" class="offline-qrcode-wrap">
  47. <text class="offline-qrcode-title">收款二維碼</text>
  48. <image class="offline-qrcode-img" :src="offlineQrFullUrl" mode="aspectFit" @tap="previewOfflineQrCode" />
  49. <text class="offline-qrcode-tip">點擊二維碼可預覽大圖</text>
  50. </view>
  51. <view v-if="offlinePaymentInfo.list && offlinePaymentInfo.list.length" class="offline-list">
  52. <view v-for="(row, idx) in offlinePaymentInfo.list" :key="idx" class="offline-list-item">
  53. <text class="offline-list-title">{{ row.title }}:</text>
  54. <text class="offline-list-name">{{ row.name }}</text>
  55. </view>
  56. </view>
  57. </view>
  58. </view>
  59. <view class="pay-bottom-placeholder"></view>
  60. </view>
  61. <!-- 固定底部按钮栏 -->
  62. <view class="wanl-pay pay-bottom-fixed">
  63. <view v-if="disabled" class="pay-bottom-buttons">
  64. <!-- #ifdef APP-PLUS -->
  65. <button v-if="isGooglePaySelected" class="google-pay-button-app">
  66. <view class="google-pay-button-content">
  67. <text class="wlIcon-jiazai wlIconfont-spin margin-right-xs google-pay-text-white"></text>
  68. <text class="google-pay-text-white">支付中...</text>
  69. </view>
  70. </button>
  71. <!-- #endif -->
  72. <!-- #ifdef H5 -->
  73. <view v-if="isGooglePaySelected" class="google-pay-web-container">
  74. <view class="gpay-web-button-wrapper">
  75. <text class="web-button-loading">支付中...</text>
  76. </view>
  77. </view>
  78. <!-- #endif -->
  79. <button v-if="!isGooglePaySelected" class="mix-btn wanl-bg-redorange">
  80. <text class="wlIcon-jiazai wlIconfont-spin margin-right-xs"></text>支付中...
  81. </button>
  82. </view>
  83. <view v-else class="pay-bottom-buttons">
  84. <!-- #ifdef APP-PLUS -->
  85. <button v-if="isGooglePaySelected" class="google-pay-button-app" @tap="confirm()">
  86. <view class="google-pay-button-content">
  87. <text class="google-pay-text-white">使用</text>
  88. <image class="google-pay-g-icon" src="/static/images/payment/google-g-icon.svg" mode="aspectFit"></image>
  89. <text class="google-pay-text-white"> Pay</text>
  90. </view>
  91. </button>
  92. <!-- #endif -->
  93. <!-- #ifdef H5 -->
  94. <view v-if="isGooglePaySelected" class="google-pay-web-container">
  95. <view id="gpay-web-button-container" class="gpay-web-button-wrapper">
  96. <text v-if="!webPayButtonReady" class="web-button-loading">加載中...</text>
  97. </view>
  98. </view>
  99. <!-- #endif -->
  100. <button v-if="!isGooglePaySelected && payNum == 1" class="mix-btn wanl-bg-redorange"
  101. :class="{ 'mix-btn-disabled': isOfflineSelected }" :disabled="isOfflineSelected"
  102. @tap="confirm()">確認支付</button>
  103. <button v-if="!isGooglePaySelected && payNum > 1" class="mix-btn wanl-bg-redorange"
  104. :class="{ 'mix-btn-disabled': isOfflineSelected }" :disabled="isOfflineSelected"
  105. @tap="confirm()">合併支付</button>
  106. </view>
  107. </view>
  108. </view>
  109. </template>
  110. <script>
  111. import { mapState } from 'vuex'
  112. import { decimalAdd, decimalSub } from '@/core/infra/runtime-bridge'
  113. import { getOfflinePaymentConfig, getStripePaymentConfig, getBalance, getPayInfo, postPayment } from '@/features/mall/mall-gateway'
  114. export default {
  115. data() {
  116. return {
  117. token: '',
  118. disabled: false,
  119. lastPayWebviewReturnTs: 0,
  120. price: '0.00',
  121. isbalance: false,
  122. order_type: 'goods',
  123. order_no: '',
  124. order_pay_no: '',
  125. payNum: 1,
  126. payList: [],
  127. offlinePaymentInfo: null,
  128. stripePaymentInfo: null,
  129. lastConfirmTap: 0,
  130. confirmDebounceMs: 600,
  131. googlePayProvider: null,
  132. webPayButtonReady: false,
  133. webPaymentsClient: null,
  134. webPayConfig: {
  135. merchantId: 'BCR2DN5TRDQ3TNZU',
  136. merchantName: 'ONE RISE LIVESTREAM TECH SDN. BHD.',
  137. environment: 'TEST'
  138. },
  139. order_id: null
  140. }
  141. },
  142. computed: {
  143. ...mapState(['user']),
  144. isGooglePaySelected() {
  145. const selected = this.payList.find(item => item.select)
  146. return selected && (selected.type === 'google' || selected.type === 'googlepay')
  147. },
  148. isOfflineSelected() {
  149. const selected = this.payList.find(item => item.select)
  150. return selected && selected.type === 'offline'
  151. },
  152. offlineSubListVisible() {
  153. const o = this.offlinePaymentInfo
  154. if (!o) return false
  155. const hasList = Array.isArray(o.list) && o.list.length > 0
  156. const hasQr = !!(o.qrcode && String(o.qrcode).trim())
  157. return hasList || hasQr
  158. },
  159. offlineQrFullUrl() {
  160. const q = this.offlinePaymentInfo && this.offlinePaymentInfo.qrcode
  161. if (!q) return ''
  162. const s = String(q).trim()
  163. if (/^https?:\/\//i.test(s) || /^data:image\//i.test(s)) return s
  164. return s.startsWith('/') ? s : '/' + s
  165. }
  166. },
  167. watch: {
  168. price(val) {
  169. if (val <= parseFloat(this.$store.state.user.money)) {
  170. this.isbalance = true
  171. this.getPayment()
  172. }
  173. },
  174. isGooglePaySelected(newVal, oldVal) {
  175. // #ifdef H5
  176. if (newVal && !oldVal) {
  177. this.$nextTick(() => {
  178. this.webPayButtonReady = false
  179. this.renderWebGooglePayButton()
  180. })
  181. }
  182. // #endif
  183. }
  184. },
  185. async onLoad(option) {
  186. try {
  187. const { data: balanceData } = await getBalance()
  188. this.$store.commit('user/apply', { money: balanceData })
  189. } catch (e) {
  190. console.error('获取余额失败:', e)
  191. }
  192. this.getPayment()
  193. // #ifdef H5
  194. this.$nextTick(() => { this.initWebGooglePay() })
  195. // #endif
  196. // #ifdef APP-PLUS
  197. this.checkGooglePaySupport()
  198. // #endif
  199. if (option.data) {
  200. var sum = 0,
  201. data = JSON.parse(option.data),
  202. order_ids = []
  203. if (data.length <= 1) {
  204. this.price = data[0].price
  205. this.order_type = data[0].type
  206. this.order_no = `訂單號:${data[0].order_no}`
  207. this.order_pay_no = data[0].pay_no
  208. this.order_id = data[0].order_id
  209. } else {
  210. data.forEach(item => {
  211. sum = decimalAdd(sum, item.price)
  212. order_ids.push(item.order_id)
  213. })
  214. this.price = sum
  215. this.order_id = order_ids
  216. this.payNum = data.length
  217. this.order_no = `合併支付 ${data.length}個訂單`
  218. }
  219. this.token = option.token
  220. } else if (option.order_id) {
  221. uni.showLoading({ title: '加載中...' })
  222. try {
  223. const data = await getPayInfo({ order_id: option.order_id, order_type: option.order_type })
  224. uni.hideLoading()
  225. this.token = data.token
  226. this.price = data.price
  227. this.order_type = data.order_type
  228. this.order_no = `訂單號:${data.order_no}`
  229. this.order_pay_no = data.pay_no
  230. this.order_id = data.order_id
  231. } catch (e) {
  232. uni.hideLoading()
  233. console.error('[pay] getPayInfo 失败:', JSON.stringify(e))
  234. uni.showToast({ title: '加載失敗', icon: 'none' })
  235. }
  236. }
  237. },
  238. onShow() {
  239. const raw = uni.getStorageSync('pay_webview_return')
  240. if (!raw) return
  241. uni.removeStorageSync('pay_webview_return')
  242. let payload = raw
  243. if (typeof raw === 'string') {
  244. try { payload = JSON.parse(raw) } catch (e) { return }
  245. }
  246. if (payload && payload.type !== 'pay') return
  247. const ts = payload && payload.ts ? Number(payload.ts) : Date.now()
  248. if (ts && ts === this.lastPayWebviewReturnTs) return
  249. this.lastPayWebviewReturnTs = ts || Date.now()
  250. uni.showModal({
  251. title: '支付確認',
  252. content: '是否已完成支付?',
  253. cancelText: '繼續支付',
  254. confirmText: '查看訂單',
  255. success: (res) => {
  256. if (!res.confirm) return
  257. let orderId = this.order_id
  258. if (Array.isArray(orderId)) orderId = orderId[0]
  259. if (orderId) {
  260. uni.redirectTo({ url: `/pages/user/order/details?id=${encodeURIComponent(orderId)}` })
  261. } else {
  262. uni.redirectTo({ url: `/pages/user/order/order?state=0` })
  263. }
  264. }
  265. })
  266. },
  267. methods: {
  268. checkStripePaymentSupport() {
  269. getStripePaymentConfig().then(res => {
  270. const data = res
  271. if (!data) {
  272. return
  273. }
  274. this.stripePaymentInfo = data
  275. if (data.status == 1) {
  276. this.payList.push({
  277. name: data.title || 'Stripe',
  278. describe: '支持信用卡 / Apple Pay / Google Pay',
  279. type: 'stripe',
  280. method: 'checkout',
  281. state: true,
  282. select: false
  283. })
  284. } else {
  285. }
  286. }).catch(e => {
  287. console.error('[StripePay] 获取配置失败:', e)
  288. })
  289. },
  290. checkOfflinePaymentSupport() {
  291. getOfflinePaymentConfig().then(res => {
  292. const data = res
  293. if (!data) return
  294. this.offlinePaymentInfo = data
  295. if (data.status == 1) {
  296. this.payList.push({
  297. name: data.pay_name || '線下支付',
  298. describe: '轉賬後請聯繫客服確認',
  299. type: 'offline',
  300. method: 'offline',
  301. state: true,
  302. select: false
  303. })
  304. }
  305. }).catch(e => {
  306. console.error('[OfflinePay] 获取配置失败:', e)
  307. })
  308. },
  309. checkGooglePaySupport() {
  310. // #ifdef APP-PLUS
  311. uni.getProvider({
  312. service: 'payment',
  313. success: (e) => {
  314. if (e.provider.includes('google')) {
  315. this.googlePayProvider = 'google'
  316. this.payList.push({
  317. name: 'Google Pay',
  318. describe: '',
  319. type: 'google',
  320. method: 'app',
  321. state: true,
  322. select: false
  323. })
  324. }
  325. }
  326. })
  327. // #endif
  328. },
  329. initWebGooglePay() {
  330. // #ifdef H5
  331. if (typeof window === 'undefined' || !window.google || !window.google.payments) return
  332. try {
  333. this.webPaymentsClient = new window.google.payments.api.PaymentsClient({
  334. environment: this.webPayConfig.environment
  335. })
  336. } catch (e) {
  337. console.warn('[GooglePay Web] 初始化失败:', e)
  338. }
  339. // #endif
  340. },
  341. renderWebGooglePayButton() {
  342. // #ifdef H5
  343. if (!this.webPaymentsClient) return
  344. this.$nextTick(() => {
  345. const container = document.getElementById('gpay-web-button-container')
  346. if (!container) return
  347. container.innerHTML = ''
  348. try {
  349. const button = this.webPaymentsClient.createButton({
  350. onClick: () => this.confirm(),
  351. buttonColor: 'black',
  352. buttonType: 'pay',
  353. buttonSizeMode: 'fill'
  354. })
  355. container.appendChild(button)
  356. this.webPayButtonReady = true
  357. } catch (e) {
  358. console.warn('[GooglePay Web] 渲染按钮失败:', e)
  359. }
  360. })
  361. // #endif
  362. },
  363. showOfflinePaymentInfo() {
  364. const info = this.offlinePaymentInfo
  365. if (!info) {
  366. uni.showModal({ content: '暫無線下支付信息', showCancel: false })
  367. return
  368. }
  369. let content = ''
  370. if (info.reminder) content += info.reminder + '\n\n'
  371. if (Array.isArray(info.list)) {
  372. content += info.list.map(item => `${item.title}:${item.name}`).join('\n')
  373. }
  374. uni.showModal({
  375. title: info.pay_name || '線下支付',
  376. content: content || '暫無線下支付信息',
  377. showCancel: false
  378. })
  379. },
  380. previewOfflineQrCode() {
  381. const url = this.offlineQrFullUrl
  382. if (!url) return
  383. uni.previewImage({ urls: [url], current: url })
  384. },
  385. openPaymentUrl(paymentUrl) {
  386. const orderType = this.order_type || 'goods'
  387. const orderId = this.order_id != null ? this.order_id : ''
  388. const payWebviewUrl = `/pages/user/money/pay-webview?url=${encodeURIComponent(paymentUrl)}&order_type=${encodeURIComponent(orderType)}&order_id=${encodeURIComponent(orderId)}`
  389. uni.navigateTo({
  390. url: payWebviewUrl,
  391. fail: (err) => {
  392. console.warn('pay-webview 打开失败,改用外部浏览器', err)
  393. this.openPaymentUrlInBrowser(paymentUrl)
  394. },
  395. success: () => {
  396. this.disabled = false
  397. }
  398. })
  399. },
  400. openPaymentUrlInBrowser(paymentUrl) {
  401. const done = () => { this.disabled = false }
  402. // #ifdef H5
  403. window.location.href = paymentUrl
  404. done()
  405. // #endif
  406. // #ifdef APP-PLUS
  407. plus.runtime.openURL(paymentUrl, (error) => {
  408. if (error) {
  409. uni.showModal({ content: '無法打開支付頁面', showCancel: false, success: () => done() })
  410. } else {
  411. done()
  412. }
  413. })
  414. // #endif
  415. },
  416. getPayment() {
  417. this.payList = [
  418. // {
  419. // name: '餘額支付',
  420. // describe: '',
  421. // type: 'balance',
  422. // method: 'balance',
  423. // state: true,
  424. // select: this.isbalance ? false : false
  425. // }
  426. ]
  427. this.checkStripePaymentSupport()
  428. this.checkOfflinePaymentSupport()
  429. },
  430. onSelect(key) {
  431. this.payList.map((value, index) => {
  432. if (index == key) {
  433. value.select = !value.select
  434. } else {
  435. value.select = false
  436. }
  437. })
  438. },
  439. confirm() {
  440. let data = null
  441. if (this.disabled) return
  442. const now = Date.now()
  443. if (now - this.lastConfirmTap < this.confirmDebounceMs) return
  444. this.lastConfirmTap = now
  445. this.payList.map(value => {
  446. if (value.select) data = value
  447. })
  448. if (!data) {
  449. uni.showModal({ content: '請選擇支付方式', showCancel: false })
  450. return
  451. }
  452. if (data.type === 'offline') {
  453. this.showOfflinePaymentInfo()
  454. return
  455. }
  456. this.disabled = true
  457. this.wanlPay(data)
  458. },
  459. async wanlPay(data) {
  460. const payData = {
  461. order_type: this.order_type,
  462. type: data.type,
  463. method: data.method,
  464. code: null,
  465. order_id: this.order_id,
  466. token: this.token
  467. }
  468. try {
  469. const resData = await postPayment(payData)
  470. switch (data.type) {
  471. case 'fiuu':
  472. if (resData && resData.payment_url) {
  473. this.openPaymentUrl(resData.payment_url)
  474. } else {
  475. uni.showModal({ content: '獲取支付地址失敗', showCancel: false, success: () => { this.disabled = false } })
  476. }
  477. break
  478. case 'stripe':
  479. const paymentUrl = resData && (resData.payment_url || resData.redirect_url)
  480. if (paymentUrl) {
  481. const openWebview = this.stripePaymentInfo && this.stripePaymentInfo.open_webview === 0
  482. if (openWebview) {
  483. this.openPaymentUrl(paymentUrl)
  484. } else {
  485. this.openPaymentUrlInBrowser(paymentUrl)
  486. }
  487. } else {
  488. uni.showModal({ content: '獲取支付地址失敗', showCancel: false, success: () => { this.disabled = false } })
  489. }
  490. break
  491. case 'google':
  492. case 'googlepay':
  493. // #ifdef H5
  494. this.handleWebGooglePay(resData)
  495. // #endif
  496. // #ifdef APP-PLUS
  497. this.handleAppGooglePay(resData)
  498. // #endif
  499. break
  500. case 'offline':
  501. this.showOfflinePaymentInfo()
  502. this.disabled = false
  503. break
  504. case 'balance':
  505. this.$store.commit('user/apply', {
  506. money: decimalSub(this.$store.state.user.money, this.price)
  507. })
  508. this.paySuccess()
  509. break
  510. default:
  511. this.disabled = false
  512. uni.showToast({ title: '暫不支持該支付方式', icon: 'none' })
  513. break
  514. }
  515. } catch (e) {
  516. this.disabled = false
  517. uni.showModal({ content: e.message || '支付失敗', showCancel: false })
  518. }
  519. },
  520. handleWebGooglePay(paymentData) {
  521. // #ifdef H5
  522. if (!this.webPaymentsClient) {
  523. uni.showModal({ content: 'Google Pay 不可用', showCancel: false, success: () => { this.disabled = false } })
  524. return
  525. }
  526. const priceStr = String(this.price || '0.00')
  527. try {
  528. const paymentDataRequest = {
  529. apiVersion: 2,
  530. apiVersionMinor: 0,
  531. allowedPaymentMethods: [{
  532. type: 'CARD',
  533. parameters: {
  534. allowedAuthMethods: ['PAN_ONLY', 'CRYPTOGRAM_3DS'],
  535. allowedCardNetworks: ['AMEX', 'DISCOVER', 'JCB', 'MASTERCARD', 'VISA']
  536. },
  537. tokenizationSpecification: {
  538. type: 'PAYMENT_GATEWAY',
  539. parameters: paymentData && paymentData.gatewayParameters ? paymentData.gatewayParameters : {
  540. gateway: 'example',
  541. gatewayMerchantId: 'exampleGatewayMerchantId'
  542. }
  543. }
  544. }],
  545. transactionInfo: {
  546. totalPriceStatus: 'FINAL',
  547. totalPrice: priceStr,
  548. currencyCode: 'MYR',
  549. countryCode: 'MY'
  550. },
  551. merchantInfo: {
  552. merchantId: this.webPayConfig.merchantId,
  553. merchantName: this.webPayConfig.merchantName
  554. }
  555. }
  556. this.webPaymentsClient.loadPaymentData(paymentDataRequest).then((result) => {
  557. this.googlePaySuccess('Google Pay', '')
  558. }).catch((err) => {
  559. console.error('[GooglePay Web] 支付失败:', err)
  560. this.disabled = false
  561. if (err.statusCode !== 'CANCELED') {
  562. uni.showModal({ content: 'Google Pay 支付失敗', showCancel: false })
  563. }
  564. })
  565. } catch (e) {
  566. console.error('[GooglePay Web] 异常:', e)
  567. this.disabled = false
  568. }
  569. // #endif
  570. },
  571. handleAppGooglePay(paymentData) {
  572. // #ifdef APP-PLUS
  573. if (!this.googlePayProvider) {
  574. uni.showModal({ content: 'Google Pay 不可用', showCancel: false, success: () => { this.disabled = false } })
  575. return
  576. }
  577. let googlePayConfig = paymentData && paymentData.config ? paymentData.config : paymentData
  578. if (typeof googlePayConfig === 'string') {
  579. try { googlePayConfig = JSON.parse(googlePayConfig) } catch (e) { googlePayConfig = null }
  580. }
  581. if (!googlePayConfig || typeof googlePayConfig !== 'object' || !googlePayConfig.currencyCode) {
  582. const priceStr = String(this.price || '0.00')
  583. googlePayConfig = {
  584. environment: 3,
  585. paymentMethodType: 'CARD',
  586. currencyCode: 'MYR',
  587. countryCode: 'MY',
  588. totalPriceStatus: 'FINAL',
  589. totalPrice: priceStr,
  590. merchantName: '88LIVE',
  591. allowedAuthMethods: ['PAN_ONLY', 'CRYPTOGRAM_3DS'],
  592. allowedCardNetworks: ['AMEX', 'DISCOVER', 'JCB', 'MASTERCARD', 'VISA'],
  593. tokenizationSpecificationType: 'PAYMENT_GATEWAY',
  594. gateway: 'example',
  595. gatewayMerchantId: 'exampleGatewayMerchantId'
  596. }
  597. }
  598. if (googlePayConfig.totalPrice && typeof googlePayConfig.totalPrice !== 'string') {
  599. googlePayConfig.totalPrice = String(googlePayConfig.totalPrice)
  600. }
  601. try {
  602. plus.payment.request(this.googlePayProvider, googlePayConfig, (result) => {
  603. let paymentType = 'Google Pay'
  604. let accountInfo = ''
  605. if (result && result.paymentMethodData) {
  606. const desc = result.paymentMethodData.description || ''
  607. const cardTypeMatch = desc.match(/(Visa|Mastercard|American Express|Discover|JCB)/i)
  608. if (cardTypeMatch) paymentType = 'Google Pay (' + cardTypeMatch[1] + ')'
  609. const cardNumberMatch = desc.match(/(\d{4})/g)
  610. if (cardNumberMatch && cardNumberMatch.length > 0) accountInfo = '•••• ' + cardNumberMatch[cardNumberMatch.length - 1]
  611. }
  612. this.googlePaySuccess(paymentType, accountInfo)
  613. }, (error) => {
  614. const errorMsg = error.message || error.msg || JSON.stringify(error)
  615. let userMessage = '支付失敗'
  616. if (error.code === 'USER_CANCELLED' || errorMsg.includes('cancel')) {
  617. userMessage = '支付已取消'
  618. } else if (errorMsg) {
  619. userMessage = errorMsg
  620. }
  621. uni.showModal({ content: userMessage, showCancel: false, success: () => { this.disabled = false } })
  622. })
  623. } catch (e) {
  624. console.error('[GooglePay APP] 异常:', e)
  625. this.disabled = false
  626. }
  627. // #endif
  628. },
  629. googlePaySuccess(paymentType, accountInfo) {
  630. this.disabled = false
  631. uni.redirectTo({
  632. url: `/pages/user/money/google-pay-success?paymentType=${encodeURIComponent(paymentType)}&accountInfo=${encodeURIComponent(accountInfo)}`
  633. })
  634. },
  635. paySuccess() {
  636. this.disabled = false
  637. if (this.order_type === 'goods') {
  638. this.$store.commit('statistics/order', {
  639. pay: decimalSub(this.$store.state.statistics.order.pay, this.payNum),
  640. delive: decimalAdd(this.$store.state.statistics.order.delive, this.payNum)
  641. })
  642. }
  643. uni.redirectTo({
  644. url: `/pages/page/success?type=pay&order_type=${this.order_type || 'goods'}`
  645. })
  646. }
  647. }
  648. }
  649. </script>
  650. <style scoped>
  651. .pay-page {
  652. min-height: 100vh;
  653. background: #fff;
  654. }
  655. .wanl-pay .price-box {
  656. display: flex;
  657. flex-direction: column;
  658. align-items: center;
  659. padding: 60rpx 0 40rpx;
  660. }
  661. .wanl-pay .price-box .text-price {
  662. font-size: 60rpx;
  663. font-weight: bold;
  664. color: #ff6b35;
  665. }
  666. .payment-method-title {
  667. padding: 20rpx 0 20rpx 40rpx;
  668. }
  669. .payment-method-text {
  670. font-size: 32rpx;
  671. font-weight: 600;
  672. color: #333;
  673. }
  674. .list-box {
  675. padding: 0 30rpx;
  676. }
  677. .list-box .item {
  678. display: flex;
  679. align-items: center;
  680. padding: 30rpx 0;
  681. border-bottom: 1rpx solid #f5f5f5;
  682. }
  683. .fiuu-pay-icon {
  684. width: 60rpx;
  685. height: 60rpx;
  686. margin-right: 24rpx;
  687. flex-shrink: 0;
  688. }
  689. .list-box .item .action {
  690. flex: 1;
  691. }
  692. .list-box .item .action .title {
  693. font-size: 30rpx;
  694. display: block;
  695. margin-bottom: 6rpx;
  696. }
  697. .list-box .item .action view {
  698. font-size: 24rpx;
  699. color: #999;
  700. }
  701. .list-box .item .radio {
  702. padding: 0 20rpx;
  703. }
  704. .pay-bottom-placeholder {
  705. height: calc(160rpx + env(safe-area-inset-bottom, 0));
  706. }
  707. .pay-bottom-fixed {
  708. position: fixed;
  709. left: 0;
  710. right: 0;
  711. bottom: 0;
  712. background: #fff;
  713. padding: 30rpx 30rpx 0;
  714. padding-bottom: calc(30rpx + env(safe-area-inset-bottom, 0));
  715. border-top: 1rpx solid #eee;
  716. }
  717. .pay-bottom-buttons {
  718. display: flex;
  719. justify-content: center;
  720. }
  721. .mix-btn {
  722. width: 630rpx;
  723. height: 80rpx;
  724. line-height: 80rpx;
  725. text-align: center;
  726. font-size: 32rpx;
  727. color: #fff;
  728. border: none;
  729. border-radius: 10rpx;
  730. }
  731. .wanl-bg-redorange {
  732. background: linear-gradient(135deg, #ff6b35, #ff3d2e) !important;
  733. }
  734. /* 线下支付子列表 */
  735. .offline-sub-list {
  736. margin: 0 30rpx 20rpx;
  737. padding: 20rpx 24rpx;
  738. background: #f8f8f8;
  739. border-radius: 12rpx;
  740. border: 1rpx solid #eee;
  741. }
  742. .offline-reminder {
  743. font-size: 26rpx;
  744. color: #e54d42;
  745. line-height: 1.5;
  746. margin-bottom: 20rpx;
  747. }
  748. .offline-qrcode-wrap {
  749. display: flex;
  750. flex-direction: column;
  751. align-items: center;
  752. margin-bottom: 24rpx;
  753. padding: 24rpx 16rpx;
  754. background: #fff;
  755. border-radius: 12rpx;
  756. border: 1rpx solid #e5e5e5;
  757. }
  758. .offline-qrcode-title {
  759. font-size: 28rpx;
  760. color: #333;
  761. font-weight: 600;
  762. margin-bottom: 16rpx;
  763. align-self: flex-start;
  764. }
  765. .offline-qrcode-img {
  766. width: 360rpx;
  767. height: 360rpx;
  768. background: #fafafa;
  769. }
  770. .offline-qrcode-tip {
  771. margin-top: 12rpx;
  772. font-size: 24rpx;
  773. color: #999;
  774. }
  775. .offline-list {
  776. display: flex;
  777. flex-direction: column;
  778. gap: 16rpx;
  779. }
  780. .offline-list-item {
  781. display: flex;
  782. align-items: center;
  783. padding: 16rpx 20rpx;
  784. background: #fff;
  785. border-radius: 10rpx;
  786. border: 1rpx solid #e5e5e5;
  787. }
  788. .offline-list-title {
  789. font-size: 26rpx;
  790. color: #666;
  791. min-width: 180rpx;
  792. }
  793. .offline-list-name {
  794. font-size: 28rpx;
  795. color: #333;
  796. flex: 1;
  797. word-break: break-all;
  798. }
  799. /* Google Pay 按钮 */
  800. .google-pay-button-app {
  801. width: 630rpx;
  802. height: 80rpx;
  803. background: #000;
  804. border: none;
  805. border-radius: 10rpx;
  806. display: flex;
  807. align-items: center;
  808. justify-content: center;
  809. }
  810. .google-pay-button-content {
  811. display: flex;
  812. align-items: center;
  813. justify-content: center;
  814. }
  815. .google-pay-text-white {
  816. color: #fff;
  817. font-size: 28rpx;
  818. }
  819. .google-pay-g-icon {
  820. width: 32rpx;
  821. height: 32rpx;
  822. margin: 0 4rpx;
  823. }
  824. .google-pay-web-container {
  825. width: 630rpx;
  826. }
  827. .gpay-web-button-wrapper {
  828. min-height: 80rpx;
  829. display: flex;
  830. align-items: center;
  831. justify-content: center;
  832. }
  833. .web-button-loading {
  834. font-size: 28rpx;
  835. color: #666;
  836. }
  837. .mix-btn-disabled {
  838. opacity: 0.5;
  839. }
  840. </style>