1393 lines
40 KiB
Vue
1393 lines
40 KiB
Vue
<template>
|
||
<view class="confirm-payment-page">
|
||
<view class="cp-scroll-wrap">
|
||
<scroll-view scroll-y class="cp-scroll">
|
||
<view class="cp-body">
|
||
<!-- 订单信息 -->
|
||
<view class="cp-section">
|
||
<text class="cp-section__title">订单信息</text>
|
||
<view class="cp-order">
|
||
<view class="cp-order__item">
|
||
<text class="cp-order__label">订单编号</text>
|
||
<text class="cp-order__value">{{ orderNo }}</text>
|
||
</view>
|
||
<view class="cp-order__item">
|
||
<text class="cp-order__label">下单时间</text>
|
||
<text class="cp-order__value">{{ orderTime }}</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 商品信息 -->
|
||
<view class="cp-section" v-if="!isRecharge">
|
||
<text class="cp-section__title">购买商品</text>
|
||
<view class="cp-goods">
|
||
<view class="cp-goods__item" v-for="item in cart" :key="item.card.id">
|
||
<view class="cp-goods__icon-wrap">
|
||
<uni-icons type="gift" size="32rpx" color="#FF6B35" />
|
||
</view>
|
||
<view class="cp-goods__info">
|
||
<text class="cp-goods__name">{{ item.card.cardName }}</text>
|
||
<text class="cp-goods__desc">{{ getCardTypeName(item.card.cardType) }} · {{ getCardDesc(item.card) }}</text>
|
||
</view>
|
||
<view class="cp-goods__right">
|
||
<text class="cp-goods__count">×{{ item.count }}</text>
|
||
<text class="cp-goods__price">¥{{ (item.card.price * item.count).toFixed(2) }}</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 充值信息 -->
|
||
<view class="cp-section" v-else>
|
||
<text class="cp-section__title">充值信息</text>
|
||
<view class="cp-recharge">
|
||
<view class="cp-recharge__icon-wrap">
|
||
<uni-icons type="wallet" size="32rpx" color="#FFFFFF" />
|
||
</view>
|
||
<view class="cp-recharge__info">
|
||
<text class="cp-recharge__name">储值卡充值</text>
|
||
<text class="cp-recharge__amount">¥{{ totalPrice.toFixed(2) }}</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 支付方式 -->
|
||
<view class="cp-section">
|
||
<text class="cp-section__title">支付方式</text>
|
||
<view class="cp-payment">
|
||
<view
|
||
class="cp-payment__item"
|
||
:class="{ 'cp-payment__item--selected': selectedPayment === 'alipay' }"
|
||
@tap="selectPayment('alipay')"
|
||
>
|
||
<view class="cp-payment__icon-wrap cp-payment__icon-wrap--alipay">
|
||
<image class="cp-payment__icon-img" src="https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/images/alipay.png" mode="aspectFit" />
|
||
</view>
|
||
<view class="cp-payment__info">
|
||
<text class="cp-payment__name">支付宝支付</text>
|
||
<text class="cp-payment__desc">推荐使用,安全快捷</text>
|
||
</view>
|
||
<view class="cp-payment__radio">
|
||
<view class="cp-payment__radio-inner" :class="{ 'cp-payment__radio-inner--checked': selectedPayment === 'alipay' }"></view>
|
||
</view>
|
||
</view>
|
||
|
||
<view
|
||
class="cp-payment__item"
|
||
:class="{
|
||
'cp-payment__item--selected': selectedPayment === 'stored',
|
||
'cp-payment__item--disabled': storedBalance < totalPrice
|
||
}"
|
||
v-if="!isRecharge"
|
||
@tap="storedBalance >= totalPrice && selectPayment('stored')"
|
||
>
|
||
<view class="cp-payment__icon-wrap cp-payment__icon-wrap--stored">
|
||
<uni-icons type="wallet" size="32rpx" color="#FFFFFF" />
|
||
</view>
|
||
<view class="cp-payment__info">
|
||
<text class="cp-payment__name">储值卡支付</text>
|
||
<text class="cp-payment__desc">余额: ¥{{ storedBalance.toFixed(2) }}</text>
|
||
</view>
|
||
<view class="cp-payment__radio">
|
||
<view class="cp-payment__radio-inner" :class="{ 'cp-payment__radio-inner--checked': selectedPayment === 'stored' }"></view>
|
||
</view>
|
||
<view class="cp-payment__tag" v-if="storedBalance < totalPrice">
|
||
<text>余额不足</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 金额明细 -->
|
||
<view class="cp-section">
|
||
<text class="cp-section__title">金额明细</text>
|
||
<view class="cp-amount">
|
||
<view class="cp-amount__item">
|
||
<text class="cp-amount__label">{{ isRecharge ? '充值金额' : '商品金额' }}</text>
|
||
<text class="cp-amount__value">¥{{ totalPrice.toFixed(2) }}</text>
|
||
</view>
|
||
<view class="cp-amount__item" v-if="!isRecharge && selectedPayment === 'stored' && storedBalance > 0">
|
||
<text class="cp-amount__label">储值抵扣</text>
|
||
<text class="cp-amount__value cp-amount__value--discount">-¥{{ Math.min(storedBalance, totalPrice).toFixed(2) }}</text>
|
||
</view>
|
||
<view class="cp-amount__item cp-amount__item--total">
|
||
<text class="cp-amount__label cp-amount__label--total">实付金额</text>
|
||
<text class="cp-amount__value cp-amount__value--total">¥{{ actualPay.toFixed(2) }}</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</scroll-view>
|
||
</view>
|
||
|
||
<!-- 底部支付栏 -->
|
||
<view class="cp-bottom">
|
||
<view class="cp-bottom__left">
|
||
<text class="cp-bottom__label">实付金额</text>
|
||
<text class="cp-bottom__price">¥{{ actualPay.toFixed(2) }}</text>
|
||
</view>
|
||
<view
|
||
class="cp-bottom__btn"
|
||
:class="{ 'cp-bottom__btn--disabled': !canPay || loading }"
|
||
@tap="handlePay()"
|
||
>
|
||
<text class="cp-bottom__btn-text">{{ loading ? '处理中...' : '确认支付' }}</text>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 购买成功弹窗 -->
|
||
<view class="success-modal-overlay" v-if="showSuccessModalVisible" @tap="closeSuccessModal">
|
||
<view class="success-modal-dialog" @tap.stop>
|
||
<view class="success-modal__icon-wrap">
|
||
<uni-icons type="checkmarkempty" size="56rpx" color="#FFFFFF" />
|
||
</view>
|
||
<text class="success-modal__title">{{ isRecharge ? '充值成功' : '购买成功' }}</text>
|
||
<text class="success-modal__desc">{{ isRecharge ? '恭喜您,储值卡充值成功!' : '恭喜您,会员卡已成功开通!' }}</text>
|
||
<view class="success-modal__buttons">
|
||
<view class="success-modal__btn success-modal__btn--primary" @tap="handleGoToCardFromConfirm">
|
||
<text>{{ isRecharge ? '查看储值卡' : '去我的会员卡' }}</text>
|
||
</view>
|
||
<view class="success-modal__btn success-modal__btn--secondary" @tap="handleBackFromConfirm">
|
||
<text>返回上一页</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 旧订单提示弹窗 -->
|
||
<view class="pending-order-modal-overlay" v-if="showPendingOrderModal" @tap.stop>
|
||
<view class="pending-order-modal-dialog">
|
||
<view class="pending-order-modal__header">
|
||
<uni-icons type="info" size="40rpx" color="#F2994A" />
|
||
<text class="pending-order-modal__title">发现未完成订单</text>
|
||
</view>
|
||
<view class="pending-order-modal__body">
|
||
<text class="pending-order-modal__desc">您有一笔未支付的订单:</text>
|
||
<view class="pending-order-modal__info" v-if="pendingOrder">
|
||
<text class="pending-order-modal__order-no">订单号:{{ pendingOrder.orderId }}</text>
|
||
<text class="pending-order-modal__amount">金额:¥{{ (pendingOrder.transAmt / 100 || pendingOrder.transAmt).toFixed(2) }}</text>
|
||
<text class="pending-order-modal__time">创建时间:{{ pendingOrder.createdAt }}</text>
|
||
</view>
|
||
<text class="pending-order-modal__question">请选择您的操作:</text>
|
||
</view>
|
||
<view class="pending-order-modal__buttons">
|
||
<view class="pending-order-modal__btn pending-order-modal__btn--primary" @tap="handlePayPendingOrder">
|
||
<text>支付旧订单</text>
|
||
</view>
|
||
<view class="pending-order-modal__btn pending-order-modal__btn--secondary" @tap="handleCreateNewOrder">
|
||
<text>创建新订单</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 支付密码验证弹窗 -->
|
||
<view class="pay-password-modal-overlay" v-if="showPayPasswordModal" @tap="closePayPasswordModal">
|
||
<view class="pay-password-modal-dialog" @tap.stop>
|
||
<view class="pay-password-modal__header">
|
||
<text class="pay-password-modal__header-title">验证支付密码</text>
|
||
<view class="pay-password-modal__header-close" @tap="closePayPasswordModal">
|
||
<uni-icons type="close" size="20rpx" color="#A0AEC0" />
|
||
</view>
|
||
</view>
|
||
<view class="pay-password-modal__body">
|
||
<text class="pay-password-modal__amount">¥{{ totalPrice.toFixed(2) }}</text>
|
||
<VerifyCodeInput
|
||
ref="payPasswordInputRef"
|
||
v-model="payPassword"
|
||
:length="6"
|
||
:mask="true"
|
||
type="box"
|
||
:errorMsg="payPasswordError"
|
||
@complete="handlePayPasswordComplete"
|
||
/>
|
||
<view class="pay-password-modal__forgot" @tap="goForgotPassword">
|
||
<text>忘记密码?</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { ref, computed, onMounted, onUnmounted, nextTick } from 'vue'
|
||
import { onLoad, onShow } from '@dcloudio/uni-app'
|
||
import { getMyMemberCards, purchaseMemberCard, createPayment, createQrCodePayment, checkPayPasswordSet, verifyPayPassword, payWithStoredCard as storedCardPay, getStoredCardInfo, getPendingOrder, closeOrder } from '@/api/main.js'
|
||
import { getMemberId } from '@/utils/request.js'
|
||
import { getLoginMemberInfo } from '@/common/memberInfo/store.js'
|
||
import VerifyCodeInput from '@/components/global/VerifyCodeInput.vue'
|
||
|
||
const cart = ref([])
|
||
const selectedPayment = ref('alipay')
|
||
const storedBalance = ref(0)
|
||
const loading = ref(false)
|
||
const orderNo = ref('')
|
||
const orderTime = ref('')
|
||
const isRecharge = ref(false)
|
||
const rechargeDesc = ref('')
|
||
const rechargeRemark = ref('')
|
||
const returnPage = ref('')
|
||
const cardType = ref('')
|
||
const rechargeAmount = ref(0)
|
||
|
||
const pendingOrder = ref(null)
|
||
const showPendingOrderModal = ref(false)
|
||
|
||
const paymentResolved = ref(false)
|
||
const currentOrderId = ref(null)
|
||
const currentMerchantOrderId = ref(null)
|
||
const currentHfSeqId = ref(null)
|
||
const currentPayUrl = ref('')
|
||
const showSuccessModalVisible = ref(false)
|
||
let pollTimer = null
|
||
let pollCount = 0
|
||
const MAX_POLL_COUNT = 10
|
||
|
||
const showPayPasswordModal = ref(false)
|
||
const payPassword = ref('')
|
||
const payPasswordError = ref('')
|
||
const payPasswordInputRef = ref(null)
|
||
|
||
const totalPrice = computed(() => {
|
||
if (isRecharge.value) {
|
||
return rechargeAmount.value
|
||
}
|
||
return cart.value.reduce((sum, item) => sum + (item.card.price * item.count), 0)
|
||
})
|
||
|
||
const actualPay = computed(() => {
|
||
if (isRecharge.value) {
|
||
return rechargeAmount.value
|
||
}
|
||
if (selectedPayment.value === 'stored') {
|
||
return Math.max(0, totalPrice.value - storedBalance.value)
|
||
}
|
||
return totalPrice.value
|
||
})
|
||
|
||
const canPay = computed(() => {
|
||
if (isRecharge.value) {
|
||
if (selectedPayment.value === 'alipay') {
|
||
return true
|
||
}
|
||
return storedBalance.value >= totalPrice.value
|
||
}
|
||
if (cart.value.length === 0) return false
|
||
if (selectedPayment.value === 'alipay') {
|
||
return true
|
||
}
|
||
return storedBalance.value >= totalPrice.value
|
||
})
|
||
|
||
function getCardTypeName(type) {
|
||
const map = {
|
||
DURATION: '时长卡',
|
||
TIME_CARD: '时长卡',
|
||
COUNT: '次卡',
|
||
COUNT_CARD: '次卡',
|
||
BALANCE: '储值卡',
|
||
STORED_VALUE_CARD: '储值卡'
|
||
}
|
||
return map[type] || type
|
||
}
|
||
|
||
function getCardDesc(card) {
|
||
if (!card) return ''
|
||
if (card.validityDays) return `${card.validityDays}天`
|
||
if (card.totalTimes) return `${card.totalTimes}次`
|
||
if (card.amount) return `${card.amount}元储值`
|
||
return ''
|
||
}
|
||
|
||
async function selectPayment(type) {
|
||
if (type === 'stored') {
|
||
const memberId = getMemberId()
|
||
if (!memberId) {
|
||
uni.showToast({ title: '请先登录', icon: 'none' })
|
||
return
|
||
}
|
||
try {
|
||
const res = await getStoredCardInfo(memberId)
|
||
storedBalance.value = res.balance || res.totalAmount || 0
|
||
} catch (e) {
|
||
console.error('获取储值卡信息失败:', e)
|
||
storedBalance.value = 0
|
||
}
|
||
}
|
||
selectedPayment.value = type
|
||
}
|
||
|
||
async function loadStoredBalance() {
|
||
const memberId = getMemberId()
|
||
if (!memberId) return
|
||
try {
|
||
const res = await getStoredCardInfo(memberId)
|
||
storedBalance.value = res.balance || res.totalAmount || 0
|
||
} catch (e) {
|
||
console.error('获取储值卡余额失败:', e)
|
||
}
|
||
}
|
||
|
||
function handlePay() {
|
||
if (!canPay.value || loading.value) return
|
||
if (selectedPayment.value === 'alipay') {
|
||
createAlipayOrder()
|
||
} else if (selectedPayment.value === 'stored') {
|
||
showPayPasswordModal.value = true
|
||
}
|
||
}
|
||
|
||
async function createAlipayOrder() {
|
||
loading.value = true
|
||
try {
|
||
const memberId = getMemberId()
|
||
if (!memberId) {
|
||
uni.showToast({ title: '请先登录', icon: 'none' })
|
||
loading.value = false
|
||
return
|
||
}
|
||
|
||
let res
|
||
if (isRecharge.value) {
|
||
res = await createPayment({
|
||
memberId,
|
||
transAmt: String(Math.round(totalPrice.value * 100)),
|
||
orderType: 'STORED_CARD_RECHARGE',
|
||
goodsDesc: rechargeDesc.value || '储值卡充值',
|
||
remark: rechargeRemark.value || ''
|
||
})
|
||
} else {
|
||
const cardIds = cart.value.map(item => item.card.id)
|
||
const cardItem = cart.value[0]
|
||
const cardName = cardItem?.card?.cardName || '会员卡'
|
||
res = await createPayment({
|
||
memberId,
|
||
transAmt: String(Math.round(totalPrice.value * 100)),
|
||
orderType: 'MEMBER_CARD',
|
||
goodsDesc: cardName,
|
||
remark: ''
|
||
})
|
||
}
|
||
|
||
const payData = res && (res.data || res)
|
||
if (payData && (payData.payUrl || payData.qrCode || payData.qrCodeUrl)) {
|
||
currentOrderId.value = payData.orderId || payData.merchantOrderId
|
||
currentPayUrl.value = payData.payUrl || payData.qrCode || payData.qrCodeUrl
|
||
paymentResolved.value = false
|
||
loading.value = false
|
||
startPolling()
|
||
openAlipay(payData.payUrl || payData.qrCode || payData.qrCodeUrl)
|
||
} else {
|
||
uni.showToast({ title: '创建订单失败', icon: 'none' })
|
||
loading.value = false
|
||
}
|
||
} catch (e) {
|
||
console.error('创建订单失败:', e)
|
||
uni.showToast({ title: '创建订单失败', icon: 'none' })
|
||
loading.value = false
|
||
}
|
||
}
|
||
|
||
function openAlipay(payUrl) {
|
||
if (!payUrl) {
|
||
uni.showToast({ title: '支付链接为空', icon: 'none' })
|
||
return
|
||
}
|
||
console.log('[Alipay] 准备唤起支付宝:', payUrl)
|
||
if (typeof plus !== 'undefined') {
|
||
plus.runtime.openURL(payUrl, (err) => {
|
||
console.error('[Alipay] 唤起支付宝失败:', err)
|
||
uni.showToast({ title: '请安装支付宝App', icon: 'none' })
|
||
})
|
||
} else {
|
||
uni.showModal({ title: '提示', content: '请在 App 内完成支付', showCancel: false })
|
||
}
|
||
}
|
||
|
||
function closeSuccessModal() {
|
||
showSuccessModalVisible.value = false
|
||
handleBackFromConfirm()
|
||
}
|
||
|
||
function handleBackFromConfirm() {
|
||
const pages = getCurrentPages()
|
||
if (pages.length > 2) {
|
||
uni.navigateBack({ delta: 2 })
|
||
} else if (pages.length > 1) {
|
||
uni.navigateBack()
|
||
} else {
|
||
uni.switchTab({ url: '/pages/memberInfo/memberInfo' })
|
||
}
|
||
}
|
||
|
||
function handleGoToCardFromConfirm() {
|
||
showSuccessModalVisible.value = false
|
||
if (isRecharge.value) {
|
||
uni.redirectTo({ url: '/pages/memberInfo/rechargeStoredCard' })
|
||
} else {
|
||
uni.redirectTo({ url: '/pages/memberInfo/memberCard' })
|
||
}
|
||
}
|
||
|
||
function closePayPasswordModal() {
|
||
showPayPasswordModal.value = false
|
||
payPassword.value = ''
|
||
payPasswordError.value = ''
|
||
}
|
||
|
||
function handlePayPasswordComplete() {
|
||
handleStoredCardPay()
|
||
}
|
||
|
||
async function handleStoredCardPay() {
|
||
if (!payPassword.value || payPassword.value.length < 6) return
|
||
loading.value = true
|
||
try {
|
||
const memberId = getMemberId()
|
||
const password = payPassword.value
|
||
const res = await verifyPayPassword(memberId, password)
|
||
const verifyData = res && (res.data || res)
|
||
if (verifyData && (verifyData.code === 200 || verifyData.valid)) {
|
||
closePayPasswordModal()
|
||
const payRes = await storedCardPay(memberId, password, totalPrice.value)
|
||
const payData = payRes && (payRes.data || payRes)
|
||
if (payData && (payData.code === 200 || payData.success)) {
|
||
paymentResolved.value = true
|
||
if (isRecharge.value) {
|
||
showSuccessModalVisible.value = true
|
||
} else {
|
||
await purchaseCards()
|
||
}
|
||
} else {
|
||
uni.showToast({ title: payData?.message || '支付失败', icon: 'none' })
|
||
}
|
||
} else {
|
||
payPasswordError.value = '密码错误,请重试'
|
||
payPassword.value = ''
|
||
if (payPasswordInputRef.value) {
|
||
payPasswordInputRef.value.clear()
|
||
}
|
||
}
|
||
} catch (e) {
|
||
console.error('储值卡支付失败:', e)
|
||
uni.showToast({ title: '支付失败', icon: 'none' })
|
||
closePayPasswordModal()
|
||
} finally {
|
||
loading.value = false
|
||
}
|
||
}
|
||
|
||
function goForgotPassword() {
|
||
closePayPasswordModal()
|
||
uni.navigateTo({ url: '/pages/memberInfo/setPayPassword' })
|
||
}
|
||
|
||
async function purchaseCards() {
|
||
if (cart.value.length === 0) return
|
||
|
||
uni.showLoading({ title: '开通会员卡...' })
|
||
let successCount = 0
|
||
|
||
for (const item of cart.value) {
|
||
try {
|
||
const memberRes = await purchaseMemberCard({ memberCardId: item.card.id })
|
||
if (memberRes.code === 200 || memberRes.id || memberRes.memberCardRecordId) {
|
||
successCount += item.count
|
||
}
|
||
} catch (e) {
|
||
console.error('购买会员卡失败:', e)
|
||
}
|
||
}
|
||
|
||
uni.hideLoading()
|
||
if (successCount > 0) {
|
||
showSuccessModalVisible.value = true
|
||
} else {
|
||
uni.showToast({ title: '开通会员卡失败', icon: 'none' })
|
||
}
|
||
}
|
||
|
||
async function handlePayPendingOrder() {
|
||
showPendingOrderModal.value = false
|
||
if (pendingOrder.value) {
|
||
currentOrderId.value = pendingOrder.value.orderId
|
||
currentPayUrl.value = pendingOrder.value.payUrl
|
||
paymentResolved.value = false
|
||
startPolling()
|
||
openAlipay(pendingOrder.value.payUrl)
|
||
}
|
||
}
|
||
|
||
function handleCreateNewOrder() {
|
||
showPendingOrderModal.value = false
|
||
if (pendingOrder.value?.orderId) {
|
||
closeOrder(pendingOrder.value.orderId).catch(() => {})
|
||
}
|
||
pendingOrder.value = null
|
||
}
|
||
|
||
async function checkPendingOrder() {
|
||
try {
|
||
const memberId = getMemberId()
|
||
if (!memberId) return
|
||
const res = await getPendingOrder(memberId)
|
||
if (res && (res.orderId || res.data?.orderId)) {
|
||
pendingOrder.value = res.data || res
|
||
showPendingOrderModal.value = true
|
||
}
|
||
} catch (e) {
|
||
console.error('检查待支付订单失败:', e)
|
||
}
|
||
}
|
||
|
||
onLoad((options) => {
|
||
if (options && options.returnPage) {
|
||
returnPage.value = options.returnPage
|
||
}
|
||
if (options && options.cardType) {
|
||
cardType.value = options.cardType
|
||
}
|
||
if (options && options.orderType && options.orderType === 'STORED_CARD_RECHARGE') {
|
||
isRecharge.value = true
|
||
if (options.desc) {
|
||
rechargeDesc.value = decodeURIComponent(options.desc)
|
||
}
|
||
if (options.remark) {
|
||
rechargeRemark.value = decodeURIComponent(options.remark)
|
||
}
|
||
}
|
||
|
||
if (isRecharge.value && options && options.amount) {
|
||
const amt = parseFloat(options.amount)
|
||
if (!isNaN(amt)) {
|
||
rechargeAmount.value = amt
|
||
}
|
||
}
|
||
|
||
if (options && options.orderId) {
|
||
currentOrderId.value = options.orderId
|
||
setTimeout(() => {
|
||
checkPaymentStatus()
|
||
}, 500)
|
||
}
|
||
|
||
try {
|
||
if (options && options.cartData) {
|
||
cart.value = JSON.parse(decodeURIComponent(options.cartData))
|
||
} else if (options && options.cardInfo) {
|
||
const cardInfo = JSON.parse(decodeURIComponent(options.cardInfo))
|
||
const purchaseCount = parseInt(options.purchaseCount) || 1
|
||
cart.value = [{ card: cardInfo, count: purchaseCount }]
|
||
}
|
||
} catch (e) {
|
||
console.error('解析购物车数据失败:', e)
|
||
cart.value = []
|
||
}
|
||
|
||
const now = new Date()
|
||
orderNo.value = `PAY${now.getFullYear()}${String(now.getMonth() + 1).padStart(2, '0')}${String(now.getDate()).padStart(2, '0')}${String(now.getHours()).padStart(2, '0')}${String(now.getMinutes()).padStart(2, '0')}${String(now.getSeconds()).padStart(2, '0')}`
|
||
orderTime.value = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')} ${String(now.getHours()).padStart(2, '0')}:${String(now.getMinutes()).padStart(2, '0')}`
|
||
|
||
loadStoredBalance()
|
||
checkPendingOrder()
|
||
})
|
||
|
||
function handleAppResume() {
|
||
if (paymentResolved.value || !currentOrderId.value) {
|
||
return
|
||
}
|
||
console.log('[Payment] APP从后台返回,查询支付结果')
|
||
startPolling()
|
||
}
|
||
|
||
function startPolling() {
|
||
if (paymentResolved.value || !currentOrderId.value) {
|
||
return
|
||
}
|
||
stopPolling()
|
||
pollCount = 0
|
||
console.log('[Payment] 开始轮询支付状态')
|
||
checkPaymentStatus()
|
||
pollTimer = setInterval(() => {
|
||
pollCount++
|
||
console.log(`[Payment] 轮询第 ${pollCount} 次`)
|
||
if (pollCount >= MAX_POLL_COUNT) {
|
||
console.log('[Payment] 轮询次数达到上限,停止轮询')
|
||
stopPolling()
|
||
return
|
||
}
|
||
checkPaymentStatus(true)
|
||
}, 2000)
|
||
}
|
||
|
||
function stopPolling() {
|
||
if (pollTimer) {
|
||
clearInterval(pollTimer)
|
||
pollTimer = null
|
||
}
|
||
}
|
||
|
||
function checkPaymentStatus(silent = false) {
|
||
if (paymentResolved.value || !currentOrderId.value) {
|
||
stopPolling()
|
||
return
|
||
}
|
||
if (!silent) {
|
||
uni.showLoading({ title: '查询支付结果...' })
|
||
}
|
||
|
||
const queryOrderId = currentOrderId.value
|
||
import('@/api/main.js').then(({ getPaymentStatus }) => {
|
||
getPaymentStatus(queryOrderId)
|
||
.then(res => {
|
||
if (!silent) {
|
||
uni.hideLoading()
|
||
}
|
||
if (res.code === 200 && res.data) {
|
||
const status = res.data.status || res.data.payStatus
|
||
console.log('[Payment] 支付状态:', status)
|
||
if (status === 'SUCCESS') {
|
||
paymentResolved.value = true
|
||
stopPolling()
|
||
uni.showToast({ title: '支付成功', icon: 'success', duration: 2000 })
|
||
handlePaymentSuccess()
|
||
} else if (status === 'FAIL') {
|
||
paymentResolved.value = true
|
||
stopPolling()
|
||
uni.showToast({ title: '支付失败', icon: 'error' })
|
||
loading.value = false
|
||
} else if (status === 'CLOSED') {
|
||
paymentResolved.value = true
|
||
stopPolling()
|
||
uni.showToast({ title: '订单已关闭', icon: 'none' })
|
||
loading.value = false
|
||
}
|
||
} else {
|
||
if (!silent) {
|
||
uni.hideLoading()
|
||
}
|
||
}
|
||
})
|
||
.catch(() => {
|
||
if (!silent) {
|
||
uni.hideLoading()
|
||
}
|
||
console.error('[Payment] 查询支付状态失败')
|
||
})
|
||
})
|
||
}
|
||
|
||
async function handlePaymentSuccess() {
|
||
try {
|
||
const res = await getStoredCardInfo(getMemberId())
|
||
storedBalance.value = res.balance || 0
|
||
} catch (e) {
|
||
console.error('支付成功后更新余额失败:', e)
|
||
}
|
||
|
||
if (isRecharge.value) {
|
||
uni.showToast({ title: '充值成功', icon: 'success' })
|
||
showSuccessModalVisible.value = true
|
||
return
|
||
}
|
||
|
||
if (cart.value.length === 0) return
|
||
|
||
uni.showLoading({ title: '开通会员卡...' })
|
||
let successCount = 0
|
||
|
||
for (const item of cart.value) {
|
||
try {
|
||
const memberRes = await purchaseMemberCard({ memberCardId: item.card.id })
|
||
if (memberRes.code === 200 || memberRes.id || memberRes.memberCardRecordId) {
|
||
successCount += item.count
|
||
}
|
||
} catch (e) {
|
||
console.error('购买会员卡失败:', e)
|
||
}
|
||
}
|
||
|
||
uni.hideLoading()
|
||
if (successCount > 0) {
|
||
showSuccessModalVisible.value = true
|
||
}
|
||
}
|
||
|
||
onMounted(() => {
|
||
if (typeof plus !== 'undefined') {
|
||
plus.globalEvent.addEventListener('resume', handleAppResume)
|
||
}
|
||
})
|
||
|
||
onUnmounted(() => {
|
||
paymentResolved.value = true
|
||
stopPolling()
|
||
if (typeof plus !== 'undefined') {
|
||
plus.globalEvent.removeEventListener('resume', handleAppResume)
|
||
}
|
||
})
|
||
</script>
|
||
|
||
<style lang="scss" scoped>
|
||
@import '@/common/style/memberInfo/pages/page-reset.css';
|
||
@import '@/common/style/memberInfo/member-info-tap.css';
|
||
|
||
// ================== 页面布局 ==================
|
||
.confirm-payment-page {
|
||
display: flex;
|
||
flex-direction: column;
|
||
height: 100vh;
|
||
overflow: hidden;
|
||
background: #F5F7FA;
|
||
}
|
||
|
||
.cp-scroll-wrap {
|
||
flex: 1;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.cp-scroll {
|
||
height: 100%;
|
||
overflow-y: auto;
|
||
}
|
||
|
||
.cp-body {
|
||
padding: 24rpx 32rpx;
|
||
padding-bottom: 160rpx;
|
||
}
|
||
|
||
// ================== 区块 ==================
|
||
.cp-section {
|
||
background: #FFFFFF;
|
||
border-radius: 16rpx;
|
||
padding: 24rpx 32rpx;
|
||
margin-bottom: 24rpx;
|
||
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.04);
|
||
}
|
||
|
||
.cp-section__title {
|
||
font-size: 28rpx;
|
||
font-weight: 600;
|
||
color: #1E2A3A;
|
||
margin-bottom: 20rpx;
|
||
display: block;
|
||
}
|
||
|
||
// ================== 订单信息 ==================
|
||
.cp-order {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 16rpx;
|
||
}
|
||
|
||
.cp-order__item {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
}
|
||
|
||
.cp-order__label {
|
||
font-size: 26rpx;
|
||
color: #8A99B4;
|
||
}
|
||
|
||
.cp-order__value {
|
||
font-size: 26rpx;
|
||
color: #2D3748;
|
||
}
|
||
|
||
// ================== 商品信息 ==================
|
||
.cp-goods {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 16rpx;
|
||
}
|
||
|
||
.cp-goods__item {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 16rpx;
|
||
padding: 20rpx;
|
||
background: #F7FAFC;
|
||
border-radius: 12rpx;
|
||
}
|
||
|
||
.cp-goods__icon-wrap {
|
||
width: 64rpx;
|
||
height: 64rpx;
|
||
border-radius: 16rpx;
|
||
background: rgba(255, 107, 53, 0.1);
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.cp-goods__info {
|
||
flex: 1;
|
||
min-width: 0;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 6rpx;
|
||
}
|
||
|
||
.cp-goods__name {
|
||
font-size: 28rpx;
|
||
font-weight: 600;
|
||
color: #2D3748;
|
||
}
|
||
|
||
.cp-goods__desc {
|
||
font-size: 24rpx;
|
||
color: #8A99B4;
|
||
}
|
||
|
||
.cp-goods__right {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: flex-end;
|
||
gap: 6rpx;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.cp-goods__count {
|
||
font-size: 24rpx;
|
||
color: #A0AEC0;
|
||
}
|
||
|
||
.cp-goods__price {
|
||
font-size: 28rpx;
|
||
font-weight: 600;
|
||
color: #E53E3E;
|
||
}
|
||
|
||
// ================== 充值信息 ==================
|
||
.cp-recharge {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 20rpx;
|
||
}
|
||
|
||
.cp-recharge__icon-wrap {
|
||
width: 64rpx;
|
||
height: 64rpx;
|
||
border-radius: 16rpx;
|
||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.cp-recharge__info {
|
||
flex: 1;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
}
|
||
|
||
.cp-recharge__name {
|
||
font-size: 28rpx;
|
||
font-weight: 600;
|
||
color: #2D3748;
|
||
}
|
||
|
||
.cp-recharge__amount {
|
||
font-size: 28rpx;
|
||
font-weight: 600;
|
||
color: #E53E3E;
|
||
}
|
||
|
||
// ================== 支付方式 ==================
|
||
.cp-payment {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 16rpx;
|
||
}
|
||
|
||
.cp-payment__item {
|
||
display: flex;
|
||
align-items: center;
|
||
padding: 24rpx;
|
||
border: 2rpx solid #E2E8F0;
|
||
border-radius: 16rpx;
|
||
transition: all 0.2s;
|
||
position: relative;
|
||
gap: 16rpx;
|
||
}
|
||
|
||
.cp-payment__item--selected {
|
||
border-color: #1677FF;
|
||
background: #F0F7FF;
|
||
}
|
||
|
||
.cp-payment__item--disabled {
|
||
opacity: 0.55;
|
||
}
|
||
|
||
.cp-payment__icon-wrap {
|
||
width: 64rpx;
|
||
height: 64rpx;
|
||
border-radius: 16rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.cp-payment__icon-wrap--alipay {
|
||
background: #1677FF;
|
||
}
|
||
|
||
.cp-payment__icon-img {
|
||
width: 40rpx;
|
||
height: 40rpx;
|
||
}
|
||
|
||
.cp-payment__icon-wrap--stored {
|
||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||
}
|
||
|
||
.cp-payment__info {
|
||
flex: 1;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 6rpx;
|
||
}
|
||
|
||
.cp-payment__name {
|
||
font-size: 28rpx;
|
||
font-weight: 600;
|
||
color: #2D3748;
|
||
}
|
||
|
||
.cp-payment__desc {
|
||
font-size: 24rpx;
|
||
color: #8A99B4;
|
||
}
|
||
|
||
.cp-payment__radio {
|
||
width: 36rpx;
|
||
height: 36rpx;
|
||
border: 3rpx solid #CBD5E0;
|
||
border-radius: 50%;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.cp-payment__item--selected .cp-payment__radio {
|
||
border-color: #1677FF;
|
||
}
|
||
|
||
.cp-payment__radio-inner {
|
||
width: 20rpx;
|
||
height: 20rpx;
|
||
border-radius: 50%;
|
||
background: transparent;
|
||
transition: all 0.2s;
|
||
}
|
||
|
||
.cp-payment__radio-inner--checked {
|
||
background: #1677FF;
|
||
}
|
||
|
||
.cp-payment__tag {
|
||
position: absolute;
|
||
right: 32rpx;
|
||
top: 50%;
|
||
transform: translateY(-50%);
|
||
padding: 8rpx 20rpx;
|
||
background: #FED7D7;
|
||
border-radius: 20rpx;
|
||
}
|
||
|
||
.cp-payment__tag text {
|
||
font-size: 22rpx;
|
||
color: #E53E3E;
|
||
}
|
||
|
||
// ================== 金额明细 ==================
|
||
.cp-amount {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 16rpx;
|
||
}
|
||
|
||
.cp-amount__item {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
}
|
||
|
||
.cp-amount__item--total {
|
||
padding-top: 16rpx;
|
||
border-top: 1rpx dashed #E2E8F0;
|
||
margin-top: 8rpx;
|
||
}
|
||
|
||
.cp-amount__label {
|
||
font-size: 26rpx;
|
||
color: #8A99B4;
|
||
}
|
||
|
||
.cp-amount__label--total {
|
||
font-size: 28rpx;
|
||
font-weight: 600;
|
||
color: #1E2A3A;
|
||
}
|
||
|
||
.cp-amount__value {
|
||
font-size: 26rpx;
|
||
color: #2D3748;
|
||
}
|
||
|
||
.cp-amount__value--discount {
|
||
color: #38A169;
|
||
}
|
||
|
||
.cp-amount__value--total {
|
||
font-size: 32rpx;
|
||
font-weight: 700;
|
||
color: #E53E3E;
|
||
}
|
||
|
||
// ================== 底部支付栏 ==================
|
||
.cp-bottom {
|
||
position: fixed;
|
||
bottom: 0;
|
||
left: 0;
|
||
right: 0;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
padding: 24rpx 32rpx;
|
||
padding-bottom: calc(24rpx + env(safe-area-inset-bottom));
|
||
background: #FFFFFF;
|
||
box-shadow: 0 -4rpx 20rpx rgba(0, 0, 0, 0.05);
|
||
z-index: 100;
|
||
}
|
||
|
||
.cp-bottom__left {
|
||
display: flex;
|
||
align-items: baseline;
|
||
gap: 12rpx;
|
||
}
|
||
|
||
.cp-bottom__label {
|
||
font-size: 26rpx;
|
||
color: #8A99B4;
|
||
}
|
||
|
||
.cp-bottom__price {
|
||
font-size: 40rpx;
|
||
font-weight: 700;
|
||
color: #E53E3E;
|
||
line-height: 1;
|
||
}
|
||
|
||
.cp-bottom__btn {
|
||
background: linear-gradient(135deg, #1677FF 0%, #0958D9 100%);
|
||
border-radius: 48rpx;
|
||
padding: 24rpx 56rpx;
|
||
transition: all 0.2s;
|
||
|
||
&:active:not(.cp-bottom__btn--disabled) {
|
||
transform: scale(0.97);
|
||
opacity: 0.9;
|
||
}
|
||
}
|
||
|
||
.cp-bottom__btn--disabled {
|
||
background: #A0AEC0;
|
||
}
|
||
|
||
.cp-bottom__btn-text {
|
||
font-size: 30rpx;
|
||
font-weight: 600;
|
||
color: #FFFFFF;
|
||
}
|
||
|
||
// ================== 成功弹窗 ==================
|
||
.success-modal-overlay {
|
||
position: fixed;
|
||
top: 0;
|
||
left: 0;
|
||
right: 0;
|
||
bottom: 0;
|
||
background: rgba(0, 0, 0, 0.5);
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
z-index: 2000;
|
||
animation: fadeIn 0.3s ease;
|
||
}
|
||
|
||
.success-modal-dialog {
|
||
width: 600rpx;
|
||
max-width: 600rpx;
|
||
background: #FFFFFF;
|
||
border-radius: 32rpx;
|
||
padding: 56rpx 40rpx;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
animation: modalPop 0.3s ease;
|
||
}
|
||
|
||
.success-modal__icon-wrap {
|
||
width: 96rpx;
|
||
height: 96rpx;
|
||
border-radius: 48rpx;
|
||
background: linear-gradient(135deg, #4CAF50 0%, #66BB6A 100%);
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
margin-bottom: 24rpx;
|
||
}
|
||
|
||
.success-modal__title {
|
||
font-size: 36rpx;
|
||
font-weight: 700;
|
||
color: #1A202C;
|
||
margin-bottom: 12rpx;
|
||
}
|
||
|
||
.success-modal__desc {
|
||
font-size: 28rpx;
|
||
color: #8A99B4;
|
||
margin-bottom: 40rpx;
|
||
}
|
||
|
||
.success-modal__buttons {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 16rpx;
|
||
width: 100%;
|
||
}
|
||
|
||
.success-modal__btn {
|
||
padding: 24rpx;
|
||
border-radius: 48rpx;
|
||
text-align: center;
|
||
font-size: 30rpx;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.success-modal__btn--primary {
|
||
background: linear-gradient(135deg, #1677FF 0%, #0958D9 100%);
|
||
color: #FFFFFF;
|
||
}
|
||
|
||
.success-modal__btn--secondary {
|
||
background: #F5F7FA;
|
||
color: #4A5568;
|
||
}
|
||
|
||
// ================== 旧订单弹窗 ==================
|
||
.pending-order-modal-overlay {
|
||
position: fixed;
|
||
top: 0;
|
||
left: 0;
|
||
right: 0;
|
||
bottom: 0;
|
||
background: rgba(0, 0, 0, 0.5);
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
z-index: 1000;
|
||
animation: fadeIn 0.2s ease-out;
|
||
}
|
||
|
||
.pending-order-modal-dialog {
|
||
width: 600rpx;
|
||
background: #FFFFFF;
|
||
border-radius: 32rpx;
|
||
padding: 40rpx;
|
||
animation: slideUp 0.3s ease-out;
|
||
}
|
||
|
||
.pending-order-modal__header {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
gap: 16rpx;
|
||
margin-bottom: 24rpx;
|
||
}
|
||
|
||
.pending-order-modal__title {
|
||
font-size: 32rpx;
|
||
font-weight: 600;
|
||
color: #1A202C;
|
||
}
|
||
|
||
.pending-order-modal__body {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 16rpx;
|
||
margin-bottom: 32rpx;
|
||
}
|
||
|
||
.pending-order-modal__desc {
|
||
font-size: 26rpx;
|
||
color: #8A99B4;
|
||
}
|
||
|
||
.pending-order-modal__info {
|
||
background: #F7FAFC;
|
||
border-radius: 16rpx;
|
||
padding: 24rpx;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 12rpx;
|
||
}
|
||
|
||
.pending-order-modal__order-no {
|
||
font-size: 24rpx;
|
||
color: #2D3748;
|
||
}
|
||
|
||
.pending-order-modal__amount {
|
||
font-size: 28rpx;
|
||
font-weight: 600;
|
||
color: #E53E3E;
|
||
}
|
||
|
||
.pending-order-modal__time {
|
||
font-size: 24rpx;
|
||
color: #8A99B4;
|
||
}
|
||
|
||
.pending-order-modal__question {
|
||
font-size: 26rpx;
|
||
color: #2D3748;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.pending-order-modal__buttons {
|
||
display: flex;
|
||
gap: 16rpx;
|
||
}
|
||
|
||
.pending-order-modal__btn {
|
||
flex: 1;
|
||
padding: 24rpx;
|
||
border-radius: 48rpx;
|
||
text-align: center;
|
||
font-size: 28rpx;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.pending-order-modal__btn--primary {
|
||
background: linear-gradient(135deg, #1677FF 0%, #0958D9 100%);
|
||
color: #FFFFFF;
|
||
}
|
||
|
||
.pending-order-modal__btn--secondary {
|
||
background: #F7FAFC;
|
||
color: #2D3748;
|
||
border: 2rpx solid #E2E8F0;
|
||
}
|
||
|
||
// ================== 支付密码弹窗 ==================
|
||
.pay-password-modal-overlay {
|
||
position: fixed;
|
||
top: 0;
|
||
left: 0;
|
||
right: 0;
|
||
bottom: 0;
|
||
background: rgba(0, 0, 0, 0.5);
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
z-index: 2000;
|
||
animation: fadeIn 0.3s ease;
|
||
}
|
||
|
||
.pay-password-modal-dialog {
|
||
width: 700rpx;
|
||
max-width: 95vw;
|
||
background: #FFFFFF;
|
||
border-radius: 32rpx;
|
||
overflow: visible;
|
||
animation: modalPop 0.3s ease;
|
||
}
|
||
|
||
.pay-password-modal__header {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
padding: 32rpx 36rpx 20rpx;
|
||
}
|
||
|
||
.pay-password-modal__header-title {
|
||
font-size: 32rpx;
|
||
font-weight: 600;
|
||
color: #1A202C;
|
||
}
|
||
|
||
.pay-password-modal__header-close {
|
||
width: 48rpx;
|
||
height: 48rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
|
||
.pay-password-modal__body {
|
||
padding: 0 24rpx 48rpx;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
}
|
||
|
||
.pay-password-modal__amount {
|
||
font-size: 48rpx;
|
||
font-weight: 700;
|
||
color: #E53E3E;
|
||
margin-bottom: 32rpx;
|
||
}
|
||
|
||
.pay-password-modal__forgot {
|
||
margin-top: 24rpx;
|
||
width: 100%;
|
||
text-align: right;
|
||
}
|
||
|
||
.pay-password-modal__forgot text {
|
||
font-size: 26rpx;
|
||
color: #1677FF;
|
||
}
|
||
|
||
// ================== 动画 ==================
|
||
@keyframes fadeIn {
|
||
from { opacity: 0; }
|
||
to { opacity: 1; }
|
||
}
|
||
|
||
@keyframes modalPop {
|
||
from {
|
||
opacity: 0;
|
||
transform: scale(0.9);
|
||
}
|
||
to {
|
||
opacity: 1;
|
||
transform: scale(1);
|
||
}
|
||
}
|
||
|
||
@keyframes slideUp {
|
||
from {
|
||
opacity: 0;
|
||
transform: translateY(40rpx);
|
||
}
|
||
to {
|
||
opacity: 1;
|
||
transform: translateY(0);
|
||
}
|
||
}
|
||
</style>
|