feat: 为活动支付和购买集成次数卡功能。

This commit is contained in:
邹方成 2026-01-02 16:15:00 +08:00
parent 61df7fca5e
commit 7009b47de6
4 changed files with 197 additions and 1 deletions

View File

@ -74,6 +74,7 @@ async function fetchPackages() {
// res
let list = []
if (Array.isArray(res)) list = res
else if (res && Array.isArray(res.packages)) list = res.packages
else if (res && Array.isArray(res.list)) list = res.list
else if (res && Array.isArray(res.data)) list = res.data

View File

@ -276,6 +276,7 @@ async function onPaymentConfirm(paymentData) {
channel: 'miniapp',
count: selectedSlots.length,
coupon_id: paymentData.coupon ? Number(paymentData.coupon.id) : 0,
use_game_pass: !!paymentData.useGamePass,
slot_index: selectedSlots.map(Number)
}

View File

@ -67,6 +67,7 @@
v-model:visible="paymentVisible"
:amount="paymentAmount"
:coupons="coupons"
:gamePasses="gamePasses"
:propCards="propCards"
@confirm="onPaymentConfirm"
/>
@ -80,12 +81,33 @@
:activity-id="activityId"
/>
<CabinetPreviewPopup
v-model:visible="cabinetVisible"
:activity-id="activityId"
/>
<!-- 开奖加载弹窗 -->
<DrawLoadingPopup
:visible="showDrawLoading"
:progress="drawProgress"
:total="drawTotal"
/>
<GamePassPurchasePopup
v-model:visible="purchasePopupVisible"
:activity-id="activityId"
@success="onPurchaseSuccess"
/>
<!-- 悬浮次数卡入口 -->
<view v-if="gamePassRemaining > 0 || true" class="game-pass-float" @tap="openPurchasePopup">
<view class="badge-content">
<text class="badge-icon">🎮</text>
<text class="badge-text" v-if="gamePassRemaining > 0">{{ gamePassRemaining }}</text>
<text class="badge-text" v-else>充值</text>
</view>
<view class="badge-label">超值卡</view>
</view>
</template>
</ActivityPageLayout>
</template>
@ -105,6 +127,8 @@ import CabinetPreviewPopup from '@/components/activity/CabinetPreviewPopup.vue'
import LotteryResultPopup from '@/components/activity/LotteryResultPopup.vue'
import DrawLoadingPopup from '@/components/activity/DrawLoadingPopup.vue'
import PaymentPopup from '@/components/PaymentPopup.vue'
import GamePassPurchasePopup from '@/components/GamePassPurchasePopup.vue'
import { getGamePasses } from '@/api/appUser'
// Composables
import { useActivity, useIssues, useRewards, useRecords } from '../../composables'
// API
@ -160,6 +184,30 @@ const propCards = ref([])
const pendingCount = ref(1)
const selectedCoupon = ref(null)
const selectedCard = ref(null)
const useGamePassFlag = ref(false)
// ============ ============
const gamePasses = ref(null)
const gamePassRemaining = computed(() => gamePasses.value?.total_remaining || 0)
const purchasePopupVisible = ref(false)
async function fetchPasses() {
if (!activityId.value) return
try {
const res = await getGamePasses(activityId.value)
gamePasses.value = res || null
} catch (e) {
gamePasses.value = null
}
}
function openPurchasePopup() {
purchasePopupVisible.value = true
}
function onPurchaseSuccess() {
fetchPasses()
}
// ============ ============
function showRules() {
@ -198,6 +246,7 @@ function openPayment(count) {
async function onPaymentConfirm(data) {
selectedCoupon.value = data?.coupon || null
selectedCard.value = data?.card || null
useGamePassFlag.value = data?.useGamePass || false
paymentVisible.value = false
await onMachineDraw(pendingCount.value)
}
@ -317,9 +366,15 @@ async function onMachineDraw(count) {
channel: 'miniapp',
count: times,
coupon_id: selectedCoupon.value?.id ? Number(selectedCoupon.value.id) : 0,
item_card_id: selectedCard.value?.id ? Number(selectedCard.value.id) : 0
item_card_id: selectedCard.value?.id ? Number(selectedCard.value.id) : 0,
use_game_pass: useGamePassFlag.value
})
//
if (useGamePassFlag.value) {
fetchPasses()
}
const orderNo = joinRes?.order_no || joinRes?.data?.order_no || joinRes?.result?.order_no
if (!orderNo) throw new Error('未获取到订单号')
@ -386,6 +441,7 @@ onLoad(async (opts) => {
if (currentIssueId.value) {
fetchWinRecords(id, currentIssueId.value)
}
fetchPasses()
})
//
@ -553,4 +609,46 @@ watch(currentIssueId, (newId) => {
border: none;
}
}
/* 次数卡悬浮入口 */
.game-pass-float {
position: fixed;
right: 32rpx;
bottom: calc(180rpx + env(safe-area-inset-bottom));
z-index: 990;
display: flex;
flex-direction: column;
align-items: center;
animation: float 3s ease-in-out infinite;
}
.badge-content {
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(10rpx);
border-radius: 30rpx;
padding: 8rpx 16rpx;
display: flex;
align-items: center;
box-shadow: 0 8rpx 20rpx rgba(0,0,0,0.15);
border: 1rpx solid rgba($brand-primary, 0.2);
}
.badge-icon { font-size: 28rpx; margin-right: 6rpx; }
.badge-text { font-size: 24rpx; font-weight: 800; color: $brand-primary; }
.badge-label {
font-size: 20rpx;
color: #fff;
background: $gradient-brand;
padding: 2rpx 8rpx;
border-radius: 8rpx;
margin-top: -6rpx;
z-index: 2;
box-shadow: 0 2rpx 6rpx rgba(0,0,0,0.2);
}
@keyframes float {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-10rpx); }
}
</style>

View File

@ -73,10 +73,22 @@
<!-- 固定底部操作栏 -->
<view class="float-bar" v-show="!isPaymentVisible">
<view class="float-bar-inner">
<view class="selection-info" v-if="selectedCount > 0">
已选 <text class="highlight">{{ selectedCount }}</text> 个位置
</view>
<view class="selection-info" v-if="selectedCount > 0">
已选 <text class="highlight">{{ selectedCount }}</text> 个位置
</view>
<view class="selection-info" v-else>
<!-- 次数卡余额 / 购买入口 -->
<view v-if="gamePassRemaining > 0" class="game-pass-badge" @tap="() => {}">
<text class="badge-icon">🎮</text>
<text class="badge-text" style="font-size: 24rpx; font-weight: bold; color: #10B981;">{{ gamePassRemaining }}</text>
</view>
<!-- 充值入口 -->
<view class="game-pass-buy-btn" @tap="openPurchasePopup">
<text>充值特惠</text>
</view>
请选择位置
</view>
<view class="action-buttons">
@ -121,10 +133,17 @@
v-model:visible="paymentVisible"
:amount="paymentAmount"
:coupons="paymentCoupons"
:gamePasses="gamePasses"
:showCards="false"
@confirm="onPaymentConfirm"
@cancel="onPaymentCancel"
/>
<GamePassPurchasePopup
v-model:visible="purchasePopupVisible"
:activity-id="activityId"
@success="onPurchaseSuccess"
/>
</template>
</ActivityPageLayout>
</template>
@ -144,6 +163,8 @@ import CabinetPreviewPopup from '@/components/activity/CabinetPreviewPopup.vue'
import FlipGrid from '@/components/FlipGrid.vue'
import YifanSelector from '@/components/YifanSelector.vue'
import PaymentPopup from '@/components/PaymentPopup.vue'
import GamePassPurchasePopup from '@/components/GamePassPurchasePopup.vue'
import { getGamePasses } from '@/api/appUser'
// Composables
import { useActivity, useIssues, useRewards, useRecords } from '../../composables'
// Utils
@ -249,6 +270,29 @@ function handlePayment() {
}
}
// ============ ============
const gamePasses = ref(null)
const gamePassRemaining = computed(() => gamePasses.value?.total_remaining || 0)
const purchasePopupVisible = ref(false)
async function fetchPasses() {
if (!activityId.value) return
try {
const res = await getGamePasses(activityId.value)
gamePasses.value = res || null
} catch (e) {
gamePasses.value = null
}
}
function openPurchasePopup() {
purchasePopupVisible.value = true
}
function onPurchaseSuccess() {
fetchPasses()
}
// ============ ============
const nowMs = ref(Date.now())
let nowTimer = null
@ -364,6 +408,8 @@ onLoad(async (opts) => {
if (currentIssueId.value) {
fetchWinRecords(id, currentIssueId.value)
}
//
fetchPasses()
})
onUnload(() => {
@ -610,3 +656,53 @@ watch(currentIssueId, (newId) => {
}
}
</style>
<style lang="scss" scoped>
/* 浮动操作栏扩展 - 充值按钮 & Badge */
.game-pass-badge {
display: flex;
align-items: center;
background: rgba(16, 185, 129, 0.15);
padding: 6rpx 16rpx;
border-radius: 30rpx;
border: 1rpx solid rgba(16, 185, 129, 0.3);
margin: 0 12rpx;
animation: pulse 2s infinite;
.badge-icon {
font-size: 28rpx;
margin-right: 6rpx;
}
.badge-text {
font-size: 24rpx;
color: #10B981;
font-weight: 600;
}
&:active {
opacity: 0.8;
}
}
.game-pass-buy-btn {
background: linear-gradient(90deg, #FF9F43, #FF6B00);
color: #fff;
font-size: 22rpx;
padding: 6rpx 16rpx;
border-radius: 24rpx;
margin-right: 12rpx;
font-weight: 600;
box-shadow: 0 4rpx 8rpx rgba(255, 107, 0, 0.2);
&:active {
transform: scale(0.95);
}
}
@keyframes pulse {
0% { transform: scale(1); }
50% { transform: scale(1.05); }
100% { transform: scale(1); }
}
</style>