<template>
|
<view class="page">
|
<view class="content">
|
<view class="page-inner">
|
<view class="top-panel">
|
<view class="custom-nav">
|
<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" @tap="goBack" hover-class="back-button-active">
|
<image src="/static/news-assets/arrow-left.svg"></image>
|
</view>
|
<text class="nav-title">订单支付</text>
|
<view class="capsule-space" :style="'width:' + navWidth + 'px'"></view>
|
</view>
|
</view>
|
|
<view class="product-summary">
|
<image class="product-image" :src="productImage" mode="aspectFill"></image>
|
<view class="product-copy">
|
<text class="product-name">{{productName}}</text>
|
<view class="product-price"><text class="price-symbol">¥</text><text>{{salePriceText}}</text></view>
|
</view>
|
</view>
|
</view>
|
|
<view class="price-card">
|
<view class="price-item"><text class="price-label">人才折扣</text><text class="talent-discount">{{productDiscount}}</text></view>
|
<view class="price-item coupon-item" @tap="chooseCoupon" hover-class="price-item-active">
|
<text class="price-label">商品优惠</text>
|
<view class="coupon-value"><text>{{couponText}}</text><text class="chevron">›</text></view>
|
</view>
|
<view class="price-item"><text class="price-label">原价</text><text class="price-value">¥ {{originalAmountText}}</text></view>
|
<view class="price-item"><text class="price-label">优惠金额</text><text class="price-value">-¥ {{couponAmountText}}</text></view>
|
<view class="price-item"><text class="price-label">用户实付</text><text class="price-value strong">¥ {{actualPayAmountText}}</text></view>
|
<view class="price-item"><text class="price-label">补贴金额</text><text class="price-value">¥ {{subsidyAmountText}}</text></view>
|
</view>
|
|
<view class="actual-payment">
|
<view class="actual-price"><text class="actual-symbol">¥</text><text>{{actualPayAmountText}}</text></view>
|
<text class="actual-label">实际支付</text>
|
</view>
|
|
<view class="confirm-button" :class="isSubmitting ? 'confirm-button-disabled' : ''" hover-class="confirm-button-active" @tap="confirmPayment">
|
<text>{{isSubmitting ? '支付中...' : '确认支付'}}</text>
|
</view>
|
<view class="safe-bottom"></view>
|
</view>
|
</view>
|
</view>
|
</template>
|
|
<script setup lang="uts">
|
import { safeBack } from '../../utils/navigation.uts'
|
import { abandonPendingOrderPayment, createAppOrder, createPendingOrderPayment, getOrderCouponQuote } from '../../services/portal.uts'
|
import { resolveApiUrl } from '../../services/http.uts'
|
import { getSession } from '../../services/session.uts'
|
|
const navTop = ref(28)
|
const navHeight = ref(32)
|
const navWidth = ref(88)
|
const navRight = ref(10)
|
const isSubmitting = ref(false)
|
const merchantId = ref('')
|
const productId = ref('')
|
const orderId = ref('')
|
const talentId = ref('')
|
const couponText = ref('无')
|
const couponAmount = ref(0.0)
|
const productName = ref('【川味盛宴】川菜12人餐')
|
const productImage = ref('/static/food-assets/food-1.jpg')
|
const salePrice = ref(138.0)
|
const originalAmount = ref(266.0)
|
const productDiscount = ref('6折')
|
const salePriceText = computed(() => salePrice.value.toFixed(2))
|
const originalAmountText = computed(() => originalAmount.value.toFixed(2))
|
const couponAmountText = computed(() => couponAmount.value.toFixed(2))
|
const subsidyAmountText = ref('0.00')
|
const actualPayAmountText = ref('0.00')
|
const createOrderIdempotencyKey = ref('')
|
let quoteSequence = 0
|
|
const applyProductById = (id: string) => {
|
if (id == 'food-set-002') { productName.value = '【川香四溢】川菜4人餐'; productImage.value = '/static/food-assets/food-2.jpg'; salePrice.value = 118.0; originalAmount.value = 198.0; productDiscount.value = '6折'; return }
|
if (id == 'food-set-003') { productName.value = '【欢聚时光】招牌菜6人餐'; salePrice.value = 168.0; originalAmount.value = 248.0; productDiscount.value = '6.8折'; return }
|
if (id == 'food-set-004') { productName.value = '【暖心小聚】特色菜双人餐'; productImage.value = '/static/food-assets/food-2.jpg'; salePrice.value = 88.0; originalAmount.value = 126.0; productDiscount.value = '7折'; return }
|
if (id == 'food-set-005') { productName.value = '【家庭聚餐】经典川菜8人餐'; salePrice.value = 238.0; originalAmount.value = 366.0; productDiscount.value = '6.5折'; return }
|
if (id == 'food-set-006') { productName.value = '【人气必点】招牌菜4人餐'; productImage.value = '/static/food-assets/food-2.jpg'; salePrice.value = 128.0; originalAmount.value = 188.0; productDiscount.value = '6.8折'; return }
|
if (id == 'food-set-007') { productName.value = '【周末欢聚】特色菜10人餐'; salePrice.value = 298.0; originalAmount.value = 498.0; productDiscount.value = '6折'; return }
|
if (id == 'food-set-008') { productName.value = '【一人食】暖心工作餐'; productImage.value = '/static/food-assets/food-2.jpg'; salePrice.value = 39.0; originalAmount.value = 49.0; productDiscount.value = '8折'; return }
|
}
|
|
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['merchantId'] != null) merchantId.value = options['merchantId'] as string
|
if (options != null && options['productId'] != null) productId.value = options['productId'] as string
|
if (options != null && options['orderId'] != null) orderId.value = options['orderId'] as string
|
const session = getSession()
|
if (session != null && session.talentId != null) talentId.value = '' + session.talentId
|
applyProductById(productId.value)
|
if (options != null && options['name'] != null) productName.value = decodeURIComponent(options['name'] as string)
|
if (options != null && options['image'] != null) productImage.value = decodeURIComponent(options['image'] as string)
|
if (options != null && options['price'] != null) {
|
const parsedPrice = parseFloat(options['price'] as string)
|
if (!isNaN(parsedPrice)) salePrice.value = parsedPrice
|
}
|
if (options != null && options['originalPrice'] != null) {
|
const parsedOriginalPrice = parseFloat(options['originalPrice'] as string)
|
if (!isNaN(parsedOriginalPrice)) originalAmount.value = parsedOriginalPrice
|
}
|
if (options != null && options['discount'] != null) productDiscount.value = decodeURIComponent(options['discount'] as string)
|
if (orderId.value == '') orderId.value = 'order-draft-' + merchantId.value + '-' + productId.value
|
createOrderIdempotencyKey.value = 'create-order-' + Date.now() + '-' + Math.floor(Math.random() * 1000000)
|
loadOrderQuote('')
|
})
|
|
onShow(() => {
|
const selectedCouponId: any = uni.getStorageSync(couponStorageKey())
|
loadOrderQuote(selectedCouponId != null ? '' + selectedCouponId : '')
|
})
|
|
const goBack = () => safeBack('/pages/benefits/benefits')
|
const couponStorageKey = () : string => 'selectedOrderCouponId:' + orderId.value
|
const validOrderContext = () : boolean => /^\d+$/.test(merchantId.value) && /^\d+$/.test(productId.value) && orderId.value != ''
|
const toAmount = (value: any) : number => {
|
const parsed = Number(value)
|
return isNaN(parsed) ? 0.0 : parsed
|
}
|
const loadOrderQuote = (couponId: string) => {
|
if (!validOrderContext()) return
|
const sequence = quoteSequence + 1
|
quoteSequence = sequence
|
getOrderCouponQuote(merchantId.value, productId.value, orderId.value, couponId, talentId.value, (data: any) => {
|
if (sequence != quoteSequence || data == null) return
|
productName.value = data.productName != null ? data.productName as string : productName.value
|
if (data.coverUrl != null && data.coverUrl != '') productImage.value = resolveApiUrl(data.coverUrl as string)
|
salePrice.value = toAmount(data.productAmount)
|
originalAmount.value = toAmount(data.originalAmount)
|
couponAmount.value = toAmount(data.couponAmount)
|
couponText.value = data.couponLabel != null ? data.couponLabel as string : '无'
|
productDiscount.value = data.discountText != null ? data.discountText as string : productDiscount.value
|
subsidyAmountText.value = toAmount(data.subsidyAmount).toFixed(2)
|
actualPayAmountText.value = toAmount(data.payAmount).toFixed(2)
|
}, (error) => {
|
if (sequence != quoteSequence) return
|
if (couponId != '') {
|
uni.removeStorageSync(couponStorageKey())
|
couponText.value = '无'
|
uni.showToast({ title: error.message, icon: 'none' })
|
loadOrderQuote('')
|
}
|
})
|
}
|
const chooseCoupon = () => uni.navigateTo({
|
url: '/pages/coupon-select/coupon-select?source=payment-confirm'
|
+ '&merchantId=' + encodeURIComponent(merchantId.value)
|
+ '&productId=' + encodeURIComponent(productId.value)
|
+ '&orderId=' + encodeURIComponent(orderId.value)
|
})
|
const confirmPayment = () => {
|
if (isSubmitting.value) return
|
if (!validOrderContext()) {
|
uni.showToast({ title: '订单信息不完整,请重新选择商品', icon: 'none' })
|
return
|
}
|
const session = getSession()
|
const talentId = session != null && session.talentId != null ? '' + session.talentId : ''
|
if (session == null || talentId == '') {
|
uni.showToast({ title: '请先登录后再支付', icon: 'none' })
|
uni.navigateTo({ url: '/pages/login/login' })
|
return
|
}
|
isSubmitting.value = true
|
const selectedCouponId: any = uni.getStorageSync(couponStorageKey())
|
const couponId = selectedCouponId != null ? '' + selectedCouponId : ''
|
createAppOrder(merchantId.value, productId.value, couponId, createOrderIdempotencyKey.value, (data: any) => {
|
uni.removeStorageSync(couponStorageKey())
|
const realOrderNo = data != null && data.orderNo != null ? '' + data.orderNo : ''
|
if (realOrderNo == '') {
|
isSubmitting.value = false
|
uni.showToast({ title: '订单创建失败,请稍后重试', icon: 'none' })
|
return
|
}
|
if (data != null && data.paid === true) {
|
isSubmitting.value = false
|
uni.redirectTo({ url: '/pages/payment-success/payment-success?orderId=' + encodeURIComponent(realOrderNo) })
|
return
|
}
|
// 一次点击即支付:用户既然点了「确认支付」,就不要再让他到第二页点同一个按钮——
|
// 建单之后立刻发起预支付并拉起收银台,只有放弃或失败才落到待支付页。
|
startPayment(realOrderNo, talentId, data != null && data.priceVersion != null ? Number(data.priceVersion) : 1)
|
}, (error) => {
|
isSubmitting.value = false
|
uni.showToast({ title: error.message, icon: 'none' })
|
})
|
}
|
const startPayment = (orderNo: string, talentId: string, priceVersion: number) => {
|
createPendingOrderPayment(orderNo, talentId, priceVersion, (data: any) => {
|
if (data != null && data.paymentStatus == 'PAID') {
|
openPaymentSuccess(orderNo)
|
return
|
}
|
if (data != null && data.paymentStatus == 'PAYMENT_PROCESSING') {
|
// 关单中或结果未定:落到待支付页由它等结论(那里会显示「支付处理中」并退避轮询)。
|
openPendingPayment(orderNo, talentId, true)
|
return
|
}
|
const params: any = data != null ? data.paymentParams : null
|
if (params == null || params.timeStamp == null || params.nonceStr == null || params.package == null || params.paySign == null) {
|
uni.showToast({ title: '支付参数获取失败,请在待支付页重试', icon: 'none' })
|
openPendingPayment(orderNo, talentId, false)
|
return
|
}
|
presentCashier(orderNo, talentId, params)
|
}, (error: any) => {
|
uni.showToast({ title: error.message, icon: 'none' })
|
openPendingPayment(orderNo, talentId, false)
|
})
|
}
|
const presentCashier = (orderNo: string, talentId: string, 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: () => openPendingPayment(orderNo, talentId, true),
|
fail: (error: any) => {
|
const message = error != null && error.errMsg != null ? '' + error.errMsg : ''
|
if (message.indexOf('cancel') >= 0) {
|
// 用户关掉了收银台:告诉服务端一声,让它立刻尝试关单(服务端不据此判定收款)。
|
abandonPendingOrderPayment(orderNo, talentId, '', () => openPendingPayment(orderNo, talentId, false), () => openPendingPayment(orderNo, talentId, false))
|
return
|
}
|
uni.showToast({ title: '支付未完成,请在待支付页继续', icon: 'none' })
|
openPendingPayment(orderNo, talentId, false)
|
}
|
})
|
// #endif
|
// #ifndef MP-WEIXIN
|
uni.showToast({ title: '请在微信小程序中完成支付', icon: 'none' })
|
openPendingPayment(orderNo, talentId, false)
|
// #endif
|
}
|
const openPendingPayment = (orderNo: string, talentId: string, waitingPayment: boolean) => {
|
isSubmitting.value = false
|
uni.redirectTo({
|
url: '/pages/pending-payment/pending-payment?orderId=' + encodeURIComponent(orderNo)
|
+ '&talentId=' + encodeURIComponent(talentId)
|
+ (waitingPayment ? '&waiting=1' : '')
|
})
|
}
|
const openPaymentSuccess = (orderNo: string) => {
|
isSubmitting.value = false
|
uni.redirectTo({ url: '/pages/payment-success/payment-success?orderId=' + encodeURIComponent(orderNo) })
|
}
|
</script>
|
|
<style>
|
.page { width: 100%; height: 100vh; background-color: #F7F7F7; }
|
.content { width: 100%; height: 100%; }
|
.page-inner { min-height: 100%; background-color: #F7F7F7; }
|
.top-panel { padding-bottom: 30rpx; background-image: linear-gradient(180deg, #FFF0B8 0%, #FFF8E5 45%, #F7F7F7 100%); }
|
.custom-nav { width: 100%; }
|
.safe-space { height: 28px; }
|
.nav-row { display: flex; height: 32px; box-sizing: border-box; padding-left: 26rpx; padding-right: 10px; position: relative; flex-direction: row; align-items: center; justify-content: space-between; }
|
.back-button { display: flex; width: 60rpx; height: 60rpx; margin-left: -8rpx; justify-content: center; align-items: center; }
|
.back-button image { width: 38rpx; height: 38rpx; }
|
.back-button-active { opacity: .55; }
|
.nav-title { position: absolute; left: 50%; top: 50%; color: #181A1E; font-size: 36rpx; font-weight: 700; transform: translate(-50%, -50%); white-space: nowrap; }
|
.capsule-space { width: 88px; height: 100%; flex-shrink: 0; }
|
.product-summary { display: flex; margin-top: 34rpx; padding: 0 30rpx; flex-direction: row; align-items: flex-start; }
|
.product-image { width: 184rpx; height: 184rpx; border-radius: 17rpx; flex-shrink: 0; background-color: #E8E8E8; }
|
.product-copy { display: flex; min-width: 0; flex: 1; min-height: 184rpx; padding-left: 29rpx; flex-direction: column; }
|
.product-name { color: #27282B; font-size: 31rpx; font-weight: 600; line-height: 43rpx; }
|
.product-price { display: flex; margin-top: 35rpx; color: #17181B; font-size: 34rpx; font-weight: 700; flex-direction: row; align-items: baseline; }
|
.price-symbol { margin-right: 3rpx; font-size: 28rpx; }
|
.price-card { margin: 0 24rpx; padding: 22rpx 28rpx; border-radius: 17rpx; background-color: #FFFFFF; }
|
.price-item { display: flex; min-height: 66rpx; flex-direction: row; align-items: center; justify-content: space-between; }
|
.price-label { color: #6A6D73; font-size: 27rpx; }
|
.price-value { color: #33363B; font-size: 27rpx; }
|
.price-value.strong { color: #25272B; font-weight: 700; }
|
.talent-discount { color: #FF6E21; font-size: 28rpx; }
|
.coupon-item { margin: 0 -12rpx; padding: 0 12rpx; border-radius: 10rpx; }
|
.price-item-active { background-color: #F7F7F7; }
|
.coupon-value { display: flex; color: #A5A8AD; font-size: 26rpx; flex-direction: row; align-items: center; }
|
.chevron { margin-left: 16rpx; color: #62666C; font-size: 38rpx; line-height: 32rpx; }
|
.actual-payment { display: flex; margin-top: 120rpx; flex-direction: column; align-items: center; }
|
.actual-price { display: flex; color: #17191D; font-size: 57rpx; font-weight: 800; line-height: 68rpx; flex-direction: row; align-items: baseline; }
|
.actual-symbol { margin-right: 5rpx; font-size: 29rpx; }
|
.actual-label { margin-top: 8rpx; color: #666970; font-size: 29rpx; }
|
.confirm-button { display: flex; width: 514rpx; height: 86rpx; margin: 146rpx auto 0; border-radius: 45rpx; background-image: linear-gradient(90deg, #FF4C32 0%, #FF841C 100%); justify-content: center; align-items: center; }
|
.confirm-button text { color: #FFFFFF; font-size: 30rpx; font-weight: 600; }
|
.confirm-button-active { opacity: .82; transform: scale(.99); }
|
.confirm-button-disabled { opacity: .62; transform: none; }
|
.safe-bottom { height: 44rpx; height: calc(44rpx + env(safe-area-inset-bottom)); }
|
</style>
|