3887 lines
123 KiB
Vue
3887 lines
123 KiB
Vue
<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"> </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>
|