689 lines
19 KiB
Vue
689 lines
19 KiB
Vue
<template>
|
||
<ActivityPageLayout :cover-url="coverUrl" bottom-padding="220rpx">
|
||
<template #header>
|
||
<ActivityHeader
|
||
:title="detail.name || detail.title || '无限赏'"
|
||
:price="detail.price_draw"
|
||
price-unit="/发"
|
||
:cover-url="coverUrl"
|
||
:tags="['公开透明', '可验证']"
|
||
@show-rules="showRules"
|
||
@go-cabinet="goCabinet"
|
||
/>
|
||
</template>
|
||
|
||
<template #content>
|
||
<ActivityTabs v-model="tabActive" :stagger="1">
|
||
<template #pool>
|
||
<RewardsPreview
|
||
title="奖池配置"
|
||
:rewards="currentIssueRewards"
|
||
:grouped="true"
|
||
@view-all="rewardsVisible = true"
|
||
/>
|
||
</template>
|
||
<template #records>
|
||
<RecordsList :records="winRecords" />
|
||
</template>
|
||
</ActivityTabs>
|
||
</template>
|
||
|
||
<template #footer>
|
||
<!-- 底部多档位抽赏按钮 -->
|
||
<view class="bottom-actions">
|
||
<view class="tier-btn" @tap="openPayment(1)">
|
||
<text class="tier-price">¥{{ (pricePerDraw * 1).toFixed(2) }}</text>
|
||
<text class="tier-label">抽1发</text>
|
||
</view>
|
||
<view class="tier-btn" @tap="openPayment(3)">
|
||
<text class="tier-price">¥{{ (pricePerDraw * 3).toFixed(2) }}</text>
|
||
<text class="tier-label">抽3发</text>
|
||
</view>
|
||
<view class="tier-btn" @tap="openPayment(5)">
|
||
<text class="tier-price">¥{{ (pricePerDraw * 5).toFixed(2) }}</text>
|
||
<text class="tier-label">抽5发</text>
|
||
</view>
|
||
<view class="tier-btn tier-hot" @tap="openPayment(10)">
|
||
<text class="tier-price">¥{{ (pricePerDraw * 10).toFixed(2) }}</text>
|
||
<text class="tier-label">抽10发</text>
|
||
</view>
|
||
</view>
|
||
</template>
|
||
|
||
<template #modals>
|
||
<RewardsPopup
|
||
v-model:visible="rewardsVisible"
|
||
:title="`${currentIssueTitle} · 奖池与概率`"
|
||
:reward-groups="rewardGroups"
|
||
/>
|
||
|
||
<LotteryResultPopup
|
||
v-model:visible="showResultPopup"
|
||
:results="drawResults"
|
||
:show-retry-button="lastDrawUsedGamePass"
|
||
@close="onResultClose"
|
||
@retry="onRetryDraw"
|
||
/>
|
||
|
||
<PaymentPopup
|
||
v-model:visible="paymentVisible"
|
||
:amount="paymentAmount"
|
||
:coupons="coupons"
|
||
:gamePasses="gamePasses"
|
||
:propCards="propCards"
|
||
@confirm="onPaymentConfirm"
|
||
/>
|
||
<RulesPopup
|
||
v-model:visible="rulesVisible"
|
||
:content="detail.gameplay_intro"
|
||
/>
|
||
|
||
<CabinetPreviewPopup
|
||
v-model:visible="cabinetVisible"
|
||
: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>
|
||
|
||
<script setup>
|
||
import { ref, computed, nextTick, watch } from 'vue'
|
||
import { onLoad } from '@dcloudio/uni-app'
|
||
// 公共组件 - 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 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
|
||
import { joinLottery, createWechatOrder, getLotteryResult, getItemCards, getUserCoupons } from '@/api/appUser'
|
||
|
||
// ============ 使用Composables ============
|
||
const activityId = ref('')
|
||
|
||
const {
|
||
detail,
|
||
coverUrl,
|
||
fetchDetail,
|
||
setNavigationTitle
|
||
} = useActivity(activityId)
|
||
|
||
const pricePerDraw = computed(() => Number(detail.value?.price_draw || 0) / 100)
|
||
|
||
const {
|
||
issues,
|
||
currentIssueId,
|
||
currentIssueTitle,
|
||
fetchIssues
|
||
} = 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 showResultPopup = ref(false)
|
||
const drawResults = ref([])
|
||
const drawLoading = ref(false)
|
||
const showDrawLoading = ref(false)
|
||
const drawProgress = ref(0)
|
||
const drawTotal = ref(1)
|
||
const lastDrawUsedGamePass = ref(false) // 记录最后一次抽奖是否使用了次数卡
|
||
const lastDrawCount = ref(1) // 记录最后一次抽奖的数量
|
||
|
||
// 支付相关
|
||
const paymentVisible = ref(false)
|
||
const paymentAmount = ref('0.00')
|
||
const coupons = ref([])
|
||
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() {
|
||
rulesVisible.value = true
|
||
}
|
||
|
||
function goCabinet() {
|
||
cabinetVisible.value = true
|
||
}
|
||
|
||
function onResultClose() {
|
||
showResultPopup.value = false
|
||
drawResults.value = []
|
||
}
|
||
|
||
function onRetryDraw() {
|
||
// 关闭结果弹窗
|
||
showResultPopup.value = false
|
||
drawResults.value = []
|
||
|
||
// 检查是否还有剩余次数卡
|
||
if (gamePassRemaining.value > 0) {
|
||
// 使用次数卡直接抽奖
|
||
useGamePassFlag.value = true
|
||
selectedCoupon.value = null
|
||
selectedCard.value = null
|
||
onMachineDraw(lastDrawCount.value)
|
||
} else {
|
||
// 次数卡已用完,打开支付弹窗
|
||
openPayment(lastDrawCount.value)
|
||
}
|
||
}
|
||
|
||
function openPayment(count) {
|
||
const times = Math.max(1, Number(count || 1))
|
||
pendingCount.value = times
|
||
paymentAmount.value = (pricePerDraw.value * times).toFixed(2)
|
||
const token = uni.getStorageSync('token')
|
||
// 使用统一的手机号绑定检查
|
||
const hasPhoneBound = uni.getStorageSync('login_method') === 'wechat_phone' || uni.getStorageSync('login_method') === 'sms' || uni.getStorageSync('phone_number')
|
||
if (!token || !hasPhoneBound) {
|
||
uni.showModal({
|
||
title: '提示',
|
||
content: '请先登录并绑定手机号',
|
||
confirmText: '去登录',
|
||
success: (res) => { if (res.confirm) uni.navigateTo({ url: '/pages/login/index' }) }
|
||
})
|
||
return
|
||
}
|
||
paymentVisible.value = true
|
||
// 并行获取道具卡和优惠券
|
||
Promise.all([fetchPropCards(), fetchCoupons()])
|
||
}
|
||
|
||
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)
|
||
}
|
||
|
||
async function fetchPropCards() {
|
||
const user_id = uni.getStorageSync('user_id')
|
||
if (!user_id) return
|
||
try {
|
||
const res = await getItemCards(user_id)
|
||
let list = Array.isArray(res) ? res : (res?.list || res?.data || [])
|
||
|
||
// Group identical cards by name
|
||
const groupedMap = new Map()
|
||
list.forEach((i, idx) => {
|
||
const name = i.name ?? i.title ?? '道具卡'
|
||
if (!groupedMap.has(name)) {
|
||
groupedMap.set(name, {
|
||
id: i.id ?? i.card_id ?? String(idx),
|
||
name: name,
|
||
count: 0
|
||
})
|
||
}
|
||
groupedMap.get(name).count++
|
||
})
|
||
|
||
propCards.value = Array.from(groupedMap.values())
|
||
} catch (e) {
|
||
propCards.value = []
|
||
}
|
||
}
|
||
|
||
async function fetchCoupons() {
|
||
const user_id = uni.getStorageSync('user_id')
|
||
if (!user_id) return
|
||
try {
|
||
const res = await getUserCoupons(user_id, 0, 1, 100)
|
||
let list = Array.isArray(res) ? res : (res?.list || res?.data || [])
|
||
coupons.value = list.map((i, idx) => {
|
||
const amountCents = i.remaining ?? i.amount ?? i.value ?? 0
|
||
const amt = isNaN(amountCents) ? 0 : (Number(amountCents) / 100)
|
||
return {
|
||
id: i.id ?? i.coupon_id ?? String(idx),
|
||
name: i.name ?? i.title ?? '优惠券',
|
||
amount: amt.toFixed(2)
|
||
}
|
||
})
|
||
} catch (e) {
|
||
coupons.value = []
|
||
}
|
||
}
|
||
|
||
function extractResultList(resultRes) {
|
||
const root = resultRes?.data ?? resultRes?.result ?? resultRes
|
||
if (!root) return []
|
||
// Backend now returns results array with all draw logs including doubled
|
||
if (resultRes?.results && Array.isArray(resultRes.results) && resultRes.results.length > 0) {
|
||
return resultRes.results
|
||
}
|
||
return root.results || root.list || root.items || root.data || []
|
||
}
|
||
|
||
function mapResultsToFlipItems(resultRes, poolRewards) {
|
||
const list = extractResultList(resultRes)
|
||
const poolArr = Array.isArray(poolRewards) ? poolRewards : []
|
||
const lookup = new Map()
|
||
poolArr.forEach(it => {
|
||
const id = it?.id ?? it?.reward_id ?? it?.product_id
|
||
if (id !== undefined) lookup.set(Number(id), it)
|
||
})
|
||
|
||
return list.filter(Boolean).map(d => {
|
||
const rewardId = d.reward_id ?? d.rewardId ?? d.product_id ?? d.productId ?? d.id
|
||
const rewardName = String(d.reward_name ?? d.rewardName ?? d.product_name ?? d.productName ?? d.title ?? d.name ?? '')
|
||
const fromId = Number.isFinite(Number(rewardId)) ? lookup.get(Number(rewardId)) : null
|
||
const fromName = !fromId && rewardName ? poolArr.find(x => x?.title === rewardName) : null
|
||
const it = fromId || fromName || null
|
||
return {
|
||
reward_id: rewardId, // 添加reward_id用于正确分组
|
||
title: rewardName || it?.title || '奖励',
|
||
image: d.image || it?.image || d.img || d.pic || d.product_image || ''
|
||
}
|
||
})
|
||
}
|
||
|
||
async function onMachineDraw(count) {
|
||
const aid = activityId.value
|
||
const iid = currentIssueId.value
|
||
if (!aid || !iid) {
|
||
uni.showToast({ title: '期数未选择', icon: 'none' })
|
||
return
|
||
}
|
||
|
||
const token = uni.getStorageSync('token')
|
||
// 使用统一的手机号绑定检查
|
||
const hasPhoneBound = uni.getStorageSync('login_method') === 'wechat_phone' || uni.getStorageSync('login_method') === 'sms' || uni.getStorageSync('phone_number')
|
||
if (!token || !hasPhoneBound) {
|
||
uni.showModal({
|
||
title: '提示',
|
||
content: '请先登录并绑定手机号',
|
||
confirmText: '去登录',
|
||
success: (res) => { if (res.confirm) uni.navigateTo({ url: '/pages/login/index' }) }
|
||
})
|
||
return
|
||
}
|
||
|
||
const openid = uni.getStorageSync('openid')
|
||
if (!openid) {
|
||
uni.showToast({ title: '缺少OpenID,请重新登录', icon: 'none' })
|
||
return
|
||
}
|
||
|
||
drawLoading.value = true
|
||
try {
|
||
const times = Math.max(1, Number(count || 1))
|
||
const joinRes = await joinLottery({
|
||
activity_id: Number(aid),
|
||
issue_id: Number(iid),
|
||
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,
|
||
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('未获取到订单号')
|
||
|
||
// Check if order is already paid (e.g. via Game Pass or Points)
|
||
const isPaid = (joinRes?.status === 2) || (joinRes?.actual_amount <= 0)
|
||
|
||
if (!isPaid) {
|
||
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 || 'MD5',
|
||
paySign: payRes.paySign,
|
||
success: resolve,
|
||
fail: reject
|
||
})
|
||
})
|
||
}
|
||
|
||
// 支付成功后立即显示开奖加载弹窗
|
||
drawTotal.value = times
|
||
drawProgress.value = 0
|
||
showDrawLoading.value = true
|
||
|
||
// 轮询等待开奖完成
|
||
let resultRes = await getLotteryResult(orderNo)
|
||
let pollCount = 0
|
||
const maxPolls = 15 // 最多轮询15次,每次2秒,共30秒
|
||
|
||
while (resultRes?.status === 'paid_waiting' &&
|
||
resultRes?.completed < resultRes?.count &&
|
||
pollCount < maxPolls) {
|
||
// 更新进度
|
||
drawProgress.value = resultRes?.completed || 0
|
||
await new Promise(r => setTimeout(r, resultRes?.nextPollMs || 2000))
|
||
resultRes = await getLotteryResult(orderNo)
|
||
pollCount++
|
||
}
|
||
|
||
// 隐藏加载弹窗
|
||
showDrawLoading.value = false
|
||
|
||
const items = mapResultsToFlipItems(resultRes, currentIssueRewards.value)
|
||
|
||
drawResults.value = items
|
||
|
||
// 记录最后一次抽奖是否使用了次数卡,用于"再来一次"按钮
|
||
lastDrawUsedGamePass.value = useGamePassFlag.value
|
||
lastDrawCount.value = times
|
||
|
||
showResultPopup.value = true
|
||
} catch (e) {
|
||
showDrawLoading.value = false
|
||
uni.showToast({ title: e.message || '操作失败', icon: 'none' })
|
||
} finally {
|
||
drawLoading.value = false
|
||
}
|
||
}
|
||
|
||
// ============ 生命周期 ============
|
||
onLoad(async (opts) => {
|
||
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()
|
||
})
|
||
|
||
// 监听期切换,刷新记录
|
||
watch(currentIssueId, (newId) => {
|
||
if (newId && activityId.value) {
|
||
fetchWinRecords(activityId.value, newId)
|
||
}
|
||
})
|
||
</script>
|
||
|
||
<style lang="scss" scoped>
|
||
/* 底部多档位操作按钮 - 原始设计 */
|
||
.bottom-actions {
|
||
position: fixed;
|
||
left: 0;
|
||
right: 0;
|
||
bottom: 0;
|
||
display: flex;
|
||
gap: 20rpx;
|
||
padding: 32rpx 32rpx;
|
||
padding-bottom: calc(32rpx + env(safe-area-inset-bottom));
|
||
background: rgba(255, 255, 255, 0.85);
|
||
backdrop-filter: blur(30rpx);
|
||
box-shadow: 0 -12rpx 40rpx rgba(0, 0, 0, 0.08);
|
||
z-index: 999;
|
||
border-top: 1rpx solid rgba(255, 255, 255, 0.8);
|
||
}
|
||
|
||
.tier-btn {
|
||
flex: 1;
|
||
min-width: 0;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
padding: 24rpx 10rpx;
|
||
background: #FFF;
|
||
border: 2rpx solid rgba($brand-primary, 0.1);
|
||
border-radius: 28rpx;
|
||
box-shadow: 0 4rpx 12rpx rgba(0,0,0,0.03);
|
||
transition: all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1);
|
||
margin: 0;
|
||
line-height: normal;
|
||
|
||
&::after {
|
||
border: none;
|
||
}
|
||
|
||
&:active {
|
||
transform: scale(0.92);
|
||
background: #F9F9F9;
|
||
box-shadow: none;
|
||
}
|
||
}
|
||
|
||
.tier-price {
|
||
font-size: 34rpx;
|
||
font-weight: 900;
|
||
color: $text-main;
|
||
font-family: 'DIN Alternate', sans-serif;
|
||
letter-spacing: -1rpx;
|
||
}
|
||
|
||
.tier-label {
|
||
font-size: 22rpx;
|
||
color: $brand-primary;
|
||
margin-top: 6rpx;
|
||
font-weight: 800;
|
||
font-style: italic;
|
||
}
|
||
|
||
/* 热门/最高档位 - 高级动效 */
|
||
.tier-hot {
|
||
background: $gradient-brand !important;
|
||
border: none !important;
|
||
box-shadow: 0 12rpx 32rpx rgba($brand-primary, 0.35) !important;
|
||
position: relative;
|
||
overflow: hidden;
|
||
|
||
.tier-price {
|
||
color: #FFF !important;
|
||
text-shadow: 0 2rpx 4rpx rgba(0, 0, 0, 0.1);
|
||
}
|
||
|
||
.tier-label {
|
||
color: rgba(255, 255, 255, 0.9) !important;
|
||
text-shadow: 0 1rpx 2rpx rgba(0, 0, 0, 0.1);
|
||
}
|
||
|
||
/* 流光效果 */
|
||
&::before {
|
||
content: '';
|
||
position: absolute;
|
||
top: -50%;
|
||
left: -150%;
|
||
width: 200%;
|
||
height: 200%;
|
||
background: linear-gradient(
|
||
to right,
|
||
transparent,
|
||
rgba(255, 255, 255, 0.25),
|
||
transparent
|
||
);
|
||
transform: rotate(30deg);
|
||
animation: shine 3s ease-in-out infinite;
|
||
}
|
||
|
||
&:active {
|
||
transform: scale(0.92);
|
||
box-shadow: 0 6rpx 16rpx rgba($brand-primary, 0.25) !important;
|
||
}
|
||
}
|
||
|
||
@keyframes shine {
|
||
0% { left: -150%; }
|
||
50%, 100% { left: 150%; }
|
||
}
|
||
|
||
/* 翻牌弹窗 */
|
||
.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);
|
||
backdrop-filter: blur(10rpx);
|
||
}
|
||
|
||
.flip-content {
|
||
position: relative;
|
||
width: 90%;
|
||
max-height: 85vh;
|
||
background: rgba($bg-card, 0.95);
|
||
border-radius: $radius-xl;
|
||
padding: $spacing-lg;
|
||
overflow: hidden;
|
||
box-shadow: $shadow-card;
|
||
}
|
||
|
||
.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;
|
||
}
|
||
}
|
||
|
||
/* 次数卡悬浮入口 */
|
||
.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>
|