2026-05-23 20:44:17 +08:00

3887 lines
123 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>
<div class="fullPage">
<!-- 页面同步预加载首屏关键图片加载完成前展示 loading -->
<div
v-if="isLoading"
style="display: flex; min-height: 100vh; align-items: center; justify-content: center"
>
<LoadingContent>{{ t('loading') }}...</LoadingContent>
</div>
<div
v-else
class="scroll"
:dir="isRtlLayout ? 'rtl' : 'ltr'"
style="min-height: 100vh; position: relative; overflow-x: hidden"
>
<!-- 页面主背景:背景图内包含活动标题和顶部视觉信息 -->
<BackgroundLayer :backgroundImages="[imageUrl('bg')]" />
<main
style="
position: relative;
z-index: 1;
width: min(100vw, 480px);
min-height: 100vh;
margin: 0 auto;
padding: 100vw 0 12vw;
"
>
<!-- 规则入口:默认贴右,阿语 RTL 时通过样式贴左 -->
<section class="rule-row">
<itemCenter
:imgUrl="imageUrl('ruleBtBg')"
style="width: 20vw; flex: 0 0 20vw"
contentStyle="padding: 4vw 0 4vw 3vw;"
@click="openRulePopup"
>
<span
style="
color: #fff8d7;
font-size: 1em;
font-weight: 800;
line-height: 1;
text-align: center;
text-shadow: 0 1px 2px rgba(63, 20, 57, 0.5);
white-space: nowrap;
"
>
{{ t('rule') }}
</span>
</itemCenter>
</section>
<!-- 倒计时:天/时/分/秒分别用 timeBg 承载,时分秒之间显示英文冒号 -->
<section style="direction: ltr; display: flex; justify-content: center; margin-bottom: 4vw">
<div
style="
width: min(95vw, 440px);
display: flex;
align-items: center;
justify-content: space-between;
gap: 1vw;
padding: 0 2vw;
overflow: hidden;
"
>
<itemCenter
class="countdown-cell"
:imgUrl="imageUrl('timeBg')"
contentStyle="padding: 1.8vw 1vw;"
>
<span class="countdown-value">{{ Days }}D</span>
</itemCenter>
<span class="countdown-colon"></span>
<itemCenter
class="countdown-cell"
:imgUrl="imageUrl('timeBg')"
contentStyle="padding: 1.8vw 1vw;"
>
<span class="countdown-value">{{ Hours }}</span>
</itemCenter>
<span class="countdown-colon">:</span>
<itemCenter
class="countdown-cell"
:imgUrl="imageUrl('timeBg')"
contentStyle="padding: 1.8vw 1vw;"
>
<span class="countdown-value">{{ Minutes }}</span>
</itemCenter>
<span class="countdown-colon">:</span>
<itemCenter
class="countdown-cell"
:imgUrl="imageUrl('timeBg')"
contentStyle="padding: 1.8vw 1vw;"
>
<span class="countdown-value">{{ Seconds }}</span>
</itemCenter>
</div>
</section>
<!-- 一级标签CollectCards / Personal Rank / Room Rank -->
<section
style="
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
align-items: stretch;
gap: 2vw;
margin: 0 0 4vw;
"
>
<button
v-for="tab in visibleTopTabs"
:key="tab.value"
type="button"
class="image-tab top-tab"
:class="{ 'image-tab-active': activeTopTab === tab.value }"
@click="changeTopTab(tab.value)"
>
<itemCenter
class="tab-background"
:imgUrl="imageUrl('tagBg')"
contentStyle="padding: 4vw 4vw 0;"
>
<img
v-show="activeTopTab === tab.value"
v-smart-img.noFade
:src="imageUrl('tagActiveBg')"
class="tab-active-bg"
alt=""
/>
<span style="position: relative; z-index: 1; font-weight: 700">{{
t(tab.label)
}}</span>
</itemCenter>
</button>
</section>
<!-- 标签内容区:根据当前标签逐步展示对应业务模块 -->
<section style="min-height: 86vw; padding: 0; margin: 0">
<div v-if="currentTopSection">
<!-- 二级标签:个人榜和房间榜下的 Reward / Ranking -->
<div
v-if="visibleChildTabs.length"
style="
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 3vw;
padding: 0 5vw;
margin-bottom: 4vw;
"
>
<button
v-for="tab in visibleChildTabs"
:key="tab.value"
type="button"
class="image-tab child-tab"
:class="{ 'image-tab-active': activeChildTab === tab.value }"
@click="changeChildTab(tab.value)"
>
<itemCenter
class="tab-background"
:imgUrl="imageUrl('moduleBg')"
contentStyle="padding: 1vw 4vw 0;"
>
<img
v-show="activeChildTab === tab.value"
v-smart-img.noFade
:src="imageUrl('moduleActiveBg')"
class="tab-active-bg"
alt=""
/>
<span style="position: relative; z-index: 1; font-weight: 700">{{
t(tab.label)
}}</span>
</itemCenter>
</button>
</div>
<!-- 当前模块容器背景container 大背景图统一用 itemCenter 自上而下渲染 -->
<div
v-if="activeContainerImages.length"
style="
display: flex;
flex-direction: column;
align-items: center;
gap: 0;
margin: 0;
padding: 0;
"
>
<template v-for="imageName in activeContainerImages" :key="imageName">
<!-- 弹幕模块:中奖飘窗接口返回后横向展示一行 -->
<div
v-if="imageName === 'barrage' && barrageRenderList.length"
style="
width: 100vw;
height: 8vw;
overflow: hidden;
margin: 0 0 4vw 0;
padding: 0;
direction: ltr;
"
>
<div
class="barrage-track"
style="display: flex; align-items: center; gap: 3vw; height: 100%"
>
<itemCenter
v-for="(item, index) in barrageRenderList"
:key="`${item.account || item.nickName || index}-${item.drawTime || index}`"
:imgUrl="imageUrl('barrageBg')"
style="width: 45vw; height: 8vw; flex: 0 0 45vw; margin: 0; padding: 0"
contentStyle="padding: 0 2vw; display: flex; align-items: center; justify-content: flex-start; gap: 1vw; box-sizing: border-box;"
>
<img
v-smart-img.noFade
:src="item.avatar"
alt=""
style="
width: 6vw;
height: 6vw;
flex: 0 0 6vw;
border-radius: 50%;
object-fit: cover;
display: block;
"
@error="handleAvatarImageError"
/>
<div
style="
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
justify-content: center;
gap: 0.5vw;
"
>
<div
style="
width: 100%;
color: #fff;
font-size: 1em;
font-weight: 700;
line-height: 1.1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
"
>
{{ item.nickName || item.account || '' }}
</div>
<div
style="
width: 100%;
color: #fff;
font-size: 1em;
font-weight: 600;
line-height: 1.1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
"
>
{{ t('gulben_barrage_get_a') }} {{ item.prizeName || '' }}
</div>
</div>
</itemCenter>
</div>
</div>
<!-- lotteryBg -->
<itemCenter
v-else-if="imageName === 'lotteryBg'"
:imgUrl="imageUrl(imageName)"
style="width: 100vw; margin: 0; padding: 0"
contentStyle="padding: 8vw 5vw 0; align-items: stretch; justify-content: flex-start; box-sizing: border-box;"
>
<itemCenter
:imgUrl="imageUrl('ruleBtBg')"
style="
position: absolute;
top: 3vw;
right: 0vw;
z-index: 2;
width: 20vw;
margin: 0;
padding: 0;
direction: ltr;
"
contentStyle="padding: 4vw 0 4vw 3vw;"
@click="openHistoryPopup"
>
<span
style="
color: #fff8d7;
font-size: 1em;
font-weight: 800;
line-height: 1;
text-align: center;
text-shadow: 0 1px 2px rgba(63, 20, 57, 0.5);
white-space: nowrap;
"
>
{{ t('history') }}
</span>
</itemCenter>
<!-- 抽奖区域:上层放左右切换按钮和奖品,下层放抽奖按钮 -->
<div
style="width: 100%; height: 100%; display: flex; flex-direction: column; gap: 0"
>
<!-- 抽奖区域上层:左 / 奖品 / 右三列 -->
<div style="display: flex; flex: 1; align-items: center; gap: 4vw">
<div
style="
width: 10vw;
flex: 0 0 10vw;
display: flex;
align-items: center;
justify-content: center;
"
@click="changeLotteryPoolMode(-1)"
>
<img
v-smart-img.noFade
:src="imageUrl('leftBt')"
alt=""
class="pool-arrow pool-arrow-prev"
/>
</div>
<div style="flex: 1; min-width: 0">
<div
:class="['lottery-prize-list', activeLotteryPoolMode.listClass]"
style="
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
gap: var(--lottery-prize-gap);
"
>
<div
v-for="(row, rowIndex) in activeLotteryPrizeRows"
:key="`${activeLotteryPoolMode.name}-${rowIndex}`"
style="
width: 100%;
display: flex;
align-items: center;
justify-content: center;
gap: var(--lottery-prize-gap);
"
>
<itemCenter
v-for="prize in row"
:key="prize.renderKey"
:imgUrl="imageUrl('lotteryPrizeBg')"
:class="[
'lottery-prize-item',
activeLotteryPoolMode.itemClass,
{ 'lottery-prize-item-active': prize.index === activeLotteryIndex },
]"
contentStyle="padding: 1vw; flex-direction: column; gap: 0.5vw; align-items: center; justify-content: center; box-sizing: border-box;"
>
<img
v-show="prize.index === activeLotteryIndex"
v-smart-img.noFade
:src="imageUrl('lotteryPrizeActiveBg')"
class="lottery-prize-active-bg"
alt=""
/>
<img
v-if="prize.prizeImage"
v-smart-img.noFade
:src="prize.prizeImage"
alt=""
style="
position: relative;
z-index: 1;
width: 60%;
display: block;
object-fit: contain;
"
@error="handleRewardImageError"
/>
<div
style="
position: relative;
z-index: 1;
width: 100%;
color: #000;
font-size: 0.7em;
font-weight: 700;
line-height: 1.1;
text-align: center;
overflow-wrap: anywhere;
"
>
{{ getLotteryPrizeDisplayText(prize) }}
</div>
</itemCenter>
</div>
</div>
</div>
<div
style="
width: 10vw;
flex: 0 0 10vw;
display: flex;
align-items: center;
justify-content: center;
"
@click="changeLotteryPoolMode(1)"
>
<img
v-smart-img.noFade
:src="imageUrl('leftBt')"
alt=""
class="pool-arrow pool-arrow-next"
/>
</div>
</div>
<!-- 抽奖区域下层单抽和5连抽按钮内部放当前奖池票券图标 -->
<div
style="
display: flex;
align-items: center;
justify-content: space-around;
gap: 0;
margin: 0;
padding: 0;
"
>
<itemCenter
:imgUrl="imageUrl('draw1Bt')"
:class="{ 'collect-action-disabled': !canLotteryRewardDraw(1) }"
style="width: 38vw; margin: 0; padding: 0"
contentStyle="padding: 0; align-items: flex-end;padding: 0vw"
@click="handleLotteryDraw(1)"
>
<img
v-smart-img.noFade
:src="imageUrl(activeLotteryTicketImage)"
alt=""
style="width: 10vw; display: block"
/>
</itemCenter>
<itemCenter
:imgUrl="imageUrl('draw5BtBg')"
:class="{ 'collect-action-disabled': !canLotteryRewardDraw(5) }"
style="width: 38vw; margin: 0; padding: 0"
contentStyle="padding: 0; align-items: flex-end;padding: 0vw"
@click="handleLotteryDraw(5)"
>
<img
v-smart-img.noFade
:src="imageUrl(activeLotteryTicketImage)"
alt=""
style="width: 10vw; display: block"
/>
</itemCenter>
</div>
</div>
</itemCenter>
<!-- collectGameBg -->
<itemCenter
v-else-if="imageName === 'collectGameBg'"
:imgUrl="imageUrl(imageName)"
style="width: 100vw; margin: 0; padding: 0"
contentStyle="padding: 0; align-items: stretch; justify-content: flex-start; box-sizing: border-box;"
>
<!-- 集卡玩法区域:票券、提示、标题、奖池和操作按钮五层结构 -->
<div
style="
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
justify-content: space-between;
margin: 0;
padding: 0;
"
>
<!-- 第一层:三种票券数量 -->
<div
style="
display: flex;
align-items: center;
justify-content: center;
gap: 2vw;
padding: 15vw 9vw 15vw;
margin: 0;
"
>
<div
v-for="(ticket, index) in collectTicketItems"
:key="ticket.type"
style="
position: relative;
flex: 1;
min-width: 0;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
gap: 0.5vw;
"
>
<div style="width: 3vw; height: 100%">
<img
v-smart-img.noFade
:src="imageUrl('helpBt')"
alt=""
style="width: 100%; display: block"
/>
</div>
<img
v-smart-img.noFade
:src="imageUrl(ticket.image)"
alt=""
style="width: 13vw; display: block"
/>
<span style="color: #fff; font-size: 1.7em; font-weight: 600">
:{{ getTicketRemainingCount(ticket.type) }}
</span>
</div>
</div>
<!-- 第二层:玩法提示文案 -->
<div
style="
color: #d2d2d2;
font-size: 0.7em;
font-weight: 600;
text-align: center;
padding: 0;
margin: 0;
"
>
{{ t('gulben_collect_tip') }}
</div>
<!-- 第三层:当前奖池标题 -->
<div
style="
height: 9vw;
display: flex;
align-items: center;
justify-content: center;
padding: 0;
margin: 0;
"
>
<img
v-smart-img.noFade
:src="imageUrl(activeCollectGameTitle)"
alt=""
style="height: 6.5vw; width: auto; display: block"
/>
</div>
<!-- 第四层:当前奖池卡牌 -->
<div
style="
height: 25vw;
display: flex;
align-items: center;
gap: 0;
padding: 0;
margin: 0;
"
>
<div
style="
width: 10vw;
flex: 0 0 10vw;
display: flex;
align-items: center;
justify-content: center;
"
@click="changeCollectGameMode(-1)"
>
<img
v-smart-img.noFade
:src="imageUrl('leftBt')"
alt=""
class="pool-arrow pool-arrow-prev"
/>
</div>
<div
style="
flex: 1;
min-width: 0;
height: 24vw;
display: flex;
align-items: center;
justify-content: space-around;
gap: 0;
"
>
<template v-for="card in activeCollectCards" :key="card.cardTemplateId">
<img
v-if="card.isCardBack"
v-smart-img.noFade
:src="imageUrl('cardBackBg')"
alt=""
style="height: 100%; width: auto; display: block"
/>
<itemCenter
v-else
:imgUrl="imageUrl(card.imageName)"
style="
height: 100%;
width: auto;
aspect-ratio: 159 / 261;
flex: 0 0 auto;
"
contentStyle="padding: 0;"
>
<div
style="
position: absolute;
top: 0.8vw;
right: 0.8vw;
border-radius: 33px 33px 33px 34px;
background: rgba(0, 0, 0, 0.1);
backdrop-filter: blur(2px);
-webkit-backdrop-filter: blur(2px);
padding: 4px;
color: #fff;
font-size: 1em;
font-weight: 600;
line-height: 1;
"
>
{{ card.quantity }}
</div>
</itemCenter>
</template>
</div>
<div
style="
width: 10vw;
flex: 0 0 10vw;
display: flex;
align-items: center;
justify-content: center;
"
@click="changeCollectGameMode(1)"
>
<img
v-smart-img.noFade
:src="imageUrl('leftBt')"
alt=""
class="pool-arrow pool-arrow-next"
/>
</div>
</div>
<!-- 第五层:当前奖池操作按钮 -->
<div
style="
height: 25vw;
display: flex;
align-items: center;
justify-content: space-around;
gap: 0;
padding: 0;
margin: 0;
"
>
<itemCenter
v-if="activeCollectGameMode.name === 'gameR'"
:imgUrl="imageUrl('GameDrawBtBg')"
style="width: 40vw; margin: 0; padding: 0"
contentStyle="padding: 10vw 6vw 5vw; flex-direction: column; gap: 0.5vw; align-items: center; justify-content: center;"
@click="handleCardDraw()"
>
<div
style="
color: #fff;
font-size: 0.75em;
font-weight: 700;
line-height: 1.1;
text-align: center;
white-space: nowrap;
"
>
{{ t('gulben_remaining_tickets', { count: cardDrawTicketCount }) }}
</div>
<div
style="
color: #fff;
font-size: 0.75em;
font-weight: 700;
line-height: 1.1;
text-align: center;
white-space: nowrap;
"
>
{{ t('gulben_coins_cost', { count: 1000 }) }}
</div>
</itemCenter>
<itemCenter
v-if="activeCollectGameMode.name !== 'gameSSR'"
:imgUrl="imageUrl('ticketHandleBtBg')"
:class="{ 'collect-action-disabled': !canCombineActiveCards }"
style="width: 25vw; margin: 0; padding: 0"
contentStyle="padding: 0; align-items: center; justify-content: center;"
@click="handleCardCombine"
>
<div
style="
color: #fff;
font-size: 1em;
font-weight: 700;
line-height: 1.1;
text-align: center;
white-space: nowrap;
"
>
{{ t('gulben_combine_count', { count: 1 }) }}
</div>
</itemCenter>
<itemCenter
:imgUrl="imageUrl('ticketHandleBtBg')"
:class="{ 'collect-action-disabled': !canRedeemActiveCards }"
style="width: 25vw; margin: 0; padding: 0"
contentStyle="padding: 0; flex-direction: column; align-items: center; justify-content: center;"
@click="handleCardRedeem"
>
<div
style="
color: #fff;
font-size: 1em;
font-weight: 700;
line-height: 1.1;
text-align: center;
white-space: nowrap;
"
>
{{ t('gulben_redeem') }}
</div>
<div
style="
display: flex;
align-items: center;
justify-content: center;
gap: 0.5vw;
color: #fff;
font-size: 0.8em;
font-weight: 700;
line-height: 1;
white-space: nowrap;
"
>
<img
v-smart-img.noFade
:src="imageUrl(activeCollectTicketImage)"
alt=""
style="width: 6vw; display: block"
/>
<span style="color: #fff; font-size: 1em; font-weight: 700"> *1 </span>
</div>
</itemCenter>
</div>
</div>
</itemCenter>
<!-- taskModule -->
<div
v-else-if="imageName === 'taskModule'"
style="
width: 100vw;
display: flex;
flex-direction: column;
gap: 2vw;
margin: 0;
padding: 0 2vw;
box-sizing: border-box;
"
>
<!-- 任务模块:复用 Season5 的任务列表字段,不展示任务类型图标和票券图标 -->
<itemCenter
v-for="task in taskList"
:key="task.taskCode || task.id"
:imgUrl="imageUrl('taskItemBg')"
style="width: 100%; margin: 0; padding: 0"
contentStyle="display: flex; padding: 7vw; justify-content: space-between; align-items: center;"
>
<div
style="flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 1vw"
>
<div
style="color: #fff; font-size: 0.9em; font-weight: 590; line-height: 1.2"
>
{{ showTarget(task) }}
</div>
<div style="font-size: 0.9em; display: flex; align-items: center; gap: 1vw">
<span style="color: #fff; font-size: 1em; font-weight: 590">{{
t('Rewards:')
}}</span>
<img
v-smart-img.noFade
:src="imageUrl('Gameticket')"
:alt="t('ticket')"
width="20vw"
style="display: block"
/>
<span style="color: #fff; font-size: 1em; font-weight: 590"
>*{{ task.rewardValue }}</span
>
</div>
</div>
<div style="width: 17vw; flex: 0 0 17vw">
<!-- 前往任务 -->
<itemCenter
v-if="task.taskStatus == 0"
style=""
:imgUrl="imageUrl('taskBtGo')"
>
<div style="font-size: 1em; font-weight: 600; white-space: nowrap">
{{ t('task_go') }}
</div>
</itemCenter>
<!-- 领取抽奖券 -->
<itemCenter
v-else-if="task.taskStatus === 1"
style=""
:imgUrl="imageUrl('taskBtReceive')"
@click="receiveTaskReward(task.taskCode)"
>
<div style="font-size: 1em; font-weight: 600; white-space: nowrap">
{{ t('task_receive') }}
</div>
</itemCenter>
<!-- 宸插畬鎴?-->
<itemCenter v-else style="" :imgUrl="imageUrl('taskBtReceived')">
<div style="font-size: 0.9em; font-weight: 600; white-space: nowrap">
{{ t('task_received') }}
</div>
</itemCenter>
</div>
</itemCenter>
</div>
<itemCenter
v-else
:imgUrl="imageUrl(imageName)"
style="width: 100vw; margin: 0; padding: 0"
contentStyle="padding: 0;"
/>
</template>
</div>
<div
v-else-if="isPersonalRewardSection"
style="
width: 100vw;
margin: 0;
padding: 0;
box-sizing: border-box;
display: flex;
flex-direction: column;
gap: 2vw;
"
>
<!-- Personal Rank / Reward三套奖励从上往下展示 -->
<itemCenter
v-for="(rewardTop, topIndex) in personalRewardTops"
:key="rewardTop.rule?.id || topIndex"
:imgUrl="imageUrl(`RewardTop${topIndex + 1}`)"
style="width: 100vw; margin: 0; padding: 0"
contentStyle="padding: 25.5vw 9vw 5vw; align-items: flex-start; justify-content: center; box-sizing: border-box;"
>
<div
style="
width: 100%;
display: flex;
align-items: center;
gap: 3.5vw;
direction: ltr;
"
>
<!-- 左侧主奖励:每套奖励的第一个奖励大图展示 -->
<div
v-if="getRewardTopMainReward(rewardTop, topIndex)"
style="
width: 33vw;
flex: 0 0 33vw;
height: 100%;
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: center;
gap: 4vw;
"
>
<div
style="
width: 100%;
height: 68vw;
display: flex;
align-items: center;
justify-content: center;
"
>
<img
v-if="hasRewardCover(getRewardTopMainReward(rewardTop, topIndex))"
:src="getRewardTopMainReward(rewardTop, topIndex).cover || ''"
alt=""
style="width: 92%; display: block; object-fit: contain"
@error="
(event) =>
handleRewardImageError(
event,
getRewardTopMainReward(rewardTop, topIndex).type,
)
"
/>
<div v-else aria-hidden="true" style="width: 92%; aspect-ratio: 1/1"></div>
</div>
<div
style="
color: #fff;
font-size: 1em;
font-weight: 700;
line-height: 1.1;
text-align: center;
"
>
{{ getRewardDisplayText(getRewardTopMainReward(rewardTop, topIndex)) }}
</div>
</div>
<!-- 右侧小奖励:前两行每行两个,第三行单个展示位居中 -->
<div
style="flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 1.7vw"
>
<div
v-for="(row, rowIndex) in getRewardTopGridRows(rewardTop, topIndex)"
:key="`${topIndex}-${rowIndex}`"
style="display: flex; justify-content: center; gap: 2.7vw"
>
<div
v-if="rowIndex === 2 && row.length === 0"
aria-hidden="true"
style="width: calc((100% - 2vw) / 2); min-width: 0; visibility: hidden"
>
<div style="width: 100%; aspect-ratio: 1/1"></div>
<div style="font-size: 0.8em; line-height: 1.1">&nbsp;</div>
</div>
<div
v-for="(reward, rewardIndex) in row"
:key="getRewardRenderKey(reward, rewardIndex)"
style="
width: calc((100% - 2vw) / 2);
min-width: 0;
display: flex;
flex-direction: column;
align-items: center;
gap: 0.5vw;
"
>
<div
style="
width: 100%;
aspect-ratio: 1/1;
display: flex;
align-items: center;
justify-content: center;
"
>
<img
v-if="hasRewardCover(reward)"
:src="reward.cover || ''"
alt=""
style="width: 88%; display: block; object-fit: contain"
@error="(event) => handleRewardImageError(event, reward.type)"
/>
<div
v-else
aria-hidden="true"
style="width: 88%; aspect-ratio: 1/1"
></div>
</div>
<div
style="
width: 100%;
color: #fff;
font-size: 0.8em;
font-weight: 700;
line-height: 1.1;
text-align: center;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
"
>
{{ getRewardDisplayText(reward) }}
</div>
</div>
</div>
</div>
</div>
</itemCenter>
</div>
<div
v-else-if="isRoomRewardSection"
style="width: 100vw; margin: 0; padding: 0; box-sizing: border-box"
>
<!-- Room Rank / Reward规则图作为背景规则内容贴顶部展示 -->
<itemCenter
:imgUrl="imageUrl('ruleActivity')"
style="width: 100vw; margin: 0; padding: 0"
contentStyle="align-items: flex-start; justify-content: flex-start; flex-direction: column; gap: 1.5vw; padding: 28vw 8vw 0; box-sizing: border-box;"
>
<div class="room-rule-text" style="font-weight: 800">
{{ t('gulben_room_rule_title') }}
</div>
<div class="room-rule-text">
{{ t('gulben_room_rule_1') }}
</div>
<div class="room-rule-text">
{{ t('gulben_room_rule_2') }}
</div>
<div class="room-rule-text">
{{ t('gulben_room_rule_3') }}
</div>
</itemCenter>
</div>
<div
v-else-if="isRankingSection"
style="
width: 100vw;
margin: 0;
padding: 0 0 12vw;
box-sizing: border-box;
display: flex;
flex-direction: column;
gap: 4vw;
"
>
<!-- 活动礼物模块:参考 KingAndQueen 每周礼物结构,使用 giftBg 承载 -->
<itemCenter
v-if="isPersonalRankingSection"
:imgUrl="imageUrl('giftBg')"
style="width: 100%; margin: 0; padding: 0"
contentStyle="padding: 0;flex-direction: column;align-items: center;"
>
<div
style="
width: 89vw;
display: flex;
align-items: flex-start;
gap: 4vw;
margin-top: 18vw;
"
>
<div
v-for="(gift, index) in weeklyGifts"
:key="gift.giftId || index"
style="
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
align-items: center;
gap: 2vw;
"
>
<div
style="
width: 100%;
aspect-ratio: 1/1;
display: flex;
justify-content: center;
align-items: center;
"
>
<img
v-if="gift.giftPhoto"
:src="gift.giftPhoto"
alt=""
style="width: 70%; aspect-ratio: 1/1; object-fit: contain; display: block"
/>
<div v-else style="width: 70%; aspect-ratio: 1/1"></div>
</div>
<div
style="
display: flex;
align-items: center;
justify-content: center;
gap: 0.4vw;
"
>
<img
v-smart-img.noFade
:src="coinIcon"
alt=""
style="display: block; width: 1.3em; aspect-ratio: 1/1"
/>
<div
style="
color: #fff;
font-size: 1em;
font-weight: 590;
text-align: center;
white-space: nowrap;
"
>
{{ gift.giftCandy }}
</div>
</div>
</div>
</div>
</itemCenter>
<!-- 排行榜前三名:使用 GulbenFestival 私有 TopUser 组件 -->
<div
style="
display: flex;
align-items: center;
justify-content: center;
gap: 2vw;
margin: 0;
"
>
<div style="flex: 1; min-width: 0">
<TopUser
ranking="2"
:BorderImgUrl="imageUrl('top2')"
:avatarUrl="rankingTop3[1].userAvatar"
:name="rankingTop3[1].userName"
:distributionValue="rankingTop3[1].amount"
:valueIconUrl="coinIcon"
style="width: 100%"
@click="handleRankingClick(rankingTop3[1])"
/>
</div>
<div style="flex: 1.5; min-width: 0">
<TopUser
ranking="1"
:BorderImgUrl="imageUrl('top1')"
:avatarUrl="rankingTop3[0].userAvatar"
:name="rankingTop3[0].userName"
:distributionValue="rankingTop3[0].amount"
:valueIconUrl="coinIcon"
style="width: 100%"
@click="handleRankingClick(rankingTop3[0])"
/>
</div>
<div style="flex: 1; min-width: 0">
<TopUser
ranking="3"
:BorderImgUrl="imageUrl('top3')"
:avatarUrl="rankingTop3[2].userAvatar"
:name="rankingTop3[2].userName"
:distributionValue="rankingTop3[2].amount"
:valueIconUrl="coinIcon"
style="width: 100%"
@click="handleRankingClick(rankingTop3[2])"
/>
</div>
</div>
<!-- 第四名开始的排行榜 -->
<div v-if="rankingListAfterTop3.length" style="position: relative; z-index: 2">
<itemCenter
v-for="(listItem, index) in rankingListAfterTop3"
:key="listItem.userId || index"
:imgUrl="imageUrl('RankingItem')"
:contentStyle="`padding: 0 8vw`"
style="width: 100%; margin: 0 0 5vw; padding: 0"
@click="handleRankingClick(listItem)"
>
<div style="flex: 1; min-width: 0; display: flex; align-items: center; gap: 2vw">
<div style="color: #fff; font-size: 1.5em; font-weight: 700">
{{ listItem.rank }}
</div>
<img
:src="listItem.userAvatar || ''"
alt=""
:class="['ranking-avatar', { 'ranking-avatar-square': isRoomRankingSection }]"
style="display: block; width: 3.5em; aspect-ratio: 1/1; object-fit: cover"
/>
<div style="flex: 1; min-width: 0; display: flex; flex-direction: column">
<div
style="
color: #fff;
font-size: 1em;
font-weight: 860;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
"
>
{{ listItem.userName }}
</div>
<div style="color: #fff; font-size: 1em; font-weight: 590">
{{ t('user_id_prefix') }} {{ listItem.specialAccount || listItem.account }}
</div>
<div
v-if="!isRoomRankingSection"
style="display: flex; align-items: center; gap: 1vw; margin-top: 0.5vw"
>
<img
v-if="getVipLevelImageName(listItem.vipLevelName)"
v-smart-img.noFade
:src="imageUrl(getVipLevelImageName(listItem.vipLevelName))"
alt=""
style="width: 12vw; display: block"
/>
<itemCenter
:imgUrl="imageUrl(getLevelBgImageName('userLevel', listItem.charmLevel))"
style="width: 12vw; margin: 0; padding: 0"
contentStyle="padding: 0 0 0 5vw; align-items: center; justify-content: center;"
>
<div
style="color: #fff; font-size: 1em; font-weight: 700; line-height: 1"
>
{{ listItem.charmLevel || 0 }}
</div>
</itemCenter>
<itemCenter
:imgUrl="
imageUrl(getLevelBgImageName('wealthLevel', listItem.wealthLevel))
"
style="width: 12vw; margin: 0; padding: 0"
contentStyle="padding: 0 0 0 4vw; align-items: center; justify-content: center;"
>
<div
style="color: #fff; font-size: 1em; font-weight: 700; line-height: 1"
>
{{ listItem.wealthLevel || 0 }}
</div>
</itemCenter>
</div>
</div>
</div>
<div
style="
display: flex;
align-items: center;
justify-content: center;
gap: 0.4vw;
color: #fff;
font-size: 1em;
font-weight: 700;
"
>
<img
v-smart-img.noFade
:src="coinIcon"
alt=""
style="display: block; width: 1.3em; aspect-ratio: 1/1"
/>
<span style="color: #fff; font-size: 1em; font-weight: 700">
{{ listItem.amount || 0 }}
</span>
</div>
</itemCenter>
</div>
<div
ref="rankingLoadmore"
style="width: 100%; height: 1px; margin: 0; padding: 0"
></div>
</div>
<!-- 当前模块临时内容:只保留无边框、无额外间距的预览结构 -->
<div
v-else
style="
min-height: 58vw;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 0;
margin: 0;
padding: 0;
"
>
<img
v-for="imageName in activePreviewImages"
:key="imageName"
v-smart-img.noFade
:src="imageUrl(imageName)"
alt=""
class="preview-image"
/>
<div
style="
color: rgba(255, 246, 216, 0.86);
font-size: 1em;
font-weight: 700;
text-align: center;
"
>
{{ activeContentTitle }}
</div>
</div>
</div>
</section>
</main>
<!-- Personal Rank / Ranking 下固定贴底展示我的排名 -->
<itemCenter
v-if="isRankingSection"
style="
position: fixed;
bottom: 0;
left: 0;
right: 0;
z-index: 3;
width: min(100vw, 480px);
margin: 0 auto;
padding: 0;
"
:imgUrl="imageUrl('RankingItem')"
:contentStyle="`padding: 2vw 8vw;`"
>
<div
style="
flex: 1;
min-width: 0;
display: flex;
justify-content: space-around;
align-items: center;
gap: 2vw;
"
>
<div style="color: #fff; font-size: 1em; font-weight: 700">
{{ myRanking.rank || 999 }}
</div>
<img
:src="myRanking.userAvatar || ''"
alt=""
:class="['ranking-avatar', { 'ranking-avatar-square': isRoomRankingSection }]"
style="display: block; width: 4em; aspect-ratio: 1/1; object-fit: cover"
/>
<div style="flex: 1; min-width: 0">
<div
style="
color: #fff;
font-size: 1em;
font-weight: 860;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
"
>
{{ myRanking.userName }}
</div>
<div
v-if="isRoomRankingSection"
style="
color: #fff;
font-size: 1em;
font-weight: 590;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
"
>
{{ t('user_id_prefix') }} {{ myRanking.specialAccount || myRanking.account }}
</div>
<div v-else style="display: flex; align-items: center; gap: 1vw; margin-top: 0.5vw">
<img
v-if="getVipLevelImageName(myRanking.vipLevelName)"
v-smart-img.noFade
:src="imageUrl(getVipLevelImageName(myRanking.vipLevelName))"
alt=""
style="width: 12vw; display: block"
/>
<itemCenter
:imgUrl="imageUrl(getLevelBgImageName('userLevel', myRanking.charmLevel))"
style="width: 12vw; margin: 0; padding: 0"
contentStyle="padding: 0 0 0 5vw; align-items: center; justify-content: center;"
>
<div style="color: #fff; font-size: 1em; font-weight: 700; line-height: 1">
{{ myRanking.charmLevel || 0 }}
</div>
</itemCenter>
<itemCenter
:imgUrl="imageUrl(getLevelBgImageName('wealthLevel', myRanking.wealthLevel))"
style="width: 12vw; margin: 0; padding: 0"
contentStyle="padding: 0 0 0 4vw; align-items: center; justify-content: center;"
>
<div style="color: #fff; font-size: 1em; font-weight: 700; line-height: 1">
{{ myRanking.wealthLevel || 0 }}
</div>
</itemCenter>
</div>
</div>
</div>
<div
style="
display: flex;
align-items: center;
justify-content: center;
gap: 0.4vw;
color: #fff;
font-size: 1em;
font-weight: 700;
"
>
<img
v-smart-img.noFade
:src="coinIcon"
alt=""
style="display: block; width: 1.3em; aspect-ratio: 1/1"
/>
<span style="color: #fff; font-size: 1em; font-weight: 700">
{{ myRanking.amount || 0 }}
</span>
</div>
</itemCenter>
<!-- 弹窗:点击遮罩关闭,规则、中奖历史和抽卡结果共用 MaskLayer -->
<MaskLayer :maskLayerShow="maskLayerShow">
<div
style="
min-height: 100%;
display: flex;
align-items: center;
justify-content: center;
padding: 8vw 0;
box-sizing: border-box;
"
@click="closePopup"
@touchend.self.prevent="closePopup"
>
<!-- 规则弹窗内容使用 ruleInfoBg 背景承载 -->
<div
v-if="ruleShow"
style="position: relative; width: min(92vw, 440px)"
@click.stop
@touchend.stop
>
<itemCenter
:imgUrl="imageUrl('ruleInfoBg')"
style="width: 100%; margin: 0; padding: 0"
contentStyle="padding: 27vw 7vw 12vw; align-items: stretch; justify-content: flex-start; box-sizing: border-box;"
>
<div
class="rule-popup-scroll"
style="
max-height: 62vh;
overflow-y: auto;
color: #fff;
font-size: 1.5em;
font-weight: 600;
line-height: 1.35;
text-align: left;
"
>
<div>{{ t('gulben_rule_title_earn') }}</div>
<div>{{ t('gulben_rule_earn_1') }}</div>
<div style="margin-top: 2vw">{{ t('gulben_rule_title_use') }}</div>
<div>{{ t('gulben_rule_use_1') }}</div>
<div style="margin-top: 2vw">{{ t('gulben_rule_title_combine') }}</div>
<div>{{ t('gulben_rule_combine_1') }}</div>
<div>{{ t('gulben_rule_combine_2') }}</div>
<div style="margin-top: 2vw">{{ t('gulben_rule_title_redeem') }}</div>
<div>{{ t('gulben_rule_redeem_1') }}</div>
<div>{{ t('gulben_rule_redeem_2') }}</div>
<div>{{ t('gulben_rule_redeem_3') }}</div>
</div>
</itemCenter>
</div>
<!-- 中奖历史弹窗内容使用 historyBg 背景承载底部监听点触发分页加载 -->
<div
v-if="historyShow"
style="position: relative; width: min(95vw, 456px); direction: ltr"
@click.stop
@touchend.stop
>
<itemCenter
:imgUrl="imageUrl('historyBg')"
style="width: 100%; min-height: 97.53vw; margin: 0; padding: 0"
contentStyle="padding: 24vw 6vw 11vw; align-items: stretch; justify-content: flex-start; box-sizing: border-box;"
>
<div
class="history-popup-scroll"
style="
width: 100%;
max-height: 62vh;
overflow-y: auto;
display: flex;
flex-direction: column;
gap: 8px;
"
>
<div
v-for="(record, index) in historyRecords"
:key="record.recordNo || record.id || index"
style="
border-radius: 8px;
border: 1px solid rgba(64, 64, 64, 0.5);
background: rgba(240, 240, 240, 0.3);
box-shadow:
-42px 103px 31px 0 rgba(145, 145, 145, 0),
-27px 66px 29px 0 rgba(145, 145, 145, 0.01),
-15px 37px 24px 0 rgba(145, 145, 145, 0.03),
-7px 17px 18px 0 rgba(145, 145, 145, 0.04),
-2px 4px 10px 0 rgba(145, 145, 145, 0.05);
backdrop-filter: blur(7.5px);
display: flex;
padding: 12px;
justify-content: space-between;
align-items: center;
gap: 2vw;
"
>
<div style="display: flex; align-items: center; gap: 4px; min-width: 0">
<img
v-if="getHistoryRecordImage(record)"
v-smart-img.noFade
:src="getHistoryRecordImage(record)"
alt=""
style="
width: 10vw;
aspect-ratio: 1/1;
flex: 0 0 10vw;
object-fit: contain;
display: block;
"
@error="handleRewardImageError"
/>
<div v-else style="width: 10vw; aspect-ratio: 1/1; flex: 0 0 10vw"></div>
<div
style="
min-width: 0;
color: #fff;
font-size: 0.9em;
font-weight: 590;
line-height: 1.2;
text-align: left;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
"
>
{{ getHistoryRecordPrizeText(record) }}
</div>
</div>
<div
style="
color: #fff;
font-size: 0.8em;
font-weight: 510;
line-height: 1.2;
text-align: right;
white-space: nowrap;
"
>
{{ formatHistoryRecordTime(record.drawTime) }}
</div>
</div>
<div
ref="historyLoadmore"
style="width: 100%; height: 1px; flex: 0 0 1px; margin: 0; padding: 0"
></div>
</div>
</itemCenter>
</div>
<!-- 抽卡结果弹窗展示本次抽中的卡片下方保留抽卡按钮用于连续抽卡 -->
<div
v-if="cardDrawResultShow"
style="position: relative; width: min(92vw, 440px); direction: ltr"
@click.stop
@touchend.stop
>
<itemCenter
:imgUrl="imageUrl('resultBg')"
style="width: 100%; min-height: 90vw; margin: 0; padding: 0"
contentStyle="padding: 22vw 8vw 8vw; flex-direction: column; align-items: center; justify-content: space-between; box-sizing: border-box;"
>
<div
style="
width: 100%;
display: flex;
align-items: center;
justify-content: space-between;
gap: 2vw;
"
>
<div
style="
width: 8vw;
min-width: 8vw;
display: flex;
align-items: center;
justify-content: center;
"
>
<img
v-if="showRewardResultSwitcher"
v-smart-img.noFade
:src="imageUrl('leftBt')"
alt=""
class="result-popup-arrow result-popup-arrow-prev"
@click="changeCardDrawResultIndex(-1)"
@touchend.stop.prevent="changeCardDrawResultIndex(-1)"
/>
</div>
<img
v-if="currentCardDrawResultImageUrl"
v-smart-img.noFade
:src="currentCardDrawResultImageUrl"
alt=""
:style="{
width: cardResultActionMode === 'rewardDraw' ? '45vw' : '28vw',
display: 'block',
objectFit: 'contain',
}"
@error="handleRewardImageError"
/>
<div
v-else
:style="{
width: cardResultActionMode === 'rewardDraw' ? '45vw' : '28vw',
aspectRatio: '1/1',
flex: '0 0 auto',
}"
></div>
<div
style="
width: 8vw;
min-width: 8vw;
display: flex;
align-items: center;
justify-content: center;
"
>
<img
v-if="showRewardResultSwitcher"
v-smart-img.noFade
:src="imageUrl('leftBt')"
alt=""
class="result-popup-arrow result-popup-arrow-next"
@click="changeCardDrawResultIndex(1)"
@touchend.stop.prevent="changeCardDrawResultIndex(1)"
/>
</div>
</div>
<div
v-if="cardResultActionMode === 'rewardDraw'"
style="
position: relative;
width: 100%;
min-height: 1.2em;
color: #fff;
font-size: 1em;
font-weight: 700;
line-height: 1.2;
text-align: center;
"
>
<span style="color: #fff; font-size: 1em; font-weight: 700">
{{ currentCardDrawResultText }}
</span>
<span
v-if="showRewardResultSwitcher"
style="
position: absolute;
top: 50%;
right: 0;
transform: translateY(-50%);
color: #fff;
font-size: 1em;
font-weight: 700;
line-height: 1;
white-space: nowrap;
"
>
{{ cardDrawResultIndex + 1 }}/{{ cardDrawResultList.length }}
</span>
</div>
<itemCenter
v-if="cardResultActionMode === 'draw'"
:imgUrl="imageUrl('GameDrawBtBg')"
style="width: 40vw; margin: 0; padding: 0"
contentStyle="padding: 10vw 6vw 5vw; flex-direction: column; gap: 0.5vw; align-items: center; justify-content: center;"
@click="handleCardDraw()"
@touchend.stop.prevent="handleCardDraw()"
>
<div
style="
color: #fff;
font-size: 0.75em;
font-weight: 700;
line-height: 1.1;
text-align: center;
white-space: nowrap;
"
>
{{ t('gulben_remaining_tickets', { count: cardDrawTicketCount }) }}
</div>
<div
style="
color: #fff;
font-size: 0.75em;
font-weight: 700;
line-height: 1.1;
text-align: center;
white-space: nowrap;
"
>
{{ t('gulben_coins_cost', { count: 1000 }) }}
</div>
</itemCenter>
<div
v-else-if="cardResultActionMode === 'combine'"
style="
width: 100%;
display: flex;
align-items: center;
justify-content: center;
gap: 4vw;
"
>
<itemCenter
:imgUrl="imageUrl('ticketHandleBtBg')"
style="width: 25vw; margin: 0; padding: 0"
contentStyle="padding: 0; align-items: center; justify-content: center;"
@click="closePopup"
@touchend.stop.prevent="closePopup"
>
<div
style="
color: #fff;
font-size: 1em;
font-weight: 700;
line-height: 1.1;
text-align: center;
white-space: nowrap;
"
>
{{ t('gulben_accept') }}
</div>
</itemCenter>
<itemCenter
:imgUrl="imageUrl('ticketHandleBtBg')"
:class="{ 'collect-action-disabled': !canCombineActiveCards }"
style="width: 25vw; margin: 0; padding: 0"
contentStyle="padding: 0; align-items: center; justify-content: center;"
@click="handleCardCombine"
@touchend.stop.prevent="handleCardCombine"
>
<div
style="
color: #fff;
font-size: 1em;
font-weight: 700;
line-height: 1.1;
text-align: center;
white-space: nowrap;
"
>
{{ t('gulben_combine_count', { count: 1 }) }}
</div>
</itemCenter>
</div>
<itemCenter
v-else
:imgUrl="imageUrl('ticketHandleBtBg')"
style="width: 30vw; margin: 0; padding: 0"
contentStyle="padding: 0; align-items: center; justify-content: center;"
@click="closePopup"
@touchend.stop.prevent="closePopup"
>
<div
style="
color: #fff;
font-size: 1em;
font-weight: 700;
line-height: 1.1;
text-align: center;
white-space: nowrap;
"
>
{{ t('confirm') }}
</div>
</itemCenter>
</itemCenter>
</div>
</div>
</MaskLayer>
<!-- 抽卡动效层接口成功后全屏展示 1.5 再打开中奖弹窗 -->
<div
v-if="cardDrawAnimationShow"
style="
position: fixed;
inset: 0;
z-index: 9999;
width: 100vw;
height: 100vh;
display: flex;
align-items: center;
justify-content: center;
pointer-events: auto;
"
@click.stop
>
<img
v-smart-img.noFade
:src="cardDrawAnimationUrl"
alt=""
style="width: 100vw; height: 100vh; display: block; object-fit: contain"
/>
</div>
</div>
</div>
</template>
<script setup>
import { computed, nextTick, onMounted, onUnmounted, ref, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import { setDocumentDirection } from '@/locales/i18n'
import { useLangStore } from '@/stores/lang'
import { getWebpUrl } from '@/config/imagePaths.js'
import { preloadImages, preloadImagesIdle } from '@/utils/image/imagePreloader.js'
import { showError } from '@/utils/toast.js'
import { viewUserInfo } from '@/utils/appBridge.js'
import { connectToApp } from '@/utils/appConnector.js'
import { handleAvatarImageError, handleRewardImageError } from '@/utils/image/imageHandler.js'
import { formatRewardDisplay } from '@/utils/rewardFormatter.js'
import { formatUTCCustom } from '@/utils/utcFormat.js'
import { useDebounce } from '@/utils/useDebounce.js'
import {
apiGetCardRewardMarquee,
apiGetCardRewardPrizeList,
apiGetCardTicketList,
apiPostCardBagList,
apiPostCardCombine,
apiPostCardDraw,
apiPostCardRedeem,
apiPostCardRewardDraw,
apiPostCardRewardRecordList,
getRankingListAndMyRanking,
getThisWeekRewardsAndGifts,
} from '@/api/activity.js'
import { ActTaskList, activityDetail, receiveTickets } from '@/api/lottery'
import { getRoomContributionTotalRank } from '@/api/ranking.js'
import BackgroundLayer from '@/components/BackgroundLayer.vue'
import itemCenter from '@/components/itemCenter.vue'
import MaskLayer from '@/components/MaskLayer.vue'
import TopUser from './components/topUser.vue'
import { pageConfig } from './page.config.js'
const { t, locale } = useI18n()
const langStore = useLangStore()
const coinIcon = new URL('../../../assets/icon/Azizi/coin.png', import.meta.url).href
const gulbenActivityId = '1001'
const templateId = ref(pageConfig.activity.templateId || '2057676469390872578')
const activityType = ref(pageConfig.activity.activityType || '')
const roomRankingParams = {
startDate: '20260523',
endDate: '20260606',
size: 20,
}
const isLoading = ref(true)
const activeTopTab = ref(pageConfig.modules.tabs.defaultValue)
const activeChildTab = ref('')
const activeLotteryIndex = ref(0)
const isLotteryRolling = ref(false)
const isCardDrawing = ref(false)
const isCardCombining = ref(false)
const isCardRedeeming = ref(false)
const activeLotteryPoolIndex = ref(0)
const activeCollectGameIndex = ref(0)
const ruleShow = ref(false)
const historyShow = ref(false)
const cardDrawResultShow = ref(false)
const cardDrawResultImageUrl = ref('')
const cardDrawResultList = ref([])
const cardDrawResultIndex = ref(0)
const cardResultActionMode = ref('draw')
const cardDrawAnimationShow = ref(false)
const cardDrawAnimationUrl = ref('')
const activityCode = ref(pageConfig.activity.activityCode || '')
const activity = ref({})
const Days = ref(0)
const Hours = ref('00')
const Minutes = ref('00')
const Seconds = ref('00')
const weeklyGifts = ref([
{
giftPhoto: '',
giftCandy: 0,
},
{
giftPhoto: '',
giftCandy: 0,
},
{
giftPhoto: '',
giftCandy: 0,
},
])
const ranking = ref([])
const myRanking = ref({})
const rankingLoaded = ref(false)
const loadedRankingType = ref('')
const rankingLoading = ref(false)
const rankingHasMore = ref(true)
const rankingLoadmore = ref(null)
const historyRecords = ref([])
const historyPage = ref(1)
const historySize = 10
const historyLoading = ref(false)
const historyHasMore = ref(true)
const historyLoadmore = ref(null)
const personalRewardLoaded = ref(false)
const kingQueenEffectiveData = ref(null)
const activityRewards = ref([])
const cardTicketList = ref([])
const cardRewardPrizePools = ref({})
const cardRewardMarqueeList = ref([])
const cardBagPools = ref({})
const taskList = ref([])
let countdownTimer = null
let lotteryAnimationTimer = null
let rankingObserver = null
let historyObserver = null
let cardDrawAnimationTimer = null
let kingQueenEffectiveRequest = null
const createLotteryPrizes = (poolName, count) =>
Array.from({ length: count }, (_, index) => ({
id: `${poolName}-${index}`,
index,
renderKey: `${poolName}-${index}`,
}))
const lotteryPoolModes = [
{
name: 'lotteryR',
poolType: 'STANDARD',
ticketImage: 'ticketR',
rowCounts: [2, 3, 2],
listClass: '',
itemClass: '',
prizes: createLotteryPrizes('lotteryR', 7),
},
{
name: 'lotterySR',
poolType: 'PREMIUM',
ticketImage: 'ticketSR',
rowCounts: [3],
listClass: 'lottery-prize-list-large',
itemClass: 'lottery-prize-item-sr',
prizes: createLotteryPrizes('lotterySR', 3),
},
{
name: 'lotterySSR',
poolType: 'LUXURY',
ticketImage: 'ticketSSR',
rowCounts: [2],
listClass: 'lottery-prize-list-large',
itemClass: 'lottery-prize-item-ssr',
prizes: createLotteryPrizes('lotterySSR', 2),
},
]
const collectTicketItems = [
{
type: 'STANDARD',
image: 'ticketR',
},
{
type: 'PREMIUM',
image: 'ticketSR',
},
{
type: 'LUXURY',
image: 'ticketSSR',
},
]
const collectGameModes = [
{
name: 'gameR',
cardGrade: 'R',
poolType: 'STANDARD',
title: 'gameTitleR',
ticketImage: 'ticketR',
cardTemplateIds: [2001, 2002, 2003, 2004, 2005],
},
{
name: 'gameSR',
cardGrade: 'SR',
poolType: 'PREMIUM',
title: 'gameTitleSR',
ticketImage: 'ticketSR',
cardTemplateIds: [2011, 2012, 2013],
},
{
name: 'gameSSR',
cardGrade: 'SSR',
poolType: 'LUXURY',
title: 'gameTitleSSR',
ticketImage: 'ticketSSR',
cardTemplateIds: [2021, 2022],
},
]
// 卡包模板图片映射:接口返回对应 cardTemplateId 后,替换默认 cardBackBg。
const cardTemplateImageMap = {
2001: 'redeemR2001',
2002: 'redeemR2002',
2003: 'redeemR2003',
2004: 'redeemR2004',
2005: 'redeemR2005',
2011: 'redeemSR2011',
2012: 'redeemSR2012',
2013: 'redeemSR2013',
2021: 'redeemSSR2021',
2022: 'redeemSSR2022',
}
const cardTemplateAnimationMap = Object.fromEntries(
Object.entries(cardTemplateImageMap).map(([cardTemplateId, imageName]) => [
Number(cardTemplateId),
imageName.replace('redeem', 'anima'),
]),
)
// 多语言方向:当前只根据语言类型判断是否需要 RTL 布局。
const currentLangType = computed(() => {
return langStore.selectedLang?.type || 'en'
})
const isRtlLayout = computed(() => currentLangType.value === 'ar')
const imageUrl = (filename) => getWebpUrl(pageConfig.assetPath, filename)
const webpUrl = (filename) => getWebpUrl(pageConfig.assetPath, filename)
// 多语言图片:只在 page.config.js 声明已有阿语图时切换 `_ar`,没有就继续使用原图。
const getImgName = (filename) => {
if (currentLangType.value !== 'ar') {
return filename
}
const localizedImages = pageConfig.images?.localized || {}
const arImageList = localizedImages.ar
const imageConfig = localizedImages[filename]
if (Array.isArray(arImageList) && arImageList.includes(filename)) {
return `${filename}_ar`
}
if (typeof imageConfig === 'string') {
return imageConfig
}
if (Array.isArray(imageConfig) && imageConfig.includes('ar')) {
return `${filename}_ar`
}
if (imageConfig?.ar) {
return imageConfig.ar === true ? `${filename}_ar` : imageConfig.ar
}
return filename
}
// 一级标签配置归一化:字符串写法和对象写法都转成统一结构。
const normalizeTab = (item, index) => {
if (typeof item === 'string') {
return {
value: item,
label: item,
section: pageConfig.modules.sections[index],
}
}
return {
value: item.value,
label: item.label || item.labelKey || item.value,
section: pageConfig.modules.sections[index],
}
}
// 一级标签:只展示已启用的 section。
const visibleTopTabs = computed(() => {
if (!pageConfig.modules.tabs.enabled) {
return []
}
return pageConfig.modules.tabs.items
.map(normalizeTab)
.filter((tab) => tab.section?.enabled !== false)
})
const currentTopTab = computed(() => {
return (
visibleTopTabs.value.find((tab) => tab.value === activeTopTab.value) || visibleTopTabs.value[0]
)
})
// 当前一级标签和对应 section。
const currentTopSection = computed(() => {
return currentTopTab.value?.section
})
// 二级标签:读取当前一级 section.children.tabs / children.sections。
const visibleChildTabs = computed(() => {
const children = currentTopSection.value?.children
if (!children?.tabs?.enabled) {
return []
}
return children.tabs.items
.map((item, index) => {
const section = children.sections?.[index]
const value = typeof item === 'string' ? item : item.value
return {
value,
label: typeof item === 'string' ? item : item.label || item.labelKey || value,
section,
}
})
.filter((tab) => tab.section?.enabled !== false)
})
const currentChildTab = computed(() => {
return (
visibleChildTabs.value.find((tab) => tab.value === activeChildTab.value) ||
visibleChildTabs.value[0]
)
})
// 当前真正生效的内容区:优先取二级标签,没有二级标签时取一级 section。
const activeSection = computed(() => {
return currentChildTab.value?.section || currentTopSection.value
})
const isPersonalRankingSection = computed(() => {
return (
currentTopSection.value?.name === 'personalRank' && currentChildTab.value?.value === 'Ranking'
)
})
const isRoomRewardSection = computed(() => {
return currentTopSection.value?.name === 'roomRank' && currentChildTab.value?.value === 'Reward'
})
const isRoomRankingSection = computed(() => {
return currentTopSection.value?.name === 'roomRank' && currentChildTab.value?.value === 'Ranking'
})
const isRankingSection = computed(() => {
return isPersonalRankingSection.value || isRoomRankingSection.value
})
const activePreviewImages = computed(() => {
return activeSection.value?.previewImages || []
})
// 模块大背景容器md 中 images.container 或 page.config 中 containerImages 都会按顺序渲染。
const activeContainerImages = computed(() => {
return activeSection.value?.containerImages || activeSection.value?.images?.container || []
})
const barrageRenderList = computed(() => {
const marqueeList = Array.isArray(cardRewardMarqueeList.value) ? cardRewardMarqueeList.value : []
if (!marqueeList.length) {
return []
}
return marqueeList.length === 1
? [...marqueeList, ...marqueeList, ...marqueeList, ...marqueeList]
: [...marqueeList, ...marqueeList]
})
const activeLotteryPoolMode = computed(() => {
return lotteryPoolModes[activeLotteryPoolIndex.value] || lotteryPoolModes[0]
})
const activeLotteryPrizes = computed(() => {
const poolPrizes = cardRewardPrizePools.value[activeLotteryPoolMode.value.poolType]
if (!Array.isArray(poolPrizes) || !poolPrizes.length) {
return activeLotteryPoolMode.value.prizes || []
}
return [...poolPrizes]
.sort((a, b) => Number(a.sortOrder || 0) - Number(b.sortOrder || 0))
.map((prize, index) => ({
...prize,
index,
renderKey: `${prize.poolType || activeLotteryPoolMode.value.poolType}-${prize.id}-${index}`,
}))
})
const activeLotteryTicketImage = computed(
() => activeLotteryPoolMode.value.ticketImage || 'ticketR',
)
const activeLotteryTicketCount = computed(() =>
getTicketRemainingCount(activeLotteryPoolMode.value.poolType),
)
const canLotteryRewardDraw = (drawCount) => {
return activeLotteryTicketCount.value >= drawCount
}
const activeLotteryPrizeRows = computed(() => {
const prizes = activeLotteryPrizes.value
const rows = []
let cursor = 0
activeLotteryPoolMode.value.rowCounts.forEach((count) => {
const row = prizes.slice(cursor, cursor + count)
if (row.length) {
rows.push(row)
}
cursor += count
})
if (cursor < prizes.length) {
rows.push(prizes.slice(cursor))
}
return rows
})
const activeCollectGameMode = computed(() => {
return collectGameModes[activeCollectGameIndex.value] || collectGameModes[0]
})
const activeCollectGameTitle = computed(() => activeCollectGameMode.value.title)
const activeCollectTicketImage = computed(
() => activeCollectGameMode.value.ticketImage || 'ticketR',
)
const activeCollectCards = computed(() => {
const cardGrade = activeCollectGameMode.value.cardGrade
const cards = cardBagPools.value[cardGrade] || []
const cardMap = new Map(cards.map((card) => [Number(card.cardTemplateId), card]))
return activeCollectGameMode.value.cardTemplateIds.map((cardTemplateId) => {
const card = cardMap.get(cardTemplateId)
return {
...card,
cardTemplateId,
imageName: card ? cardTemplateImageMap[cardTemplateId] || 'cardBackBg' : 'cardBackBg',
isCardBack: !card || !cardTemplateImageMap[cardTemplateId],
quantity: Number(card?.quantity || 0),
}
})
})
const activeOwnedCollectCardTemplateIds = computed(() => {
return activeCollectCards.value
.filter((card) => !card.isCardBack && card.quantity > 0)
.map((card) => String(card.cardTemplateId))
})
const activeCollectRequiredCardCount = computed(() => {
return activeCollectGameMode.value.cardTemplateIds.length
})
const canCombineActiveCards = computed(() => {
return (
activeCollectGameMode.value.name !== 'gameSSR' &&
activeOwnedCollectCardTemplateIds.value.length >= activeCollectRequiredCardCount.value
)
})
const canRedeemActiveCards = computed(() => {
return activeOwnedCollectCardTemplateIds.value.length >= activeCollectRequiredCardCount.value
})
const cardDrawTicketCount = computed(() => getTicketRemainingCount('CARD'))
const maskLayerShow = computed(
() => ruleShow.value || historyShow.value || cardDrawResultShow.value,
)
const activeRankingLoadKey = computed(() => {
if (isRoomRankingSection.value) {
return `room-${roomRankingParams.startDate}-${roomRankingParams.endDate}-${roomRankingParams.size}`
}
return `personal-${activityType.value}`
})
const rankingTotal = computed(() => ranking.value)
const rankingTop3 = computed(() => {
const top3 = [...rankingTotal.value]
if (top3.length < 3) {
top3.push(
...Array.from({ length: 3 - top3.length }, () => ({
userAvatar: '',
userName: '',
amount: '',
})),
)
}
return top3
})
const rankingListAfterTop3 = computed(() => rankingTotal.value.filter((_, index) => index >= 3))
const isPersonalRewardSection = computed(() => {
return (
currentTopSection.value?.name === 'personalRank' && currentChildTab.value?.value === 'Reward'
)
})
const personalRewardTops = computed(() => {
return activityRewards.value.slice(0, 3)
})
const changeCollectGameMode = (step) => {
if (
isCardDrawing.value ||
isCardCombining.value ||
isCardRedeeming.value ||
cardDrawAnimationShow.value
) {
return
}
activeCollectGameIndex.value =
(activeCollectGameIndex.value + step + collectGameModes.length) % collectGameModes.length
}
const changeLotteryPoolMode = (step) => {
if (isLotteryRolling.value) {
return
}
activeLotteryPoolIndex.value =
(activeLotteryPoolIndex.value + step + lotteryPoolModes.length) % lotteryPoolModes.length
activeLotteryIndex.value = 0
}
// 任务目标展示:接口任务码带 CARD_ 前缀时,按后缀匹配到对应翻译。
const showTarget = (task) => {
const taskCode = task.taskCode || ''
if (task.targetType === 'MIC_TIME') {
const timeType = task.targetValue < 60
let currentValue = timeType ? task.currentValue : task.currentValue / 60
let targetValue = timeType ? task.targetValue : task.targetValue / 60
currentValue = Math.floor(currentValue * 10) / 10
targetValue = Math.floor(targetValue * 10) / 10
const timeUnit = timeType ? t('minutes_short') : t('hours_short')
return t('on_mic_progress', {
current: currentValue,
total: targetValue,
unit: timeUnit,
})
}
if (task.targetType === 'SEND_GIFT') {
if (taskCode.endsWith('SP_SEND_GIFT_5')) {
return t('Send 5 Ramadan gifts ({currentValue}/{targetValue})', {
currentValue: task.currentValue,
targetValue: task.targetValue,
})
}
}
if (task.targetType === 'SAVE_COINS') {
if (taskCode.endsWith('SP_SAVE_50000')) {
return t('Win 50,000 coins in total across any games ({currentValue}/{targetValue})', {
currentValue: task.currentValue,
targetValue: task.targetValue,
})
}
if (taskCode.endsWith('SP_SAVE_100000')) {
return t('Win 100,000 coins in total across any games ({currentValue}/{targetValue})', {
currentValue: task.currentValue,
targetValue: task.targetValue,
})
}
if (taskCode.endsWith('SP_SAVE_500000')) {
return t('Win 500,000 coins in total across any games ({currentValue}/{targetValue})', {
currentValue: task.currentValue,
targetValue: task.targetValue,
})
}
}
if (task.targetType === 'RECHARGE_AMOUNT') {
if (taskCode.endsWith('SP_RECHARGE_1')) {
return t('Daily total recharge amount: $1 ({currentValue}/{targetValue})', {
currentValue: task.currentValue,
targetValue: task.targetValue,
})
}
if (taskCode.endsWith('SP_RECHARGE_10')) {
return t('Daily total recharge amount: $10 ({currentValue}/{targetValue})', {
currentValue: task.currentValue,
targetValue: task.targetValue,
})
}
if (taskCode.endsWith('SP_RECHARGE_50')) {
return t('Daily total recharge amount: $50 ({currentValue}/{targetValue})', {
currentValue: task.currentValue,
targetValue: task.targetValue,
})
}
if (taskCode.endsWith('SP_RECHARGE_100')) {
return t('Daily total recharge amount: $100 ({currentValue}/{targetValue})', {
currentValue: task.currentValue,
targetValue: task.targetValue,
})
}
}
return `${task.taskName}(${task.currentValue}/${task.targetValue})`
}
const getActivityDetail = async () => {
if (!activityCode.value) {
activity.value = {}
return
}
const resDetail = await activityDetail(activityCode.value)
if (resDetail.status && resDetail.body) {
activity.value = resDetail.body
} else {
activity.value = {}
}
}
const getTaskList = async () => {
if (!gulbenActivityId) {
return
}
const resTaskList = await ActTaskList(gulbenActivityId)
if (resTaskList.status && resTaskList.body) {
taskList.value = resTaskList.body
}
}
const getTicketRemainingCount = (ticketType) => {
const ticket = cardTicketList.value.find((item) => item.ticketType === ticketType)
return Number(ticket?.remainingCount || 0)
}
const getCardTicketList = async () => {
const resTicketList = await apiGetCardTicketList(gulbenActivityId)
if (resTicketList.status && resTicketList.body) {
cardTicketList.value = resTicketList.body
}
}
const getCardRewardPrizePools = async () => {
const prizePoolEntries = await Promise.all(
lotteryPoolModes.map(async (pool) => {
const resPrizeList = await apiGetCardRewardPrizeList(gulbenActivityId, pool.poolType)
return [pool.poolType, resPrizeList.status && resPrizeList.body ? resPrizeList.body : []]
}),
)
cardRewardPrizePools.value = Object.fromEntries(prizePoolEntries)
}
const getCardRewardMarquee = async () => {
const resMarquee = await apiGetCardRewardMarquee(gulbenActivityId)
if (resMarquee.status && resMarquee.body) {
cardRewardMarqueeList.value = resMarquee.body
}
}
const getCardBagListByGrade = async (cardGrade) => {
if (!cardGrade) {
return []
}
const resCardBag = await apiPostCardBagList({
activityId: gulbenActivityId,
cardGrade,
current: 1,
size: 20,
})
const records = resCardBag.status && resCardBag.body?.records ? resCardBag.body.records : []
cardBagPools.value = {
...cardBagPools.value,
[cardGrade]: records,
}
return records
}
const getCardBagPools = async () => {
const cardBagEntries = await Promise.all(
collectGameModes.map(async (mode) => {
const records = await getCardBagListByGrade(mode.cardGrade)
return [mode.cardGrade, records]
}),
)
cardBagPools.value = Object.fromEntries(cardBagEntries)
}
const refreshCardBagGrades = async (cardGrades) => {
const uniqueGrades = [...new Set(cardGrades.filter(Boolean))]
await Promise.all(uniqueGrades.map((cardGrade) => getCardBagListByGrade(cardGrade)))
}
const getCardDrawResultItem = (body) => {
if (Array.isArray(body)) {
return body[0] || null
}
if (!body || typeof body !== 'object') {
return null
}
const candidateLists = [body.cards, body.records, body.results, body.rewards]
const listItem = candidateLists.find((list) => Array.isArray(list) && list.length)?.[0]
return listItem || body.card || body.cardInfo || body
}
const getCardTemplateId = (item = {}) => {
return (
item.cardTemplateId ||
item.templateId ||
item.cardId ||
item.prizeId ||
item.card?.cardTemplateId ||
item.card?.templateId ||
item.prize?.cardTemplateId ||
item.prize?.templateId ||
item.prize?.prizeId ||
''
)
}
const getCardTemplateLocalImageUrl = (item = {}) => {
const cardTemplateId = getCardTemplateId(item)
const imageName = cardTemplateImageMap[Number(cardTemplateId)]
return imageName ? imageUrl(imageName) : ''
}
const getCardDrawAnimationUrl = (body) => {
const resultItem = getCardDrawResultItem(body)
const animationName = cardTemplateAnimationMap[Number(getCardTemplateId(resultItem || {}))]
return animationName ? webpUrl(animationName) : ''
}
const getCardDrawResultImageUrl = (body) => {
const resultItem = getCardDrawResultItem(body)
if (!resultItem) {
return imageUrl('cardBackBg')
}
const localImage = getCardTemplateLocalImageUrl(resultItem)
if (localImage) {
return localImage
}
return (
resultItem.cardImage ||
resultItem.prizeImage ||
resultItem.image ||
resultItem.cover ||
resultItem.card?.cardImage ||
resultItem.card?.image ||
imageUrl('cardBackBg')
)
}
const getLotteryRewardResultItem = (drawResult = {}) => {
const rewards = Array.isArray(drawResult?.results) ? drawResult.results : []
return rewards.find((reward) => reward?.isWin) || rewards[0] || null
}
const getLotteryRewardResultImageUrl = (drawResult = {}) => {
const resultItem = getLotteryRewardResultItem(drawResult)
return resultItem?.prizeImage || resultItem?.prize?.prizeImage || ''
}
const getLotteryRewardResultList = (drawResult = {}) => {
const rewards = Array.isArray(drawResult?.results) ? drawResult.results : []
return rewards.map((reward, index) => ({
...reward,
imageUrl: reward?.prizeImage || reward?.prize?.prizeImage || '',
renderKey: reward?.recordNo || reward?.prizeId || `${drawResult.batchNo || 'reward'}-${index}`,
}))
}
const currentCardDrawResultImageUrl = computed(() => {
return (
cardDrawResultList.value[cardDrawResultIndex.value]?.imageUrl || cardDrawResultImageUrl.value
)
})
const currentCardDrawResult = computed(() => {
return cardDrawResultList.value[cardDrawResultIndex.value] || null
})
const currentCardDrawResultText = computed(() => {
if (cardResultActionMode.value !== 'rewardDraw') {
return ''
}
const result = currentCardDrawResult.value
if (!result) {
return ''
}
const days = result?.prize?.days ?? result.days
if (days !== undefined && days !== null && days !== '') {
return t('reward_days', { quantity: days })
}
const prizeValue = result?.prize?.prizeValue ?? result.prizeValue
return prizeValue !== undefined && prizeValue !== null && prizeValue !== ''
? String(prizeValue)
: ''
})
const showRewardResultSwitcher = computed(() => {
return cardResultActionMode.value === 'rewardDraw' && cardDrawResultList.value.length > 1
})
const changeCardDrawResultIndex = (step) => {
const resultCount = cardDrawResultList.value.length
if (resultCount <= 1) {
return
}
cardDrawResultIndex.value = (cardDrawResultIndex.value + step + resultCount) % resultCount
}
// 弹窗互斥:打开某个弹窗前先关闭其它弹窗,避免多个内容同时挂在同一遮罩层中。
const closeOtherPopups = (activePopup = '') => {
if (activePopup !== 'rule') {
ruleShow.value = false
}
if (activePopup !== 'history') {
historyShow.value = false
if (historyObserver) {
historyObserver.disconnect()
}
}
if (activePopup !== 'result') {
cardDrawResultShow.value = false
cardDrawResultList.value = []
cardDrawResultIndex.value = 0
}
}
const showCardDrawAnimationThenResult = (animationUrl, actionMode = 'draw') => {
if (cardDrawAnimationTimer) {
window.clearTimeout(cardDrawAnimationTimer)
}
closeOtherPopups()
cardResultActionMode.value = actionMode
cardDrawAnimationUrl.value = animationUrl
cardDrawAnimationShow.value = Boolean(animationUrl)
if (!animationUrl) {
closeOtherPopups('result')
cardDrawResultShow.value = true
return
}
cardDrawAnimationTimer = window.setTimeout(() => {
cardDrawAnimationShow.value = false
cardDrawAnimationTimer = null
closeOtherPopups('result')
cardDrawResultShow.value = true
}, 1500)
}
const handleCardDrawRequest = async (drawCount = 1) => {
if (isCardDrawing.value || cardDrawAnimationShow.value) {
return
}
isCardDrawing.value = true
try {
const resDraw = await apiPostCardDraw({
activityId: gulbenActivityId,
drawCount,
needCoins: cardDrawTicketCount.value === 0,
})
if (resDraw.status) {
cardDrawResultImageUrl.value = getCardDrawResultImageUrl(resDraw.body)
cardDrawResultList.value = []
cardDrawResultIndex.value = 0
showCardDrawAnimationThenResult(getCardDrawAnimationUrl(resDraw.body), 'draw')
await Promise.all([getCardTicketList(), getCardBagListByGrade('R')])
}
} catch (error) {
showError(error.errorMsg)
} finally {
isCardDrawing.value = false
}
}
const handleCardDraw = useDebounce(handleCardDrawRequest, 600, true)
const getCombineTargetGrade = () => {
if (activeCollectGameMode.value.cardGrade === 'R') {
return 'SR'
}
if (activeCollectGameMode.value.cardGrade === 'SR') {
return 'SSR'
}
return ''
}
const handleCardCombineRequest = async () => {
if (isCardCombining.value || cardDrawAnimationShow.value || !canCombineActiveCards.value) {
return
}
isCardCombining.value = true
const sourceCardGrade = activeCollectGameMode.value.cardGrade
const targetCardGrade = getCombineTargetGrade()
const cardTemplateIds = [...activeOwnedCollectCardTemplateIds.value]
try {
const resCombine = await apiPostCardCombine({
activityId: gulbenActivityId,
cardTemplateIds,
})
if (resCombine.status) {
cardDrawResultImageUrl.value = getCardDrawResultImageUrl(resCombine.body)
cardDrawResultList.value = []
cardDrawResultIndex.value = 0
showCardDrawAnimationThenResult(getCardDrawAnimationUrl(resCombine.body), 'combine')
await refreshCardBagGrades([sourceCardGrade, targetCardGrade])
}
} catch (error) {
showError(error.errorMsg)
} finally {
isCardCombining.value = false
}
}
const handleCardCombine = useDebounce(handleCardCombineRequest, 600, true)
const handleCardRedeemRequest = async () => {
if (isCardRedeeming.value || !canRedeemActiveCards.value) {
return
}
isCardRedeeming.value = true
const sourceCardGrade = activeCollectGameMode.value.cardGrade
const cardTemplateIds = [...activeOwnedCollectCardTemplateIds.value]
try {
const resRedeem = await apiPostCardRedeem({
activityId: gulbenActivityId,
cardTemplateIds,
})
if (resRedeem.status) {
await Promise.all([getCardTicketList(), getCardBagListByGrade(sourceCardGrade)])
}
} catch (error) {
showError(error.errorMsg)
} finally {
isCardRedeeming.value = false
}
}
const handleCardRedeem = useDebounce(handleCardRedeemRequest, 600, true)
const initData = async () => {
await Promise.all([
getActivityDetail(), // 获取活动详情
getTaskList(), // 获取任务列表
getCardTicketList(), // 查询用户票券数量
getCardRewardPrizePools(), // 查询奖励池奖品列表
getCardRewardMarquee(), // 查询中奖飘窗列表
getCardBagPools(), // 查询用户卡包
])
}
const receiveTaskRewardRequest = async (code) => {
if (!gulbenActivityId || !code) {
return
}
try {
const resReceive = await receiveTickets({
activityId: gulbenActivityId,
taskCode: code,
})
if (resReceive.status && resReceive.body) {
getTaskList() // 获取任务列表
getCardTicketList() // 查询用户票券数量
}
} catch (error) {
showError(error.errorMsg)
}
}
const receiveTaskReward = useDebounce(receiveTaskRewardRequest, 600, true)
const getWeeklyGifts = async () => {
const rewardsAndGifts = await getKingQueenEffectiveData()
if (rewardsAndGifts.gifts?.length) {
weeklyGifts.value = rewardsAndGifts.gifts.slice(0, 3)
}
}
const getPersonalRewardExtraItem = (topIndex) => {
if (topIndex === 0) {
return {
type: 'EXTRA_REWARD_ITEM',
quantity: 30,
cover: imageUrl('top1ExtraRewardItem'),
isExtraPreviewReward: true,
}
}
if (topIndex === 1) {
return {
type: 'EXTRA_REWARD_ITEM',
quantity: 30,
cover: imageUrl('top2ExtraRewardItem'),
isExtraPreviewReward: true,
}
}
return null
}
const getPersonalRewardList = (rewardTop, topIndex) => {
const rewards = rewardTop?.rewards || rewardTop?.propsGroup?.activityRewardProps
if (!Array.isArray(rewards)) {
return []
}
const rewardList = rewards.slice(0, 6)
const extraReward = getPersonalRewardExtraItem(topIndex)
if (extraReward && rewardList.length < 6) {
rewardList.push(extraReward)
}
if (rewardList.length === 5) {
rewardList.push({
isEmptyPreviewReward: true,
})
}
return rewardList
}
const getRewardTopMainReward = (rewardTop, topIndex) => {
return getPersonalRewardList(rewardTop, topIndex)[0] || null
}
const getRewardTopGridRows = (rewardTop, topIndex) => {
const rewards = getPersonalRewardList(rewardTop, topIndex).slice(1, 6)
return [rewards.slice(0, 2), rewards.slice(2, 4), rewards.slice(4, 5)]
}
const getRewardDisplayText = (reward) => {
if (!reward || reward.isEmptyPreviewReward) {
return ''
}
return formatRewardDisplay(reward.type, reward)
}
const hasRewardCover = (reward) => {
return Boolean(reward?.cover)
}
const vipLevelImageMap = {
VISCOUNT: 'vip1',
EARL: 'vip2',
MARQUIS: 'vip3',
DUKEI: 'vip4',
KING: 'vip5',
EMPEROR: 'vip6',
}
const getVipLevelImageName = (vipLevelName = '') => {
return vipLevelImageMap[String(vipLevelName || '').toUpperCase()] || ''
}
const getLevelBgImageName = (prefix, level) => {
const levelValue = Number(level) || 0
const imageIndex = Math.min(5, Math.max(1, Math.ceil(levelValue / 10) || 1))
return `${prefix}${imageIndex}Bg`
}
const getLotteryPrizeDisplayText = (prize) => {
if (!prize) {
return ''
}
if (prize.days !== undefined && prize.days !== null && prize.days !== '') {
return t('reward_days', { quantity: prize.days })
}
return prize.prizeValue ?? ''
}
const getHistoryRecordImage = (record = {}) => {
return (
getCardTemplateLocalImageUrl(record) || record?.prize?.prizeImage || record.prizeImage || ''
)
}
const getHistoryRecordPrizeText = (record = {}) => {
const days = record?.prize?.days ?? record.days
if (days !== undefined && days !== null && days !== '') {
return t('reward_days', { quantity: days })
}
const prizeValue = record?.prize?.prizeValue ?? record.prizeValue
return prizeValue !== undefined && prizeValue !== null && prizeValue !== '' ? prizeValue : ''
}
const formatHistoryRecordTime = (time) => {
return time ? formatUTCCustom(time) : ''
}
const normalizeHistoryRecords = (body) => {
if (Array.isArray(body?.records)) {
return body.records
}
if (Array.isArray(body)) {
return body
}
return []
}
const getHistoryRecords = async (isReset = false) => {
if (historyLoading.value || (!historyHasMore.value && !isReset)) {
return
}
historyLoading.value = true
try {
const resRecords = await apiPostCardRewardRecordList({
activityId: gulbenActivityId,
poolType: activeLotteryPoolMode.value.poolType,
size: historySize,
current: historyPage.value,
})
const body = resRecords.status ? resRecords.body : null
const records = normalizeHistoryRecords(body)
historyRecords.value = isReset ? records : [...historyRecords.value, ...records]
const total = Number(body?.total || 0)
historyHasMore.value = total
? historyRecords.value.length < total
: records.length >= historySize
} catch (error) {
showError(error.errorMsg)
} finally {
historyLoading.value = false
observeHistoryLoadmore()
}
}
const getRewardRenderKey = (reward, index) => {
return [
index,
reward?.isEmptyPreviewReward ? 'empty' : '',
reward?.type || '',
reward?.detailType || '',
reward?.cover || '',
].join('-')
}
const getKingQueenEffectiveData = async () => {
if (kingQueenEffectiveData.value) {
return kingQueenEffectiveData.value
}
if (!kingQueenEffectiveRequest) {
kingQueenEffectiveRequest = getThisWeekRewardsAndGifts(templateId.value)
}
try {
const resRewardsAndGifts = await kingQueenEffectiveRequest
kingQueenEffectiveData.value =
resRewardsAndGifts.status && resRewardsAndGifts.body ? resRewardsAndGifts.body : {}
return kingQueenEffectiveData.value
} finally {
kingQueenEffectiveRequest = null
}
}
const getPersonalRewardConfigs = async () => {
if (personalRewardLoaded.value) {
return
}
try {
const rewardsAndGifts = await getKingQueenEffectiveData()
activityRewards.value = (rewardsAndGifts.butOneRewards || []).slice(0, 3)
personalRewardLoaded.value = true
} catch (error) {
showError(error.errorMsg)
}
}
const normalizePersonalRankingItem = (item = {}, index = 0) => {
return {
...item,
rank: item.rank || item.ranking || (index === null ? '' : index + 1),
userId: item.userId || item.id || '',
userAvatar: item.avatar || item.userAvatar || '',
userName: item.userName || item.userNickname || item.nickname || '',
amount: item.amount ?? item.quantity ?? item.total ?? 0,
account: item.specialAccount || item.account || '',
specialAccount: item.specialAccount || '',
wealthLevel: item.wealthLevel ?? item.userProfile?.wealthLevel ?? 0,
charmLevel: item.charmLevel ?? item.userProfile?.charmLevel ?? 0,
vipLevelName: item.vipLevelName || item.userProfile?.vipLevelName || '',
}
}
const normalizeRoomContributionRankingItem = (item = {}, index = 0) => {
const profile = item.userProfile || {}
const specialAccount = profile.ownSpecialId?.account || ''
const fallbackAccount = profile.account || ''
return {
...item,
rank: item.rank ?? (index === null ? '' : index + 1),
userId: profile.id || item.userId || '',
roomId: profile.inRoomId || '',
userAvatar: profile.userAvatar || profile.roomIcon || '',
userName: profile.userNickname || profile.account || '',
amount: item.totalFormat || item.total || 0,
account: specialAccount || fallbackAccount,
specialAccount,
}
}
const getPersonalRanking = async () => {
if (rankingLoading.value || !rankingHasMore.value) {
return
}
rankingLoading.value = true
try {
const resRanking = await getRankingListAndMyRanking({
activityType: activityType.value,
})
if (resRanking.status && resRanking.body) {
myRanking.value = normalizePersonalRankingItem(resRanking.body.currentUserRank || {}, null)
ranking.value = (resRanking.body.rankingList || []).map((item, index) =>
normalizePersonalRankingItem(item, index),
)
rankingHasMore.value = false
return
}
ranking.value = []
myRanking.value = {}
rankingHasMore.value = false
} finally {
rankingLoading.value = false
}
}
const getRoomContributionRanking = async () => {
if (rankingLoading.value || !rankingHasMore.value) {
return
}
rankingLoading.value = true
try {
const resRanking = await getRoomContributionTotalRank(roomRankingParams)
if (resRanking.status && resRanking.body) {
const records = Array.isArray(resRanking.body.rankList) ? resRanking.body.rankList : []
ranking.value = records.map((item, index) =>
normalizeRoomContributionRankingItem(item, index),
)
myRanking.value = resRanking.body.current
? normalizeRoomContributionRankingItem(resRanking.body.current, null)
: {}
rankingHasMore.value = false
return
}
ranking.value = []
myRanking.value = {}
rankingHasMore.value = false
} finally {
rankingLoading.value = false
}
}
const getCurrentRankingList = async () => {
if (isRoomRankingSection.value) {
await getRoomContributionRanking()
return
}
await getPersonalRanking()
}
const handleRankingClickRequest = (rankingItem = {}) => {
if (isPersonalRankingSection.value) {
console.log('[GulbenFestival] Personal Rank click userId:', rankingItem.userId, rankingItem)
viewUserInfo(rankingItem.userId)
}
}
const handleRankingClick = useDebounce(handleRankingClickRequest, 600, true)
const loadRankingSection = async () => {
const rankingType = activeRankingLoadKey.value
if (rankingLoaded.value && loadedRankingType.value === rankingType) {
observeRankingLoadmore()
return
}
rankingLoaded.value = true
loadedRankingType.value = rankingType
ranking.value = []
myRanking.value = {}
rankingHasMore.value = true
try {
const loaders = isRoomRankingSection.value
? [getRoomContributionRanking()]
: [getPersonalRanking()]
if (isPersonalRankingSection.value) {
loaders.unshift(getWeeklyGifts())
}
await Promise.all(loaders)
observeRankingLoadmore()
} catch (error) {
showError(error.errorMsg)
}
}
const observeRankingLoadmore = async () => {
await nextTick()
if (!rankingObserver || !rankingLoadmore.value || !isRankingSection.value) {
return
}
rankingObserver.disconnect()
rankingObserver.observe(rankingLoadmore.value)
}
const observeHistoryLoadmore = async () => {
await nextTick()
if (!historyObserver || !historyLoadmore.value || !historyShow.value || !historyHasMore.value) {
return
}
historyObserver.disconnect()
historyObserver.observe(historyLoadmore.value)
}
const loadMoreHistoryRecords = useDebounce(async () => {
if (historyLoading.value || !historyHasMore.value) {
return
}
console.log('上拉加载')
historyPage.value += 1
await getHistoryRecords()
}, 300)
const normalizeLotteryIndex = (index) => {
const prizeCount = activeLotteryPrizes.value.length
if (!prizeCount) {
return 0
}
return (((Number(index) || 0) % prizeCount) + prizeCount) % prizeCount
}
// 抽奖结果位置计算:优先用接口返回的奖品 id 在奖池中查找位置。
const getLotteryResultIndex = (drawResult) => {
const rewards = Array.isArray(drawResult?.results) ? drawResult.results : []
const resultReward = rewards.find((reward) => reward?.isWin) || rewards[0] || drawResult
const prizeList = activeLotteryPrizes.value
const matchedPrizeIndex = prizeList.findIndex((prize) => {
const samePrizeId =
resultReward?.prizeId !== undefined && String(prize.id) === String(resultReward.prizeId)
const samePrizeImage =
resultReward?.prizeImage && prize.prizeImage && prize.prizeImage === resultReward.prizeImage
const samePrizeTypeAndValue =
resultReward?.prizeType &&
prize.prizeType &&
resultReward.prizeType === prize.prizeType &&
Number(resultReward?.prizeValue || 0) === Number(prize?.prizeValue || 0)
return samePrizeId || samePrizeImage || samePrizeTypeAndValue
})
if (matchedPrizeIndex >= 0) {
return normalizeLotteryIndex(matchedPrizeIndex)
}
if (typeof drawResult?.prizeIndex === 'number') {
return normalizeLotteryIndex(drawResult.prizeIndex)
}
return normalizeLotteryIndex(0)
}
// 抽奖动画:从左到右、从上到下依次切换奖品背景,最终停在接口返回的中奖位置。
const startLotteryAnimation = (resultIndex) => {
return new Promise((resolve) => {
const prizeCount = activeLotteryPrizes.value.length
if (!prizeCount) {
resolve()
return
}
if (lotteryAnimationTimer) {
window.clearTimeout(lotteryAnimationTimer)
}
const targetIndex = normalizeLotteryIndex(resultIndex)
const startIndex =
activeLotteryIndex.value >= 0 ? normalizeLotteryIndex(activeLotteryIndex.value) : 0
const distanceToTarget = (targetIndex - startIndex + prizeCount) % prizeCount
const minRounds = 3
const totalSteps = prizeCount * minRounds + distanceToTarget + 1
let step = 0
const roll = () => {
if (step >= totalSteps) {
activeLotteryIndex.value = targetIndex
lotteryAnimationTimer = null
window.setTimeout(resolve, 350)
return
}
activeLotteryIndex.value = (startIndex + step) % prizeCount
step += 1
const progress = step / totalSteps
const delay = 130 + Math.round(progress * progress * 220)
lotteryAnimationTimer = window.setTimeout(roll, delay)
}
roll()
})
}
// 奖励池抽奖:消耗当前奖池奖励票,成功后刷新第一层三种票券数量。
const requestLotteryDraw = async (drawCount) => {
const resRewardDraw = await apiPostCardRewardDraw({
activityId: gulbenActivityId,
ticketType: activeLotteryPoolMode.value.poolType,
drawCount,
})
if (resRewardDraw.status) {
await getCardTicketList()
} else {
return null
}
const responseBody = resRewardDraw.body || {}
const rewards = Array.isArray(responseBody?.rewards)
? responseBody.rewards
: Array.isArray(responseBody?.results)
? responseBody.results
: Array.isArray(responseBody)
? responseBody
: responseBody
? [responseBody]
: []
return {
drawCount,
batchNo: responseBody?.batchNo || '',
results: rewards,
raw: responseBody,
}
}
const handleLotteryDrawRequest = async (drawCount = 1) => {
if (isLotteryRolling.value || !canLotteryRewardDraw(drawCount)) {
return
}
isLotteryRolling.value = true
try {
const drawResult = await requestLotteryDraw(drawCount)
if (!drawResult) {
return
}
const resultIndex = getLotteryResultIndex(drawResult)
await startLotteryAnimation(resultIndex)
closeOtherPopups('result')
cardResultActionMode.value = 'rewardDraw'
cardDrawResultImageUrl.value = getLotteryRewardResultImageUrl(drawResult)
cardDrawResultList.value = getLotteryRewardResultList(drawResult)
cardDrawResultIndex.value = 0
cardDrawResultShow.value = true
} catch (error) {
showError(error.errorMsg)
} finally {
isLotteryRolling.value = false
}
}
const handleLotteryDraw = useDebounce(handleLotteryDrawRequest, 600, true)
const activeContentTitle = computed(() => {
if (currentChildTab.value) {
return `${t(currentTopTab.value?.label || '')} / ${t(currentChildTab.value.label)}`
}
return t(currentTopTab.value?.label || '')
})
const padTime = (value) => String(value).padStart(2, '0')
// 获取目标时间2026年6月7日上午5点北京时间
const getTargetTime = () => {
const target = new Date(2026, 5, 7, 5, 0, 0) // 月份从0开始5代表6月
return target.getTime()
}
// 计算倒计时:写法参考 LuckyDollars Season5四个展示字段单独维护。
const calculateCountdown = () => {
const targetTime = getTargetTime()
const diff = targetTime - Date.now()
if (!targetTime || diff <= 0) {
Days.value = 0
Hours.value = '00'
Minutes.value = '00'
Seconds.value = '00'
if (countdownTimer) {
window.clearInterval(countdownTimer)
countdownTimer = null
}
return
}
const totalSeconds = Math.floor(diff / 1000)
Days.value = Math.floor(totalSeconds / 86400)
Hours.value = padTime(Math.floor((totalSeconds % 86400) / 3600))
Minutes.value = padTime(Math.floor((totalSeconds % 3600) / 60))
Seconds.value = padTime(totalSeconds % 60)
}
// 启动倒计时:立即执行一次,之后每秒刷新。
const startCountdown = () => {
if (countdownTimer) {
window.clearInterval(countdownTimer)
}
calculateCountdown()
countdownTimer = window.setInterval(calculateCountdown, 1000)
}
const changeTopTab = (value) => {
activeTopTab.value = value
}
const changeChildTab = (value) => {
activeChildTab.value = value
}
// 规则弹窗:参考 LuckyDollars Season5点击规则入口打开点击遮罩关闭。
const openRulePopup = () => {
closeOtherPopups('rule')
ruleShow.value = true
}
const closeRulePopup = () => {
closePopup()
}
const openHistoryPopupRequest = async () => {
closeOtherPopups('history')
historyShow.value = true
historyPage.value = 1
historyHasMore.value = true
historyRecords.value = []
await getHistoryRecords(true)
observeHistoryLoadmore()
}
const openHistoryPopup = useDebounce(openHistoryPopupRequest, 600, true)
const closePopup = () => {
ruleShow.value = false
historyShow.value = false
cardDrawResultShow.value = false
cardDrawResultList.value = []
cardDrawResultIndex.value = 0
cardDrawAnimationShow.value = false
if (cardDrawAnimationTimer) {
window.clearTimeout(cardDrawAnimationTimer)
cardDrawAnimationTimer = null
}
if (historyObserver) {
historyObserver.disconnect()
}
}
// 图片预加载:支持普通图片名和带 localized 标记的配置项。
const resolvePreloadUrls = (items = []) => {
return items.map((item) => {
if (typeof item === 'string') {
return imageUrl(getImgName(item))
}
return imageUrl(item.localized ? getImgName(item.name) : item.name)
})
}
// 进入页面前阻塞加载首屏关键图,进入后后台加载其它图片。
const completePreloading = async () => {
try {
await Promise.all([initData(), preloadImages(resolvePreloadUrls(pageConfig.preload.blocking))])
} catch (error) {
console.error('预加载过程中发生错误:', error)
} finally {
isLoading.value = false
preloadImagesIdle([
...resolvePreloadUrls(pageConfig.preload.background),
...Object.values(cardTemplateAnimationMap).map((imageName) => webpUrl(imageName)),
])
observeRankingLoadmore()
}
}
const connectToAppHandler = async () => {
await connectToApp(() => {
completePreloading()
})
}
// 切换一级标签时,同步选中当前模块的默认二级标签。
watch(
currentTopSection,
(section) => {
const firstChild = section?.children?.tabs?.defaultValue || section?.children?.tabs?.items?.[0]
activeChildTab.value = typeof firstChild === 'string' ? firstChild : firstChild?.value || ''
},
{ immediate: true },
)
watch(
isRankingSection,
(enabled) => {
if (enabled) {
loadRankingSection()
} else if (rankingObserver) {
rankingObserver.disconnect()
}
},
{ immediate: true },
)
watch(activeRankingLoadKey, () => {
if (isRankingSection.value) {
loadRankingSection()
}
})
watch(
isPersonalRewardSection,
(enabled) => {
if (enabled) {
getPersonalRewardConfigs()
}
},
{ immediate: true },
)
// 页面初始化:设置文档方向、加载首屏图片并启动倒计时刷新。
onMounted(() => {
locale.value && setDocumentDirection(locale.value)
if ('IntersectionObserver' in window) {
rankingObserver = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
if (
entry.intersectionRatio > 0 &&
isRankingSection.value &&
rankingTotal.value.length !== 0 &&
rankingHasMore.value &&
!rankingLoading.value
) {
getCurrentRankingList()
}
})
})
historyObserver = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.intersectionRatio > 0 && historyShow.value) {
loadMoreHistoryRecords()
}
})
},
{
rootMargin: '80px',
threshold: 0,
},
)
}
connectToAppHandler()
startCountdown()
})
// 页面卸载:清理倒计时定时器。
onUnmounted(() => {
if (countdownTimer) {
window.clearInterval(countdownTimer)
}
if (lotteryAnimationTimer) {
window.clearTimeout(lotteryAnimationTimer)
}
if (rankingObserver) {
rankingObserver.disconnect()
}
if (historyObserver) {
historyObserver.disconnect()
}
if (cardDrawAnimationTimer) {
window.clearTimeout(cardDrawAnimationTimer)
}
})
</script>
<style lang="scss" scoped>
* {
color: #fff;
}
.fullPage {
width: 100%;
height: 100vh;
overflow-y: auto;
overflow-x: hidden;
background: #0b190d;
scrollbar-width: none;
-ms-overflow-style: none;
}
.fullPage::-webkit-scrollbar,
.scroll::-webkit-scrollbar {
display: none;
}
.scroll {
scrollbar-width: none;
-ms-overflow-style: none;
}
@media screen and (max-width: 360px) {
* {
font-size: 10px;
}
}
@media screen and (min-width: 360px) {
* {
font-size: 12px;
}
}
@media screen and (min-width: 768px) {
* {
font-size: 24px;
}
}
@media screen and (min-width: 1024px) {
* {
font-size: 32px;
}
}
/* 第一行:规则按钮 */
.rule-row {
display: flex;
justify-content: flex-end;
margin-bottom: 4vw;
}
[dir='rtl'] .rule-row {
justify-content: flex-start;
}
/* 第二行:倒计时 */
.countdown-cell {
width: min(18vw, 88px);
min-width: 0;
flex: 0 1 min(18vw, 88px);
}
.countdown-value {
width: 100%;
color: #fff8d7;
font-size: 2em;
font-weight: 600;
line-height: 1;
text-align: center;
white-space: nowrap;
text-shadow: 0 1px 2px rgba(63, 20, 57, 0.5);
}
.countdown-colon {
flex: 0 0 auto;
color: #fff8d7;
font-size: 2em;
font-weight: 800;
line-height: 1;
text-shadow: 0 1px 2px rgba(63, 20, 57, 0.5);
}
/* 第三行:一级标签和内容区内的二级标签 */
.image-tab {
min-width: 0;
border: 0;
padding: 0;
background-color: transparent;
color: #6d3426;
font-weight: 800;
line-height: 1.1;
text-align: center;
}
.tab-background {
height: 100%;
overflow: hidden;
contain: layout paint;
isolation: isolate;
transform: translateZ(0);
}
.tab-background :deep(.scrollY) {
z-index: 1;
overflow: hidden;
}
.tab-active-bg {
position: absolute;
inset: 0;
z-index: 0;
width: 100%;
height: 100%;
display: block;
object-fit: fill;
pointer-events: none;
}
.image-tab span {
display: block;
width: 100%;
font-size: 1em;
overflow-wrap: anywhere;
color: #9fff71;
}
.top-tab {
min-height: 18vw;
font-size: 1em;
}
.child-tab {
min-height: 12vw;
font-size: 1em;
}
.image-tab-active {
text-shadow: 0 1px 2px rgba(74, 30, 27, 0.55);
}
.image-tab-active span {
font-size: 1em;
color: #5b0000;
}
/* 第四行:当前标签内容区 */
.barrage-track {
width: max-content;
animation: gulben-barrage-scroll 80s linear infinite;
will-change: transform;
}
@keyframes gulben-barrage-scroll {
0% {
transform: translateX(100vw);
}
100% {
transform: translateX(-100%);
}
}
.pool-arrow {
width: 8vw;
display: block;
}
.pool-arrow-next {
transform: scaleX(-1);
}
.result-popup-arrow {
width: 8vw;
display: block;
}
.result-popup-arrow-next {
transform: scaleX(-1);
}
[dir='rtl'] .pool-arrow-prev {
transform: scaleX(-1);
}
[dir='rtl'] .pool-arrow-next {
transform: scaleX(1);
}
.lottery-prize-list {
--lottery-prize-gap: 3vw;
}
.lottery-prize-list-large {
--lottery-prize-gap: 1vw;
}
.lottery-prize-item {
width: calc((100% - var(--lottery-prize-gap) * 2) / 3);
min-width: 0;
flex: 0 0 calc((100% - var(--lottery-prize-gap) * 2) / 3);
overflow: hidden;
contain: layout paint;
isolation: isolate;
transform: translateZ(0);
}
.lottery-prize-item :deep(.scrollY) {
z-index: 1;
}
.lottery-prize-active-bg {
position: absolute;
inset: 0;
z-index: 0;
width: 100%;
height: 100%;
display: block;
object-fit: fill;
pointer-events: none;
}
.lottery-prize-item-sr {
width: calc((100% - var(--lottery-prize-gap) * 2) / 3);
flex-basis: calc((100% - var(--lottery-prize-gap) * 2) / 3);
}
.lottery-prize-item-ssr {
width: calc((100% - var(--lottery-prize-gap)) / 2.2);
flex-basis: calc((100% - var(--lottery-prize-gap)) / 2.2);
}
.preview-image {
width: min(58vw, 280px);
max-height: 42vw;
display: block;
object-fit: contain;
}
.room-rule-text {
width: 100%;
color: #fff;
font-size: 1.2em;
font-weight: 600;
line-height: 1.35;
text-align: left;
white-space: pre-wrap;
}
[dir='rtl'] .room-rule-text {
text-align: right;
}
.ranking-avatar {
border-radius: 50%;
}
.ranking-avatar-square {
border-radius: 4px;
}
.rule-popup-scroll,
.history-popup-scroll {
scrollbar-width: none;
-ms-overflow-style: none;
}
.rule-popup-scroll::-webkit-scrollbar,
.history-popup-scroll::-webkit-scrollbar {
display: none;
}
.collect-action-disabled {
filter: grayscale(1);
opacity: 0.55;
}
</style>