Files
gym-manage/gym-manage-uniapp/pages/memberInfo/purchaseCard.vue
T

1071 lines
32 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="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>