index.vue 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462
  1. <template>
  2. <view v-if="!isLoading" class="container" :style="appThemeStyle">
  3. <!-- 订单信息 -->
  4. <view class="order-info">
  5. <!-- 支付剩余时间 -->
  6. <view v-if="order.showExpiration" class="order-countdown">
  7. <text class="m-r-6">剩余时间</text>
  8. <count-down :date="order.expirationTime" separator="zh" theme="text" textColor="#666666" customNumColor="#666666" />
  9. </view>
  10. <!-- 付款金额 -->
  11. <view class="order-amount">
  12. <text class="unit">¥</text>
  13. <text class="amount">{{ order.pay_price }}</text>
  14. </view>
  15. </view>
  16. <!-- 支付方式 -->
  17. <view class="payment-method">
  18. <view v-for="(item, index) in methods" :key="index" class="pay-item dis-flex flex-x-between" @click="handleSelectPayType(index)">
  19. <view class="item-left dis-flex flex-y-center">
  20. <view class="item-left_icon" :class="[item.method]">
  21. <text class="iconfont" :class="[PayMethodIconEnum[item.method]]"></text>
  22. </view>
  23. <view class="item-left_text">
  24. <text>{{ PayMethodEnum[item.method].name }}</text>
  25. </view>
  26. <view v-if="item.method === PayMethodEnum.BALANCE.value" class="user-balance">
  27. <text>(可用¥{{ personal.balance }}元)</text>
  28. </view>
  29. </view>
  30. <view class="item-right col-m" v-if="curPaymentItem && curPaymentItem.method == item.method">
  31. <text class="iconfont icon-check"></text>
  32. </view>
  33. </view>
  34. </view>
  35. <!-- 确认按钮 -->
  36. <view class="footer-fixed">
  37. <view class="btn-wrapper">
  38. <view class="btn-item btn-item-main" :class="{ disabled }" @click="handleSubmit()">确认支付</view>
  39. </view>
  40. </view>
  41. <!-- 支付确认弹窗 -->
  42. <!-- #ifdef H5 -->
  43. <u-modal v-if="tempUnifyData" v-model="showConfirmModal" title="支付确认" show-cancel-button confirm-text="已完成支付"
  44. :confirm-color="appTheme.mainBg" negative-top="100" :asyncClose="true"
  45. @confirm="onTradeQuery(tempUnifyData.outTradeNo, tempUnifyData.method)">
  46. <view class="modal-content">
  47. <text>请在{{ PayMethodClientNameEnum[tempUnifyData.method] }}内完成支付,如果您已经支付成功,请点击“已完成支付”按钮</text>
  48. </view>
  49. </u-modal>
  50. <!-- #endif -->
  51. </view>
  52. </template>
  53. <script>
  54. import storage from '@/utils/storage'
  55. import { inArray, urlEncode } from '@/utils/util'
  56. import { Alipay, Wechat } from '@/core/payment'
  57. import CountDown from '@/components/countdown'
  58. import { PayMethodEnum } from '@/common/enum/payment'
  59. import { PayStatusEnum } from '@/common/enum/order'
  60. import * as CashierApi from '@/api/cashier'
  61. // 支付方式对应的图标
  62. const PayMethodIconEnum = {
  63. [PayMethodEnum.WECHAT.value]: 'icon-wechat-pay',
  64. [PayMethodEnum.ALIPAY.value]: 'icon-alipay',
  65. [PayMethodEnum.BALANCE.value]: 'icon-balance-pay'
  66. }
  67. // 支付方式的终端名称
  68. const PayMethodClientNameEnum = {
  69. [PayMethodEnum.WECHAT.value]: '微信',
  70. [PayMethodEnum.ALIPAY.value]: '支付宝'
  71. }
  72. // H5端支付下单时的数据
  73. // 用于从第三方支付页返回到收银台页面后拿到下单数据
  74. const getTempUnifyData = orderKey => {
  75. // if (window.performance && window.performance.navigation.type == 2) {
  76. const tempUnifyData = storage.get('tempUnifyData_' + orderKey)
  77. if (tempUnifyData) {
  78. storage.remove('tempUnifyData_' + orderKey)
  79. return tempUnifyData
  80. }
  81. // }
  82. return null
  83. }
  84. export default {
  85. components: {
  86. CountDown
  87. },
  88. data() {
  89. return {
  90. // 加载中
  91. isLoading: true,
  92. // 确认按钮禁用
  93. disabled: false,
  94. // 枚举类
  95. PayMethodEnum,
  96. PayMethodIconEnum,
  97. PayMethodClientNameEnum,
  98. // 当前选中的支付方式
  99. curPaymentItem: null,
  100. // 当前订单ID
  101. orderId: null,
  102. // 当前结算订单信息
  103. order: {},
  104. // 个人信息
  105. personal: { balance: '0.00' },
  106. // 当前客户端的支付方式列表(后端根据platform判断)
  107. methods: [],
  108. // 支付确认弹窗
  109. showConfirmModal: false,
  110. // #ifdef H5
  111. // 当前第三方支付信息 (临时数据, 仅用于H5端)
  112. tempUnifyData: { outTradeNo: '', method: '' },
  113. // #endif
  114. }
  115. },
  116. /**
  117. * 生命周期函数--监听页面加载
  118. */
  119. onLoad({ orderId }) {
  120. // 记录订单ID
  121. this.orderId = Number(orderId)
  122. },
  123. /**
  124. * 生命周期函数--监听页面显示
  125. */
  126. onShow() {
  127. // 获取收银台信息
  128. this.getCashierInfo()
  129. },
  130. methods: {
  131. // 获取收银台信息
  132. getCashierInfo() {
  133. const app = this
  134. app.isLoading = true
  135. CashierApi.orderInfo(app.orderId, { client: app.platform })
  136. .then(result => {
  137. app.order = result.data.order
  138. app.personal = result.data.personal
  139. app.methods = result.data.paymentMethods
  140. app.isLoading = false
  141. app.setDefaultPayType()
  142. app.checkOrderPayStatus()
  143. // #ifdef H5
  144. // 判断当前页面来源于浏览器返回
  145. this.performance()
  146. // #endif
  147. })
  148. },
  149. // 设置默认的支付方式
  150. setDefaultPayType() {
  151. const app = this
  152. if (app.disabled) return
  153. if (app.curPaymentItem) return
  154. const defaultIndex = app.methods.findIndex(item => item.is_default == true)
  155. defaultIndex > -1 && app.handleSelectPayType(defaultIndex)
  156. },
  157. // 判断当前订单是否为已支付
  158. checkOrderPayStatus() {
  159. const app = this
  160. if (app.order.pay_status == PayStatusEnum.SUCCESS.value) {
  161. app.$toast('恭喜您,订单已付款成功')
  162. app.onSuccessNav()
  163. }
  164. },
  165. // 选择支付方式
  166. handleSelectPayType(index) {
  167. this.curPaymentItem = this.methods[index]
  168. },
  169. // 判断当前页面来源于浏览器返回并提示手动查单
  170. // #ifdef H5
  171. performance() {
  172. const app = this
  173. // 判断订单状态, 异步回调会将订单状态变为已支付, 那么就不需要让用户手动查单了
  174. if (app.order.pay_status == PayStatusEnum.PENDING.value) {
  175. const performanceData = getTempUnifyData(app.orderId)
  176. if (performanceData) {
  177. app.tempUnifyData = performanceData
  178. app.showConfirmModal = true
  179. }
  180. }
  181. },
  182. // #endif
  183. // 确认支付
  184. handleSubmit() {
  185. const app = this
  186. // 判断是否选择了支付方式
  187. if (!app.curPaymentItem) {
  188. app.$toast('您还没有选择支付方式')
  189. return
  190. }
  191. // 按钮禁用
  192. if (app.disabled) return
  193. app.disabled = true
  194. // 提交到后端API
  195. CashierApi.orderPay(app.orderId, {
  196. method: app.curPaymentItem.method,
  197. client: app.platform,
  198. extra: app.getExtraAsUnify(app.curPaymentItem.method)
  199. })
  200. .then(result => app.onSubmitCallback(result))
  201. .finally(err => setTimeout(() => app.disabled = false, 10))
  202. },
  203. // 获取第三方支付的扩展参数
  204. getExtraAsUnify(method) {
  205. if (method === PayMethodEnum.ALIPAY.value) {
  206. return Alipay.extraAsUnify()
  207. }
  208. if (method === PayMethodEnum.WECHAT.value) {
  209. return Wechat.extraAsUnify()
  210. }
  211. return {}
  212. },
  213. // 订单提交成功后回调
  214. onSubmitCallback(result) {
  215. const app = this
  216. const method = app.curPaymentItem.method
  217. const paymentData = result.data.payment
  218. // 余额支付
  219. if (method === PayMethodEnum.BALANCE.value) {
  220. app.onShowSuccess(result)
  221. }
  222. // 发起支付宝支付
  223. if (method === PayMethodEnum.ALIPAY.value) {
  224. console.log('paymentData', paymentData)
  225. Alipay.payment({ orderKey: app.orderId, ...paymentData })
  226. .then(res => app.onPaySuccess(res))
  227. .catch(err => app.onPayFail(err))
  228. }
  229. // 发起微信支付
  230. if (method === PayMethodEnum.WECHAT.value) {
  231. console.log('paymentData', paymentData)
  232. Wechat.payment({ orderKey: app.orderId, ...paymentData })
  233. .then(res => app.onPaySuccess(res))
  234. .catch(err => app.onPayFail(err))
  235. }
  236. },
  237. // 订单支付成功的回调方法
  238. // 这里只是前端支付api返回结果success,实际订单是否支付成功 以后端的查单和异步通知为准
  239. onPaySuccess({ res, option: { isRequireQuery, outTradeNo, method } }) {
  240. const app = this
  241. // 判断是否需要主动查单
  242. // isRequireQuery为true代表需要主动查单
  243. if (isRequireQuery) {
  244. app.onTradeQuery(outTradeNo, method)
  245. return true
  246. }
  247. this.onShowSuccess(res)
  248. },
  249. // 显示支付成功信息并页面跳转
  250. onShowSuccess({ message }) {
  251. this.$toast(message || '订单支付成功')
  252. this.onSuccessNav()
  253. },
  254. // 订单支付失败
  255. onPayFail(err) {
  256. console.log('onPayFail', err)
  257. const errMsg = err.message || '订单未支付'
  258. this.$error(errMsg)
  259. },
  260. // 已完成支付按钮事件: 请求后端查单
  261. onTradeQuery(outTradeNo, method) {
  262. const app = this
  263. // 交易查询
  264. // 查询第三方支付订单是否付款成功
  265. CashierApi.tradeQuery({ outTradeNo, method, client: app.platform })
  266. .then(result => result.data.isPay ? app.onShowSuccess(result) : app.onPayFail(result))
  267. .finally(() => app.showConfirmModal = false)
  268. },
  269. // 支付成功后的跳转
  270. onSuccessNav() {
  271. // 相应全局事件订阅: 刷新上级页面数据
  272. uni.$emit('syncRefresh', true)
  273. // 获取上级页面
  274. const pages = getCurrentPages()
  275. const lastPage = pages.length < 2 ? null : pages[pages.length - 2]
  276. const backRoutes = [
  277. 'pages/order/index',
  278. 'pages/order/detail'
  279. ]
  280. setTimeout(() => {
  281. if (lastPage && inArray(lastPage.route, backRoutes)) {
  282. uni.navigateBack()
  283. } else {
  284. this.$navTo('pages/order/index', {}, 'redirectTo')
  285. }
  286. }, 1200)
  287. },
  288. }
  289. }
  290. </script>
  291. <style>
  292. page {
  293. background: #F4F4F4;
  294. }
  295. </style>
  296. <style lang="scss" scoped>
  297. .container {
  298. background-color: #F4F4F4;
  299. }
  300. // 订单信息
  301. .order-info {
  302. padding: 80rpx 0;
  303. text-align: center;
  304. .order-countdown {
  305. display: flex;
  306. justify-content: center;
  307. font-size: 26rpx;
  308. color: #666666;
  309. margin-bottom: 20rpx;
  310. }
  311. .order-amount {
  312. margin: 0 auto;
  313. max-width: 50%;
  314. display: flex;
  315. align-items: center;
  316. justify-content: center;
  317. color: #111111;
  318. .unit {
  319. font-size: 30rpx;
  320. margin-bottom: -18rpx;
  321. }
  322. .amount {
  323. font-size: 56rpx;
  324. }
  325. }
  326. }
  327. // 支付方式
  328. .payment-method {
  329. width: 94%;
  330. margin: 0 auto 20rpx auto;
  331. padding: 0 40rpx;
  332. background-color: #ffffff;
  333. border-radius: 20rpx;
  334. .pay-item {
  335. padding: 26rpx 0;
  336. font-size: 28rpx;
  337. border-bottom: 1rpx solid rgb(248, 248, 248);
  338. &:last-child {
  339. border-bottom: none;
  340. }
  341. .item-left_icon {
  342. margin-right: 20rpx;
  343. font-size: 44rpx;
  344. &.wechat {
  345. color: #00c800;
  346. }
  347. &.alipay {
  348. color: #009fe8;
  349. }
  350. &.balance {
  351. color: #ff9700;
  352. }
  353. }
  354. .item-left_text {
  355. font-size: 28rpx;
  356. }
  357. .item-right {
  358. font-size: 32rpx;
  359. }
  360. .user-balance {
  361. margin-left: 20rpx;
  362. font-size: 26rpx;
  363. }
  364. }
  365. }
  366. // 支付确认弹窗
  367. .modal-content {
  368. padding: 40rpx 48rpx;
  369. font-size: 30rpx;
  370. line-height: 50rpx;
  371. text-align: left;
  372. color: #606266;
  373. // height: 620rpx;
  374. box-sizing: border-box;
  375. }
  376. // 底部操作栏
  377. .footer-fixed {
  378. position: fixed;
  379. bottom: var(--window-bottom);
  380. left: var(--window-left);
  381. right: var(--window-right);
  382. z-index: 11;
  383. box-shadow: 0 -4rpx 40rpx 0 rgba(151, 151, 151, 0.24);
  384. background: #fff;
  385. // 设置ios刘海屏底部横线安全区域
  386. padding-bottom: constant(safe-area-inset-bottom);
  387. padding-bottom: env(safe-area-inset-bottom);
  388. .btn-wrapper {
  389. height: 120rpx;
  390. display: flex;
  391. align-items: center;
  392. padding: 0 40rpx;
  393. }
  394. .btn-item {
  395. flex: 1;
  396. font-size: 28rpx;
  397. height: 80rpx;
  398. color: #fff;
  399. border-radius: 50rpx;
  400. display: flex;
  401. justify-content: center;
  402. align-items: center;
  403. }
  404. .btn-item-main {
  405. background: linear-gradient(to right, $main-bg, $main-bg2);
  406. color: $main-text;
  407. // 禁用按钮
  408. &.disabled {
  409. opacity: 0.6;
  410. }
  411. }
  412. }
  413. </style>