payment.vue 10.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374
  1. <template>
  2. <view class="container">
  3. <view v-if="loading" class="loading-wrap">
  4. <text class="loading-text">加载中...</text>
  5. </view>
  6. <scroll-view v-else scroll-y class="content">
  7. <!-- Header Section -->
  8. <view class="header-section">
  9. <text class="header-title">订单待支付</text>
  10. <text class="header-countdown" v-if="countdown > 0">
  11. 支付剩余时间: {{ formatCountdown }}
  12. </text>
  13. <text class="header-cancelled" v-else>
  14. 订单已取消
  15. </text>
  16. </view>
  17. <!-- Order Info Card -->
  18. <view class="section">
  19. <view class="info-row">
  20. <text class="info-label">订单编号</text>
  21. <text class="info-value">{{ orderNo }}</text>
  22. </view>
  23. <view class="info-row" v-if="pointsUsed > 0">
  24. <text class="info-label">积分抵扣</text>
  25. <text class="info-value points-value">-{{ formatPrice(pointsUsed) }} ({{ pointsUsed }}积分)</text>
  26. </view>
  27. <view class="info-row">
  28. <text class="info-label">订单金额</text>
  29. <text class="order-amount">{{ formatPriceWithSymbol(amount) }}</text>
  30. </view>
  31. </view>
  32. <!-- Payment Method -->
  33. <view class="section">
  34. <text class="section-title">支付方式</text>
  35. <view class="payment-method-row">
  36. <view class="payment-method-left">
  37. <text class="payment-icon">💳</text>
  38. <text class="payment-name">微信支付</text>
  39. </view>
  40. <text class="payment-checked">✓</text>
  41. </view>
  42. </view>
  43. <!-- Pay Button -->
  44. <view class="pay-section">
  45. <button
  46. :class="['pay-btn', (paying || countdown <= 0) ? 'disabled' : '']"
  47. :disabled="paying || countdown <= 0"
  48. @click="onPay"
  49. >
  50. {{ paying ? '支付中...' : '微信支付 ' + formatPriceWithSymbol(amount) }}
  51. </button>
  52. <text class="cancel-link" @click="onCancelOrder">取消订单</text>
  53. </view>
  54. </scroll-view>
  55. </view>
  56. </template>
  57. <script>
  58. import config from '@/config.js'
  59. export default {
  60. data() {
  61. return {
  62. orderNo: '',
  63. amount: 0,
  64. countdown: 900,
  65. paying: false,
  66. loading: false,
  67. timer: null,
  68. pointsUsed: 0,
  69. pointsUsed: 0
  70. }
  71. },
  72. computed: {
  73. formatCountdown() {
  74. var min = Math.floor(this.countdown / 60)
  75. var sec = this.countdown % 60
  76. return String(min).padStart(2, '0') + ':' + String(sec).padStart(2, '0')
  77. }
  78. },
  79. onLoad(options) {
  80. if (options.orderNo) {
  81. this.orderNo = options.orderNo
  82. }
  83. if (options.amount) {
  84. this.amount = parseFloat(options.amount)
  85. }
  86. if (options.pointsUsed) {
  87. this.pointsUsed = parseInt(options.pointsUsed)
  88. }
  89. this.startCountdown()
  90. },
  91. onUnload() {
  92. if (this.timer) {
  93. clearInterval(this.timer)
  94. this.timer = null
  95. }
  96. },
  97. methods: {
  98. startCountdown() {
  99. var that = this
  100. this.timer = setInterval(function() {
  101. if (that.countdown > 0) {
  102. that.countdown = that.countdown - 1
  103. } else {
  104. if (that.timer) {
  105. clearInterval(that.timer)
  106. that.timer = null
  107. }
  108. }
  109. }, 1000)
  110. },
  111. onPay() {
  112. if (this.countdown <= 0) {
  113. uni.showToast({ title: '订单已取消,无法支付', icon: 'none' })
  114. return
  115. }
  116. this.paying = true
  117. var that = this
  118. uni.request({
  119. url: config.api('/api/product/order/pay'),
  120. method: 'POST',
  121. data: { orderNo: this.orderNo },
  122. header: {
  123. 'Content-Type': 'application/json',
  124. 'Authorization': 'Bearer ' + uni.getStorageSync('token')
  125. },
  126. success: function(res) {
  127. that.paying = false
  128. if (res.data && res.data.code === 200) {
  129. var data = res.data.data
  130. if (data.noMoneyPayment) {
  131. // Points fully covered the order, no WeChat needed
  132. uni.request({
  133. url: config.api('/api/product/order/handlePaymentSuccess'),
  134. method: 'POST',
  135. data: { orderNo: that.orderNo },
  136. header: {
  137. 'Content-Type': 'application/json',
  138. 'Authorization': 'Bearer ' + uni.getStorageSync('token')
  139. },
  140. success: function(payRes) {
  141. if (payRes.data && payRes.data.code === 200) {
  142. uni.showToast({ title: '支付成功', icon: 'success' })
  143. setTimeout(function() {
  144. uni.redirectTo({
  145. url: '/pages/shop/order-list/order-list?tab=paid'
  146. })
  147. }, 1500)
  148. } else {
  149. uni.showToast({ title: payRes.data.message || '处理失败', icon: 'none' })
  150. }
  151. },
  152. fail: function() {
  153. uni.showToast({ title: '网络请求失败', icon: 'none' })
  154. }
  155. })
  156. } else if (data.wechatPayParams) {
  157. var params = data.wechatPayParams
  158. uni.requestPayment({
  159. timeStamp: params.timeStamp,
  160. nonceStr: params.nonceStr,
  161. package: params.package,
  162. signType: params.signType || 'RSA',
  163. paySign: params.paySign,
  164. success: function() {
  165. // Notify backend to update order status
  166. uni.request({
  167. url: config.api('/api/product/order/handlePaymentSuccess'),
  168. method: 'POST',
  169. data: { orderNo: that.orderNo },
  170. header: {
  171. 'Content-Type': 'application/json',
  172. 'Authorization': 'Bearer ' + uni.getStorageSync('token')
  173. }
  174. })
  175. uni.showToast({ title: '支付成功', icon: 'success' })
  176. setTimeout(function() {
  177. uni.redirectTo({
  178. url: '/pages/shop/order-list/order-list?tab=paid'
  179. })
  180. }, 1500)
  181. },
  182. fail: function() {
  183. uni.showToast({ title: '支付失败', icon: 'none' })
  184. }
  185. })
  186. } else {
  187. uni.showToast({ title: '获取支付参数失败', icon: 'none' })
  188. }
  189. } else {
  190. uni.showToast({ title: res.data.message || '支付失败', icon: 'none' })
  191. }
  192. },
  193. fail: function() {
  194. that.paying = false
  195. uni.showToast({ title: '网络请求失败', icon: 'none' })
  196. }
  197. })
  198. },
  199. onCancelOrder() {
  200. var that = this
  201. uni.showModal({
  202. title: '确认取消',
  203. content: '确定取消该订单吗?',
  204. success: function(confirm) {
  205. if (confirm.confirm) {
  206. uni.request({
  207. url: config.api('/api/product/order/cancel'),
  208. method: 'POST',
  209. data: { orderNo: that.orderNo },
  210. header: {
  211. 'Content-Type': 'application/json',
  212. 'Authorization': 'Bearer ' + uni.getStorageSync('token')
  213. },
  214. success: function(res) {
  215. if (res.data && res.data.code === 200) {
  216. uni.showToast({ title: '订单已取消', icon: 'success' })
  217. setTimeout(function() {
  218. uni.navigateBack()
  219. }, 1500)
  220. } else {
  221. uni.showToast({ title: res.data.message || '取消失败', icon: 'none' })
  222. }
  223. }
  224. })
  225. }
  226. }
  227. })
  228. },
  229. formatPrice(price) {
  230. if (price === null || price === undefined) return '0.00'
  231. return (Number(price) / 100).toFixed(2)
  232. }
  233. }
  234. }
  235. </script>
  236. <style scoped>
  237. .container {
  238. min-height: 100vh;
  239. background: #f5f5f5;
  240. }
  241. .loading-wrap {
  242. display: flex;
  243. justify-content: center;
  244. padding-top: 200rpx;
  245. }
  246. .loading-text {
  247. font-size: 28rpx;
  248. color: #999;
  249. }
  250. .content {
  251. height: calc(100vh - 120rpx);
  252. }
  253. .header-section {
  254. background: linear-gradient(135deg, #F97316, #FB923C);
  255. padding: 50rpx 30rpx;
  256. display: flex;
  257. flex-direction: column;
  258. align-items: center;
  259. }
  260. .header-title {
  261. font-size: 36rpx;
  262. font-weight: bold;
  263. color: #fff;
  264. margin-bottom: 12rpx;
  265. }
  266. .header-countdown {
  267. font-size: 26rpx;
  268. color: rgba(255,255,255,0.9);
  269. }
  270. .header-cancelled {
  271. font-size: 26rpx;
  272. color: rgba(255,255,255,0.7);
  273. }
  274. .section {
  275. background: #fff;
  276. margin: 20rpx;
  277. border-radius: 16rpx;
  278. padding: 24rpx;
  279. }
  280. .section-title {
  281. font-size: 28rpx;
  282. font-weight: bold;
  283. color: #333;
  284. margin-bottom: 20rpx;
  285. display: block;
  286. }
  287. .info-row {
  288. display: flex;
  289. justify-content: space-between;
  290. align-items: center;
  291. padding: 16rpx 0;
  292. border-bottom: 1rpx solid #f5f5f5;
  293. }
  294. .info-row:last-child {
  295. border-bottom: none;
  296. }
  297. .info-label {
  298. font-size: 26rpx;
  299. color: #999;
  300. }
  301. .info-value {
  302. font-size: 26rpx;
  303. color: #333;
  304. }
  305. .order-amount {
  306. font-size: 40rpx;
  307. color: #F97316;
  308. font-weight: bold;
  309. }
  310. .payment-method-row {
  311. display: flex;
  312. align-items: center;
  313. justify-content: space-between;
  314. padding: 16rpx 0;
  315. }
  316. .payment-method-left {
  317. display: flex;
  318. align-items: center;
  319. }
  320. .payment-icon {
  321. font-size: 40rpx;
  322. margin-right: 16rpx;
  323. }
  324. .payment-name {
  325. font-size: 28rpx;
  326. color: #333;
  327. }
  328. .payment-checked {
  329. font-size: 32rpx;
  330. color: #52c41a;
  331. font-weight: bold;
  332. }
  333. .pay-section {
  334. display: flex;
  335. flex-direction: column;
  336. align-items: center;
  337. padding: 40rpx 30rpx;
  338. }
  339. .pay-btn {
  340. width: 100%;
  341. height: 88rpx;
  342. line-height: 88rpx;
  343. background: linear-gradient(135deg, #F97316, #FB923C);
  344. color: #fff;
  345. font-size: 32rpx;
  346. font-weight: bold;
  347. border-radius: 44rpx;
  348. border: none;
  349. text-align: center;
  350. }
  351. .pay-btn::after {
  352. border: none;
  353. }
  354. .pay-btn.disabled {
  355. background: #ddd;
  356. color: #999;
  357. }
  358. .cancel-link {
  359. margin-top: 24rpx;
  360. font-size: 26rpx;
  361. color: #999;
  362. text-decoration: underline;
  363. padding: 10rpx;
  364. },
  365. .points-value {
  366. color: #F97316;
  367. }
  368. </style>