bindbox-mini/components/GamePassPurchasePopup.vue

304 lines
7.3 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

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>
<view v-if="visible" class="popup-mask" @tap="handleClose">
<view class="popup-content" @tap.stop>
<view class="popup-header">
<text class="title"> 超值次数卡</text>
<view class="close-btn" @tap="handleClose">×</view>
</view>
<scroll-view scroll-y class="packages-list">
<view v-if="loading" class="loading-state">
<text>加载中...</text>
</view>
<view v-else-if="!packages.length" class="empty-state">
<text>暂无优惠套餐</text>
</view>
<view
v-else
v-for="(pkg, index) in packages"
:key="pkg.id"
class="package-item"
:class="{ 'best-value': pkg.is_best_value }"
@tap="handlePurchase(pkg)"
>
<view class="pkg-tag" v-if="pkg.tag">{{ pkg.tag }}</view>
<view class="pkg-left">
<view class="pkg-name">{{ pkg.name }}</view>
<view class="pkg-count"> {{ pkg.pass_count }} 次游戏</view>
<view class="pkg-validity" v-if="pkg.valid_days > 0">有效期 {{ pkg.valid_days }} </view>
<view class="pkg-validity" v-else>永久有效</view>
</view>
<view class="pkg-right">
<view class="pkg-price-row">
<text class="currency">¥</text>
<text class="price">{{ (pkg.price / 100).toFixed(2) }}</text>
</view>
<view class="pkg-original-price" v-if="pkg.original_price > pkg.price">
¥{{ (pkg.original_price / 100).toFixed(2) }}
</view>
<button class="btn-buy" :loading="purchasingId === pkg.id">购买</button>
</view>
</view>
</scroll-view>
</view>
</view>
</view>
</template>
<script setup>
import { ref, watch } from 'vue'
import { getGamePassPackages, purchaseGamePass, createWechatOrder } from '@/api/appUser'
const props = defineProps({
visible: { type: Boolean, default: false },
activityId: { type: [String, Number], default: '' }
})
const emit = defineEmits(['update:visible', 'success'])
const loading = ref(false)
const packages = ref([])
const purchasingId = ref(null)
watch(() => props.visible, (val) => {
if (val) {
fetchPackages()
}
})
async function fetchPackages() {
loading.value = true
try {
const res = await getGamePassPackages(props.activityId)
// res 应该是数组
let list = []
if (Array.isArray(res)) list = res
else if (res && Array.isArray(res.list)) list = res.list
else if (res && Array.isArray(res.data)) list = res.data
// 简单处理:给第一个或最优惠的打标签
// 这里随机模拟一下 "热销" 或计算折扣力度
packages.value = list.map(p => {
let tag = ''
const discount = 1 - (p.price / p.original_price)
if (p.original_price > 0 && discount >= 0.2) {
tag = `${Math.floor(discount * 100)}%`
}
return { ...p, tag }
})
} catch (e) {
console.error(e)
packages.value = []
} finally {
loading.value = false
}
}
async function handlePurchase(pkg) {
if (purchasingId.value) return
purchasingId.value = pkg.id
try {
uni.showLoading({ title: '创建订单...' })
// 1. 调用购买接口 (后端创建订单 + 预下单)
// 注意根据后端实现purchaseGamePass 可能直接返回支付参数,或者需要我们自己调 createWechatOrder
// 之前分析 game_passes_app.go它似乎返回的是 simple success?
// 让我们再确认一下 game_passes_app.go 的 PurchaseGamePassPackage
// 既然我看不到代码,按常规逻辑:
// 如果返回 order_no则需要 createWechatOrder
// 如果返回 pay_params则直接支付
// 假设 API 返回 { order_no, ... }
const res = await purchaseGamePass(pkg.id)
const orderNo = res.order_no || res.orderNo
if (!orderNo) throw new Error('下单失败')
// 2. 拉起支付
const openid = uni.getStorageSync('openid')
const payRes = await createWechatOrder({ openid, order_no: orderNo })
await new Promise((resolve, reject) => {
uni.requestPayment({
provider: 'wxpay',
timeStamp: payRes.timeStamp || payRes.timestamp,
nonceStr: payRes.nonceStr || payRes.noncestr,
package: payRes.package,
signType: payRes.signType || 'RSA',
paySign: payRes.paySign,
success: resolve,
fail: reject
})
})
uni.showToast({ title: '购买成功', icon: 'success' })
emit('success')
handleClose()
} catch (e) {
if (e?.errMsg && e.errMsg.includes('cancel')) {
uni.showToast({ title: '取消支付', icon: 'none' })
} else {
uni.showToast({ title: e.message || '购买失败', icon: 'none' })
}
} finally {
uni.hideLoading()
purchasingId.value = null
}
}
function handleClose() {
emit('update:visible', false)
}
</script>
<style lang="scss" scoped>
.popup-mask {
position: fixed;
top: 0; left: 0; right: 0; bottom: 0;
background: rgba(0, 0, 0, 0.6);
z-index: 999;
display: flex;
align-items: flex-end;
}
.popup-content {
width: 100%;
background: #FFFFFF;
border-radius: 32rpx 32rpx 0 0;
padding-bottom: env(safe-area-inset-bottom);
max-height: 80vh;
display: flex;
flex-direction: column;
}
.popup-header {
padding: 32rpx;
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: 1rpx solid #F3F4F6;
.title {
font-size: 36rpx;
font-weight: bold;
color: #1F2937;
}
.close-btn {
font-size: 48rpx;
color: #9CA3AF;
line-height: 0.8;
padding: 10rpx;
}
}
.packages-list {
padding: 32rpx;
max-height: 60vh;
}
.loading-state, .empty-state {
text-align: center;
padding: 60rpx 0;
color: #9CA3AF;
font-size: 28rpx;
}
.package-item {
position: relative;
background: linear-gradient(135deg, #FFFFFF, #F9FAFB);
border: 2rpx solid #E5E7EB;
border-radius: 24rpx;
padding: 24rpx 32rpx;
margin-bottom: 24rpx;
display: flex;
justify-content: space-between;
align-items: center;
transition: all 0.2s;
overflow: hidden;
&:active {
transform: scale(0.98);
background: #F3F4F6;
}
}
.pkg-tag {
position: absolute;
top: 0;
left: 0;
background: #FF6B00;
color: #FFF;
font-size: 20rpx;
padding: 4rpx 12rpx;
border-bottom-right-radius: 12rpx;
font-weight: bold;
}
.pkg-left {
flex: 1;
}
.pkg-name {
font-size: 32rpx;
font-weight: bold;
color: #1F2937;
margin-bottom: 8rpx;
}
.pkg-count {
font-size: 26rpx;
color: #4B5563;
margin-bottom: 4rpx;
}
.pkg-validity {
font-size: 22rpx;
color: #9CA3AF;
}
.pkg-right {
display: flex;
flex-direction: column;
align-items: flex-end;
}
.pkg-price-row {
color: #FF6B00;
font-weight: bold;
margin-bottom: 4rpx;
.currency {
font-size: 24rpx;
}
.price {
font-size: 40rpx;
}
}
.pkg-original-price {
font-size: 22rpx;
color: #9CA3AF;
text-decoration: line-through;
margin-bottom: 12rpx;
}
.btn-buy {
background: linear-gradient(90deg, #FF6B00, #FF9F43);
color: #FFF;
font-size: 24rpx;
padding: 0 24rpx;
height: 56rpx;
line-height: 56rpx;
border-radius: 28rpx;
border: none;
font-weight: 600;
&[loading] {
opacity: 0.8;
}
}
</style>