1071 lines
32 KiB
Vue
1071 lines
32 KiB
Vue
<template>
|
||
<view class="scroll-container theme-light">
|
||
<view class="purchase-card-page">
|
||
<MemberInfoSubNav title="购买会员卡" @back="goBack" />
|
||
<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>
|
||
|
||
<!-- 卡列表 -->
|
||
<view class="pc-list">
|
||
<view
|
||
v-for="card in cardTypes"
|
||
:key="card.id"
|
||
class="pc-card"
|
||
:class="{ 'pc-card--selected': selectedCardId === card.id }"
|
||
@tap="selectCard(card)"
|
||
>
|
||
<view class="pc-card__check">
|
||
<view
|
||
class="pc-card__check-inner"
|
||
:class="{ 'pc-card__check-inner--checked': selectedCardId === 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">
|
||
<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">
|
||
<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>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 支付方式 -->
|
||
<view class="pc-payment">
|
||
<text class="pc-payment__title">支付方式</text>
|
||
<view class="pc-payment__methods">
|
||
<view class="pc-payment__method pc-payment__method--selected">
|
||
<image src="https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/images/alipay.png" mode="aspectFit" class="pc-payment__icon" />
|
||
<text class="pc-payment__name">{{ isWebEnv ? '扫码支付 (Web环境)' : 'App支付' }}</text>
|
||
<view class="pc-payment__check"></view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 底部 -->
|
||
<view class="pc-bottom">
|
||
<view class="pc-bottom__total">
|
||
<text class="pc-bottom__total-label">合计</text>
|
||
<text class="pc-bottom__total-price">¥{{ selectedCard?.price || 0 }}</text>
|
||
</view>
|
||
<view
|
||
class="pc-bottom__btn"
|
||
:class="{ 'pc-bottom__btn--disabled': !selectedCard || loading }"
|
||
@tap="handlePurchase"
|
||
>
|
||
<text class="pc-bottom__btn-text">{{ loading ? '处理中...' : '立即购买' }}</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">{{ selectedCard?.cardName }} × 1</text>
|
||
</view>
|
||
<view class="qr-payment-dialog__amount">
|
||
<text class="qr-payment-dialog__label">支付金额</text>
|
||
<text class="qr-payment-dialog__money">¥{{ selectedCard?.price || 0 }}</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>
|
||
</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>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { ref, onMounted, onUnmounted } from 'vue'
|
||
import { onBackPress } from '@dcloudio/uni-app'
|
||
import MemberInfoSubNav from '@/components/memberInfo/MemberInfoSubNav.vue'
|
||
import { purchaseMemberCard, createPayment, createQrCodePayment, getPaymentStatus } from '@/api/main.js'
|
||
import { loadMemberStore } from '@/common/memberInfo/store.js'
|
||
|
||
// ==================== Web环境检测 ====================
|
||
// #ifdef H5
|
||
const isWebEnv = typeof navigator !== 'undefined' && !typeof plus !== 'undefined'
|
||
// #endif
|
||
// #ifndef H5
|
||
const isWebEnv = false
|
||
// #endif
|
||
|
||
// ==================== 二维码支付相关状态 ====================
|
||
const showQrPayment = ref(false)
|
||
const qrPaymentStatus = ref('pending') // pending | success | fail
|
||
const qrCodeUrl = ref('')
|
||
let qrPaymentResolve = null
|
||
let qrPaymentReject = null
|
||
let currentQrOrderId = null
|
||
let qrPollingTimer = null
|
||
let isQrPolling = false
|
||
|
||
const cardTypes = ref([
|
||
{ id: 1, cardName: '月卡', cardType: 'DURATION', validityDays: 30, price: 1, originalPrice: 399 },
|
||
{ id: 2, cardName: '季卡', cardType: 'DURATION', validityDays: 90, price: 1, originalPrice: 999 },
|
||
{ id: 3, cardName: '年卡', cardType: 'DURATION', validityDays: 365, price: 1, originalPrice: 2999 },
|
||
{ id: 4, cardName: '10次卡', cardType: 'COUNT', validityDays: 90, totalTimes: 10, price: 1 },
|
||
{ id: 5, cardName: '50次卡', cardType: 'COUNT', validityDays: 180, totalTimes: 50, price: 1 },
|
||
{ id: 6, cardName: '100次卡', cardType: 'COUNT', validityDays: 365, totalTimes: 100, price: 1 },
|
||
{ id: 7, cardName: '储值500', cardType: 'BALANCE', validityDays: 365, amount: 500, price: 1 },
|
||
{ id: 8, cardName: '储值1000', cardType: 'BALANCE', validityDays: 365, amount: 1000, price: 1 },
|
||
])
|
||
|
||
const selectedCardId = ref(null)
|
||
const loading = ref(false)
|
||
const selectedCard = ref(null)
|
||
const pollingTimer = ref(null)
|
||
const isPolling = ref(false)
|
||
|
||
// 支付方式
|
||
const TRADE_TYPE = 'ALIPAY'
|
||
|
||
// ==================== 工具函数 ====================
|
||
function getCardTypeName(type) {
|
||
const map = { DURATION: '时长卡', COUNT: '次卡', BALANCE: '储值卡' }
|
||
return map[type] || type
|
||
}
|
||
|
||
function getTagClass(type) {
|
||
const map = { DURATION: 'time', COUNT: 'count', BALANCE: 'value' }
|
||
return map[type] || 'default'
|
||
}
|
||
|
||
function selectCard(card) {
|
||
selectedCardId.value = card.id
|
||
selectedCard.value = card
|
||
}
|
||
|
||
function goBack() {
|
||
uni.redirectTo({
|
||
url: '/pages/memberInfo/memberCard'
|
||
})
|
||
}
|
||
|
||
onBackPress(() => {
|
||
uni.redirectTo({
|
||
url: '/pages/memberInfo/memberCard'
|
||
})
|
||
return true
|
||
})
|
||
|
||
// ==================== 唤起支付宝 App ====================
|
||
function invokeAlipayApp(payUrl) {
|
||
return new Promise((resolve, reject) => {
|
||
if (!payUrl) {
|
||
reject(new Error('支付链接为空'))
|
||
return
|
||
}
|
||
|
||
console.log('[Alipay] 准备唤起支付宝:', payUrl)
|
||
|
||
// 检查是否在 App 环境
|
||
if (typeof plus === 'undefined') {
|
||
uni.showModal({
|
||
title: '提示',
|
||
content: '请在 App 内完成支付',
|
||
showCancel: false
|
||
})
|
||
reject(new Error('非 App 环境'))
|
||
return
|
||
}
|
||
|
||
// 使用 plus.runtime.openURL 唤起支付宝
|
||
plus.runtime.openURL(payUrl, function(err) {
|
||
console.error('[Alipay] 唤起支付宝失败:', err)
|
||
uni.showModal({
|
||
title: '提示',
|
||
content: '无法唤起支付宝,请确认已安装支付宝 App',
|
||
showCancel: false
|
||
})
|
||
reject(new Error('唤起支付宝失败: ' + (err?.message || '未知错误')))
|
||
})
|
||
|
||
// 直接 resolve,支付结果由轮询处理
|
||
resolve()
|
||
})
|
||
}
|
||
|
||
// ==================== 轮询支付状态 ====================
|
||
function startPolling(orderId) {
|
||
return new Promise((resolve, reject) => {
|
||
if (isPolling.value) return
|
||
isPolling.value = true
|
||
|
||
let pollCount = 0
|
||
const maxRetry = 60 // 60秒
|
||
let isResolved = false
|
||
|
||
// 监听 App 回到前台
|
||
const handleResume = () => {
|
||
if (isResolved) return
|
||
console.log('[Poll] App 回到前台,立即查询')
|
||
doQuery()
|
||
}
|
||
|
||
if (typeof plus !== 'undefined') {
|
||
plus.globalEvent.addEventListener('resume', handleResume)
|
||
}
|
||
|
||
function doQuery() {
|
||
if (isResolved) return
|
||
pollCount++
|
||
|
||
getPaymentStatus(orderId)
|
||
.then(res => {
|
||
if (res.code === 200 && res.data) {
|
||
const status = res.data.status || res.data.payStatus
|
||
console.log(`[Poll] 第 ${pollCount} 次查询,状态:`, status)
|
||
|
||
if (status === 'SUCCESS') {
|
||
isResolved = true
|
||
cleanup()
|
||
resolve(true)
|
||
return
|
||
} else if (status === 'FAIL' || status === 'CLOSED') {
|
||
isResolved = true
|
||
cleanup()
|
||
reject(new Error('支付失败'))
|
||
return
|
||
}
|
||
}
|
||
|
||
// 继续轮询
|
||
if (pollCount < maxRetry && !isResolved) {
|
||
pollingTimer.value = setTimeout(doQuery, 1000)
|
||
} else if (!isResolved) {
|
||
isResolved = true
|
||
cleanup()
|
||
reject(new Error('支付超时,请确认是否已完成支付'))
|
||
}
|
||
})
|
||
.catch(err => {
|
||
console.error('[Poll] 查询异常:', err)
|
||
if (pollCount < maxRetry && !isResolved) {
|
||
pollingTimer.value = setTimeout(doQuery, 1000)
|
||
} else if (!isResolved) {
|
||
isResolved = true
|
||
cleanup()
|
||
reject(new Error('支付超时,请确认是否已完成支付'))
|
||
}
|
||
})
|
||
}
|
||
|
||
function cleanup() {
|
||
isPolling.value = false
|
||
if (pollingTimer.value) {
|
||
clearTimeout(pollingTimer.value)
|
||
pollingTimer.value = null
|
||
}
|
||
if (typeof plus !== 'undefined') {
|
||
plus.globalEvent.removeEventListener('resume', handleResume)
|
||
}
|
||
}
|
||
|
||
// 延迟 1.5 秒开始轮询
|
||
pollingTimer.value = setTimeout(doQuery, 1500)
|
||
})
|
||
}
|
||
|
||
// ==================== Web环境扫码支付 ====================
|
||
function openQrPayment(qrCode, orderId) {
|
||
qrCodeUrl.value = qrCode
|
||
currentQrOrderId = orderId
|
||
qrPaymentStatus.value = 'pending'
|
||
showQrPayment.value = true
|
||
|
||
// 开始轮询支付状态
|
||
startQrPolling(orderId)
|
||
}
|
||
|
||
function closeQrPayment() {
|
||
showQrPayment.value = false
|
||
stopQrPolling()
|
||
|
||
if (qrPaymentStatus.value === 'pending' && qrPaymentReject) {
|
||
qrPaymentReject(new Error('用户取消支付'))
|
||
}
|
||
|
||
qrPaymentResolve = null
|
||
qrPaymentReject = null
|
||
currentQrOrderId = null
|
||
qrCodeUrl.value = ''
|
||
}
|
||
|
||
// ==================== 二维码支付轮询 ====================
|
||
function startQrPolling(orderId) {
|
||
if (isQrPolling) return
|
||
isQrPolling = true
|
||
|
||
let pollCount = 0
|
||
const maxRetry = 300 // 5分钟超时 (300秒)
|
||
|
||
function doQuery() {
|
||
if (!showQrPayment.value || qrPaymentStatus.value !== 'pending') return
|
||
pollCount++
|
||
|
||
getPaymentStatus(orderId)
|
||
.then(res => {
|
||
if (res.code === 200 && res.data) {
|
||
const status = res.data.status || res.data.payStatus
|
||
console.log(`[QrPoll] 第 ${pollCount} 次查询,状态:`, status)
|
||
|
||
if (status === 'SUCCESS') {
|
||
qrPaymentStatus.value = 'success'
|
||
stopQrPolling()
|
||
if (qrPaymentResolve) {
|
||
qrPaymentResolve(true)
|
||
}
|
||
return
|
||
} else if (status === 'FAIL' || status === 'CLOSED') {
|
||
qrPaymentStatus.value = 'fail'
|
||
stopQrPolling()
|
||
if (qrPaymentReject) {
|
||
qrPaymentReject(new Error('支付失败'))
|
||
}
|
||
return
|
||
}
|
||
}
|
||
|
||
// 继续轮询
|
||
if (pollCount < maxRetry && showQrPayment.value && qrPaymentStatus.value === 'pending') {
|
||
qrPollingTimer = setTimeout(doQuery, 1000)
|
||
} else if (qrPaymentStatus.value === 'pending') {
|
||
qrPaymentStatus.value = 'fail'
|
||
stopQrPolling()
|
||
if (qrPaymentReject) {
|
||
qrPaymentReject(new Error('支付超时'))
|
||
}
|
||
}
|
||
})
|
||
.catch(err => {
|
||
console.error('[QrPoll] 查询异常:', err)
|
||
if (pollCount < maxRetry && showQrPayment.value && qrPaymentStatus.value === 'pending') {
|
||
qrPollingTimer = setTimeout(doQuery, 1000)
|
||
} else if (qrPaymentStatus.value === 'pending') {
|
||
qrPaymentStatus.value = 'fail'
|
||
stopQrPolling()
|
||
if (qrPaymentReject) {
|
||
qrPaymentReject(new Error('支付查询失败'))
|
||
}
|
||
}
|
||
})
|
||
}
|
||
|
||
// 延迟 2 秒开始轮询
|
||
qrPollingTimer = setTimeout(doQuery, 2000)
|
||
}
|
||
|
||
function stopQrPolling() {
|
||
isQrPolling = false
|
||
if (qrPollingTimer) {
|
||
clearTimeout(qrPollingTimer)
|
||
qrPollingTimer = null
|
||
}
|
||
}
|
||
|
||
// ==================== 核心购买逻辑 ====================
|
||
async function handlePurchase() {
|
||
if (!selectedCard.value) {
|
||
uni.showToast({ title: '请选择会员卡', icon: 'none' })
|
||
return
|
||
}
|
||
|
||
if (loading.value) return
|
||
loading.value = true
|
||
|
||
try {
|
||
const store = loadMemberStore()
|
||
const memberId = store.memberProfile?.id || store.profile?.id
|
||
|
||
// 金额转换:元 -> 分
|
||
const transAmt = String(Math.round(selectedCard.value.price * 100))
|
||
const cardName = selectedCard.value.cardName
|
||
|
||
// 根据环境选择支付方式
|
||
if (isWebEnv) {
|
||
// ==================== Web环境:扫码支付 ====================
|
||
uni.showLoading({ title: '创建订单...' })
|
||
|
||
const payRes = await createQrCodePayment({
|
||
memberId: memberId,
|
||
orderType: 'MEMBER_CARD',
|
||
goodsDesc: `购买${cardName}`,
|
||
transAmt: transAmt,
|
||
tradeType: 'QRCODE',
|
||
remark: `会员卡类型ID:${selectedCard.value.id}`
|
||
})
|
||
|
||
if (payRes.code !== 200) {
|
||
uni.hideLoading()
|
||
uni.showToast({ title: payRes.message || '创建支付订单失败', icon: 'none' })
|
||
loading.value = false
|
||
return
|
||
}
|
||
|
||
const payData = payRes.data
|
||
const orderId = payData.orderId
|
||
const qrCode = payData.qrCode
|
||
|
||
if (!qrCode) {
|
||
uni.hideLoading()
|
||
uni.showToast({ title: '获取二维码失败', icon: 'none' })
|
||
loading.value = false
|
||
return
|
||
}
|
||
|
||
// 显示二维码支付对话框
|
||
uni.hideLoading()
|
||
await new Promise((resolve, reject) => {
|
||
qrPaymentResolve = resolve
|
||
qrPaymentReject = reject
|
||
openQrPayment(qrCode, orderId)
|
||
})
|
||
|
||
// 支付成功后继续购买会员卡
|
||
} else {
|
||
// ==================== App环境:唤起支付宝App ====================
|
||
uni.showLoading({ title: '创建订单...' })
|
||
|
||
const payRes = await createPayment({
|
||
memberId: memberId,
|
||
orderType: 'MEMBER_CARD',
|
||
goodsDesc: `购买${cardName}`,
|
||
transAmt: transAmt,
|
||
tradeType: TRADE_TYPE,
|
||
remark: `会员卡类型ID:${selectedCard.value.id}`
|
||
})
|
||
|
||
if (payRes.code !== 200) {
|
||
uni.hideLoading()
|
||
uni.showToast({ title: payRes.message || '创建支付订单失败', icon: 'none' })
|
||
loading.value = false
|
||
return
|
||
}
|
||
|
||
const payData = payRes.data
|
||
const orderId = payData.orderId
|
||
const payUrl = payData.payUrl || payData.payInfo || payData.h5PayUrl
|
||
|
||
if (!payUrl) {
|
||
uni.hideLoading()
|
||
uni.showToast({ title: '获取支付链接失败', icon: 'none' })
|
||
loading.value = false
|
||
return
|
||
}
|
||
|
||
// 唤起支付宝
|
||
uni.hideLoading()
|
||
uni.showLoading({ title: '正在唤起支付宝...' })
|
||
|
||
try {
|
||
await invokeAlipayApp(payUrl)
|
||
} catch (err) {
|
||
uni.hideLoading()
|
||
uni.showToast({ title: err.message || '唤起支付宝失败', icon: 'none' })
|
||
loading.value = false
|
||
return
|
||
}
|
||
|
||
// 轮询支付状态
|
||
uni.showLoading({ title: '等待支付结果...' })
|
||
await startPolling(orderId)
|
||
}
|
||
|
||
// 支付成功,购买会员卡
|
||
uni.hideLoading()
|
||
const memberRes = await purchaseMemberCard({
|
||
memberId: memberId,
|
||
memberCardId: selectedCard.value.id,
|
||
sourceOrderId: currentQrOrderId || currentQrOrderId
|
||
})
|
||
|
||
if (memberRes.code === 200) {
|
||
// 关闭二维码对话框
|
||
if (showQrPayment.value) {
|
||
showQrPayment.value = false
|
||
}
|
||
uni.showToast({ title: '购买成功 🎉', icon: 'success' })
|
||
setTimeout(() => {
|
||
uni.redirectTo({
|
||
url: '/pages/memberInfo/memberCard'
|
||
})
|
||
}, 1500)
|
||
} else {
|
||
uni.showToast({ title: memberRes.message || '会员卡购买失败', icon: 'none' })
|
||
}
|
||
} catch (e) {
|
||
console.error('[purchaseCard] 购买失败:', e)
|
||
uni.hideLoading()
|
||
const errorMsg = e.message || '购买失败'
|
||
uni.showToast({ title: errorMsg, icon: 'none' })
|
||
} finally {
|
||
loading.value = false
|
||
}
|
||
}
|
||
|
||
// ==================== 生命周期 ====================
|
||
onMounted(() => {
|
||
if (cardTypes.value.length > 0) {
|
||
selectCard(cardTypes.value[0])
|
||
}
|
||
})
|
||
|
||
onUnmounted(() => {
|
||
// 清理轮询
|
||
if (pollingTimer.value) {
|
||
clearTimeout(pollingTimer.value)
|
||
pollingTimer.value = null
|
||
}
|
||
isPolling.value = false
|
||
stopQrPolling()
|
||
if (typeof plus !== 'undefined') {
|
||
plus.globalEvent.removeEventListener('resume', () => {})
|
||
}
|
||
})
|
||
</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: 50%;
|
||
transition: all 0.2s;
|
||
|
||
&--checked {
|
||
border-color: #2C5282;
|
||
background: #2C5282;
|
||
|
||
&::after {
|
||
content: '';
|
||
display: block;
|
||
width: 6px;
|
||
height: 6px;
|
||
background: #FFFFFF;
|
||
border-radius: 50%;
|
||
margin: 4px auto;
|
||
}
|
||
}
|
||
}
|
||
|
||
.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: baseline;
|
||
gap: 8px;
|
||
}
|
||
|
||
.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-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 {
|
||
display: flex;
|
||
align-items: baseline;
|
||
gap: 8px;
|
||
}
|
||
|
||
.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;
|
||
}
|
||
|
||
// ==================== 模拟支付对话框 ====================
|
||
.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;
|
||
}
|
||
}
|
||
</style> |