304 lines
7.7 KiB
Vue
304 lines
7.7 KiB
Vue
<template>
|
|
<view class="pay-page container page-bottom-safe" v-if="order">
|
|
<view class="status-card card">
|
|
<text class="business">{{ order.business_type_label }}</text>
|
|
<text class="amount">¥{{ order.amount }}</text>
|
|
<text v-if="order.expire_at" class="expire">{{ paymentCountdownText }}</text>
|
|
</view>
|
|
|
|
<view class="card order-card">
|
|
<view class="info-row">
|
|
<text>订单号</text>
|
|
<text>{{ order.order_no }}</text>
|
|
</view>
|
|
<view class="info-row">
|
|
<text>创建时间</text>
|
|
<text>{{ formatDateTime(order.created_at) }}</text>
|
|
</view>
|
|
<view class="info-row">
|
|
<text>订单状态</text>
|
|
<text>{{ order.status_label }}</text>
|
|
</view>
|
|
</view>
|
|
|
|
<view class="card method-card">
|
|
<text class="card-title">支付方式</text>
|
|
<view
|
|
v-if="!isWechatOnlyOrder"
|
|
class="method-item"
|
|
:class="{ active: payMethod === 'balance', disabled: !canUseBalance }"
|
|
@tap="selectMethod('balance')"
|
|
>
|
|
<view>
|
|
<text class="method-title">余额支付</text>
|
|
<text class="method-sub">当前余额 ¥{{ balance.balance }}</text>
|
|
</view>
|
|
<text class="method-check">{{ payMethod === 'balance' ? '已选' : '' }}</text>
|
|
</view>
|
|
<view class="method-item" :class="{ active: payMethod === 'wechat' }" @tap="selectMethod('wechat')">
|
|
<view>
|
|
<text class="method-title">微信支付</text>
|
|
<text class="method-sub">使用微信支付完成订单</text>
|
|
</view>
|
|
<text class="method-check">{{ payMethod === 'wechat' ? '已选' : '' }}</text>
|
|
</view>
|
|
</view>
|
|
|
|
<button v-if="order.status === 'pending_pay'" class="ghost-button cancel-btn" @tap="closeCurrentOrder">取消订单</button>
|
|
|
|
<view class="fixed-action-bar">
|
|
<view class="bar-price">
|
|
<text class="bar-label">待支付</text>
|
|
<text class="price-text">¥{{ order.amount }}</text>
|
|
</view>
|
|
<button class="primary-button submit-btn" :disabled="!canPay || isSubmitting" @tap="pay">
|
|
{{ payButtonText }}
|
|
</button>
|
|
</view>
|
|
</view>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { computed, onUnmounted, ref } from 'vue'
|
|
import { onLoad } from '@dcloudio/uni-app'
|
|
import { memberApi } from '@/modules/comprehensive/api/member'
|
|
import { orderApi } from '@/modules/comprehensive/api/order'
|
|
import { formatDateTime } from '@/modules/comprehensive/utils/date'
|
|
import { toAmountNumber } from '@/modules/comprehensive/utils/money'
|
|
|
|
const orderId = ref('')
|
|
const order = ref(null)
|
|
const balance = ref({ balance: '0.00' })
|
|
const payMethod = ref('balance')
|
|
const isSubmitting = ref(false)
|
|
const nowTimestamp = ref(Date.now())
|
|
let countdownTimer = null
|
|
|
|
const isWechatOnlyOrder = computed(() => order.value?.business_type !== 'private_booking')
|
|
const canUseBalance = computed(() => {
|
|
return toAmountNumber(balance.value.balance) >= toAmountNumber(order.value?.amount)
|
|
})
|
|
const canPay = computed(() => order.value?.status === 'pending_pay' && (payMethod.value !== 'balance' || canUseBalance.value))
|
|
const payButtonText = computed(() => {
|
|
if (order.value?.status !== 'pending_pay') return '订单不可支付'
|
|
return payMethod.value === 'balance' ? '余额支付' : '微信支付'
|
|
})
|
|
const paymentRemainingSeconds = computed(() => {
|
|
if (!order.value?.expire_at) return 0
|
|
return Math.max(0, Math.ceil((new Date(order.value.expire_at).getTime() - nowTimestamp.value) / 1000))
|
|
})
|
|
const paymentCountdownText = computed(() => {
|
|
const seconds = paymentRemainingSeconds.value
|
|
if (seconds <= 0) return '支付倒计时 00:00:00'
|
|
const hours = Math.floor(seconds / 3600)
|
|
const minutes = Math.floor((seconds % 3600) / 60)
|
|
const remainSeconds = seconds % 60
|
|
return `支付倒计时 ${padTime(hours)}:${padTime(minutes)}:${padTime(remainSeconds)}`
|
|
})
|
|
|
|
onLoad(async (query) => {
|
|
orderId.value = query.order_id || query.id
|
|
await loadPage()
|
|
})
|
|
|
|
onUnmounted(() => {
|
|
stopCountdown()
|
|
})
|
|
|
|
const loadPage = async () => {
|
|
order.value = await orderApi.getOrderDetail(orderId.value)
|
|
if (isWechatOnlyOrder.value) {
|
|
payMethod.value = 'wechat'
|
|
} else {
|
|
balance.value = await memberApi.getBalance()
|
|
payMethod.value = canUseBalance.value ? 'balance' : 'wechat'
|
|
}
|
|
updateCountdownTimer()
|
|
}
|
|
|
|
const updateCountdownTimer = () => {
|
|
nowTimestamp.value = Date.now()
|
|
stopCountdown()
|
|
if (!order.value?.expire_at || order.value.status !== 'pending_pay') return
|
|
countdownTimer = setInterval(() => {
|
|
nowTimestamp.value = Date.now()
|
|
if (paymentRemainingSeconds.value <= 0) stopCountdown()
|
|
}, 1000)
|
|
}
|
|
|
|
const stopCountdown = () => {
|
|
if (!countdownTimer) return
|
|
clearInterval(countdownTimer)
|
|
countdownTimer = null
|
|
}
|
|
|
|
const padTime = (value) => String(value).padStart(2, '0')
|
|
|
|
const selectMethod = (method) => {
|
|
if (method === 'balance' && !canUseBalance.value) {
|
|
uni.showToast({ title: '余额不足', icon: 'none' })
|
|
return
|
|
}
|
|
payMethod.value = method
|
|
}
|
|
|
|
const pay = async () => {
|
|
if (!canPay.value || isSubmitting.value) return
|
|
isSubmitting.value = true
|
|
try {
|
|
if (payMethod.value === 'balance') {
|
|
await orderApi.payOrderByBalance(order.value.id)
|
|
uni.showToast({ title: '支付成功', icon: 'success' })
|
|
} else {
|
|
const paymentData = await orderApi.payOrderByWechat(order.value.id)
|
|
await requestWechatPayment(paymentData.wechat)
|
|
uni.showToast({ title: '支付已提交', icon: 'success' })
|
|
}
|
|
await loadPage()
|
|
setTimeout(() => {
|
|
uni.redirectTo({ url: `/pages/comprehensive/orders/detail?id=${order.value.id}` })
|
|
}, 350)
|
|
} finally {
|
|
isSubmitting.value = false
|
|
}
|
|
}
|
|
|
|
const closeCurrentOrder = async () => {
|
|
await orderApi.closeOrder(order.value.id)
|
|
uni.showToast({ title: '订单已取消', icon: 'none' })
|
|
await loadPage()
|
|
}
|
|
|
|
const requestWechatPayment = (wechatPayload) => new Promise((resolve, reject) => {
|
|
uni.requestPayment({
|
|
...wechatPayload,
|
|
success: resolve,
|
|
fail: reject
|
|
})
|
|
})
|
|
</script>
|
|
|
|
<style lang="scss" scoped>
|
|
.status-card {
|
|
text-align: center;
|
|
margin-bottom: $spacing-md;
|
|
}
|
|
|
|
.business {
|
|
display: block;
|
|
color: $text-color-muted;
|
|
font-size: 26rpx;
|
|
font-weight: 700;
|
|
}
|
|
|
|
.amount {
|
|
display: block;
|
|
margin: $spacing-sm 0;
|
|
color: $text-color-main;
|
|
font-size: 68rpx;
|
|
font-weight: 900;
|
|
}
|
|
|
|
.expire {
|
|
display: block;
|
|
margin-top: $spacing-md;
|
|
color: $color-warning;
|
|
font-size: 24rpx;
|
|
}
|
|
|
|
.order-card,
|
|
.method-card {
|
|
margin-bottom: $spacing-md;
|
|
}
|
|
|
|
.info-row {
|
|
min-height: 68rpx;
|
|
display: flex;
|
|
justify-content: space-between;
|
|
gap: $spacing-md;
|
|
color: $text-color-muted;
|
|
font-size: 25rpx;
|
|
border-bottom: 1rpx solid $bg-color-hover;
|
|
}
|
|
|
|
.info-row:last-child {
|
|
border-bottom: none;
|
|
}
|
|
|
|
.info-row text:last-child {
|
|
flex: 1;
|
|
color: $text-color-main;
|
|
font-weight: 700;
|
|
text-align: right;
|
|
}
|
|
|
|
.card-title {
|
|
display: block;
|
|
margin-bottom: $spacing-sm;
|
|
color: $text-color-main;
|
|
font-size: 30rpx;
|
|
font-weight: 900;
|
|
}
|
|
|
|
.method-item {
|
|
min-height: 110rpx;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
padding: $spacing-md 0;
|
|
border-bottom: 1rpx solid $bg-color-hover;
|
|
}
|
|
|
|
.method-item:last-child {
|
|
border-bottom: none;
|
|
}
|
|
|
|
.method-item.active .method-title {
|
|
color: $color-primary;
|
|
}
|
|
|
|
.method-item.disabled {
|
|
opacity: 0.45;
|
|
}
|
|
|
|
.method-title,
|
|
.method-sub {
|
|
display: block;
|
|
}
|
|
|
|
.method-title {
|
|
color: $text-color-main;
|
|
font-size: 28rpx;
|
|
font-weight: 900;
|
|
}
|
|
|
|
.method-sub,
|
|
.method-check {
|
|
color: $text-color-muted;
|
|
font-size: 24rpx;
|
|
}
|
|
|
|
.method-check {
|
|
color: $color-primary;
|
|
font-weight: 800;
|
|
}
|
|
|
|
.cancel-btn {
|
|
margin-top: $spacing-md;
|
|
}
|
|
|
|
.bar-price {
|
|
flex: 1;
|
|
}
|
|
|
|
.bar-label {
|
|
display: block;
|
|
color: $text-color-light;
|
|
font-size: 22rpx;
|
|
}
|
|
|
|
.submit-btn {
|
|
width: 320rpx;
|
|
}
|
|
</style>
|