2034 lines
54 KiB
Vue
2034 lines
54 KiB
Vue
<template>
|
||
<view class="scroll-container theme-light">
|
||
<view class="purchase-card-page">
|
||
<view class="purchase-card-page__body">
|
||
<!-- 头部 -->
|
||
<view class="pc-intro">
|
||
<view class="pc-intro__icon">
|
||
<image src="https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/images/crown0.png" mode="aspectFit" />
|
||
</view>
|
||
<text class="pc-intro__title">选择会员卡类型</text>
|
||
<text class="pc-intro__desc">购买后立即生效,享受专属会员权益</text>
|
||
</view>
|
||
|
||
<!-- 骨架屏 -->
|
||
<ListSkeleton v-if="loadingCards" :count="3" layout="simple" />
|
||
|
||
<!-- 卡列表 -->
|
||
<view v-else-if="cardTypes.length" class="pc-list">
|
||
<view
|
||
v-for="card in cardTypes"
|
||
:key="card.id"
|
||
class="pc-card"
|
||
:class="{ 'pc-card--selected': isCardSelected(card.id) }"
|
||
>
|
||
<view class="pc-card__check" @tap="toggleCardSelection(card)">
|
||
<view
|
||
class="pc-card__check-inner"
|
||
:class="{ 'pc-card__check-inner--checked': isCardSelected(card.id) }"
|
||
></view>
|
||
</view>
|
||
<view class="pc-card__content">
|
||
<view class="pc-card__header">
|
||
<text class="pc-card__name">{{ card.cardName }}</text>
|
||
<view class="pc-card__tag" :class="'pc-card__tag--' + getTagClass(card.cardType)">
|
||
<text class="pc-card__tag-text">{{ getCardTypeName(card.cardType) }}</text>
|
||
</view>
|
||
</view>
|
||
<view class="pc-card__info" @tap="goToDetail(card)">
|
||
<view class="pc-card__info-row" v-if="card.validityDays">
|
||
<text class="pc-card__info-label">有效期</text>
|
||
<text class="pc-card__info-value">{{ card.validityDays }}天</text>
|
||
</view>
|
||
<view class="pc-card__info-row" v-if="card.cardType === 'COUNT' && card.totalTimes">
|
||
<text class="pc-card__info-label">总次数</text>
|
||
<text class="pc-card__info-value">{{ card.totalTimes }}次</text>
|
||
</view>
|
||
<view class="pc-card__info-row" v-if="card.cardType === 'BALANCE' && card.amount">
|
||
<text class="pc-card__info-label">储值金额</text>
|
||
<text class="pc-card__info-value">{{ card.amount }}元</text>
|
||
</view>
|
||
<view class="pc-card__info-row" v-if="card.description">
|
||
<text class="pc-card__info-label">说明</text>
|
||
<text class="pc-card__info-value">{{ card.description }}</text>
|
||
</view>
|
||
</view>
|
||
<view class="pc-card__footer">
|
||
<view class="pc-card__footer-left" @tap="increaseCount(card)">
|
||
<text class="pc-card__price">¥{{ card.price }}</text>
|
||
<text class="pc-card__original-price" v-if="card.originalPrice && card.originalPrice !== card.price">¥{{ card.originalPrice }}</text>
|
||
</view>
|
||
<view class="pc-card__footer-right">
|
||
<view class="pc-card__detail-btn" @tap="goToDetail(card)">
|
||
<text class="pc-card__detail-text">详情</text>
|
||
</view>
|
||
<view class="pc-card__quantity" v-if="isCardSelected(card.id)">
|
||
<view class="pc-card__qty-btn" @tap="decreaseCount(card)">
|
||
<text>-</text>
|
||
</view>
|
||
<text class="pc-card__qty-num">{{ getCartItem(card.id)?.count || 0 }}</text>
|
||
<view class="pc-card__qty-btn pc-card__qty-btn--add" @tap="increaseCount(card)">
|
||
<text>+</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 底部 -->
|
||
<view class="pc-bottom">
|
||
<view class="pc-bottom__total" @tap="showCartModal = true">
|
||
<text class="pc-bottom__total-label">合计</text>
|
||
<text class="pc-bottom__total-price">¥{{ totalPrice }}</text>
|
||
<text class="pc-bottom__total-count">已选{{ totalCount }}件</text>
|
||
</view>
|
||
<view
|
||
class="pc-bottom__btn"
|
||
:class="{ 'pc-bottom__btn--disabled': cart.length === 0 || loading }"
|
||
@tap="handlePurchase"
|
||
>
|
||
<text class="pc-bottom__btn-text">{{ loading ? '处理中...' : `立即购买 ${totalCount}张` }}</text>
|
||
</view>
|
||
</view>
|
||
</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">购买成功</text>
|
||
<text class="success-modal__desc">恭喜您,会员卡已成功开通!</text>
|
||
<view class="success-modal__buttons">
|
||
<view class="success-modal__btn success-modal__btn--secondary" @tap="handleBack">
|
||
<text>返回上一页</text>
|
||
</view>
|
||
<view class="success-modal__btn success-modal__btn--outline" @tap="handleRePurchase">
|
||
<text>再次购买</text>
|
||
</view>
|
||
<view class="success-modal__btn success-modal__btn--primary" @tap="handleGoToCard">
|
||
<text>去我的会员卡</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 购物车弹窗 -->
|
||
<view class="cart-modal-overlay" v-if="showCartModal" @tap="showCartModal = false">
|
||
<view class="cart-modal-dialog" @tap.stop>
|
||
<view class="cart-modal__header">
|
||
<text class="cart-modal__title">已选商品</text>
|
||
<view class="cart-modal__close" @tap="showCartModal = false">×</view>
|
||
</view>
|
||
<view class="cart-modal__body">
|
||
<view class="cart-modal__list" v-if="cart.length > 0">
|
||
<view class="cart-modal__item" v-for="item in cart" :key="item.card.id">
|
||
<view class="cart-modal__item-info">
|
||
<text class="cart-modal__item-name">{{ item.card.cardName }}</text>
|
||
<text class="cart-modal__item-desc">{{ getCardTypeName(item.card.cardType) }}</text>
|
||
</view>
|
||
<view class="cart-modal__item-right">
|
||
<view class="cart-modal__item-qty">
|
||
<view class="cart-modal__qty-btn" @tap="decreaseCount(item.card)">
|
||
<text>-</text>
|
||
</view>
|
||
<text class="cart-modal__qty-num">{{ item.count }}</text>
|
||
<view class="cart-modal__qty-btn cart-modal__qty-btn--add" @tap="increaseCount(item.card)">
|
||
<text>+</text>
|
||
</view>
|
||
</view>
|
||
<text class="cart-modal__item-price">¥{{ (item.card.price * item.count).toFixed(2) }}</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
<view class="cart-modal__empty" v-else>
|
||
<text>暂无已选商品</text>
|
||
</view>
|
||
</view>
|
||
<view class="cart-modal__footer">
|
||
<view class="cart-modal__total">
|
||
<text class="cart-modal__total-label">合计</text>
|
||
<text class="cart-modal__total-price">¥{{ totalPrice }}</text>
|
||
</view>
|
||
<view class="cart-modal__btn" @tap="handlePurchase">
|
||
<text>立即购买</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 二维码支付对话框 (Web环境) -->
|
||
<view class="qr-payment-overlay" v-if="showQrPayment" @tap="closeQrPayment">
|
||
<view class="qr-payment-dialog" @tap.stop>
|
||
<view class="qr-payment-dialog__header">
|
||
<text class="qr-payment-dialog__title">{{ qrPaymentStatus === 'pending' ? '扫码支付' : (qrPaymentStatus === 'success' ? '支付成功' : '支付失败') }}</text>
|
||
<view class="qr-payment-dialog__close" @tap="closeQrPayment" v-if="qrPaymentStatus !== 'pending'">×</view>
|
||
</view>
|
||
|
||
<view class="qr-payment-dialog__body">
|
||
<!-- 待支付状态:显示二维码 -->
|
||
<template v-if="qrPaymentStatus === 'pending'">
|
||
<view class="qr-payment-dialog__order">
|
||
<text class="qr-payment-dialog__label">订单信息</text>
|
||
<text class="qr-payment-dialog__value">{{ cart[0]?.card?.cardName }} × {{ cart[0]?.count || 1 }}</text>
|
||
</view>
|
||
<view class="qr-payment-dialog__amount">
|
||
<text class="qr-payment-dialog__label">支付金额</text>
|
||
<text class="qr-payment-dialog__money">¥{{ totalPrice }}</text>
|
||
</view>
|
||
<view class="qr-payment-dialog__qrcode">
|
||
<image
|
||
v-if="qrCodeUrl"
|
||
:src="'https://api.qrserver.com/v1/create-qr-code?size=200x200&data=' + encodeURIComponent(qrCodeUrl)"
|
||
class="qr-payment-dialog__qrcode-img"
|
||
mode="aspectFit"
|
||
/>
|
||
<view v-else class="qr-payment-dialog__qrcode-loading">加载中...</view>
|
||
</view>
|
||
<view class="qr-payment-dialog__tips">
|
||
<text class="qr-payment-dialog__tip">请使用支付宝扫码支付</text>
|
||
<text class="qr-payment-dialog__tip qr-payment-dialog__tip--warn" v-if="isWebEnv">⚠️ 沙箱环境测试</text>
|
||
</view>
|
||
</template>
|
||
|
||
<!-- 支付成功状态 -->
|
||
<template v-else-if="qrPaymentStatus === 'success'">
|
||
<view class="qr-payment-dialog__result">
|
||
<text class="qr-payment-dialog__icon">✅</text>
|
||
<text class="qr-payment-dialog__result-text">支付成功!</text>
|
||
</view>
|
||
</template>
|
||
|
||
<!-- 支付失败状态 -->
|
||
<template v-else>
|
||
<view class="qr-payment-dialog__result">
|
||
<text class="qr-payment-dialog__icon">❌</text>
|
||
<text class="qr-payment-dialog__result-text">支付失败</text>
|
||
</view>
|
||
</template>
|
||
</view>
|
||
|
||
<view class="qr-payment-dialog__footer">
|
||
<template v-if="qrPaymentStatus === 'pending'">
|
||
<view class="qr-payment-dialog__btn qr-payment-dialog__btn--secondary" @tap="closeQrPayment">
|
||
<text>取消支付</text>
|
||
</view>
|
||
<view class="qr-payment-dialog__btn qr-payment-dialog__btn--primary" @tap="handleQrQueryResult">
|
||
<text>我已支付</text>
|
||
</view>
|
||
</template>
|
||
<template v-else>
|
||
<view class="qr-payment-dialog__btn qr-payment-dialog__btn--primary" @tap="closeQrPayment">
|
||
<text>确定</text>
|
||
</view>
|
||
</template>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 订单倒计时提醒弹窗 -->
|
||
<view class="countdown-modal-overlay" v-if="showCountdownModal" @tap="closeCountdownModal">
|
||
<view class="countdown-modal" @tap.stop>
|
||
<view class="countdown-modal__header">
|
||
<text class="countdown-modal__title">订单待支付</text>
|
||
</view>
|
||
<view class="countdown-modal__body">
|
||
<view class="countdown-modal__info">
|
||
<text class="countdown-modal__label">购买内容</text>
|
||
<text class="countdown-modal__value">{{ cart.map(item => `${item.card.cardName}×${item.count}`).join('、') || '会员卡' }}</text>
|
||
</view>
|
||
<view class="countdown-modal__info">
|
||
<text class="countdown-modal__label">购买金额</text>
|
||
<text class="countdown-modal__value countdown-modal__value--price">¥{{ totalPrice }}</text>
|
||
</view>
|
||
<view class="countdown-modal__timer-wrap">
|
||
<text class="countdown-modal__tip">订单超时倒计时</text>
|
||
<view class="countdown-modal__timer">
|
||
<text class="countdown-modal__time-num">{{ countdownMinutes }}</text>
|
||
<text class="countdown-modal__time-unit">分</text>
|
||
<text class="countdown-modal__time-num">{{ countdownSeconds }}</text>
|
||
<text class="countdown-modal__time-unit">秒</text>
|
||
<text class="countdown-modal__time-num countdown-modal__time-num--ms">{{ countdownMs }}</text>
|
||
<text class="countdown-modal__time-unit">毫秒</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
<view class="countdown-modal__footer">
|
||
<view class="countdown-modal__btn countdown-modal__btn--cancel" @tap="closeCountdownModal">
|
||
<text>取消</text>
|
||
</view>
|
||
<view class="countdown-modal__btn countdown-modal__btn--confirm" @tap="goToPayFromCountdown">
|
||
<text>去支付</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { ref, computed, onMounted, onUnmounted } from 'vue'
|
||
import { onShow } from '@dcloudio/uni-app'
|
||
import ListSkeleton from '@/components/Skeleton/ListSkeleton.vue'
|
||
import { purchaseMemberCard, createPayment, createQrCodePayment, getPaymentStatus, queryHuifuTradeByOrderId, getActiveMemberCards } from '@/api/main.js'
|
||
import { getMemberId } from '@/utils/request.js'
|
||
|
||
// #ifdef H5
|
||
const isWebEnv = typeof navigator !== 'undefined' && !typeof plus !== 'undefined'
|
||
// #endif
|
||
// #ifndef H5
|
||
const isWebEnv = false
|
||
// #endif
|
||
|
||
const cardTypes = ref([])
|
||
const loadingCards = ref(false)
|
||
|
||
const loading = ref(false)
|
||
const cart = ref([])
|
||
const maxPurchaseCount = 10
|
||
const showQrPayment = ref(false)
|
||
const qrPaymentStatus = ref('pending')
|
||
const qrCodeUrl = ref('')
|
||
const currentOrderId = ref(null)
|
||
const currentHfSeqId = ref(null)
|
||
const currentMerchantOrderId = ref(null)
|
||
const currentPayUrl = ref('')
|
||
const paymentResolved = ref(false)
|
||
const displayStatus = ref('')
|
||
const showSuccessModalVisible = ref(false)
|
||
const showCountdownModal = ref(false)
|
||
const showCartModal = ref(false)
|
||
let countdownTimer = null
|
||
const countdownTotal = ref(15 * 60 * 1000)
|
||
const countdownRemaining = ref(15 * 60 * 1000)
|
||
let wsTask = null
|
||
let wsConnected = false
|
||
let wsResolve = null
|
||
let wsReject = null
|
||
let qrPaymentResolve = null
|
||
let qrPaymentReject = null
|
||
|
||
const WS_BASE_URL = 'ws://192.168.5.15:8084/ws/payment'
|
||
|
||
const TRADE_TYPE = 'ALIPAY'
|
||
|
||
const totalPrice = computed(() => {
|
||
const total = cart.value.reduce((sum, item) => sum + (item.card.price * item.count), 0)
|
||
return total.toFixed(2)
|
||
})
|
||
|
||
const totalCount = computed(() => {
|
||
return cart.value.reduce((sum, item) => sum + item.count, 0)
|
||
})
|
||
|
||
function getCartItem(cardId) {
|
||
const item = cart.value.find(item => item.card.id === cardId)
|
||
return item || null
|
||
}
|
||
|
||
function isCardSelected(cardId) {
|
||
return cart.value.some(item => item.card.id === cardId)
|
||
}
|
||
|
||
function toggleCardSelection(card) {
|
||
const index = cart.value.findIndex(item => item.card.id === card.id)
|
||
if (index > -1) {
|
||
cart.value.splice(index, 1)
|
||
} else {
|
||
cart.value.push({ card, count: 1 })
|
||
}
|
||
}
|
||
|
||
function increaseCount(card) {
|
||
const cartItem = getCartItem(card.id)
|
||
if (cartItem) {
|
||
if (cartItem.count < maxPurchaseCount) {
|
||
cartItem.count++
|
||
} else {
|
||
uni.showToast({ title: `最多购买${maxPurchaseCount}张`, icon: 'none' })
|
||
}
|
||
} else {
|
||
cart.value.push({ card, count: 1 })
|
||
}
|
||
}
|
||
|
||
function decreaseCount(card) {
|
||
const cartItem = getCartItem(card.id)
|
||
if (cartItem) {
|
||
if (cartItem.count > 1) {
|
||
cartItem.count--
|
||
} else {
|
||
uni.showToast({ title: '最少购买1张', icon: 'none' })
|
||
}
|
||
}
|
||
}
|
||
|
||
function getCardTypeName(type) {
|
||
const map = {
|
||
DURATION: '时长卡',
|
||
TIME_CARD: '时长卡',
|
||
COUNT: '次卡',
|
||
COUNT_CARD: '次卡',
|
||
BALANCE: '储值卡',
|
||
STORED_VALUE_CARD: '储值卡'
|
||
}
|
||
return map[type] || type
|
||
}
|
||
|
||
function normalizeCardType(type) {
|
||
const map = {
|
||
TIME_CARD: 'DURATION',
|
||
COUNT_CARD: 'COUNT',
|
||
STORED_VALUE_CARD: 'BALANCE'
|
||
}
|
||
return map[type] || type
|
||
}
|
||
|
||
async function loadCardTypes(preselectCardId = null) {
|
||
loadingCards.value = true
|
||
try {
|
||
const res = await getActiveMemberCards()
|
||
let dataList = null
|
||
if (Array.isArray(res)) {
|
||
dataList = res
|
||
} else if (res && res.value && Array.isArray(res.value)) {
|
||
dataList = res.value
|
||
} else if (res && res.code === 200 && Array.isArray(res.data)) {
|
||
dataList = res.data
|
||
} else if (res && Array.isArray(res.data)) {
|
||
dataList = res.data
|
||
}
|
||
console.log(dataList)
|
||
|
||
|
||
if (dataList && dataList.length > 0) {
|
||
cardTypes.value = dataList
|
||
.filter(item => {
|
||
const cardType = normalizeCardType(item.memberCardType)
|
||
console.log(cardType)
|
||
return cardType !== 'BALANCE'
|
||
})
|
||
.map(item => {
|
||
let description = item.description || ''
|
||
if (item.extraConfig) {
|
||
try {
|
||
const config = JSON.parse(item.extraConfig)
|
||
const descParts = []
|
||
if (config.description) descParts.push(config.description)
|
||
if (config.desc) descParts.push(config.desc)
|
||
if (config.maxUsePerDay) descParts.push(`每日限用${config.maxUsePerDay}次`)
|
||
if (descParts.length > 0) {
|
||
description = descParts.join(';')
|
||
}
|
||
} catch (e) {
|
||
description = item.extraConfig
|
||
}
|
||
}
|
||
console.log(item.id, item.memberCardId)
|
||
return {
|
||
id: item.id, // 使用memberCardId作为业务ID
|
||
cardName: item.memberCardName,
|
||
cardType: normalizeCardType(item.memberCardType),
|
||
validityDays: item.memberCardValidityDays,
|
||
price: item.memberCardPrice,
|
||
totalTimes: item.memberCardTotalTimes,
|
||
amount: item.memberCardAmount,
|
||
status: item.memberCardStatus,
|
||
description: description
|
||
}
|
||
})
|
||
if (preselectCardId) {
|
||
const targetCard = cardTypes.value.find(c => String(c.id) === String(preselectCardId))
|
||
if (targetCard) {
|
||
increaseCount(targetCard)
|
||
}
|
||
}
|
||
} else {
|
||
console.log('会员卡列表为空:', res)
|
||
uni.showToast({ title: '暂无可购买的会员卡', icon: 'none' })
|
||
}
|
||
} catch (e) {
|
||
console.error('加载会员卡列表失败:', e)
|
||
uni.showToast({ title: '加载会员卡列表失败', icon: 'none' })
|
||
} finally {
|
||
loadingCards.value = false
|
||
}
|
||
}
|
||
|
||
function getTagClass(type) {
|
||
const map = { DURATION: 'time', COUNT: 'count', BALANCE: 'value' }
|
||
return map[type] || 'default'
|
||
}
|
||
|
||
function mapTransStat(transStat) {
|
||
const map = {
|
||
S: 'SUCCESS',
|
||
F: 'FAIL',
|
||
P: 'PENDING',
|
||
C: 'CLOSED',
|
||
INIT: 'PENDING',
|
||
PARTIAL_SUCCESS: 'SUCCESS',
|
||
WAITING: 'PENDING',
|
||
PROCESSING: 'PENDING'
|
||
}
|
||
return map[transStat] || 'PENDING'
|
||
}
|
||
|
||
function getStatusText(status) {
|
||
const map = {
|
||
PENDING: '待支付',
|
||
SUCCESS: '支付成功',
|
||
FAIL: '支付失败',
|
||
CLOSED: '已关闭',
|
||
'': '未下单'
|
||
}
|
||
return map[status] || status
|
||
}
|
||
|
||
function getStatusClass(status) {
|
||
const map = {
|
||
PENDING: 'pending',
|
||
SUCCESS: 'success',
|
||
FAIL: 'fail',
|
||
CLOSED: 'closed',
|
||
'': 'none'
|
||
}
|
||
return map[status] || 'none'
|
||
}
|
||
|
||
function connectWebSocket(orderId) {
|
||
return new Promise((resolve, reject) => {
|
||
wsResolve = resolve
|
||
wsReject = reject
|
||
wsConnected = false
|
||
|
||
const wsUrl = `${WS_BASE_URL}/${orderId}`
|
||
console.log('[WebSocket] 连接中...', wsUrl)
|
||
|
||
wsTask = uni.connectSocket({
|
||
url: wsUrl,
|
||
success: function() {
|
||
console.log('[WebSocket] 连接请求已发送')
|
||
},
|
||
fail: function(err) {
|
||
console.error('[WebSocket] 连接失败:', err)
|
||
wsReject = null
|
||
wsTask = null
|
||
reject(new Error('WebSocket连接失败'))
|
||
}
|
||
})
|
||
|
||
uni.onSocketOpen(function() {
|
||
console.log('[WebSocket] 连接成功')
|
||
wsConnected = true
|
||
})
|
||
|
||
uni.onSocketMessage(function(res) {
|
||
try {
|
||
const data = JSON.parse(res.data)
|
||
if (data.type === 'ping') {
|
||
return
|
||
}
|
||
console.log('[WebSocket] 收到消息:', data)
|
||
if (data.status === 'SUCCESS' || data.status === 'FAIL' || data.status === 'CLOSED') {
|
||
displayStatus.value = data.status
|
||
closeWebSocket()
|
||
if (wsResolve) {
|
||
wsResolve(data.status)
|
||
wsResolve = null
|
||
wsReject = null
|
||
}
|
||
}
|
||
} catch (e) {
|
||
console.warn('[WebSocket] 消息解析失败:', e)
|
||
}
|
||
})
|
||
|
||
uni.onSocketClose(function() {
|
||
console.log('[WebSocket] 连接关闭')
|
||
wsConnected = false
|
||
if (wsReject) {
|
||
wsReject(new Error('WebSocket连接关闭'))
|
||
wsResolve = null
|
||
wsReject = null
|
||
}
|
||
})
|
||
|
||
uni.onSocketError(function(err) {
|
||
console.error('[WebSocket] 连接错误:', err)
|
||
wsConnected = false
|
||
if (wsReject) {
|
||
wsReject(new Error('WebSocket连接错误'))
|
||
wsResolve = null
|
||
wsReject = null
|
||
}
|
||
})
|
||
})
|
||
}
|
||
|
||
function closeWebSocket() {
|
||
if (wsTask) {
|
||
try {
|
||
uni.closeSocket()
|
||
} catch (e) {}
|
||
wsTask = null
|
||
}
|
||
wsConnected = false
|
||
wsResolve = null
|
||
wsReject = null
|
||
}
|
||
|
||
function manualQueryStatus() {
|
||
if (!currentOrderId.value) {
|
||
uni.showToast({ title: '暂无订单', icon: 'none' })
|
||
return
|
||
}
|
||
if (loading.value) return
|
||
console.log('[Debug] 手动查询支付状态, orderId:', currentOrderId.value)
|
||
uni.showLoading({ title: '查询中...' })
|
||
getPaymentStatus(currentOrderId.value)
|
||
.then(res => {
|
||
uni.hideLoading()
|
||
if (res.code === 200 && res.data) {
|
||
const status = res.data.status || res.data.payStatus
|
||
displayStatus.value = status
|
||
console.log('[Debug] 查询结果:', status)
|
||
} else {
|
||
uni.showToast({ title: '查询失败', icon: 'none' })
|
||
}
|
||
})
|
||
.catch(() => {
|
||
uni.hideLoading()
|
||
uni.showToast({ title: '查询失败', icon: 'none' })
|
||
})
|
||
}
|
||
|
||
function goToDetail(card) {
|
||
uni.navigateTo({
|
||
url: `/pages/memberInfo/cardDetail?cardId=${card.id}`
|
||
})
|
||
}
|
||
|
||
function invokeAlipayApp(payUrl) {
|
||
return new Promise((resolve, reject) => {
|
||
if (!payUrl) {
|
||
reject(new Error('支付链接为空'))
|
||
return
|
||
}
|
||
console.log('[Alipay] 准备唤起支付宝:', payUrl)
|
||
if (typeof plus === 'undefined') {
|
||
uni.showModal({ title: '提示', content: '请在 App 内完成支付', showCancel: false })
|
||
reject(new Error('非 App 环境'))
|
||
return
|
||
}
|
||
|
||
// 优先使用支付宝scheme协议唤起App
|
||
const encodedUrl = encodeURIComponent(payUrl)
|
||
const alipayScheme = `alipays://platformapi/startapp?appId=20000067&url=${encodedUrl}`
|
||
|
||
// 先尝试用支付宝App打开
|
||
plus.runtime.openURL(alipayScheme, function(err) {
|
||
console.log('[Alipay] 唤起支付宝App失败,尝试H5支付:', err)
|
||
// 唤起失败,使用H5支付
|
||
plus.runtime.openURL(payUrl, function(h5Err) {
|
||
console.error('[Alipay] H5支付也失败:', h5Err)
|
||
uni.showModal({
|
||
title: '提示',
|
||
content: '无法唤起支付宝,请确认已安装支付宝 App',
|
||
showCancel: false
|
||
})
|
||
reject(new Error('唤起支付宝失败'))
|
||
})
|
||
})
|
||
resolve()
|
||
})
|
||
}
|
||
|
||
function handleAppResume() {
|
||
if (paymentResolved.value || !currentOrderId.value) {
|
||
return
|
||
}
|
||
console.log('[Payment] APP从后台返回,查询汇付官方支付结果')
|
||
console.log('[Payment] merchantOrderId:', currentMerchantOrderId.value, 'hfSeqId:', currentHfSeqId.value)
|
||
uni.showLoading({ title: '查询支付结果...' })
|
||
|
||
const queryOrderId = currentMerchantOrderId.value || currentOrderId.value
|
||
queryHuifuTradeByOrderId(queryOrderId, currentHfSeqId.value)
|
||
.then(res => {
|
||
uni.hideLoading()
|
||
if (res.code === 200 && res.data) {
|
||
const tradeInfo = res.data.trade_info
|
||
let status = res.data.status || res.data.payStatus
|
||
|
||
if (tradeInfo) {
|
||
const transStat = tradeInfo.trans_stat
|
||
console.log('[Payment] 汇付原始状态:', transStat)
|
||
status = mapTransStat(transStat)
|
||
}
|
||
|
||
displayStatus.value = status
|
||
console.log('[Payment] 支付状态:', status, '状态描述:', getStatusText(status))
|
||
|
||
if (status === 'SUCCESS') {
|
||
paymentResolved.value = true
|
||
uni.showToast({ title: '支付成功', icon: 'success', duration: 2000 })
|
||
setTimeout(() => {
|
||
handlePaymentSuccess()
|
||
}, 1500)
|
||
} else if (status === 'FAIL') {
|
||
paymentResolved.value = true
|
||
uni.showToast({ title: '支付失败', icon: 'error' })
|
||
loading.value = false
|
||
} else if (status === 'CLOSED') {
|
||
paymentResolved.value = true
|
||
uni.showToast({ title: '订单已关闭', icon: 'none' })
|
||
loading.value = false
|
||
} else {
|
||
// 未支付,弹出倒计时提醒
|
||
openCountdownModal()
|
||
}
|
||
} else {
|
||
// 查询失败,弹出倒计时
|
||
openCountdownModal()
|
||
}
|
||
})
|
||
.catch(err => {
|
||
uni.hideLoading()
|
||
console.error('[Payment] 查询支付状态失败:', err)
|
||
// 查询失败,弹出倒计时
|
||
openCountdownModal()
|
||
})
|
||
}
|
||
|
||
function handlePaymentSuccess() {
|
||
if (cart.value.length === 0) return
|
||
|
||
uni.showLoading({ title: '开通会员卡...' })
|
||
// 逐个购买购物车中的会员卡
|
||
let successCount = 0
|
||
let failedCount = 0
|
||
for (const item of cart.value) {
|
||
purchaseMemberCard({
|
||
memberCardId: item.card.id
|
||
})
|
||
.then(memberRes => {
|
||
if (memberRes.code === 200 || memberRes.id || memberRes.memberCardRecordId) {
|
||
successCount += item.count
|
||
} else {
|
||
failedCount += item.count
|
||
}
|
||
})
|
||
.catch(() => {
|
||
failedCount += item.count
|
||
})
|
||
}
|
||
|
||
uni.hideLoading()
|
||
if (showQrPayment.value) {
|
||
showQrPayment.value = false
|
||
}
|
||
|
||
if (successCount > 0) {
|
||
showSuccessModal()
|
||
} else {
|
||
uni.showToast({ title: '开通失败', icon: 'none' })
|
||
loading.value = false
|
||
}
|
||
}
|
||
|
||
const countdownMinutes = computed(() => {
|
||
const mins = Math.floor(countdownRemaining.value / 60000)
|
||
return String(mins).padStart(2, '0')
|
||
})
|
||
|
||
const countdownSeconds = computed(() => {
|
||
const secs = Math.floor((countdownRemaining.value % 60000) / 1000)
|
||
return String(secs).padStart(2, '0')
|
||
})
|
||
|
||
const countdownMs = computed(() => {
|
||
const ms = Math.floor((countdownRemaining.value % 1000) / 10)
|
||
return String(ms).padStart(2, '0')
|
||
})
|
||
|
||
function startCountdown() {
|
||
if (countdownTimer) clearInterval(countdownTimer)
|
||
countdownRemaining.value = countdownTotal.value
|
||
|
||
countdownTimer = setInterval(() => {
|
||
countdownRemaining.value -= 10
|
||
if (countdownRemaining.value <= 0) {
|
||
countdownRemaining.value = 0
|
||
stopCountdown()
|
||
}
|
||
}, 10)
|
||
}
|
||
|
||
function stopCountdown() {
|
||
if (countdownTimer) {
|
||
clearInterval(countdownTimer)
|
||
countdownTimer = null
|
||
}
|
||
}
|
||
|
||
function openCountdownModal() {
|
||
startCountdown()
|
||
showCountdownModal.value = true
|
||
}
|
||
|
||
function closeCountdownModal() {
|
||
showCountdownModal.value = false
|
||
stopCountdown()
|
||
}
|
||
|
||
function goToPayFromCountdown() {
|
||
closeCountdownModal()
|
||
if (typeof plus !== 'undefined') {
|
||
const payUrl = currentPayUrl.value || ''
|
||
if (payUrl) {
|
||
plus.runtime.openURL(payUrl)
|
||
}
|
||
}
|
||
}
|
||
|
||
function showSuccessModal() {
|
||
showSuccessModalVisible.value = true
|
||
}
|
||
|
||
function closeSuccessModal() {
|
||
showSuccessModalVisible.value = false
|
||
}
|
||
|
||
function handleBack() {
|
||
showSuccessModalVisible.value = false
|
||
const pages = getCurrentPages()
|
||
if (pages.length > 1) {
|
||
uni.navigateBack({ delta: 1 })
|
||
} else {
|
||
// 没有页面栈时,返回会员中心
|
||
uni.redirectTo({ url: '/pages/memberInfo/memberCard' })
|
||
}
|
||
}
|
||
|
||
function handleRePurchase() {
|
||
showSuccessModalVisible.value = false
|
||
displayStatus.value = ''
|
||
paymentResolved.value = false
|
||
currentOrderId.value = null
|
||
}
|
||
|
||
function handleGoToCard() {
|
||
showSuccessModalVisible.value = false
|
||
uni.redirectTo({ url: '/pages/memberInfo/memberCard' })
|
||
}
|
||
|
||
function openQrPayment(qrCode, orderId) {
|
||
qrCodeUrl.value = qrCode
|
||
currentOrderId.value = orderId
|
||
qrPaymentStatus.value = 'pending'
|
||
paymentResolved.value = false
|
||
displayStatus.value = 'PENDING'
|
||
showQrPayment.value = true
|
||
}
|
||
|
||
function closeQrPayment() {
|
||
showQrPayment.value = false
|
||
if (qrPaymentStatus.value === 'pending' && qrPaymentReject) {
|
||
qrPaymentReject(new Error('用户取消支付'))
|
||
}
|
||
qrPaymentResolve = null
|
||
qrPaymentReject = null
|
||
qrCodeUrl.value = ''
|
||
}
|
||
|
||
function handleQrQueryResult() {
|
||
if (paymentResolved.value || !currentOrderId.value) return
|
||
uni.showLoading({ title: '查询支付结果...' })
|
||
|
||
getPaymentStatus(currentOrderId.value)
|
||
.then(res => {
|
||
uni.hideLoading()
|
||
if (res.code === 200 && res.data) {
|
||
const status = res.data.status || res.data.payStatus
|
||
displayStatus.value = status
|
||
console.log('[QrPayment] 支付状态:', status)
|
||
if (status === 'SUCCESS') {
|
||
paymentResolved.value = true
|
||
qrPaymentStatus.value = 'success'
|
||
if (qrPaymentResolve) {
|
||
qrPaymentResolve(true)
|
||
}
|
||
} else if (status === 'FAIL' || status === 'CLOSED') {
|
||
paymentResolved.value = true
|
||
qrPaymentStatus.value = 'fail'
|
||
if (qrPaymentReject) {
|
||
qrPaymentReject(new Error(status === 'FAIL' ? '支付失败' : '订单已过期'))
|
||
}
|
||
} else {
|
||
// 未支付,弹出倒计时提醒
|
||
showQrPayment.value = false
|
||
openCountdownModal()
|
||
}
|
||
} else {
|
||
// 未查到支付记录,弹出倒计时
|
||
showQrPayment.value = false
|
||
openCountdownModal()
|
||
}
|
||
})
|
||
.catch(() => {
|
||
uni.hideLoading()
|
||
showQrPayment.value = false
|
||
openCountdownModal()
|
||
})
|
||
}
|
||
|
||
async function handlePurchase() {
|
||
if (cart.value.length === 0) {
|
||
uni.showToast({ title: '请选择会员卡', icon: 'none' })
|
||
return
|
||
}
|
||
const cartData = encodeURIComponent(JSON.stringify(cart.value))
|
||
uni.navigateTo({
|
||
url: `/pages/memberInfo/confirmPayment?cartData=${cartData}`
|
||
})
|
||
}
|
||
|
||
// 每次页面显示时重置支付状态
|
||
onShow(() => {
|
||
paymentResolved.value = false
|
||
currentOrderId.value = null
|
||
currentHfSeqId.value = null
|
||
currentMerchantOrderId.value = null
|
||
currentPayUrl.value = ''
|
||
displayStatus.value = ''
|
||
loading.value = false
|
||
})
|
||
|
||
onMounted(() => {
|
||
const pages = getCurrentPages()
|
||
const currentPage = pages[pages.length - 1]
|
||
const options = currentPage.options || {}
|
||
const cardId = options.cardId
|
||
loadCardTypes(cardId)
|
||
|
||
// 注册APP从后台返回事件
|
||
if (typeof plus !== 'undefined') {
|
||
plus.globalEvent.addEventListener('resume', handleAppResume)
|
||
}
|
||
})
|
||
|
||
onUnmounted(() => {
|
||
paymentResolved.value = true
|
||
closeWebSocket()
|
||
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';
|
||
@import '@/common/style/memberInfo/member-info-tap.css';
|
||
|
||
.purchase-card-page {
|
||
min-height: 100vh;
|
||
background: #F5F7FA;
|
||
}
|
||
|
||
.purchase-card-page__body {
|
||
padding-bottom: 100px;
|
||
}
|
||
|
||
.pc-intro {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
padding: 32px 24px;
|
||
background: linear-gradient(135deg, #1A365D 0%, #2C5282 100%);
|
||
}
|
||
|
||
.pc-intro__icon {
|
||
width: 64px;
|
||
height: 64px;
|
||
margin-bottom: 16px;
|
||
background: rgba(255, 255, 255, 0.1);
|
||
border-radius: 50%;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
|
||
image {
|
||
width: 40px;
|
||
height: 40px;
|
||
}
|
||
}
|
||
|
||
.pc-intro__title {
|
||
font-size: 20px;
|
||
font-weight: 600;
|
||
color: #FFFFFF;
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
.pc-intro__desc {
|
||
font-size: 14px;
|
||
color: rgba(255, 255, 255, 0.7);
|
||
}
|
||
|
||
.pc-list {
|
||
padding: 16px;
|
||
}
|
||
|
||
.pc-payment {
|
||
padding: 16px;
|
||
margin: 0 16px;
|
||
background: #FFFFFF;
|
||
border-radius: 16px;
|
||
}
|
||
|
||
.pc-payment__title {
|
||
font-size: 16px;
|
||
font-weight: 600;
|
||
color: #1A202C;
|
||
margin-bottom: 16px;
|
||
}
|
||
|
||
.pc-payment__methods {
|
||
display: flex;
|
||
gap: 16px;
|
||
}
|
||
|
||
.pc-payment__method {
|
||
flex: 1;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
padding: 16px;
|
||
border: 2px solid #1677FF;
|
||
border-radius: 12px;
|
||
background: #F0F7FF;
|
||
position: relative;
|
||
|
||
&--selected {
|
||
border-color: #1677FF;
|
||
background: #F0F7FF;
|
||
}
|
||
}
|
||
|
||
.pc-payment__icon {
|
||
width: 40px;
|
||
height: 40px;
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
.pc-payment__name {
|
||
font-size: 14px;
|
||
color: #2D3748;
|
||
}
|
||
|
||
.pc-payment__check {
|
||
position: absolute;
|
||
top: 8px;
|
||
right: 8px;
|
||
width: 20px;
|
||
height: 20px;
|
||
background: #1677FF;
|
||
border-radius: 50%;
|
||
|
||
&::after {
|
||
content: '';
|
||
display: block;
|
||
width: 6px;
|
||
height: 6px;
|
||
background: #FFFFFF;
|
||
border-radius: 50%;
|
||
margin: 4px auto;
|
||
}
|
||
}
|
||
|
||
.pc-card {
|
||
display: flex;
|
||
align-items: flex-start;
|
||
background: #FFFFFF;
|
||
border-radius: 16px;
|
||
padding: 20px;
|
||
margin-bottom: 16px;
|
||
border: 2px solid transparent;
|
||
transition: all 0.2s;
|
||
|
||
&--selected {
|
||
border-color: #2C5282;
|
||
background: #F7FAFC;
|
||
}
|
||
}
|
||
|
||
.pc-card__check {
|
||
width: 24px;
|
||
height: 24px;
|
||
margin-right: 16px;
|
||
flex-shrink: 0;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
|
||
.pc-card__check-inner {
|
||
width: 20px;
|
||
height: 20px;
|
||
border: 2px solid #CBD5E0;
|
||
border-radius: 4px;
|
||
transition: all 0.2s;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
|
||
&--checked {
|
||
border-color: #2C5282;
|
||
background: #2C5282;
|
||
}
|
||
}
|
||
|
||
.pc-card__check-inner--checked::before {
|
||
content: '✓';
|
||
color: #FFFFFF;
|
||
font-size: 14px;
|
||
font-weight: bold;
|
||
}
|
||
|
||
.pc-card__content {
|
||
flex: 1;
|
||
}
|
||
|
||
.pc-card__header {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
margin-bottom: 12px;
|
||
}
|
||
|
||
.pc-card__name {
|
||
font-size: 18px;
|
||
font-weight: 600;
|
||
color: #1A202C;
|
||
}
|
||
|
||
.pc-card__tag {
|
||
padding: 4px 12px;
|
||
border-radius: 20px;
|
||
font-size: 12px;
|
||
|
||
&--time {
|
||
background: #EBF8FF;
|
||
.pc-card__tag-text { color: #2B6CB0; }
|
||
}
|
||
&--count {
|
||
background: #FEF3E2;
|
||
.pc-card__tag-text { color: #D69E2E; }
|
||
}
|
||
&--value {
|
||
background: #F0FFF4;
|
||
.pc-card__tag-text { color: #38A169; }
|
||
}
|
||
&--default {
|
||
background: #E2E8F0;
|
||
.pc-card__tag-text { color: #718096; }
|
||
}
|
||
}
|
||
|
||
.pc-card__tag-text {
|
||
font-size: 12px;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.pc-card__info {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 8px 24px;
|
||
margin-bottom: 12px;
|
||
}
|
||
|
||
.pc-card__info-row {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
}
|
||
|
||
.pc-card__info-label {
|
||
font-size: 13px;
|
||
color: #718096;
|
||
}
|
||
|
||
.pc-card__info-value {
|
||
font-size: 13px;
|
||
font-weight: 600;
|
||
color: #2D3748;
|
||
}
|
||
|
||
.pc-card__footer {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
gap: 8px;
|
||
}
|
||
|
||
.pc-card__footer-left {
|
||
display: flex;
|
||
align-items: baseline;
|
||
gap: 8px;
|
||
}
|
||
|
||
.pc-card__footer-right {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
}
|
||
|
||
.pc-card__detail-btn {
|
||
padding: 6px 16px;
|
||
border-radius: 20px;
|
||
background: #EDF2F7;
|
||
transition: all 0.2s;
|
||
}
|
||
|
||
.pc-card__detail-text {
|
||
font-size: 12px;
|
||
color: #4A5568;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.pc-card__quantity {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
}
|
||
|
||
.pc-card__qty-btn {
|
||
width: 28px;
|
||
height: 28px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
border-radius: 50%;
|
||
background: #EDF2F7;
|
||
font-size: 18px;
|
||
color: #4A5568;
|
||
transition: all 0.2s;
|
||
|
||
&:active {
|
||
transform: scale(0.9);
|
||
}
|
||
|
||
&--add {
|
||
background: #1677FF;
|
||
color: #FFFFFF;
|
||
}
|
||
}
|
||
|
||
.pc-card__qty-num {
|
||
font-size: 16px;
|
||
font-weight: 600;
|
||
color: #1A202C;
|
||
min-width: 24px;
|
||
text-align: center;
|
||
}
|
||
|
||
.pc-card__price {
|
||
font-size: 24px;
|
||
font-weight: 700;
|
||
color: #E53E3E;
|
||
}
|
||
|
||
.pc-card__original-price {
|
||
font-size: 14px;
|
||
color: #A0AEC0;
|
||
text-decoration: line-through;
|
||
}
|
||
|
||
.pc-debug {
|
||
margin: 16px;
|
||
background: #FFFBEB;
|
||
border: 1px solid #FCD34D;
|
||
border-radius: 12px;
|
||
overflow: hidden;
|
||
|
||
&__header {
|
||
padding: 12px 16px;
|
||
background: #FEF3C7;
|
||
border-bottom: 1px solid #FCD34D;
|
||
}
|
||
|
||
&__title {
|
||
font-size: 14px;
|
||
font-weight: 600;
|
||
color: #92400E;
|
||
}
|
||
|
||
&__body {
|
||
padding: 12px 16px;
|
||
}
|
||
|
||
&__row {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
padding: 6px 0;
|
||
border-bottom: 1px dashed #FCD34D;
|
||
|
||
&:last-child {
|
||
border-bottom: none;
|
||
}
|
||
}
|
||
|
||
&__label {
|
||
font-size: 13px;
|
||
color: #92400E;
|
||
font-weight: 500;
|
||
}
|
||
|
||
&__value {
|
||
font-size: 13px;
|
||
color: #78350F;
|
||
font-weight: 600;
|
||
max-width: 200px;
|
||
text-align: right;
|
||
word-break: break-all;
|
||
|
||
&--order {
|
||
font-size: 11px;
|
||
font-family: monospace;
|
||
}
|
||
|
||
&--pending {
|
||
color: #D97706;
|
||
}
|
||
|
||
&--success {
|
||
color: #059669;
|
||
}
|
||
|
||
&--fail {
|
||
color: #DC2626;
|
||
}
|
||
|
||
&--closed {
|
||
color: #6B7280;
|
||
}
|
||
|
||
&--none {
|
||
color: #9CA3AF;
|
||
}
|
||
}
|
||
|
||
&__actions {
|
||
margin-top: 12px;
|
||
padding-top: 12px;
|
||
border-top: 1px dashed #FCD34D;
|
||
}
|
||
|
||
&__btn {
|
||
background: #F59E0B;
|
||
color: #FFFFFF;
|
||
text-align: center;
|
||
padding: 10px 0;
|
||
border-radius: 8px;
|
||
font-size: 13px;
|
||
font-weight: 500;
|
||
}
|
||
}
|
||
|
||
.pc-cart-summary {
|
||
display: none;
|
||
}
|
||
|
||
.pc-bottom__total {
|
||
flex: 1;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: flex-start;
|
||
padding-right: 16px;
|
||
}
|
||
|
||
.pc-bottom__total-label {
|
||
font-size: 13px;
|
||
color: #718096;
|
||
}
|
||
|
||
.pc-bottom__total-price {
|
||
font-size: 28px;
|
||
font-weight: 700;
|
||
color: #E53E3E;
|
||
}
|
||
|
||
.pc-bottom__total-count {
|
||
font-size: 12px;
|
||
color: #A0AEC0;
|
||
margin-top: 4px;
|
||
}
|
||
|
||
.pc-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;
|
||
}
|
||
|
||
.pc-bottom__total-label {
|
||
font-size: 14px;
|
||
color: #718096;
|
||
}
|
||
|
||
.pc-bottom__total-price {
|
||
font-size: 28px;
|
||
font-weight: 700;
|
||
color: #E53E3E;
|
||
}
|
||
|
||
.pc-bottom__btn {
|
||
background: #1677FF;
|
||
border-radius: 12px;
|
||
padding: 16px 48px;
|
||
transition: all 0.2s;
|
||
|
||
&--disabled {
|
||
background: #A0AEC0;
|
||
}
|
||
}
|
||
|
||
.pc-bottom__btn-text {
|
||
font-size: 16px;
|
||
font-weight: 600;
|
||
color: #FFFFFF;
|
||
}
|
||
|
||
.cart-modal-overlay {
|
||
position: fixed;
|
||
top: 0;
|
||
left: 0;
|
||
right: 0;
|
||
bottom: 0;
|
||
background: rgba(0, 0, 0, 0.5);
|
||
display: flex;
|
||
align-items: flex-end;
|
||
z-index: 1000;
|
||
}
|
||
|
||
.cart-modal-dialog {
|
||
width: 100%;
|
||
background: #FFFFFF;
|
||
border-radius: 20px 20px 0 0;
|
||
max-height: 70vh;
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
|
||
.cart-modal__header {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
padding: 20px 24px;
|
||
border-bottom: 1px solid #E2E8F0;
|
||
}
|
||
|
||
.cart-modal__title {
|
||
font-size: 18px;
|
||
font-weight: 600;
|
||
color: #1A202C;
|
||
}
|
||
|
||
.cart-modal__close {
|
||
width: 32px;
|
||
height: 32px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-size: 24px;
|
||
color: #718096;
|
||
}
|
||
|
||
.cart-modal__body {
|
||
flex: 1;
|
||
overflow-y: auto;
|
||
padding: 16px 24px;
|
||
}
|
||
|
||
.cart-modal__list {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 16px;
|
||
}
|
||
|
||
.cart-modal__item {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
padding: 16px;
|
||
background: #F7FAFC;
|
||
border-radius: 12px;
|
||
}
|
||
|
||
.cart-modal__item-info {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 4px;
|
||
}
|
||
|
||
.cart-modal__item-name {
|
||
font-size: 16px;
|
||
font-weight: 600;
|
||
color: #2D3748;
|
||
}
|
||
|
||
.cart-modal__item-desc {
|
||
font-size: 13px;
|
||
color: #718096;
|
||
}
|
||
|
||
.cart-modal__item-right {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: flex-end;
|
||
gap: 8px;
|
||
}
|
||
|
||
.cart-modal__item-qty {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
}
|
||
|
||
.cart-modal__qty-btn {
|
||
width: 32px;
|
||
height: 32px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
background: #FFFFFF;
|
||
border: 1px solid #E2E8F0;
|
||
border-radius: 8px;
|
||
font-size: 18px;
|
||
color: #4A5568;
|
||
|
||
&--add {
|
||
background: #2C5282;
|
||
border-color: #2C5282;
|
||
color: #FFFFFF;
|
||
}
|
||
}
|
||
|
||
.cart-modal__qty-num {
|
||
font-size: 16px;
|
||
font-weight: 600;
|
||
color: #2D3748;
|
||
min-width: 32px;
|
||
text-align: center;
|
||
}
|
||
|
||
.cart-modal__item-price {
|
||
font-size: 16px;
|
||
font-weight: 600;
|
||
color: #E53E3E;
|
||
}
|
||
|
||
.cart-modal__empty {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
padding: 40px;
|
||
color: #A0AEC0;
|
||
font-size: 14px;
|
||
}
|
||
|
||
.cart-modal__footer {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
padding: 20px 24px;
|
||
padding-bottom: calc(20px + env(safe-area-inset-bottom));
|
||
border-top: 1px solid #E2E8F0;
|
||
background: #FFFFFF;
|
||
}
|
||
|
||
.cart-modal__total {
|
||
display: flex;
|
||
align-items: baseline;
|
||
gap: 8px;
|
||
}
|
||
|
||
.cart-modal__total-label {
|
||
font-size: 14px;
|
||
color: #718096;
|
||
}
|
||
|
||
.cart-modal__total-price {
|
||
font-size: 24px;
|
||
font-weight: 700;
|
||
color: #E53E3E;
|
||
}
|
||
|
||
.cart-modal__btn {
|
||
background: #2C5282;
|
||
border-radius: 12px;
|
||
padding: 16px 48px;
|
||
font-size: 16px;
|
||
font-weight: 600;
|
||
color: #FFFFFF;
|
||
}
|
||
|
||
// ==================== 模拟支付对话框 ====================
|
||
.mock-payment-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;
|
||
}
|
||
|
||
.qr-payment-dialog {
|
||
width: 320px;
|
||
background: #FFFFFF;
|
||
border-radius: 16px;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.qr-payment-dialog__header {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
padding: 20px 24px;
|
||
border-bottom: 1px solid #E2E8F0;
|
||
}
|
||
|
||
.qr-payment-dialog__title {
|
||
font-size: 18px;
|
||
font-weight: 600;
|
||
color: #1A202C;
|
||
}
|
||
|
||
.qr-payment-dialog__close {
|
||
width: 32px;
|
||
height: 32px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-size: 24px;
|
||
color: #A0AEC0;
|
||
}
|
||
|
||
.qr-payment-dialog__body {
|
||
padding: 24px;
|
||
}
|
||
|
||
.qr-payment-dialog__order,
|
||
.qr-payment-dialog__amount {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 12px;
|
||
}
|
||
|
||
.qr-payment-dialog__label {
|
||
font-size: 14px;
|
||
color: #718096;
|
||
}
|
||
|
||
.qr-payment-dialog__value {
|
||
font-size: 14px;
|
||
color: #2D3748;
|
||
}
|
||
|
||
.qr-payment-dialog__money {
|
||
font-size: 24px;
|
||
font-weight: 700;
|
||
color: #E53E3E;
|
||
}
|
||
|
||
.qr-payment-dialog__qrcode {
|
||
display: flex;
|
||
justify-content: center;
|
||
align-items: center;
|
||
padding: 20px 0;
|
||
background: #F7FAFC;
|
||
border-radius: 8px;
|
||
margin: 16px 0;
|
||
}
|
||
|
||
.qr-payment-dialog__qrcode-img {
|
||
width: 200px;
|
||
height: 200px;
|
||
}
|
||
|
||
.qr-payment-dialog__qrcode-loading {
|
||
font-size: 14px;
|
||
color: #A0AEC0;
|
||
padding: 80px 0;
|
||
}
|
||
|
||
.qr-payment-dialog__tips {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
gap: 4px;
|
||
}
|
||
|
||
.qr-payment-dialog__tip {
|
||
font-size: 12px;
|
||
color: #718096;
|
||
|
||
&--warn {
|
||
color: #ED8936;
|
||
}
|
||
}
|
||
|
||
.qr-payment-dialog__result {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
padding: 20px 0;
|
||
}
|
||
|
||
.qr-payment-dialog__icon {
|
||
font-size: 48px;
|
||
margin-bottom: 16px;
|
||
}
|
||
|
||
.qr-payment-dialog__result-text {
|
||
font-size: 16px;
|
||
font-weight: 600;
|
||
color: #2D3748;
|
||
}
|
||
|
||
.qr-payment-dialog__footer {
|
||
display: flex;
|
||
gap: 12px;
|
||
padding: 0 24px 24px;
|
||
}
|
||
|
||
.qr-payment-dialog__btn {
|
||
flex: 1;
|
||
padding: 14px 0;
|
||
border-radius: 8px;
|
||
text-align: center;
|
||
font-size: 16px;
|
||
font-weight: 500;
|
||
|
||
&--primary {
|
||
background: #1677FF;
|
||
color: #FFFFFF;
|
||
}
|
||
|
||
&--secondary {
|
||
background: #E2E8F0;
|
||
color: #4A5568;
|
||
}
|
||
}
|
||
|
||
.pc-payment-status {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 16px;
|
||
padding: 20px 24px;
|
||
margin: 16px;
|
||
border-radius: 16px;
|
||
background: #FFFFFF;
|
||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
|
||
animation: fadeIn 0.3s ease;
|
||
}
|
||
|
||
.pc-payment-status__icon {
|
||
width: 48px;
|
||
height: 48px;
|
||
border-radius: 50%;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-size: 24px;
|
||
flex-shrink: 0;
|
||
|
||
&--pending {
|
||
background: #EBF8FF;
|
||
}
|
||
|
||
&--success {
|
||
background: #F0FFF4;
|
||
}
|
||
|
||
&--fail {
|
||
background: #FFF5F5;
|
||
}
|
||
|
||
&--closed {
|
||
background: #FEF3E2;
|
||
}
|
||
|
||
&--none {
|
||
background: #F7FAFC;
|
||
}
|
||
}
|
||
|
||
.pc-payment-status__content {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 4px;
|
||
}
|
||
|
||
.pc-payment-status__title {
|
||
font-size: 16px;
|
||
font-weight: 600;
|
||
color: #1A202C;
|
||
}
|
||
|
||
.pc-payment-status__desc {
|
||
font-size: 13px;
|
||
color: #718096;
|
||
}
|
||
|
||
@keyframes fadeIn {
|
||
from {
|
||
opacity: 0;
|
||
transform: translateY(10px);
|
||
}
|
||
to {
|
||
opacity: 1;
|
||
transform: translateY(0);
|
||
}
|
||
}
|
||
|
||
.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;
|
||
}
|
||
|
||
@keyframes modalPop {
|
||
from {
|
||
opacity: 0;
|
||
transform: scale(0.9);
|
||
}
|
||
to {
|
||
opacity: 1;
|
||
transform: scale(1);
|
||
}
|
||
}
|
||
|
||
.success-modal__icon {
|
||
font-size: 64px;
|
||
margin-bottom: 16px;
|
||
}
|
||
|
||
.success-modal__title {
|
||
font-size: 24px;
|
||
font-weight: 700;
|
||
color: #1A202C;
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
.success-modal__desc {
|
||
font-size: 15px;
|
||
color: #718096;
|
||
margin-bottom: 32px;
|
||
text-align: center;
|
||
}
|
||
|
||
.success-modal__buttons {
|
||
width: 100%;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 12px;
|
||
}
|
||
|
||
.success-modal__btn {
|
||
padding: 16px 0;
|
||
border-radius: 12px;
|
||
text-align: center;
|
||
font-size: 16px;
|
||
font-weight: 600;
|
||
transition: all 0.2s;
|
||
|
||
&--primary {
|
||
background: #1677FF;
|
||
color: #FFFFFF;
|
||
}
|
||
|
||
&--secondary {
|
||
background: #F7FAFC;
|
||
color: #4A5568;
|
||
}
|
||
|
||
&--outline {
|
||
background: #FFFFFF;
|
||
color: #1677FF;
|
||
border: 2px solid #1677FF;
|
||
}
|
||
}
|
||
|
||
.countdown-modal-overlay {
|
||
position: fixed;
|
||
top: 0;
|
||
left: 0;
|
||
right: 0;
|
||
bottom: 0;
|
||
background: rgba(0, 0, 0, 0.6);
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
z-index: 2000;
|
||
animation: fadeIn 0.3s ease;
|
||
}
|
||
|
||
.countdown-modal {
|
||
width: 85%;
|
||
max-width: 600rpx;
|
||
background: #FFFFFF;
|
||
border-radius: 24rpx;
|
||
overflow: hidden;
|
||
animation: modalPop 0.3s ease;
|
||
}
|
||
|
||
.countdown-modal__header {
|
||
padding: 32rpx;
|
||
text-align: center;
|
||
background: linear-gradient(135deg, #FF8C42 0%, #FF6B2C 100%);
|
||
}
|
||
|
||
.countdown-modal__title {
|
||
font-size: 32rpx;
|
||
font-weight: 600;
|
||
color: #FFFFFF;
|
||
}
|
||
|
||
.countdown-modal__body {
|
||
padding: 40rpx 32rpx;
|
||
}
|
||
|
||
.countdown-modal__info {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 24rpx;
|
||
|
||
&:last-of-type {
|
||
margin-bottom: 32rpx;
|
||
}
|
||
}
|
||
|
||
.countdown-modal__label {
|
||
font-size: 28rpx;
|
||
color: #718096;
|
||
}
|
||
|
||
.countdown-modal__value {
|
||
font-size: 28rpx;
|
||
font-weight: 600;
|
||
color: #2D3748;
|
||
|
||
&--price {
|
||
color: #FF6B2C;
|
||
font-size: 36rpx;
|
||
}
|
||
}
|
||
|
||
.countdown-modal__timer-wrap {
|
||
background: #F7FAFC;
|
||
border-radius: 16rpx;
|
||
padding: 32rpx;
|
||
text-align: center;
|
||
}
|
||
|
||
.countdown-modal__tip {
|
||
font-size: 24rpx;
|
||
color: #718096;
|
||
margin-bottom: 16rpx;
|
||
display: block;
|
||
}
|
||
|
||
.countdown-modal__timer {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: 8rpx;
|
||
}
|
||
|
||
.countdown-modal__time-num {
|
||
font-size: 40rpx;
|
||
font-weight: 700;
|
||
color: #FF6B2C;
|
||
font-family: 'Courier New', monospace;
|
||
min-width: 48rpx;
|
||
|
||
&--ms {
|
||
font-size: 32rpx;
|
||
min-width: 40rpx;
|
||
}
|
||
}
|
||
|
||
.countdown-modal__time-unit {
|
||
font-size: 24rpx;
|
||
color: #718096;
|
||
margin-right: 4rpx;
|
||
}
|
||
|
||
.countdown-modal__footer {
|
||
display: flex;
|
||
gap: 20rpx;
|
||
padding: 0 32rpx 32rpx;
|
||
}
|
||
|
||
.countdown-modal__btn {
|
||
flex: 1;
|
||
padding: 24rpx 0;
|
||
border-radius: 12rpx;
|
||
text-align: center;
|
||
font-size: 28rpx;
|
||
font-weight: 600;
|
||
transition: all 0.2s;
|
||
|
||
&--cancel {
|
||
background: #F7FAFC;
|
||
color: #4A5568;
|
||
}
|
||
|
||
&--confirm {
|
||
background: linear-gradient(135deg, #FF8C42 0%, #FF6B2C 100%);
|
||
color: #FFFFFF;
|
||
}
|
||
}
|
||
</style> |