<template>
|
<view class="page">
|
<view class="page-header">
|
<view class="safe-space" :style="'height:' + navTop + 'px'"></view>
|
<view class="nav-row" :style="'height:' + navHeight + 'px;padding-right:' + navRight + 'px'">
|
<view class="back-button" hover-class="back-button-active" @tap="goBack"><image src="/static/news-assets/arrow-left.svg" mode="aspectFit"></image></view>
|
<text class="nav-title">待支付</text>
|
<view class="capsule-space" :style="'width:' + navWidth + 'px'"></view>
|
</view>
|
<view class="countdown-panel"><text class="countdown">{{countdownText}}</text><text class="countdown-tip">{{isExpired ? '订单支付时间已结束' : '请仔细核对消费金额和商家信息'}}</text></view>
|
</view>
|
|
<view v-if="loading" class="content state-content"><text>正在加载订单</text></view>
|
<view v-else-if="loadFailed" class="content state-content"><text>{{loadErrorText}}</text><text class="retry-text" @tap="loadOrder">重新加载</text></view>
|
<view v-else class="content">
|
<view class="merchant-card">
|
<view class="merchant-main"><text class="merchant-name">{{merchantName}}</text><text class="merchant-address">{{merchantAddress}}</text><view class="merchant-tag"><image src="/static/order-assets/utensils.svg" mode="aspectFit"></image><text>{{discountText}}</text></view></view>
|
<view class="merchant-actions">
|
<view class="merchant-action" hover-class="action-active" @tap="openNavigation"><image src="/static/merchant-assets/navigation.svg" mode="aspectFit"></image><text>导航</text></view>
|
<view class="merchant-action" hover-class="action-active" @tap="callMerchant"><image src="/static/merchant-assets/phone.svg" mode="aspectFit"></image><text>电话</text></view>
|
</view>
|
</view>
|
|
<view class="discount-card">
|
<view class="discount-row"><text>人才折扣</text><text class="discount-highlight">{{talentDiscountText}}</text></view>
|
<view class="discount-row" :class="couponSelectable ? 'coupon-row' : ''" hover-class="discount-row-active" :hover-stop-propagation="true" @tap="chooseCoupon">
|
<text>商品优惠</text>
|
<view v-if="couponSelectable" class="coupon-value"><text>{{couponText}}</text><text class="coupon-chevron">›</text></view>
|
<text v-else class="discount-value">{{couponText}}</text>
|
</view>
|
</view>
|
|
<view class="amount-card">
|
<text class="amount-title">金额明细</text>
|
<view class="amount-row"><text>原价</text><text>¥ {{originalAmountText}}</text></view>
|
<view class="amount-row"><text>优惠金额</text><text>- ¥ {{discountAmountText}}</text></view>
|
<view class="amount-row"><text>用户实付</text><text class="amount-strong">¥ {{actualPayAmountText}}</text></view>
|
<view class="amount-row"><text>补贴金额</text><text>¥ {{subsidyAmountText}}</text></view>
|
</view>
|
<view class="content-bottom"></view>
|
</view>
|
|
<view v-if="!loading && !loadFailed" class="payment-footer">
|
<text class="actual-label">实际支付</text>
|
<text class="actual-amount">¥ {{actualPayAmountText}}</text>
|
<text v-if="paymentChecking" class="payment-checking">支付结果确认中</text>
|
<view class="pay-button" :class="isExpired || isSubmitting ? 'disabled' : ''" hover-class="pay-button-active" @tap="confirmPayment"><text>{{isExpired ? '订单已超时' : (isSubmitting ? '处理中' : '确认支付')}}</text></view>
|
<view v-if="!isExpired && !isSubmitting" class="cancel-button" hover-class="cancel-button-active" @tap="cancelOrder"><text>取消订单</text></view>
|
</view>
|
</view>
|
</template>
|
|
<script setup lang="uts">
|
import { apiEnvironment } from '../../config/api.uts'
|
import { safeBack } from '../../utils/navigation.uts'
|
import { cancelPendingOrder, createPendingOrderPayment, getPendingOrder, getPendingOrderPaymentStatus } from '../../services/portal.uts'
|
|
const navTop = ref(28)
|
const navHeight = ref(32)
|
const navWidth = ref(88)
|
const navRight = ref(10)
|
const isSubmitting = ref(false)
|
const orderId = ref('')
|
const talentId = ref('')
|
const loading = ref(true)
|
const loadFailed = ref(false)
|
const loadErrorText = ref('订单加载失败')
|
const merchantName = ref('')
|
const merchantAddress = ref('')
|
const merchantMobile = ref('')
|
const merchantLongitude = ref(0.0)
|
const merchantLatitude = ref(0.0)
|
const merchantId = ref('')
|
const productId = ref('')
|
const priceVersion = ref(1)
|
const discountText = ref('人才专享')
|
const talentDiscountText = ref('无折扣')
|
const expireAt = ref(0)
|
const hasExpiry = ref(false)
|
const remainingSeconds = ref(300)
|
const isExpired = ref(false)
|
const paymentChecking = ref(false)
|
// 后续由待支付订单详情接口返回。true 为可选优惠券,false 保留后台返回的固定商品优惠。
|
const couponSelectable = ref(true)
|
const couponText = ref('无')
|
const originalAmount = ref(0.0)
|
const discountAmount = ref(0.0)
|
const actualPayAmount = ref(0.0)
|
const subsidyAmount = ref(0.0)
|
const originalAmountText = computed(() => originalAmount.value.toFixed(2))
|
const discountAmountText = computed(() => discountAmount.value.toFixed(2))
|
const actualPayAmountText = computed(() => actualPayAmount.value.toFixed(2))
|
const subsidyAmountText = computed(() => subsidyAmount.value.toFixed(2))
|
const countdownText = computed(() => {
|
if (!hasExpiry.value) return '--:--'
|
const minutes = Math.floor(remainingSeconds.value / 60)
|
const seconds = remainingSeconds.value % 60
|
const minutesText = minutes < 10 ? '0' + minutes : '' + minutes
|
const secondsText = seconds < 10 ? '0' + seconds : '' + seconds
|
return minutesText + ':' + secondsText
|
})
|
let countdownTimer = 0
|
let paymentPollingTimer = 0
|
let paymentPollingAttempts = 0
|
let currentPaymentNo = ''
|
let expiryCancellationRequested = false
|
|
onLoad((options: any) => {
|
// #ifdef MP-WEIXIN
|
const capsule = uni.getMenuButtonBoundingClientRect()
|
const systemInfo = uni.getSystemInfoSync()
|
navTop.value = capsule.top
|
navHeight.value = capsule.height
|
navWidth.value = capsule.width
|
navRight.value = systemInfo.windowWidth - capsule.right
|
// #endif
|
if (options != null && options['orderId'] != null && options['orderId'] != '') orderId.value = options['orderId'] as string
|
talentId.value = options != null && options['talentId'] != null ? options['talentId'] as string : (apiEnvironment == 'development' ? '2' : '')
|
loadOrder()
|
})
|
onShow(() => {
|
if (orderId.value != '' && !loading.value) loadOrder()
|
})
|
onHide(() => stopCountdown())
|
onUnload(() => { stopCountdown(); stopPaymentPolling() })
|
|
const refreshCountdown = () => {
|
if (!hasExpiry.value) return
|
const seconds = Math.ceil((expireAt.value - new Date().getTime()) / 1000)
|
remainingSeconds.value = Math.max(0, seconds)
|
isExpired.value = remainingSeconds.value <= 0
|
}
|
const loadOrder = () => {
|
if (orderId.value == '' || talentId.value == '') { loading.value = false; loadFailed.value = true; loadErrorText.value = '订单信息不完整'; return }
|
loading.value = true
|
loadFailed.value = false
|
getPendingOrder(orderId.value, talentId.value, (data: any) => {
|
loading.value = false
|
if (data != null && Number(data.orderStatus) == 2) {
|
uni.redirectTo({ url: '/pages/order-cancelled/order-cancelled?orderId=' + encodeURIComponent(orderId.value) + '&talentId=' + encodeURIComponent(talentId.value) })
|
return
|
}
|
merchantName.value = data.merchantNameSnapshot != null ? data.merchantNameSnapshot as string : '人才权益服务'
|
merchantAddress.value = data.merchantAddressSnapshot != null ? data.merchantAddressSnapshot as string : ''
|
merchantMobile.value = data.merchantMobileSnapshot != null ? data.merchantMobileSnapshot as string : ''
|
merchantLongitude.value = Number(data.merchantLongitudeSnapshot)
|
merchantLatitude.value = Number(data.merchantLatitudeSnapshot)
|
merchantId.value = data.merchantId != null ? '' + data.merchantId : ''
|
productId.value = data.productId != null ? '' + data.productId : ''
|
priceVersion.value = data.priceVersion != null ? Number(data.priceVersion) : 1
|
couponSelectable.value = data.couponSelectable === true
|
couponText.value = data.couponLabel != null ? data.couponLabel as string : '无'
|
originalAmount.value = Number(data.originalAmount)
|
discountAmount.value = Number(data.discountAmount)
|
actualPayAmount.value = Number(data.payAmount)
|
subsidyAmount.value = Number(data.subsidyAmount)
|
discountText.value = data.productNameSnapshot != null ? data.productNameSnapshot as string : '人才专享'
|
talentDiscountText.value = data.talentDiscountText != null ? data.talentDiscountText as string : '无折扣'
|
hasExpiry.value = data.expiresAt != null
|
expireAt.value = hasExpiry.value ? new Date((data.expiresAt as string).replace('T', ' ')).getTime() : 0
|
isExpired.value = data.expired === true || data.canPay !== true
|
if (hasExpiry.value) {
|
refreshCountdown()
|
if (!isExpired.value) startCountdown()
|
} else {
|
stopCountdown()
|
}
|
}, (error: any) => { loading.value = false; loadFailed.value = true; loadErrorText.value = error.message })
|
}
|
const startCountdown = () => {
|
stopCountdown()
|
countdownTimer = setInterval(() => {
|
refreshCountdown()
|
if (isExpired.value) {
|
stopCountdown()
|
expireOrder()
|
}
|
}, 1000)
|
}
|
const stopCountdown = () => {
|
if (countdownTimer == 0) return
|
clearInterval(countdownTimer)
|
countdownTimer = 0
|
}
|
const stopPaymentPolling = () => {
|
if (paymentPollingTimer == 0) return
|
clearTimeout(paymentPollingTimer)
|
paymentPollingTimer = 0
|
}
|
const expireOrder = () => {
|
if (expiryCancellationRequested || orderId.value == '' || talentId.value == '') {
|
loadOrder()
|
return
|
}
|
expiryCancellationRequested = true
|
// The server applies the deadline condition atomically. This makes the
|
// client-side countdown an immediate trigger; the scheduled server task
|
// remains the fallback when the mini program is no longer open.
|
cancelPendingOrder(orderId.value, talentId.value, () => {
|
stopCountdown()
|
uni.redirectTo({ url: '/pages/order-cancelled/order-cancelled?orderId=' + encodeURIComponent(orderId.value) + '&talentId=' + encodeURIComponent(talentId.value) })
|
}, () => {
|
expiryCancellationRequested = false
|
loadOrder()
|
})
|
}
|
const goBack = () => safeBack('/pages/order/order')
|
const openNavigation = () => { if (isNaN(merchantLatitude.value) || isNaN(merchantLongitude.value) || merchantLatitude.value == 0 || merchantLongitude.value == 0) { uni.showToast({ title: '商户暂未配置导航地址', icon: 'none' }); return }; uni.openLocation({ latitude: merchantLatitude.value, longitude: merchantLongitude.value, name: merchantName.value, address: merchantAddress.value, scale: 16 }) }
|
const callMerchant = () => { if (merchantMobile.value == '') { uni.showToast({ title: '商户暂未配置联系电话', icon: 'none' }); return }; uni.makePhoneCall({ phoneNumber: merchantMobile.value }) }
|
const chooseCoupon = () => {
|
if (!couponSelectable.value) return
|
uni.navigateTo({ url: '/pages/coupon-select/coupon-select?source=pending-payment&orderId=' + encodeURIComponent(orderId.value) + '&talentId=' + encodeURIComponent(talentId.value) + '&merchantId=' + encodeURIComponent(merchantId.value) + '&productId=' + encodeURIComponent(productId.value) + '&priceVersion=' + priceVersion.value })
|
}
|
const confirmPayment = () => {
|
if (isSubmitting.value) return
|
if (isExpired.value) {
|
uni.showToast({ title: '订单已超时,请重新下单', icon: 'none' })
|
return
|
}
|
stopPaymentPolling()
|
paymentPollingAttempts = 0
|
paymentChecking.value = false
|
isSubmitting.value = true
|
createPendingOrderPayment(orderId.value, talentId.value, priceVersion.value, (data: any) => {
|
currentPaymentNo = data != null && data.paymentNo != null ? '' + data.paymentNo : ''
|
if (data != null && data.paymentStatus == 'PAID') {
|
openPaymentSuccess()
|
return
|
}
|
if (data != null && data.paymentStatus == 'PAYMENT_PROCESSING') {
|
isSubmitting.value = false
|
uni.showToast({ title: '支付创建确认中,请稍后查看订单状态', icon: 'none' })
|
return
|
}
|
const params: any = data != null ? data.paymentParams : null
|
if (params == null || params.timeStamp == null || params.nonceStr == null || params.package == null || params.paySign == null) {
|
isSubmitting.value = false
|
uni.showToast({ title: '支付参数获取失败,请稍后重试', icon: 'none' })
|
return
|
}
|
requestWechatPayment(params)
|
}, (error) => {
|
isSubmitting.value = false
|
uni.showToast({ title: error.message, icon: 'none' })
|
})
|
}
|
const cancelOrder = () => {
|
if (isSubmitting.value || isExpired.value) return
|
uni.showModal({
|
title: '取消订单',
|
content: '确定取消当前订单吗?',
|
success: (result: any) => {
|
if (result.confirm !== true) return
|
isSubmitting.value = true
|
cancelPendingOrder(orderId.value, talentId.value, () => {
|
isSubmitting.value = false
|
stopCountdown()
|
uni.redirectTo({ url: '/pages/order-cancelled/order-cancelled?orderId=' + encodeURIComponent(orderId.value) + '&talentId=' + encodeURIComponent(talentId.value) })
|
}, (error: any) => {
|
isSubmitting.value = false
|
uni.showToast({ title: error.message, icon: 'none' })
|
})
|
}
|
})
|
}
|
const requestWechatPayment = (params: any) => {
|
// #ifdef MP-WEIXIN
|
uni.requestPayment({
|
provider: 'wxpay',
|
timeStamp: '' + params.timeStamp,
|
nonceStr: '' + params.nonceStr,
|
package: '' + params.package,
|
signType: params.signType != null ? '' + params.signType : 'RSA',
|
paySign: '' + params.paySign,
|
success: () => checkPaymentStatus(true),
|
fail: (error: any) => {
|
const message = error != null && error.errMsg != null ? '' + error.errMsg : ''
|
checkPaymentStatus(false)
|
if (message.indexOf('cancel') >= 0) uni.showToast({ title: '已取消支付', icon: 'none' })
|
else uni.showToast({ title: '支付未完成,请稍后查看订单状态', icon: 'none' })
|
}
|
})
|
// #endif
|
// #ifndef MP-WEIXIN
|
isSubmitting.value = false
|
uni.showToast({ title: '请在微信小程序中完成支付', icon: 'none' })
|
// #endif
|
}
|
const checkPaymentStatus = (pollWhenPending: boolean) => {
|
getPendingOrderPaymentStatus(orderId.value, talentId.value, currentPaymentNo, (data: any) => {
|
if (data != null && data.paymentStatus == 'PAID') {
|
openPaymentSuccess()
|
return
|
}
|
if (data != null && data.paymentStatus == 'PAYMENT_PENDING' && pollWhenPending && paymentPollingAttempts < 5) {
|
paymentChecking.value = true
|
paymentPollingAttempts += 1
|
stopPaymentPolling()
|
paymentPollingTimer = setTimeout(() => checkPaymentStatus(true), 1500)
|
return
|
}
|
paymentChecking.value = false
|
isSubmitting.value = false
|
if (pollWhenPending) uni.showToast({ title: '支付结果确认中,请稍后在订单中查看', icon: 'none' })
|
}, () => {
|
paymentChecking.value = false
|
isSubmitting.value = false
|
if (pollWhenPending) uni.showToast({ title: '支付状态查询失败,请稍后在订单中查看', icon: 'none' })
|
})
|
}
|
const openPaymentSuccess = () => {
|
stopPaymentPolling()
|
paymentChecking.value = false
|
isSubmitting.value = false
|
uni.redirectTo({ url: '/pages/payment-success/payment-success?orderId=' + encodeURIComponent(orderId.value) })
|
}
|
</script>
|
|
<style>
|
.page { display: flex; width: 100%; height: 100vh; background-color: #F5F7FA; background-image: linear-gradient(180deg, #FFF0D7 0%, #FFF2DE 250rpx, #F5F7FA 520rpx); flex-direction: column; }
|
.page-header { position: relative; z-index: 10; flex-shrink: 0; padding-bottom: 24rpx; }
|
.safe-space { height: 28px; }
|
.nav-row { display: flex; height: 32px; box-sizing: border-box; padding-left: 20rpx; padding-right: 10px; position: relative; flex-direction: row; align-items: center; justify-content: space-between; }
|
.back-button { display: flex; width: 70rpx; height: 64rpx; margin-left: -4rpx; justify-content: center; align-items: center; }
|
.back-button image { width: 36rpx; height: 36rpx; }
|
.back-button-active { opacity: .55; }
|
.nav-title { position: absolute; left: 50%; top: 50%; color: #171B22; font-size: 37rpx; line-height: 48rpx; font-weight: 700; transform: translate(-50%, -50%); white-space: nowrap; }
|
.capsule-space { width: 88px; height: 100%; flex-shrink: 0; }
|
.countdown-panel { display: flex; min-height: 118rpx; margin-top: 10rpx; flex-direction: column; align-items: center; justify-content: center; }
|
.countdown { color: #282D34; font-size: 36rpx; line-height: 43rpx; font-weight: 700; }
|
.countdown-tip { margin-top: 4rpx; color: #FF6E20; font-size: 21rpx; line-height: 30rpx; }
|
.content { min-height: 0; flex: 1; background-color: transparent; }
|
.state-content { color: #7E8794; font-size: 26rpx; flex-direction: column; justify-content: center; align-items: center; }
|
.retry-text { margin-top: 20rpx; color: #F26A38; }
|
.merchant-card { display: flex; min-height: 226rpx; margin: 32rpx 28rpx 0; box-sizing: border-box; padding: 33rpx 27rpx; border-radius: 25rpx; background-color: #FFFFFF; flex-direction: row; align-items: stretch; }
|
.merchant-main { min-width: 0; flex: 1; }
|
.merchant-name { display: block; color: #252B34; font-size: 32rpx; line-height: 43rpx; font-weight: 700; }
|
.merchant-address { display: block; margin-top: 6rpx; color: #A0A8B3; font-size: 21rpx; line-height: 31rpx; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
.merchant-tag { display: flex; width: 234rpx; height: 52rpx; margin-top: 27rpx; border-radius: 27rpx; background-image: linear-gradient(90deg, #FFF0E5 0%, rgba(255,240,229,.45) 100%); flex-direction: row; justify-content: center; align-items: center; }
|
.merchant-tag image { width: 29rpx; height: 29rpx; margin-right: 12rpx; }
|
.merchant-tag text { color: #FF812E; font-size: 21rpx; }
|
.merchant-actions { display: flex; width: 138rpx; margin-left: 15rpx; flex-shrink: 0; flex-direction: row; justify-content: space-between; align-items: center; }
|
.merchant-action { display: flex; width: 60rpx; flex-direction: column; align-items: center; }
|
.merchant-action image { width: 38rpx; height: 38rpx; }
|
.merchant-action text { margin-top: 10rpx; color: #282E38; font-size: 20rpx; font-weight: 600; }
|
.action-active { opacity: .55; }
|
.discount-card { margin: 32rpx 28rpx 0; padding: 15rpx 29rpx; border-radius: 25rpx; background-color: #FFFFFF; }
|
.discount-row { display: flex; height: 76rpx; flex-direction: row; align-items: center; justify-content: space-between; color: #77808E; font-size: 27rpx; }
|
.coupon-row { margin: 0 -12rpx; padding: 0 12rpx; border-radius: 12rpx; }
|
.discount-row-active { background-color: #F7F8FA; }
|
.discount-highlight { color: #FF6B21; font-size: 28rpx; font-weight: 600; }
|
.discount-value { color: #222831; font-size: 25rpx; }
|
.coupon-value { display: flex; color: #A0A8B3; font-size: 26rpx; flex-direction: row; align-items: center; }
|
.coupon-chevron { margin-left: 18rpx; color: #252B34; font-size: 38rpx; line-height: 32rpx; }
|
.amount-card { margin: 31rpx 28rpx 0; padding: 30rpx 28rpx 25rpx; border-radius: 25rpx; background-color: #FFFFFF; }
|
.amount-title { display: block; margin-bottom: 17rpx; color: #252B34; font-size: 28rpx; line-height: 40rpx; font-weight: 700; }
|
.amount-row { display: flex; height: 55rpx; flex-direction: row; align-items: center; justify-content: space-between; color: #687180; font-size: 24rpx; }
|
.amount-row text:last-child { color: #3C4655; }
|
.amount-row .amount-strong { color: #FF4E35; font-size: 33rpx; font-weight: 700; }
|
.content-bottom { height: 30rpx; }
|
.payment-footer { display: flex; flex-shrink: 0; box-sizing: border-box; padding: 18rpx 64rpx; padding-bottom: calc(18rpx + env(safe-area-inset-bottom)); background-color: transparent; flex-direction: column; align-items: center; }
|
.actual-label { color: #9BA3AF; font-size: 21rpx; line-height: 30rpx; }
|
.actual-amount { margin-top: 6rpx; color: #FF4D35; font-size: 47rpx; line-height: 58rpx; font-weight: 700; }
|
.payment-checking { margin-top: 8rpx; color: #FF812E; font-size: 22rpx; line-height: 30rpx; }
|
.pay-button { display: flex; min-width: 160rpx; height: 58rpx; margin: 18rpx auto 0; padding: 0 24rpx; box-sizing: border-box; border-radius: 30rpx; background-image: linear-gradient(90deg, #FF4E35 0%, #FF8318 100%); justify-content: center; align-items: center; }
|
.pay-button text { color: #FFFFFF; font-size: 25rpx; font-weight: 600; }
|
.pay-button-active { opacity: .78; }
|
.pay-button.disabled { background-image: none; background-color: #C7CBD1; }
|
.cancel-button { display: flex; min-width: 160rpx; height: 58rpx; margin-top: 18rpx; border-width: 1rpx; border-style: solid; border-color: #FF6D32; border-radius: 30rpx; justify-content: center; align-items: center; }
|
.cancel-button text { color: #FF6730; font-size: 25rpx; font-weight: 600; } .cancel-button-active { opacity: .55; }
|
</style>
|