2026-01-08 10:14:13 +08:00

1136 lines
30 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>
<ActivityPageLayout :cover-url="coverUrl">
<template #header>
<ActivityHeader
:title="detail.name || detail.title || '一番赏活动'"
:price="detail.price_draw"
price-unit="/"
:cover-url="coverUrl"
:tags="['公开透明', '拒绝套路']"
:scheduled-time="scheduledTimeText"
@show-rules="showRules"
@go-cabinet="goCabinet"
/>
</template>
<template #content>
<!-- 赏品概览 -->
<ActivityTabs v-model="tabActive" :stagger="1">
<template #pool>
<RewardsPreview
title="奖品配置"
:rewards="currentIssueRewards"
:grouped="false"
@view-all="rewardsVisible = true"
/>
</template>
<template #records>
<RecordsList :records="winRecords" />
</template>
</ActivityTabs>
<!-- 选号区域一番赏专属 -->
<view class="section-container selector-container animate-enter stagger-2">
<!-- 期号切换 -->
<view class="issue-header">
<view class="issue-switch-btn" @click="prevIssue">
<text class="arrow"></text>
</view>
<view class="issue-info-center">
<text class="issue-current-text">{{ currentIssueTitle }}</text>
<text class="issue-status-badge">进行中</text>
</view>
<view class="issue-switch-btn" @click="nextIssue">
<text class="arrow"></text>
</view>
</view>
<view class="issue-block-tip" v-if="!isOrderAllowed">
<text class="issue-block-text">{{ orderBlockedReason }}</text>
</view>
<!-- 选号组件 - 隐藏内置操作栏 -->
<view class="selector-body" v-if="shouldShowSelector">
<!-- 直接调用choice接口加载数据 -->
<view class="choice-container">
<view v-if="choicesLoading" class="loading-state">正在加载位置...</view>
<view v-else-if="choicesList.length === 0" class="empty-state">暂无可选位置</view>
<view v-else class="choices-grid">
<view
v-for="(item, index) in choicesList"
:key="item.id || index"
class="choice-item"
:class="{
'is-sold': item.status === 'sold' || item.is_sold,
'is-selected': isChoiceSelected(item),
'is-available': !item.status || item.status === 'available'
}"
@tap="handleSelectChoice(item)"
>
<text class="choice-number">{{ item.number || item.position || index + 1 }}</text>
<view class="choice-status">
<text v-if="item.status === 'sold' || item.is_sold">已售</text>
<text v-else-if="isChoiceSelected(item)">已选</text>
<text v-else>可选</text>
</view>
</view>
</view>
</view>
</view>
</view>
</template>
<template #footer>
<!-- 固定底部操作栏 -->
<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-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">
<button v-if="selectedCount === 0" class="action-btn primary" @tap="handleRandomDraw" :disabled="!isOrderAllowed">随机一发</button>
<button v-else class="action-btn primary" @tap="handlePayment" :disabled="!isOrderAllowed">去支付</button>
</view>
</view>
</view>
</template>
<template #modals>
<!-- 翻牌弹窗 -->
<view v-if="showFlip" class="flip-overlay" @touchmove.stop.prevent>
<view class="flip-mask" @tap="closeFlip"></view>
<view class="flip-content" @tap.stop>
<FlipGrid ref="flipRef" :rewards="currentIssueRewards" :controls="false" />
<button class="overlay-close" @tap="closeFlip">关闭</button>
</view>
</view>
<!-- 奖品弹窗 -->
<RewardsPopup
v-model:visible="rewardsVisible"
:title="`${currentIssueTitle} · 奖品与概率`"
:reward-groups="rewardGroups"
/>
<!-- 规则弹窗 -->
<RulesPopup
v-model:visible="rulesVisible"
:content="detail.gameplay_intro"
/>
<!-- 盒柜预览弹窗 -->
<CabinetPreviewPopup
v-model:visible="cabinetVisible"
:activity-id="activityId"
/>
<!-- 支付弹窗 YifanSelector 提升到这里确保祝福动画位置正确 -->
<PaymentPopup
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>
<script setup>
import { ref, computed, watch } from 'vue'
import { onLoad, onUnload } from '@dcloudio/uni-app'
import { getIssueChoices, joinLottery, createWechatOrder, getLotteryResult } from '@/api/appUser'
import { requestLotterySubscription } from '@/utils/subscribe'
// 公共组件 - uni-app需要直接导入.vue文件
import ActivityPageLayout from '@/components/activity/ActivityPageLayout.vue'
import ActivityHeader from '@/components/activity/ActivityHeader.vue'
import ActivityTabs from '@/components/activity/ActivityTabs.vue'
import RewardsPreview from '@/components/activity/RewardsPreview.vue'
import RewardsPopup from '@/components/activity/RewardsPopup.vue'
import RecordsList from '@/components/activity/RecordsList.vue'
import RulesPopup from '@/components/activity/RulesPopup.vue'
import CabinetPreviewPopup from '@/components/activity/CabinetPreviewPopup.vue'
import FlipGrid from '@/components/FlipGrid.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
import { formatDateTime, parseTimeMs } from '@/utils/format'
// ============ 使用Composables ============
const activityId = ref('')
const {
detail,
coverUrl,
fetchDetail,
setNavigationTitle
} = useActivity(activityId)
const {
issues,
currentIssueId,
currentIssueTitle,
fetchIssues,
prevIssue,
nextIssue
} = useIssues(activityId)
const {
currentIssueRewards,
rewardGroups,
fetchRewardsForIssues
} = useRewards(activityId, currentIssueId)
const {
winRecords,
fetchWinRecords
} = useRecords()
// ============ 本地状态 ============
const tabActive = ref('pool')
const rewardsVisible = ref(false)
const rulesVisible = ref(false)
const cabinetVisible = ref(false)
const showFlip = ref(false)
const flipRef = ref(null)
const yifanSelectorRef = ref(null)
const selectedCount = ref(0) // 从外部追踪选中数量
const isPaymentVisible = ref(false) // 支付弹窗是否显示
const paymentVisible = ref(false) // 控制支付弹窗显示
const paymentAmount = ref('0') // 支付金额
const paymentCoupons = ref([]) // 可用优惠券
// Choices 相关状态
const choicesLoading = ref(false)
const choicesList = ref([])
const selectedChoices = ref([])
// 计算属性:判断是否应该显示选号器
const shouldShowSelector = computed(() => {
const result = !!(
activityId.value &&
currentIssueId.value &&
detail.value?.play_type === 'ichiban'
)
console.log('[Yifanshang] shouldShowSelector computed:', {
activityId: activityId.value,
currentIssueId: currentIssueId.value,
play_type: detail.value?.play_type,
result
})
return result
})
// 监控 shouldShowSelector 的变化
watch(shouldShowSelector, (newVal, oldVal) => {
console.log('[Yifanshang] shouldShowSelector changed:', {
from: oldVal,
to: newVal,
yifanSelectorRef: yifanSelectorRef.value
})
if (newVal && !yifanSelectorRef.value) {
console.warn('[Yifanshang] shouldShowSelector is true but yifanSelectorRef is null!')
}
})
// 加载choices数据
async function loadChoices() {
console.log('[Yifanshang] loadChoices called with:', {
activityId: activityId.value,
issueId: currentIssueId.value
})
if (!activityId.value || !currentIssueId.value) {
console.warn('[Yifanshang] Missing activityId or issueId')
return
}
choicesLoading.value = true
try {
console.log('[Yifanshang] Calling getIssueChoices API...')
const res = await getIssueChoices(activityId.value, currentIssueId.value)
console.log('[Yifanshang] getIssueChoices response:', res)
// 处理 { total_slots: 1, available: [1], claimed: [] } 这种格式
if (res && typeof res.total_slots === 'number' && Array.isArray(res.available)) {
const total = res.total_slots
const list = []
const availableSet = new Set(res.available.map(v => Number(v)))
for (let i = 1; i <= total; i++) {
const isAvailable = availableSet.has(i)
list.push({
id: i,
number: i,
position: i,
status: isAvailable ? 'available' : 'sold',
is_sold: !isAvailable
})
}
choicesList.value = list
} else if (Array.isArray(res)) {
choicesList.value = res
} else if (res && Array.isArray(res.data)) {
choicesList.value = res.data
} else if (res && Array.isArray(res.choices)) {
choicesList.value = res.choices
} else {
choicesList.value = []
}
console.log('[Yifanshang] Choices loaded, total:', choicesList.value.length)
} catch (error) {
console.error('[Yifanshang] Failed to load choices:', error)
uni.showToast({ title: '加载位置失败', icon: 'none' })
choicesList.value = []
} finally {
choicesLoading.value = false
}
}
// 判断某个choice是否被选中
function isChoiceSelected(item) {
return selectedChoices.value.some(i => i.id === item.id || (i.position && i.position === item.position))
}
// 处理选择choice
function handleSelectChoice(item) {
if (!isOrderAllowed.value) {
uni.showToast({ title: orderBlockedReason.value, icon: 'none' })
return
}
if (item.status === 'sold' || item.is_sold) {
return
}
const index = selectedChoices.value.findIndex(i => i.id === item.id || (i.position && i.position === item.position))
if (index > -1) {
selectedChoices.value.splice(index, 1)
} else {
selectedChoices.value.push(item)
}
console.log('[Yifanshang] Selected choices:', selectedChoices.value.length)
selectedCount.value = selectedChoices.value.length
}
// 监听 shouldShowSelector 变化加载choices
watch(shouldShowSelector, (newVal) => {
if (newVal && (choicesList.value.length === 0 || !choicesList.value)) {
loadChoices()
}
})
// 接收选中变化事件
function onSelectionChange(items) {
selectedCount.value = Array.isArray(items) ? items.length : 0
}
// 接收支付弹窗显示状态变化(从 YifanSelector
function onPaymentVisibleChange(visible) {
isPaymentVisible.value = visible
paymentVisible.value = visible
}
// 接收支付金额变化
function onPaymentAmountChange(amount) {
paymentAmount.value = amount
}
// 接收优惠券变化
function onPaymentCouponsChange(coupons) {
paymentCoupons.value = coupons
}
// 支付确认处理
async function onPaymentConfirm(paymentData) {
console.log('[Yifanshang] onPaymentConfirm called with:', paymentData)
if (selectedChoices.value.length === 0) {
uni.showToast({ title: '请先选择位置', icon: 'none' })
return
}
// 请求订阅消息 (尽可能早调用以确保在用户交互上下文中)
await requestLotterySubscription()
try {
uni.showLoading({ title: '创建订单中...' })
// 第一步:调用 join 接口创建订单
const joinData = {
activity_id: Number(activityId.value),
issue_id: Number(currentIssueId.value),
channel: 'miniapp',
count: selectedChoices.value.length,
slot_index: selectedChoices.value.map(c => Number(c.id || c.number)),
coupon_id: paymentData.coupon_id || 0,
item_card_id: paymentData.item_card_id || 0,
use_game_pass: paymentData.use_game_pass || false
}
console.log('[Yifanshang] Calling join with:', joinData)
const joinResult = await joinLottery(joinData)
console.log('[Yifanshang] Join result:', joinResult)
if (!joinResult.order_no) {
throw new Error('创建订单失败:未返回订单号')
}
// 第二步:判断是否需要支付
if (joinResult.actual_amount > 0 && joinResult.status !== 2) {
// 使用订单号调用微信支付预下单接口
uni.showLoading({ title: '拉起支付...' })
// 获取 openid
const openid = uni.getStorageSync('openid')
if (!openid) {
throw new Error('缺少OpenID请重新登录')
}
const preorderData = {
openid: openid,
order_no: joinResult.order_no
}
console.log('[Yifanshang] Calling wechat preorder with:', preorderData)
const paymentParams = await createWechatOrder(preorderData)
console.log('[Yifanshang] Wechat payment params:', paymentParams)
uni.hideLoading()
// 第三步:拉起支付
// #ifdef MP-WEIXIN
const payResult = await uni.requestPayment({
provider: 'wxpay',
...paymentParams
})
console.log('[Yifanshang] Payment result:', payResult)
// #endif
// #ifdef MP-TOUTIAO
const payResult = await tt.pay({
...paymentParams
})
console.log('[Yifanshang] Payment result:', payResult)
// #endif
} else {
console.log('[Yifanshang] Order is free or paid, skipping payment flow. Amount:', joinResult.actual_amount, 'Status:', joinResult.status)
}
// 第四步:支付成功后,查询抽奖结果
uni.showLoading({ title: '查询结果中...' })
console.log('[Yifanshang] Getting lottery result with order_no:', joinResult.order_no)
const lotteryResult = await getLotteryResult(joinResult.order_no)
console.log('[Yifanshang] Lottery result:', lotteryResult)
uni.hideLoading()
// 检查状态:如果是 paid_waiting提示用户等待系统开启
const status = lotteryResult?.status || (lotteryResult?.result?.status) || ''
if (status === 'paid_waiting') {
const nextDrawTime = lotteryResult?.next_draw_time || lotteryResult?.result?.next_draw_time
const nextTimeText = nextDrawTime ? formatDateTime(nextDrawTime) : ''
const content = nextTimeText
? `下单成功,等待系统自动开启本期赏品。\n预计开赏时间${nextTimeText}`
: '下单成功,等待系统自动开启本期赏品。'
uni.showModal({
title: '下单成功',
content,
showCancel: false,
success: () => {
// 清空选择并关闭弹窗
selectedChoices.value = []
selectedCount.value = 0
paymentVisible.value = false
loadChoices()
}
})
return
}
// 抽奖成功,清空选择
selectedChoices.value = []
selectedCount.value = 0
uni.showToast({
title: '购买成功!',
icon: 'success'
})
// 关闭支付弹窗
paymentVisible.value = false
// 重新加载choices
await loadChoices()
// 显示翻牌结果
if (lotteryResult) {
showFlip.value = true
try { flipRef.value?.reset?.() } catch (_) {}
setTimeout(() => {
// 处理不同的结果格式
let items = []
if (Array.isArray(lotteryResult)) {
items = lotteryResult.map(data => ({
title: String(data?.title || data?.name || '未知奖励'),
image: String(data?.image || data?.img || '')
}))
} else if (lotteryResult.list && Array.isArray(lotteryResult.list)) {
items = lotteryResult.list.map(data => ({
title: String(data?.title || data?.name || '未知奖励'),
image: String(data?.image || data?.img || '')
}))
} else if (lotteryResult.rewards && Array.isArray(lotteryResult.rewards)) {
items = lotteryResult.rewards.map(data => ({
title: String(data?.title || data?.name || '未知奖励'),
image: String(data?.image || data?.img || '')
}))
} else if (lotteryResult.data && Array.isArray(lotteryResult.data)) {
items = lotteryResult.data.map(data => ({
title: String(data?.title || data?.name || '未知奖励'),
image: String(data?.image || data?.img || '')
}))
} else {
// 单个结果
items = [{
title: String(lotteryResult?.title || lotteryResult?.name || '未知奖励'),
image: String(lotteryResult?.image || lotteryResult?.img || '')
}]
}
console.log('[Yifanshang] Processed reward items:', items)
flipRef.value?.revealResults?.(items)
}, 100)
}
} catch (error) {
console.error('[Yifanshang] Payment/Join failed:', error)
uni.hideLoading()
// 处理支付取消
if (error.errMsg && error.errMsg.includes('cancel')) {
uni.showToast({
title: '已取消支付',
icon: 'none'
})
} else {
uni.showToast({
title: error.message || error.errMsg || '支付失败',
icon: 'none'
})
}
}
}
// 支付取消处理
function onPaymentCancel() {
console.log('[Yifanshang] Payment cancelled')
// 支付弹窗会自动关闭
}
// 触发支付
function handlePayment() {
console.log('[Yifanshang] handlePayment called, selectedChoices:', selectedChoices.value.length)
if (selectedChoices.value.length === 0) {
uni.showToast({ title: '请先选择位置', icon: 'none' })
return
}
if (!isOrderAllowed.value) {
uni.showToast({ title: orderBlockedReason.value, icon: 'none' })
return
}
// 计算支付金额
const amount = (selectedChoices.value.length * Number(detail.value.price_draw || 0) / 100).toFixed(2)
paymentAmount.value = amount
console.log('[Yifanshang] Payment amount:', amount, 'Opening payment popup...')
// 显示支付弹窗
paymentVisible.value = true
}
// 触发随机选号
function handleRandomDraw() {
console.log('[Yifanshang] handleRandomDraw called')
if (!isOrderAllowed.value) {
uni.showToast({ title: orderBlockedReason.value, icon: 'none' })
return
}
// 随机选择一个可用位置
const availableChoices = choicesList.value.filter(c => c.status === 'available' || !c.is_sold)
if (availableChoices.length === 0) {
uni.showToast({ title: '暂无可用位置', icon: 'none' })
return
}
const randomIndex = Math.floor(Math.random() * availableChoices.length)
const randomChoice = availableChoices[randomIndex]
selectedChoices.value = [randomChoice]
selectedCount.value = 1
console.log('[Yifanshang] Random choice selected:', randomChoice)
// 自动触发支付
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
function startNowTicker() {
stopNowTicker()
nowMs.value = Date.now()
nowTimer = setInterval(() => { nowMs.value = Date.now() }, 1000)
}
function stopNowTicker() {
if (nowTimer) {
clearInterval(nowTimer)
nowTimer = null
}
}
const scheduledTime = computed(() => detail.value?.scheduled_time || detail.value?.scheduledTime || '')
const scheduledTimeMs = computed(() => parseTimeMs(scheduledTime.value))
const scheduledTimeText = computed(() => formatDateTime(scheduledTime.value))
const remainMs = computed(() => {
const end = scheduledTimeMs.value
if (!end) return null
return end - nowMs.value
})
const isOrderAllowed = computed(() => {
const ms = remainMs.value
if (ms === null) return true
return ms > 25000
})
const orderBlockedReason = computed(() => {
const ms = remainMs.value
if (ms === null) return ''
if (ms <= 0) return '本期已结束,暂不可下单'
if (ms <= 25000) return '距本期结束不足25秒暂不可下单'
return ''
})
// ============ 业务方法 ============
function showRules() {
rulesVisible.value = true
}
function goCabinet() {
cabinetVisible.value = true
}
function closeFlip() {
showFlip.value = false
}
function onPaymentSuccess(payload) {
console.log('Payment Success:', payload)
const result = payload.result
const status = String(result?.status || result?.data?.status || result?.result?.status || '')
if (status === 'paid_waiting') {
const next = result?.next_draw_time || result?.nextDrawTime || result?.next_draw_at || result?.nextDrawAt
const nextText = next ? formatDateTime(next) : ''
const content = nextText
? `下单成功,等待系统自动开启本期赏品。\n预计开赏时间${nextText}`
: '下单成功,等待系统自动开启本期赏品。'
uni.showModal({
title: '下单成功',
content,
showCancel: false
})
return
}
let wonItems = []
if (Array.isArray(result)) {
wonItems = result
} else if (result?.list) {
wonItems = result.list
} else if (result?.data) {
wonItems = result.data
} else if (result?.rewards) {
wonItems = result.rewards
} else {
wonItems = result ? [result] : []
}
const items = wonItems.map(data => ({
title: String(data?.title || data?.name || data?.product_name || data?.reward_name || '未知奖励'),
image: String(data?.image || data?.img || data?.pic || data?.product_image || data?.reward_image || '')
}))
showFlip.value = true
try { flipRef.value?.reset?.() } catch (_) {}
setTimeout(() => {
flipRef.value?.revealResults?.(items)
}, 100)
}
// ============ 生命周期 ============
onLoad(async (opts) => {
startNowTicker()
const id = opts?.id || ''
if (!id) return
activityId.value = id
// 并行获取活动详情和期数信息
await Promise.all([fetchDetail(), fetchIssues()])
setNavigationTitle('一番赏')
// 期数获取完成后获取奖励
await fetchRewardsForIssues(issues.value)
// 异步获取记录(不阻塞渲染)
if (currentIssueId.value) {
fetchWinRecords(id, currentIssueId.value)
}
// 获取次数卡
fetchPasses()
})
onUnload(() => {
stopNowTicker()
})
// 监听期切换,刷新记录
watch(currentIssueId, (newId) => {
console.log('[Yifanshang] currentIssueId changed:', newId, 'activityId:', activityId.value)
if (newId && activityId.value) {
fetchWinRecords(activityId.value, newId)
}
})
// 监听 activityId 和 currentIssueId用于调试
watch([activityId, currentIssueId], ([newActId, newIssueId]) => {
console.log('[Yifanshang] Props state:', {
activityId: newActId,
currentIssueId: newIssueId,
play_type: detail.value?.play_type,
bothExist: !!(newActId && newIssueId),
shouldShowSelector: !!(newActId && newIssueId && detail.value?.play_type === 'ichiban')
})
}, { immediate: true })
</script>
<style lang="scss" scoped>
/* 选号容器 - 与原始设计一致 */
.section-container {
margin: 0 $spacing-lg $spacing-lg;
background: rgba(255, 255, 255, 0.9);
border-radius: $radius-xl;
padding: $spacing-lg;
box-shadow: $shadow-sm;
backdrop-filter: blur(10rpx);
}
.selector-container {
margin-top: $spacing-md;
}
/* 期号切换 - 与原始设计一致 */
.issue-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 30rpx;
background: $bg-grey;
border-radius: $radius-round;
padding: 10rpx;
border: 1rpx solid $border-color-light;
}
.issue-switch-btn {
width: 72rpx;
height: 72rpx;
display: flex;
align-items: center;
justify-content: center;
background: $bg-card;
border-radius: 50%;
box-shadow: $shadow-sm;
transition: all 0.2s;
color: $text-secondary;
&:active {
transform: scale(0.9);
background: $bg-secondary;
color: $brand-primary;
}
}
.arrow {
font-size: $font-sm;
font-weight: 800;
}
.issue-info-center {
display: flex;
flex-direction: column;
align-items: center;
flex: 1;
}
.issue-current-text {
font-size: $font-lg;
font-weight: 700;
color: $text-main;
}
.issue-status-badge {
font-size: $font-xs;
color: $uni-color-success;
background: rgba($uni-color-success, 0.1);
padding: 2rpx $spacing-md;
border-radius: $radius-round;
margin-top: 4rpx;
font-weight: 600;
}
.issue-block-tip {
background: rgba($color-warning, 0.1);
padding: $spacing-sm $spacing-md;
border-radius: $radius-md;
margin-bottom: $spacing-md;
}
.issue-block-text {
font-size: $font-sm;
color: $color-warning;
font-weight: 500;
}
.selector-body {
margin-top: $spacing-sm;
}
/* Choices 网格 */
.choice-container {
padding: $spacing-md;
}
.loading-state,
.empty-state {
text-align: center;
padding: 40rpx;
color: $text-tertiary;
font-size: $font-md;
}
.choices-grid {
display: grid;
grid-template-columns: repeat(5, 1fr);
gap: 16rpx;
padding: 20rpx 0;
}
.choice-item {
aspect-ratio: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background: $bg-card;
border-radius: $radius-md;
border: 2rpx solid $border-color;
transition: all 0.2s;
position: relative;
&.is-available {
background: linear-gradient(135deg, #e3f2fd, #bbdefb);
border-color: #2196f3;
cursor: pointer;
&:active {
transform: scale(0.95);
}
}
&.is-sold {
background: #f5f5f5;
border-color: #ddd;
opacity: 0.6;
}
&.is-selected {
background: linear-gradient(135deg, #fff3e0, #ffe0b2);
border-color: #ff9800;
box-shadow: 0 0 0 4rpx rgba(255, 152, 0, 0.2);
}
}
.choice-number {
font-size: 32rpx;
font-weight: 700;
color: $text-main;
margin-bottom: 8rpx;
}
.choice-status {
font-size: 20rpx;
color: $text-tertiary;
text {
padding: 4rpx 12rpx;
border-radius: 20rpx;
background: rgba(255, 255, 255, 0.8);
}
}
/* 入场动画 */
.animate-enter {
animation: slideUp 0.5s ease-out both;
}
.stagger-2 {
animation-delay: 0.2s;
}
@keyframes slideUp {
from {
opacity: 0;
transform: translateY(20rpx);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* 翻牌弹窗 */
.flip-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 1001;
display: flex;
align-items: center;
justify-content: center;
}
.flip-mask {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.6);
}
.flip-content {
position: relative;
width: 90%;
max-height: 85vh;
background: $bg-card;
border-radius: $radius-xl;
padding: $spacing-lg;
overflow: hidden;
}
.overlay-close {
margin-top: $spacing-lg;
width: 100%;
background: $gradient-brand;
color: #fff;
border: none;
border-radius: $radius-lg;
font-size: $font-md;
font-weight: 600;
padding: $spacing-md;
&::after {
border: none;
}
}
/* ============= 底部固定操作栏 ============= */
.float-bar {
position: fixed;
left: 32rpx;
right: 32rpx;
bottom: calc(40rpx + env(safe-area-inset-bottom));
z-index: 100;
animation: slideUp 0.4s cubic-bezier(0.23, 1, 0.32, 1) backwards;
}
.float-bar-inner {
background: rgba(255, 255, 255, 0.85);
backdrop-filter: blur(30rpx);
padding: 24rpx 40rpx;
border-radius: 999rpx;
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
box-shadow: 0 16rpx 48rpx rgba(0, 0, 0, 0.12);
border: 1rpx solid rgba(255, 255, 255, 0.6);
}
.selection-info {
font-size: 28rpx;
color: $text-main;
display: flex;
align-items: baseline;
font-weight: 800;
}
.highlight {
color: $brand-primary;
font-weight: 900;
font-size: 40rpx;
margin: 0 8rpx;
font-family: 'DIN Alternate', sans-serif;
}
.action-buttons {
display: flex;
gap: 20rpx;
}
.action-btn {
height: 88rpx;
line-height: 88rpx;
padding: 0 56rpx;
border-radius: 999rpx;
font-size: 30rpx;
font-weight: 900;
margin: 0;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275);
border: none;
&::after {
border: none;
}
&:active {
transform: scale(0.92);
}
&.primary {
background: $gradient-brand !important;
color: #FFFFFF !important;
box-shadow: 0 12rpx 32rpx rgba($brand-primary, 0.35);
}
}
</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>