index.vue 12 KB

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