Files
gym-manage/gym-manage-uniapp/pages/memberInfo/confirmPayment.vue
T
2026-06-28 14:49:21 +08:00

1722 lines
52 KiB
Vue
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<view class="scroll-container theme-light">
<view class="confirm-payment-page">
<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">🏋</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">💳</view>
<view class="cp-recharge__info">
<text class="cp-recharge__name">储值卡充值</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 cp-payment__icon--alipay">
<image 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__check">
<view class="cp-payment__check-inner" :class="{ 'cp-payment__check-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 cp-payment__icon--stored">💰</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__check">
<view class="cp-payment__check-inner" :class="{ 'cp-payment__check-inner--checked': selectedPayment === 'stored' }"></view>
</view>
<view class="cp-payment__disabled" 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">实付金额</text>
<text class="cp-amount__value cp-amount__value--total">¥{{ actualPay.toFixed(2) }}</text>
</view>
</view>
</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">🎉</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--secondary" @tap="handleBackFromConfirm">
<text>返回上一页</text>
</view>
<view class="success-modal__btn success-modal__btn--primary" @tap="handleGoToCardFromConfirm">
<text>{{ isRecharge ? '查看储值卡' : '去我的会员卡' }}</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">
<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__title">验证支付密码</text>
<view class="pay-password-modal__close" @tap="closePayPasswordModal">
<text></text>
</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>
</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(() => {
// 储值卡充值时使用 rechargeAmount
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' })
uni.navigateTo({ url: '/pages/memberInfo/login' })
return
}
if (storedBalance.value < totalPrice.value) {
uni.showToast({ title: '储值余额不足', icon: 'none' })
return
}
uni.showLoading({ title: '检查支付密码...' })
try {
const res = await checkPayPasswordSet(memberId)
uni.hideLoading()
console.log('[Payment] 检查支付密码返回:', JSON.stringify(res))
// 兼容多种返回格式
let isSet = false
if (res?.data?.isSet !== undefined) {
isSet = res.data.isSet
} else if (res?.isSet !== undefined) {
isSet = res.isSet
}
console.log('[Payment] 支付密码是否已设置:', isSet)
if (!isSet) {
uni.showModal({
title: '提示',
content: '您尚未设置支付密码,请先设置支付密码',
showCancel: false,
success: () => {
uni.navigateTo({ url: '/pages/memberInfo/setPayPassword' })
}
})
return
}
} catch (e) {
uni.hideLoading()
console.error('检查支付密码失败:', e)
uni.showToast({ title: '检查失败', icon: 'none' })
return
}
}
selectedPayment.value = type
}
async function loadStoredBalance() {
if (!getMemberId()) return
try {
const res = await getStoredCardInfo(getMemberId())
const balance = res.balance || 0
storedBalance.value = balance
console.log('[Payment] 从后端读取储值卡余额:', balance)
if (balance < totalPrice.value && selectedPayment.value === 'stored') {
selectedPayment.value = 'alipay'
}
} catch (e) {
console.error('加载储值余额失败:', e)
storedBalance.value = 0
}
}
async function handlePay() {
if (!canPay.value || loading.value) return
console.log('支付前会员ID:', getMemberId())
// 先检查是否有未支付的旧订单
const memberId = getMemberId()
if (!memberId) {
uni.showToast({ title: '请先登录', icon: 'none' })
return
}
try {
uni.showLoading({ title: '检查订单...' })
const orderType = isRecharge.value ? 'STORED_CARD_RECHARGE' : 'MEMBER_CARD'
const pendingRes = await getPendingOrder(memberId, orderType)
uni.hideLoading()
// 如果有未支付的旧订单,提示用户
if (pendingRes && pendingRes.data && pendingRes.data.orderId) {
pendingOrder.value = pendingRes.data
showPendingOrderModal.value = true
return
}
} catch (e) {
uni.hideLoading()
console.log('检查旧订单失败,继续支付流程:', e)
}
// 没有旧订单,继续支付
loading.value = true
try {
if (isRecharge.value || selectedPayment.value === 'alipay') {
await payWithAlipay()
} else {
await payWithStoredCard()
}
} catch (e) {
console.error('支付失败:', e)
uni.showToast({ title: e.message || '支付失败', icon: 'none' })
loading.value = false
}
}
// 处理旧订单:支付旧订单
async function handlePayPendingOrder() {
showPendingOrderModal.value = false
if (!pendingOrder.value || !pendingOrder.value.orderId) {
return
}
// 直接跳转到旧订单的支付流程
currentOrderId.value = pendingOrder.value.orderId
currentMerchantOrderId.value = pendingOrder.value.merchantOrderId
currentHfSeqId.value = pendingOrder.value.hfSeqId
// 根据支付方式处理
if (pendingOrder.value.tradeType === 'ALIPAY' || pendingOrder.value.payUrl) {
const payUrl = pendingOrder.value.payUrl
if (payUrl && typeof plus !== 'undefined') {
const encodedUrl = encodeURIComponent(payUrl)
const alipayScheme = `alipays://platformapi/startapp?appId=20000067&url=${encodedUrl}`
plus.runtime.openURL(alipayScheme, function(err) {
console.log('[Alipay] 唤起支付宝App失败,尝试H5支付:', err)
plus.runtime.openURL(payUrl, function(h5Err) {
console.error('[Alipay] H5支付也失败:', h5Err)
uni.showModal({ title: '提示', content: '无法唤起支付宝,请确认已安装', showCancel: false })
})
})
} else if (payUrl) {
window.location.href = payUrl
}
} else {
// 储值卡支付的旧订单,重新验证支付密码
loading.value = true
payPassword.value = ''
payPasswordError.value = ''
showPayPasswordModal.value = true
nextTick(() => {
if (payPasswordInputRef.value) {
payPasswordInputRef.value.focus()
}
})
}
pendingOrder.value = null
}
// 处理旧订单:关闭旧订单并创建新订单
async function handleCreateNewOrder() {
showPendingOrderModal.value = false
if (!pendingOrder.value || !pendingOrder.value.orderId) {
pendingOrder.value = null
loading.value = true
// 继续支付流程
try {
if (isRecharge.value || selectedPayment.value === 'alipay') {
await payWithAlipay()
} else {
await payWithStoredCard()
}
} catch (e) {
console.error('支付失败:', e)
uni.showToast({ title: e.message || '支付失败', icon: 'none' })
loading.value = false
}
return
}
try {
uni.showLoading({ title: '关闭旧订单...' })
await closeOrder(pendingOrder.value.orderId)
uni.hideLoading()
pendingOrder.value = null
// 继续支付流程
loading.value = true
try {
if (isRecharge.value || selectedPayment.value === 'alipay') {
await payWithAlipay()
} else {
await payWithStoredCard()
}
} catch (e) {
console.error('支付失败:', e)
uni.showToast({ title: e.message || '支付失败', icon: 'none' })
loading.value = false
}
} catch (e) {
uni.hideLoading()
uni.showToast({ title: '关闭旧订单失败,请稍后重试', icon: 'none' })
console.error('关闭旧订单失败:', e)
}
}
async function payWithStoredCard() {
if (storedBalance.value < totalPrice.value) {
uni.showToast({
title: `余额不足,当前余额:¥${storedBalance.value.toFixed(2)}`,
icon: 'none',
duration: 3000
})
loading.value = false
return
}
payPassword.value = ''
payPasswordError.value = ''
showPayPasswordModal.value = true
nextTick(() => {
payPasswordInputRef.value?.focusInput()
})
}
function closePayPasswordModal() {
showPayPasswordModal.value = false
payPassword.value = ''
payPasswordError.value = ''
if (loading.value) {
loading.value = false
}
}
function handlePayPasswordComplete(password) {
payPasswordError.value = ''
doPayWithStoredCard(password)
}
function goForgotPassword() {
uni.showToast({ title: '请联系客服重置密码', icon: 'none' })
}
async function doPayWithStoredCard(password) {
uni.showLoading({ title: '处理中...' })
try {
const payRes = await storedCardPay(getMemberId(), password, totalPrice.value)
if (payRes.code !== 200) {
uni.hideLoading()
const msg = payRes.message || '支付失败'
if (msg.includes('密码错误') || msg.includes('密码')) {
payPasswordError.value = msg
payPassword.value = ''
nextTick(() => {
payPasswordInputRef.value?.focusInput()
})
} else {
uni.showToast({ title: msg, icon: 'none' })
loading.value = false
closePayPasswordModal()
}
return
}
storedBalance.value = payRes.data?.balance || 0
closePayPasswordModal()
let successCount = 0
let failedCount = 0
let hasStoredCard = false
for (const item of cart.value) {
try {
// 使用主键id(而不是memberCardId业务字段)
const cardId = item.card.id
const res = await purchaseMemberCard({
memberCardId: cardId
})
if (res.code === 200 || res.id || res.memberCardRecordId) {
successCount += item.count
const cardType = item.card.cardType || item.card.memberCardType
if (cardType === 'STORED_VALUE_CARD' || cardType === 'BALANCE') {
hasStoredCard = true
}
} else {
failedCount += item.count
}
} catch (e) {
console.error('购买会员卡失败:', e)
failedCount += item.count
}
}
uni.hideLoading()
// 重新从后端查询余额(确保余额正确)
try {
const balanceRes = await getStoredCardInfo(getMemberId())
storedBalance.value = balanceRes.balance || 0
console.log('[Payment] 储值卡支付后更新余额:', storedBalance.value)
} catch (e) {
console.error('更新储值卡余额失败:', e)
}
if (successCount > 0) {
uni.showToast({ title: `购买成功,支付¥${totalPrice.value.toFixed(2)}`, icon: 'success' })
if (hasStoredCard) {
setTimeout(() => {
uni.showModal({
title: '设置支付密码',
content: '恭喜您购买储值卡成功!为了您的账户安全,请设置支付密码。设置后可使用储值卡余额支付。',
confirmText: '去设置',
cancelText: '稍后设置',
confirmColor: '#1677FF',
success: (modalRes) => {
if (modalRes.confirm) {
uni.navigateTo({ url: '/pages/memberInfo/setPayPassword' })
} else {
uni.redirectTo({ url: '/pages/memberInfo/memberCard' })
}
}
})
}, 1500)
} else {
setTimeout(() => {
uni.redirectTo({ url: '/pages/memberInfo/memberCard' })
}, 1500)
}
} else {
uni.showToast({ title: '会员卡购买失败,请联系客服', icon: 'none' })
loading.value = false
}
} catch (e) {
uni.hideLoading()
console.error('储值卡支付失败:', e)
uni.showToast({ title: '支付失败,请稍后重试', icon: 'none' })
loading.value = false
closePayPasswordModal()
}
}
async function payWithAlipay() {
let goodsDesc = ''
let remark = ''
let orderType = 'MEMBER_CARD'
if (isRecharge.value) {
goodsDesc = rechargeDesc.value || '储值卡充值'
remark = rechargeRemark.value || ''
orderType = 'STORED_CARD_RECHARGE'
} else {
goodsDesc = cart.value.map(item => `${item.card.cardName} × ${item.count}`).join('、')
remark = cart.value.map(item => `会员卡类型ID:${item.card.id},数量:${item.count}`).join(';')
}
const transAmt = String(Math.round(totalPrice.value * 100))
const confirmContent = isRecharge.value
? `确认支付 ¥${totalPrice.value.toFixed(2)} 进行${goodsDesc}`
: `确认支付 ¥${totalPrice.value.toFixed(2)} 购买${goodsDesc}`
uni.showModal({
title: '确认支付',
content: confirmContent,
confirmText: '确认支付',
confirmColor: '#1677FF',
success: async (modalRes) => {
if (!modalRes.confirm) {
loading.value = false
return
}
try {
const { getPendingOrder } = await import('@/api/main.js')
const pendingRes = await getPendingOrder(getMemberId(), orderType)
if (pendingRes.code === 200 && pendingRes.data) {
const pendingOrder = pendingRes.data
const pendingAmount = pendingOrder.transAmt || (pendingOrder.amount ? (pendingOrder.amount / 100).toFixed(2) : '0.00')
uni.showModal({
title: '存在未支付订单',
content: `您有一笔¥${pendingAmount}${goodsDesc}订单尚未支付,是否继续支付该订单?\n\n点击"支付旧订单"继续支付\n点击"创建新订单"关闭旧订单并重新创建`,
confirmText: '支付旧订单',
cancelText: '创建新订单',
confirmColor: '#1677FF',
success: async (chooseRes) => {
if (chooseRes.confirm) {
useExistingOrder(pendingOrder)
} else {
try {
const { closeOrder } = await import('@/api/main.js')
const orderId = pendingOrder.orderId || pendingOrder.merchantOrderId || pendingOrder.orderNo
await closeOrder(orderId)
createNewPayment(orderType, goodsDesc, remark, transAmt)
} catch (e) {
console.error('[Payment] 关闭旧订单失败:', e)
uni.showToast({ title: '关闭旧订单失败,请重试', icon: 'none' })
loading.value = false
}
}
},
fail: () => {
loading.value = false
}
})
} else {
createNewPayment(orderType, goodsDesc, remark, transAmt)
}
} catch (e) {
console.error('[Payment] 查询未支付订单失败:', e)
createNewPayment(orderType, goodsDesc, remark, transAmt)
}
},
fail: () => {
loading.value = false
}
})
}
function useExistingOrder(order) {
currentOrderId.value = order.orderId || order.merchantOrderId || order.orderNo
currentMerchantOrderId.value = order.merchantOrderId || currentOrderId.value
currentHfSeqId.value = order.hfSeqId || order.hfOrderId
currentPayUrl.value = order.payUrl || order.payInfo || order.h5PayUrl
paymentResolved.value = false
const payUrl = currentPayUrl.value
const isWebEnv = typeof navigator !== 'undefined' && typeof plus === 'undefined'
if (isWebEnv && !payUrl) {
uni.showToast({ title: '请扫码支付', icon: 'none' })
return
}
if (payUrl && typeof plus !== 'undefined') {
const encodedUrl = encodeURIComponent(payUrl)
const alipayScheme = `alipays://platformapi/startapp?appId=20000067&url=${encodedUrl}`
plus.runtime.openURL(alipayScheme, function(err) {
console.log('[Alipay] 唤起支付宝App失败,尝试H5支付:', err)
plus.runtime.openURL(payUrl, function(h5Err) {
console.error('[Alipay] H5支付也失败:', h5Err)
uni.showModal({ title: '提示', content: '无法唤起支付宝,请确认已安装', showCancel: false })
loading.value = false
})
})
} else if (payUrl) {
window.location.href = payUrl
}
}
async function createNewPayment(orderType, goodsDesc, remark, transAmt) {
const isWebEnv = typeof navigator !== 'undefined' && typeof plus === 'undefined'
if (isWebEnv) {
const { createQrCodePayment } = await import('@/api/main.js')
const payRes = await createQrCodePayment({
mid: getMemberId(),
orderType: orderType,
goodsDesc: goodsDesc,
transAmt: transAmt,
tradeType: 'QRCODE',
remark: remark
})
if (payRes.code !== 200) {
uni.showToast({ title: payRes.message || '创建订单失败', icon: 'none' })
loading.value = false
return
}
currentOrderId.value = payRes.data.orderId || payRes.data.merchantOrderId || payRes.data.tradeNo
currentMerchantOrderId.value = payRes.data.merchantOrderId || currentOrderId.value
currentHfSeqId.value = payRes.data.hfSeqId || payRes.data.hfOrderId
currentPayUrl.value = payRes.data.payUrl || payRes.data.payInfo || payRes.data.h5PayUrl
paymentResolved.value = false
uni.showToast({ title: '请扫码支付', icon: 'none' })
} else {
const { createPayment } = await import('@/api/main.js')
const payRes = await createPayment({
mid: getMemberId(),
orderType: orderType,
goodsDesc: goodsDesc,
transAmt: transAmt,
tradeType: 'ALIPAY',
remark: remark
})
if (payRes.code !== 200) {
uni.showToast({ title: payRes.message || '创建订单失败', icon: 'none' })
loading.value = false
return
}
currentOrderId.value = payRes.data.orderId || payRes.data.merchantOrderId || payRes.data.tradeNo
currentMerchantOrderId.value = payRes.data.merchantOrderId || currentOrderId.value
currentHfSeqId.value = payRes.data.hfSeqId || payRes.data.hfOrderId
currentPayUrl.value = payRes.data.payUrl || payRes.data.payInfo || payRes.data.h5PayUrl
paymentResolved.value = false
const payUrl = currentPayUrl.value
if (payUrl && typeof plus !== 'undefined') {
const encodedUrl = encodeURIComponent(payUrl)
const alipayScheme = `alipays://platformapi/startapp?appId=20000067&url=${encodedUrl}`
plus.runtime.openURL(alipayScheme, function(err) {
console.log('[Alipay] 唤起支付宝App失败,尝试H5支付:', err)
plus.runtime.openURL(payUrl, function(h5Err) {
console.error('[Alipay] H5支付也失败:', h5Err)
uni.showModal({ title: '提示', content: '无法唤起支付宝,请确认已安装', showCancel: false })
loading.value = false
})
})
} else if (payUrl) {
window.location.href = payUrl
}
}
}
onLoad((options) => {
// 打印当前登录用户ID
const loginInfo = getLoginMemberInfo()
const mid = getMemberId()
console.log('========== 确认支付页 - 当前登录用户 ==========')
console.log('会员ID (memberId):', mid)
console.log('登录信息 (loginInfo):', JSON.stringify(loginInfo, null, 2))
console.log('==============================================')
// 重置支付状态
paymentResolved.value = false
currentOrderId.value = null
currentMerchantOrderId.value = null
currentHfSeqId.value = null
currentPayUrl.value = ''
showSuccessModalVisible.value = false
loading.value = false
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()
})
// APP从后台返回时检查支付状态
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] 查询支付状态失败')
})
})
}
function mapTransStat(transStat) {
if (!transStat) return 'PENDING'
// 统一转为大写
const upper = String(transStat).toUpperCase()
const map = {
'S': 'SUCCESS',
'SUCCESS': 'SUCCESS',
'F': 'FAIL',
'FAIL': 'FAIL',
'P': 'PENDING',
'PENDING': 'PENDING',
'C': 'CLOSED',
'CLOSED': 'CLOSED',
'INIT': 'PENDING',
'WAITING': 'PENDING',
'PROCESSING': 'PENDING',
'PARTIAL_SUCCESS': 'SUCCESS',
'TRADE_SUCCESS': 'SUCCESS',
'TRADE_CLOSED': 'CLOSED',
'WAIT_BUYER_PAY': 'PENDING'
}
return map[upper] || 'PENDING'
}
async function handlePaymentSuccess() {
// 支付成功后,更新储值卡余额
try {
const res = await getStoredCardInfo(getMemberId())
storedBalance.value = res.balance || 0
console.log('[Payment] 支付成功后更新余额:', storedBalance.value)
} 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
let failedCount = 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
} else {
failedCount += item.count
}
} catch (e) {
console.error('购买会员卡失败:', e)
failedCount += item.count
}
}
uni.hideLoading()
if (successCount > 0) {
uni.showToast({ title: `成功购买${successCount}张会员卡`, icon: 'success' })
// 显示成功弹窗,让用户选择
showSuccessModalVisible.value = true
} else {
uni.showToast({ title: '购买失败', icon: 'none' })
loading.value = false
}
}
function closeSuccessModal() {
showSuccessModalVisible.value = false
}
function handleBackFromConfirm() {
closeSuccessModal()
uni.navigateBack()
}
function handleGoToCardFromConfirm() {
closeSuccessModal()
if (isRecharge.value) {
uni.redirectTo({ url: '/pages/memberInfo/memberCard?tab=stored' })
} else if (cardType.value === 'stored') {
uni.redirectTo({ url: '/pages/memberInfo/memberCard?tab=stored' })
} else {
uni.redirectTo({ url: '/pages/memberInfo/memberCard?tab=other' })
}
}
onMounted(() => {
// 注册APP从后台返回事件
if (typeof plus !== 'undefined') {
plus.globalEvent.addEventListener('resume', handleAppResume)
}
})
onShow(() => {
// 页面显示时检查支付状态
if (currentOrderId.value && !paymentResolved.value) {
console.log('[Payment] 页面显示,检查支付状态')
startPolling()
}
})
onUnmounted(() => {
paymentResolved.value = true
stopPolling()
if (typeof plus !== 'undefined') {
plus.globalEvent.removeEventListener('resume', handleAppResume)
}
})
</script>
<style lang="scss">
@import '@/common/style/base.css';
@import '@/common/style/memberInfo/pages/page-reset.css';
@import '@/common/style/memberInfo/pages/sub-page-base.css';
@import '@/common/style/memberInfo/member-info-component-reset.css';
@import '@/common/style/memberInfo/member-info-sub-nav.css';
.confirm-payment-page {
min-height: 100vh;
background: #F5F7FA;
padding-bottom: 120px;
}
.cp-body {
padding: 16px;
}
.cp-section {
background: #FFFFFF;
border-radius: 16px;
padding: 20px;
margin-bottom: 16px;
}
.cp-section__title {
font-size: 16px;
font-weight: 600;
color: #1A202C;
margin-bottom: 16px;
display: block;
}
.cp-order {
display: flex;
flex-direction: column;
gap: 12px;
}
.cp-order__item {
display: flex;
justify-content: space-between;
align-items: center;
}
.cp-order__label {
font-size: 14px;
color: #718096;
}
.cp-order__value {
font-size: 14px;
color: #2D3748;
font-family: 'Courier New', monospace;
}
.cp-goods {
display: flex;
flex-direction: column;
gap: 12px;
}
.cp-goods__item {
display: flex;
align-items: center;
gap: 12px;
padding: 12px;
background: #F7FAFC;
border-radius: 12px;
}
.cp-goods__icon {
font-size: 32px;
width: 48px;
height: 48px;
display: flex;
align-items: center;
justify-content: center;
background: #FFFFFF;
border-radius: 12px;
}
.cp-goods__info {
flex: 1;
display: flex;
flex-direction: column;
gap: 4px;
}
.cp-goods__name {
font-size: 15px;
font-weight: 600;
color: #2D3748;
}
.cp-goods__desc {
font-size: 12px;
color: #718096;
}
.cp-goods__right {
display: flex;
flex-direction: column;
align-items: flex-end;
gap: 4px;
}
.cp-goods__count {
font-size: 12px;
color: #A0AEC0;
}
.cp-goods__price {
font-size: 16px;
font-weight: 600;
color: #E53E3E;
}
.cp-recharge {
display: flex;
align-items: center;
padding: 12px 0;
}
.cp-recharge__icon {
width: 48px;
height: 48px;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 12px;
font-size: 24px;
margin-right: 12px;
}
.cp-recharge__info {
flex: 1;
display: flex;
flex-direction: column;
gap: 4px;
}
.cp-recharge__name {
font-size: 16px;
font-weight: 600;
color: #2D3748;
}
.cp-recharge__desc {
font-size: 13px;
color: #A0AEC0;
}
.cp-payment {
display: flex;
flex-direction: column;
gap: 12px;
}
.cp-payment__item {
display: flex;
align-items: center;
padding: 16px;
border: 2px solid #E2E8F0;
border-radius: 12px;
transition: all 0.2s;
position: relative;
&--selected {
border-color: #1677FF;
background: #F0F7FF;
}
&--disabled {
opacity: 0.6;
cursor: not-allowed;
}
}
.cp-payment__icon {
width: 40px;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
margin-right: 12px;
&--alipay {
background: #1677FF;
border-radius: 8px;
image {
width: 28px;
height: 28px;
}
}
&--stored {
font-size: 28px;
background: linear-gradient(135deg, #FFD700 0%, #FFA500 100%);
border-radius: 50%;
}
}
.cp-payment__info {
flex: 1;
display: flex;
flex-direction: column;
gap: 4px;
}
.cp-payment__name {
font-size: 15px;
font-weight: 600;
color: #2D3748;
}
.cp-payment__desc {
font-size: 12px;
color: #718096;
}
.cp-payment__check {
width: 24px;
height: 24px;
border: 2px solid #CBD5E0;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
}
.cp-payment__check-inner {
width: 14px;
height: 14px;
border-radius: 50%;
background: transparent;
transition: all 0.2s;
&--checked {
background: #1677FF;
}
}
.cp-payment__disabled {
position: absolute;
right: 16px;
padding: 4px 12px;
background: #FED7D7;
border-radius: 12px;
font-size: 11px;
color: #E53E3E;
}
.cp-amount {
display: flex;
flex-direction: column;
gap: 12px;
}
.cp-amount__item {
display: flex;
justify-content: space-between;
align-items: center;
&--total {
padding-top: 12px;
border-top: 1px dashed #E2E8F0;
margin-top: 8px;
}
}
.cp-amount__label {
font-size: 14px;
color: #718096;
}
.cp-amount__value {
font-size: 14px;
color: #2D3748;
&--discount {
color: #38A169;
}
&--total {
font-size: 18px;
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: 16px 24px;
padding-bottom: calc(16px + env(safe-area-inset-bottom));
background: #FFFFFF;
box-shadow: 0 -4px 20px rgba(0, 0, 0, 0.05);
z-index: 100;
}
.cp-bottom__left {
display: flex;
align-items: baseline;
gap: 8px;
}
.cp-bottom__label {
font-size: 14px;
color: #718096;
}
.cp-bottom__price {
font-size: 28px;
font-weight: 700;
color: #E53E3E;
}
.cp-bottom__btn {
background: linear-gradient(135deg, #1677FF 0%, #0958D9 100%);
border-radius: 12px;
padding: 16px 48px;
transition: all 0.2s;
&--disabled {
background: #A0AEC0;
}
&:active:not(.cp-bottom__btn--disabled) {
transform: scale(0.98);
opacity: 0.9;
}
}
.cp-bottom__btn-text {
font-size: 16px;
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: 85%;
max-width: 360px;
background: #FFFFFF;
border-radius: 20px;
padding: 40px 32px;
display: flex;
flex-direction: column;
align-items: center;
animation: modalPop 0.3s ease;
}
.success-modal__icon {
font-size: 64px;
margin-bottom: 16px;
}
.success-modal__title {
font-size: 22px;
font-weight: 700;
color: #1A202C;
margin-bottom: 8px;
}
.success-modal__desc {
font-size: 14px;
color: #718096;
margin-bottom: 24px;
}
.success-modal__buttons {
display: flex;
flex-direction: column;
gap: 12px;
width: 100%;
}
.success-modal__btn {
padding: 14px 24px;
border-radius: 12px;
text-align: center;
font-size: 16px;
font-weight: 600;
&--primary {
background: linear-gradient(135deg, #1677FF 0%, #0958D9 100%);
color: #FFFFFF;
}
&--secondary {
background: #F5F7FA;
color: #4A5568;
}
&--outline {
background: transparent;
border: 1px solid #E2E8F0;
color: #4A5568;
}
}
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes modalPop {
from {
opacity: 0;
transform: scale(0.9);
}
to {
opacity: 1;
transform: scale(1);
}
}
.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: 85%;
max-width: 360px;
background: #FFFFFF;
border-radius: 20px;
overflow: hidden;
animation: modalPop 0.3s ease;
}
.pay-password-modal__header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 20px 24px 12px;
&-title {
font-size: 18px;
font-weight: 600;
color: #1A202C;
}
&-close {
width: 24px;
height: 24px;
display: flex;
align-items: center;
justify-content: center;
color: #A0AEC0;
font-size: 16px;
}
}
.pay-password-modal__body {
padding: 0 24px 32px;
display: flex;
flex-direction: column;
align-items: center;
}
.pay-password-modal__amount {
font-size: 32px;
font-weight: 700;
color: #E53E3E;
margin-bottom: 24px;
}
.pay-password-modal__forgot {
margin-top: 16px;
text-align: right;
width: 100%;
text {
font-size: 13px;
color: #1677FF;
}
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
// 旧订单提示弹窗样式
.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: 85%;
max-width: 360px;
background: #FFFFFF;
border-radius: 20px;
padding: 24px;
animation: slideUp 0.3s ease-out;
}
.pending-order-modal__header {
text-align: center;
margin-bottom: 20px;
}
.pending-order-modal__title {
font-size: 18px;
font-weight: 600;
color: #1A202C;
}
.pending-order-modal__body {
display: flex;
flex-direction: column;
gap: 12px;
margin-bottom: 24px;
}
.pending-order-modal__desc {
font-size: 14px;
color: #718096;
}
.pending-order-modal__info {
background: #F7FAFC;
border-radius: 12px;
padding: 16px;
display: flex;
flex-direction: column;
gap: 8px;
}
.pending-order-modal__order-no {
font-size: 13px;
color: #2D3748;
font-family: 'Courier New', monospace;
}
.pending-order-modal__amount {
font-size: 16px;
font-weight: 600;
color: #E53E3E;
}
.pending-order-modal__time {
font-size: 12px;
color: #718096;
}
.pending-order-modal__question {
font-size: 14px;
color: #2D3748;
font-weight: 500;
}
.pending-order-modal__buttons {
display: flex;
gap: 12px;
}
.pending-order-modal__btn {
flex: 1;
padding: 14px;
border-radius: 12px;
text-align: center;
font-size: 15px;
font-weight: 600;
&--primary {
background: linear-gradient(135deg, #1677FF 0%, #0958D9 100%);
color: #FFFFFF;
}
&--secondary {
background: #F7FAFC;
color: #2D3748;
border: 1px solid #E2E8F0;
}
}
@keyframes slideUp {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
</style>