1449 lines
41 KiB
Vue

<template>
<div class="fullPage">
<!-- 预加载状态 -->
<div v-if="isLoading" class="loading-container">
<!-- 页面背景 -->
<BackgroundLayer :useCanvas="true" :backgroundImages="[imageUrl('bg')]" />
<!-- 页面背景 -->
<!-- 加载动画 -->
<LoadingSpinner />
<p>{{ t('loading') }}...</p>
</div>
<!-- 主要内容 -->
<div class="active-game-shell scroll">
<div class="active-game-stage">
<!-- 头部背景图 -->
<VapBg
v-if="isAndroidDevice"
class="vap-background-layer vap-background-vap-layer"
:src="bgVapUrl"
/>
<img v-else v-smart-img.noFade :src="imageUrl('bg2')" alt="" class="vap-background-layer" />
<!-- 背景层 -->
<div class="page-background-layer">
<BackgroundLayer
:useCanvas="true"
:backgroundImages="[imageUrl('bg')]"
:layerStyle="{ position: 'absolute', top: 0, left: 0, minHeight: 0, zIndex: 0 }"
/>
</div>
<!-- 页面背景 -->
<!-- 页面内容 -->
<div class="page-content">
<!-- 累计奖励 -->
<itemCenter
style="padding: 0 3vw"
:imgUrl="imageUrl('totalRewardBg')"
:contentStyle="`padding:0 7vw;gap:1vw`"
>
<img
v-smart-img
src="/src/assets/icon/Azizi/coin.png"
alt=""
style="display: block; width: 10vw; aspect-ratio: 1/1"
/>
<div
style="
flex: 1;
border-radius: 88px;
background: linear-gradient(180deg, #750a00 0%, #c71f10 48.08%, #750a00 100%);
padding: 1vw 6px;
display: flex;
justify-content: center;
align-items: center;
"
>
<div
style="
background: linear-gradient(180deg, #ffeeb0 0%, #ffcc46 100%);
background-clip: text;
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
font-size: 1.2em;
font-weight: 500;
"
>
{{ taskInfo.task1PendingCoins }}
</div>
</div>
<img
v-smart-img
:src="imageUrl(getImgName('receiveBt'))"
alt=""
style="display: block; width: 25vw"
:style="{ filter: canClaimTask1Reward ? 'none' : 'grayscale(1)' }"
@click="claimTask1Reward"
/>
</itemCenter>
<!-- 等级任务 -->
<itemCenter
style=""
:imgUrl="imageUrl('lvTaskBg')"
:contentStyle="`
flex-direction: column;
justify-content: flex-start;
padding: 10vw 8vw 5vw;
gap: 8vw;
`"
>
<!-- 标题 -->
<div style="display: flex; justify-content: center; align-items: center; height: 11vw">
<img
v-smart-img
:src="imageUrl(`titleLv${currentLevel}`)"
alt=""
style="width: 18vw"
/>
</div>
<!-- 内容 -->
<div style="width: 100%; display: flex; flex-direction: column; position: relative">
<!-- 规则弹窗展示按钮 -->
<img
v-smart-img
src="/src/assets/icon/Azizi/helpWhite.png"
alt=""
class="ruleBt"
@click="openPopup('rule')"
/>
<!-- 信息展示 -->
<div style="height: 45vw; display: flex; flex-direction: column; gap: 5vw">
<!-- 当前状态 -->
<div style="display: flex; align-items: center">
<div
style="
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
gap: 2vw;
position: relative;
"
>
<div style="font-size: 1.1em">
{{
t('active_game_current_daily_recharge_amount', {
amount: displayedRechargeAmountText,
})
}}
</div>
<div style="font-size: 1.1em">
{{
t('active_game_current_daily_game_spending', {
coins: displayedSpendCoinsText,
})
}}
</div>
</div>
<!-- 展示隐藏按钮 -->
<img
v-smart-img
:src="imageUrl(amountVisible ? 'amountShowBt' : 'amountHiddenBt')"
alt=""
style="width: 5vw; min-width: 0; display: block"
@click="toggleAmountVisible"
/>
</div>
<!-- 任务详情 -->
<div style="display: flex; flex-direction: column; gap: 2vw">
<!-- 任务一 -->
<div
style="font-size: 1.1em"
:style="{ color: task1Completed ? '#1AFF00' : '#fff' }"
>
{{
t('active_game_task1_detail', {
progress: task1ProgressText,
reward: currentTask1RewardCoinsText,
})
}}
<img
v-if="task1Completed"
v-smart-img
:src="imageUrl('done')"
alt=""
style="display: inline-block; width: 1em; object-fit: contain"
/>
</div>
<!-- 任务二 -->
<div
style="font-size: 1.1em"
:style="{ color: task2Completed ? '#1AFF00' : '#fff' }"
>
{{
t('active_game_task2_detail', {
progress: task2ProgressText,
})
}}
<img
v-if="task2Completed"
v-smart-img
:src="imageUrl('done')"
alt=""
style="display: inline-block; width: 1em; object-fit: contain"
/>
</div>
</div>
</div>
<!-- 宝箱展示 -->
<div style="display: flex; flex-direction: column; align-items: center">
<img
v-smart-img
:src="imageUrl(currentChestImageName)"
alt=""
style="width: 38vw; display: block; object-fit: contain"
/>
<!-- 宝箱等级 -->
<div style="font-size: 1.5em; font-weight: 700">
{{ t('active_game_level_label', { level: currentLevel }) }}
</div>
<!-- 领取按钮 -->
<img
v-smart-img
:src="imageUrl(getImgName(taskRewardButtonImageName))"
alt=""
style="width: 30vw; display: block; object-fit: contain"
:style="{ filter: taskRewardButtonDisabled ? 'grayscale(1)' : 'none' }"
@click="claimTask2Reward"
/>
</div>
</div>
</itemCenter>
<!-- 等级奖励 -->
<itemCenter
style=""
:imgUrl="imageUrl('LvRewardInfoBg')"
:contentStyle="` flex-direction: column;
justify-content: flex-start;
padding: 8vw 7.5vw 14vw;
gap: 4vw;`"
>
<!-- 标题 -->
<div style="display: flex; justify-content: center; align-items: center; height: 11vw">
<img
v-smart-img
:src="imageUrl(getImgName('LvRewardTitle'))"
alt=""
style="width: 40vw"
/>
</div>
<!-- 内容 -->
<div
style="
flex: 1;
min-height: 0;
width: 100%;
display: flex;
flex-direction: column;
gap: 2.5vw;
position: relative;
"
>
<!-- 开箱进度条 -->
<div
:class="[
'scroll',
'chest-progress-scroll',
{ 'chest-progress-scroll-rtl': isRtlLayout },
]"
>
<div :class="['chest-progress-row', { 'chest-progress-row-rtl': isRtlLayout }]">
<!-- 进度条 -->
<div class="chest-progress-track" :style="progressTrackStyle">
<!-- 进度条图片 -->
<div
:class="['chest-progress-fill', { 'chest-progress-fill-rtl': isRtlLayout }]"
:style="progressFillStyle"
>
<img
v-smart-img
:src="imageUrl('progressBar')"
alt=""
:class="['chest-progress-bar', { 'chest-progress-bar-rtl': isRtlLayout }]"
/>
</div>
</div>
<!-- 宝箱列表 -->
<div :class="['chest-progress-list', { 'chest-progress-list-rtl': isRtlLayout }]">
<!-- 开头占位 -->
<div></div>
<div
v-for="chest in chestProgressList"
:key="chest.level"
style="width: 14vw; cursor: pointer"
@click="selectRewardChest(chest.level)"
>
<!-- 未达成条件 -->
<img
v-if="!isChestConditionReached(chest.level)"
v-smart-img
:src="imageUrl(getChestImageName(chest))"
alt=""
style="display: block; width: 100%; object-fit: contain"
/>
<!-- 达成条件 -->
<itemCenter
v-else
style="width: 100%"
:imgUrl="imageUrl('LvActive')"
:contentStyle="``"
>
<img
v-smart-img
:src="imageUrl(getChestImageName(chest))"
alt=""
style="display: block; width: 100%; object-fit: contain"
/>
</itemCenter>
</div>
</div>
</div>
</div>
<!-- 奖励展示 -->
<div class="reward-showcase">
<div v-if="mainReward" class="reward-showcase-main">
<div
style="
width: 100%;
height: 71vw;
display: flex;
align-items: center;
justify-content: center;
"
>
<div
class="reward-showcase-vap-wrap"
:key="mainRewardRenderKey"
:class="{
'reward-showcase-vap-wrap-playable': isIosDevice && mainRewardVapSourceUrl,
}"
@click="handleMainRewardPreviewClick"
>
<VapMp4Player
v-if="shouldPlayMainRewardVap"
:key="mainRewardVapRenderKey"
:model-value="true"
:src="mainRewardVapSourceUrl"
mode="inline"
fit="contain"
:loop="true"
:mute="true"
:auto-close="false"
:show-status="false"
@error="(error) => handleRewardVapError(error, mainRewardVapSourceUrl)"
/>
<img
v-else
:src="mainReward.cover || ''"
alt=""
style="display: block; width: 90%; object-fit: contain"
@error="(e) => handleRewardImageError(e, mainReward.type)"
/>
<button
v-if="isIosDevice && mainRewardVapSourceUrl"
type="button"
class="reward-showcase-play-button"
@click.stop="handleMainRewardPreviewClick"
>
<span class="reward-showcase-play-icon"></span>
</button>
</div>
</div>
<div
style="
flex: 1;
min-height: 0;
display: flex;
align-items: center;
justify-content: center;
"
>
<div>{{ getRewardDisplayText(mainReward) }}</div>
</div>
</div>
<div class="reward-showcase-grid">
<div
v-for="(reward, index) in rewardGridList"
:key="getRewardRenderKey(reward, index)"
class="reward-showcase-item"
>
<div
style="
width: 100%;
aspect-ratio: 1/1;
display: flex;
align-items: center;
justify-content: center;
"
>
<img
v-if="isLocalRewardAsset(reward)"
v-smart-img.noFade
:src="reward.cover || ''"
alt=""
style="display: block; width: 90%; object-fit: contain"
@error="(e) => handleRewardImageError(e, reward.type)"
/>
<img
v-else
:src="reward.cover || ''"
alt=""
style="display: block; width: 90%; object-fit: contain"
@error="(e) => handleRewardImageError(e, reward.type)"
/>
</div>
<div
style="
flex: 1;
min-height: 0;
width: 100%;
min-width: 0;
overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
"
>
<div
v-if="isLocalRewardAsset(reward)"
class="reward-extra-scroll-viewport"
:class="{ 'reward-extra-scroll-viewport-rtl': isRtlLayout }"
>
<div
class="reward-extra-scroll-wrapper"
:class="{ 'reward-extra-scroll-wrapper-rtl': isRtlLayout }"
>
<div v-for="value in 2" :key="value" class="reward-extra-scroll-content">
{{ getRewardDisplayText(reward) }}
</div>
</div>
</div>
<div v-else>{{ getRewardDisplayText(reward) }}</div>
</div>
</div>
</div>
</div>
</div>
</itemCenter>
</div>
<!-- 弹窗遮罩层 -->
<maskLayer :maskLayerShow="maskLayerShow" @click="closedPopup">
<!-- 居中弹窗 -->
<div
style="
width: 100vw;
height: 100vh;
display: flex;
justify-content: center;
align-items: center;
"
@click="closedPopup"
>
<!-- 规则弹窗 -->
<itemCenter
style=""
:imgUrl="imageUrl('ruleInfoBg')"
:contentStyle="` flex-direction: column;
justify-content: flex-start;
padding: 8vw 7.5vw 5vw;
gap: 4vw;`"
>
<!-- 标题 -->
<div
style="display: flex; justify-content: center; align-items: center; height: 11vw"
>
<img
v-smart-img
:src="imageUrl(getImgName('ruleTitle'))"
alt=""
style="width: 18vw"
/>
</div>
<div
style="
flex: 1;
min-height: 0;
width: 100%;
display: flex;
flex-direction: column;
gap: 4px;
font-size: 1.1em;
"
>
<div style="font-size: 1em; color: #fff; font-weight: 700">
{{ t('active_game_rule_1') }}
</div>
<div style="font-size: 1em; color: #fff; font-weight: 700">
{{ t('active_game_rule_2') }}
</div>
<div style="font-size: 1em; color: #fff; font-weight: 700">
{{ t('active_game_rule_3') }}
</div>
<div style="font-size: 1em; color: #fff; font-weight: 700">
{{ t('active_game_rule_4') }}
</div>
</div>
</itemCenter>
</div>
</maskLayer>
</div>
</div>
</div>
</template>
<script setup>
import { computed, defineAsyncComponent, onBeforeUnmount, onMounted, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import { setDocumentDirection } from '@/locales/i18n'
import { connectToApp } from '@/utils/appConnector.js'
import {
isAndroidDevice as detectAndroidDevice,
isIosDevice as detectIosDevice,
notifyAppPlayVap,
} from '@/utils/appBridge.js'
import { getMp4Url, getPngUrl } from '@/config/imagePaths.js'
import { preloadImages } from '@/utils/image/imagePreloader.js'
import { useLangStore } from '@/stores/lang'
import { showError } from '@/utils/toast.js'
import {
apiGetActivityResource,
claimDailyRechargeTask1,
claimDailyRechargeTask2,
getDailyRechargeInfo,
} from '@/api/activity.js'
import { handleRewardImageError } from '@/utils/image/imageHandler.js'
import { formatRewardDisplay } from '@/utils/rewardFormatter.js'
import { useThrottle } from '@/utils/useDebounce.js'
import { useDailyRechargeRewardVap } from './composables/useDailyRechargeRewardVap.js'
import BackgroundLayer from '@/components/BackgroundLayer.vue'
import itemCenter from '@/components/itemCenter.vue'
import maskLayer from '@/components/MaskLayer.vue'
const VapMp4Player = defineAsyncComponent(() => import('@/components/VapMp4Player.vue'))
const VapBg = defineAsyncComponent(() => import('./components/vapBg.vue'))
const isLoading = ref(true)
const { t, locale } = useI18n()
const langStore = useLangStore()
const currentLangType = computed(() => {
return langStore.selectedLang?.type || 'en'
})
const isRtlLayout = computed(() => {
return currentLangType.value === 'ar'
})
const DEVICE_SIMULATION_STORAGE_KEY = 'daily-recharge-device-simulation'
const DEVICE_SIMULATION_MODES = ['auto', 'android', 'ios']
const normalizeDeviceSimulationMode = (mode) => {
return DEVICE_SIMULATION_MODES.includes(mode) ? mode : 'auto'
}
const getDeviceSimulationQueryMode = () => {
if (typeof window === 'undefined') {
return ''
}
const searchMode = new URLSearchParams(window.location.search).get('dailyRechargeDevice')
if (searchMode) {
return searchMode
}
const hashQueryIndex = window.location.hash.indexOf('?')
if (hashQueryIndex === -1) {
return ''
}
return new URLSearchParams(window.location.hash.slice(hashQueryIndex + 1)).get(
'dailyRechargeDevice',
)
}
const getInitialDeviceSimulationMode = () => {
if (!import.meta.env.DEV || typeof window === 'undefined') {
return 'auto'
}
const queryMode = getDeviceSimulationQueryMode()
const storageMode = window.localStorage.getItem(DEVICE_SIMULATION_STORAGE_KEY)
return normalizeDeviceSimulationMode(queryMode || storageMode || 'auto')
}
const simulatedDeviceMode = ref(getInitialDeviceSimulationMode())
const isAndroidDevice = computed(() => {
if (import.meta.env.DEV && simulatedDeviceMode.value === 'android') {
return true
}
if (import.meta.env.DEV && simulatedDeviceMode.value === 'ios') {
return false
}
return detectAndroidDevice()
})
const isIosDevice = computed(() => {
if (import.meta.env.DEV && simulatedDeviceMode.value === 'ios') {
return true
}
if (import.meta.env.DEV && simulatedDeviceMode.value === 'android') {
return false
}
return detectIosDevice()
})
const setSimulatedDeviceMode = (mode = 'auto') => {
if (!import.meta.env.DEV) {
return
}
simulatedDeviceMode.value = normalizeDeviceSimulationMode(mode)
if (typeof window !== 'undefined') {
window.localStorage.setItem(DEVICE_SIMULATION_STORAGE_KEY, simulatedDeviceMode.value)
}
}
locale.value && setDocumentDirection(locale.value)
const imageUrl = (filename) => getPngUrl('Activities/DailyRecharge/', filename)
const mp4Url = (filename) => getMp4Url('Activities/DailyRecharge/', filename)
const bgVapUrl = mp4Url('bg')
const CHEST_PROGRESS_GAP_VW = 2
const CHEST_PROGRESS_ITEM_WIDTH_VW = 14
const CHEST_PROGRESS_FIRST_CENTER_VW = CHEST_PROGRESS_GAP_VW + CHEST_PROGRESS_ITEM_WIDTH_VW / 2
const CHEST_PROGRESS_CENTER_STEP_VW = CHEST_PROGRESS_GAP_VW + CHEST_PROGRESS_ITEM_WIDTH_VW
const CHEST_PROGRESS_TRACK_WIDTH_VW =
10 * CHEST_PROGRESS_GAP_VW + 10 * CHEST_PROGRESS_ITEM_WIDTH_VW - CHEST_PROGRESS_ITEM_WIDTH_VW / 2
// 根据语言获取图片名
const getImgName = (filename) => {
return currentLangType.value == 'en' ? filename : `${filename}_${currentLangType.value}`
}
const ruleShow = ref(false)
const amountVisible = ref(false)
const claimingTask1Reward = ref(false)
const claimingTask2Reward = ref(false)
const selectedRewardLevel = ref(1)
const activityRewards = ref([])
const createDefaultTaskInfo = () => ({
currentLevel: 1,
task1SpendCoins: 0,
task1TodayRewardCoins: 0,
task1PendingCoins: 0,
task2RechargeAmount: 0,
task2TargetAmount: 0,
task2Completed: false,
task2Claimed: false,
task2RewardCoins: 0,
levelList: [],
})
const taskInfo = ref(createDefaultTaskInfo())
const maskLayerShow = computed(() => {
return ruleShow.value
})
const normalizeLevel = (levelValue) => {
const level = Number(levelValue) || 1
return Math.min(Math.max(level, 1), 10)
}
const levelMap = computed(() => {
return new Map(taskInfo.value.levelList.map((item) => [Number(item.level), item]))
})
const currentLevel = computed(() => {
return normalizeLevel(taskInfo.value.currentLevel || 1)
})
const currentLevelInfo = computed(() => {
return levelMap.value.get(currentLevel.value) || {}
})
const currentTask2Claimed = computed(() => {
return Boolean(currentLevelInfo.value.task2Claimed)
})
const currentChestImageName = computed(() => {
return `Lv${currentLevel.value}${currentLevelInfo.value.task2Claimed ? '_open' : ''}`
})
const canClaimTask1Reward = computed(() => {
return Number(taskInfo.value.task1PendingCoins) > 0 && !claimingTask1Reward.value
})
const currentTask2RewardTop = computed(() => {
return activityRewards.value[currentLevel.value - 1] || {}
})
const canClaimTask2Reward = computed(() => {
return (
!currentTask2Claimed.value &&
task2Completed.value &&
Boolean(currentTask2RewardTop.value?.rule?.id) &&
!claimingTask2Reward.value
)
})
const taskRewardButtonDisabled = computed(() => {
return !currentTask2Claimed.value && !canClaimTask2Reward.value
})
const taskRewardButtonImageName = computed(() => {
return currentTask2Claimed.value ? 'receivedBt' : 'receiveBt'
})
const chestProgressList = computed(() => {
return Array.from({ length: 10 }, (_, index) => {
const level = index + 1
const levelInfo = levelMap.value.get(level) || {}
return {
level,
task2Claimed: Boolean(levelInfo.task2Claimed),
}
})
})
const progressBarWidth = computed(() => {
const targetLevel = Math.min(Math.max(Number(currentLevel.value) || 0, 0), 10)
const progressWidth =
targetLevel === 0
? 0
: CHEST_PROGRESS_FIRST_CENTER_VW + (targetLevel - 1) * CHEST_PROGRESS_CENTER_STEP_VW
return `${progressWidth}vw`
})
const progressTrackStyle = computed(() => {
return {
width: `${CHEST_PROGRESS_TRACK_WIDTH_VW}vw`,
left: isRtlLayout.value ? 'auto' : '0',
right: isRtlLayout.value ? '0' : 'auto',
}
})
const progressFillStyle = computed(() => {
return {
width: progressBarWidth.value,
marginLeft: isRtlLayout.value ? 'auto' : '0',
marginRight: isRtlLayout.value ? '0' : 'auto',
}
})
const task1Completed = computed(() => {
return Number(taskInfo.value.task1SpendCoins) >= Number(currentTask1TargetCoins.value)
})
const currentTask1TargetCoins = computed(() => {
return currentLevelInfo.value.spendCoinsTarget || 0
})
const currentTask2TargetAmount = computed(() => {
return currentLevelInfo.value.rechargeTarget || 0
})
const task2Completed = computed(() => {
const rechargeAmount = Number(taskInfo.value.task2RechargeAmount) || 0
const targetAmount = Number(currentTask2TargetAmount.value) || 0
return targetAmount > 0 && rechargeAmount >= targetAmount
})
const currentTask1RewardCoins = computed(() => {
return currentLevelInfo.value.rewardCoins || taskInfo.value.task1TodayRewardCoins || 0
})
const displayedSpendCoins = computed(() => {
return amountVisible.value ? taskInfo.value.task1SpendCoins : '***'
})
const displayedRechargeAmount = computed(() => {
return amountVisible.value ? taskInfo.value.task2RechargeAmount : '***'
})
const isolateLtrText = (value) => {
return `\u2066${value}\u2069`
}
const displayedSpendCoinsText = computed(() => {
return isolateLtrText(displayedSpendCoins.value)
})
const displayedRechargeAmountText = computed(() => {
return isolateLtrText(`$${displayedRechargeAmount.value}`)
})
const currentTask1RewardCoinsText = computed(() => {
return isolateLtrText(currentTask1RewardCoins.value)
})
const task1ProgressText = computed(() => {
return isolateLtrText(`${displayedSpendCoins.value}/${currentTask1TargetCoins.value}`)
})
const task2ProgressText = computed(() => {
return isolateLtrText(`$${displayedRechargeAmount.value}/$${currentTask2TargetAmount.value}`)
})
const selectedRewardTop = computed(() => {
return activityRewards.value[selectedRewardLevel.value - 1] || {}
})
const selectedRewardList = computed(() => {
return selectedRewardTop.value?.propsGroup?.activityRewardProps || []
})
const mainReward = computed(() => {
return selectedRewardList.value[0] || null
})
const {
failedRewardVapUrls,
getRewardVapRetryVersion,
getRewardVapSourceUrl,
handleRewardVapError,
preloadCriticalVap,
queueRewardVapPreload,
resetRewardVapState,
cleanupRewardVap,
} = useDailyRechargeRewardVap({
isAndroidDevice,
getCurrentMainRewardVapSourceUrl: () => mainRewardVapSourceUrl.value,
})
const mainRewardVapSourceUrl = computed(() => {
return getRewardVapSourceUrl(mainReward.value)
})
const shouldPlayMainRewardVap = computed(() => {
return (
isAndroidDevice.value &&
Boolean(mainRewardVapSourceUrl.value) &&
!failedRewardVapUrls.value.has(mainRewardVapSourceUrl.value)
)
})
const rewardGridList = computed(() => {
return selectedRewardList.value.slice(1, 7)
})
const mainRewardRenderKey = computed(() => {
return mainReward.value ? getRewardRenderKey(mainReward.value, 0) : 'empty-main-reward'
})
const mainRewardVapRetryVersion = computed(() => {
return getRewardVapRetryVersion(mainRewardVapSourceUrl.value)
})
const mainRewardVapRenderKey = computed(() => {
return `${mainRewardRenderKey.value}-vap-${mainRewardVapRetryVersion.value}`
})
let pageInitializationStarted = false
const isLocalRewardAsset = (reward) => {
return Boolean(reward?.localRewardAsset)
}
const getRewardDisplayText = (reward) => {
return formatRewardDisplay(reward.type, reward)
}
const getRewardRenderKey = (reward, index) => {
return [
selectedRewardLevel.value,
index,
reward?.type || '',
reward?.detailType || '',
reward?.cover || '',
getRewardVapSourceUrl(reward) || '',
].join('-')
}
const getExtraRewardsByLevel = (level) => {
switch (Number(level)) {
case 8:
return [
{
type: 'GIFT',
cover: imageUrl('reward_gift'),
content: t('active_game_reward_gift'),
localRewardAsset: true,
},
]
case 9:
return [
{
type: 'GIFT',
detailType: 'AVATAR_FRAME',
cover: imageUrl('reward_frame'),
content: t('active_game_reward_frame'),
localRewardAsset: true,
},
]
case 10:
return [
{
type: 'GIFT',
detailType: 'RIDE',
cover: imageUrl('reward_ride'),
content: t('active_game_reward_ride'),
localRewardAsset: true,
},
{
type: 'GIFT',
cover: imageUrl('reward_banner'),
content: t('active_game_reward_banner'),
localRewardAsset: true,
},
]
default:
return []
}
}
const appendExtraRewards = (rewardTopList) => {
return rewardTopList.map((rewardTop, index) => {
const level = Number(rewardTop?.rule?.level || index + 1)
const extraRewards = getExtraRewardsByLevel(level)
if (!extraRewards.length) {
return rewardTop
}
const propsGroup = rewardTop.propsGroup || {}
const activityRewardProps = Array.isArray(propsGroup.activityRewardProps)
? propsGroup.activityRewardProps
: []
return {
...rewardTop,
propsGroup: {
...propsGroup,
activityRewardProps: [...activityRewardProps, ...extraRewards],
},
}
})
}
const toggleAmountVisible = () => {
amountVisible.value = !amountVisible.value
}
const selectRewardChest = (level) => {
selectedRewardLevel.value = normalizeLevel(level)
}
const isChestConditionReached = (level) => {
const chestLevel = Number(level)
return chestLevel > 0 && chestLevel <= currentLevel.value
}
const getChestImageName = (chest) => {
return `Lv${chest.level}${chest.task2Claimed ? '_open' : ''}`
}
const closedPopup = () => {
ruleShow.value = false
}
const openPopup = (type) => {
closedPopup()
switch (type) {
case 'rule':
ruleShow.value = true
break
default:
break
}
}
const handleMainRewardPreviewClick = () => {
if (!mainRewardVapSourceUrl.value) {
return
}
if (isIosDevice.value) {
notifyAppPlayVap(mainRewardVapSourceUrl.value)
}
}
const claimTask1Reward = useThrottle(async () => {
if (!canClaimTask1Reward.value) {
return
}
claimingTask1Reward.value = true
try {
const res = await claimDailyRechargeTask1({})
if (res.status) {
await getDailyRechargeTaskInfo()
}
} catch (error) {
showError(error?.errorMsg || error?.message || t('active_game_claim_reward_failed'))
} finally {
claimingTask1Reward.value = false
}
}, 1000)
const claimTask2Reward = useThrottle(async () => {
if (!canClaimTask2Reward.value) {
return
}
claimingTask2Reward.value = true
try {
const res = await claimDailyRechargeTask2({
activityType: 'DAILY_RECHARGE_TASK2',
ruleId: currentTask2RewardTop.value.rule.id,
level: currentLevel.value,
})
if (res.status) {
await getDailyRechargeTaskInfo()
}
} catch (error) {
showError(error?.errorMsg || error?.message || t('active_game_claim_reward_failed'))
} finally {
claimingTask2Reward.value = false
}
}, 1000)
const getDailyRechargeTaskInfo = async () => {
const res = await getDailyRechargeInfo()
if (res.status && res.body) {
taskInfo.value = {
...createDefaultTaskInfo(),
...res.body,
levelList: Array.isArray(res.body.levelList) ? res.body.levelList : [],
}
selectedRewardLevel.value = currentLevel.value
return true
}
throw new Error('Invalid daily recharge task info response')
}
const getActivityRewards = async () => {
const res = await apiGetActivityResource('DAILY_RECHARGE_TASK2')
if (res.status && res.body) {
activityRewards.value = Array.isArray(res.body) ? appendExtraRewards(res.body) : []
resetRewardVapState()
return true
}
throw new Error('Invalid daily recharge activity reward response')
}
const delay = (duration) => {
return new Promise((resolve) => {
window.setTimeout(resolve, duration)
})
}
const runWithRetry = async (runner, retryCount = 1, retryDelay = 600) => {
let lastError = null
for (let attempt = 0; attempt <= retryCount; attempt += 1) {
try {
return await runner()
} catch (error) {
lastError = error
if (attempt < retryCount) {
await delay(retryDelay)
}
}
}
throw lastError
}
const initData = async () => {
const [taskResult, rewardResult] = await Promise.allSettled([
runWithRetry(getDailyRechargeTaskInfo, 2, 800),
runWithRetry(getActivityRewards, 1, 800),
])
if (taskResult.status === 'rejected') {
console.error('Failed to initialize daily recharge task info:', taskResult.reason)
}
if (rewardResult.status === 'rejected') {
console.error('Failed to initialize daily recharge activity rewards:', rewardResult.reason)
}
if (taskResult.status === 'rejected' && rewardResult.status === 'rejected') {
throw taskResult.reason || rewardResult.reason
}
}
const preloadCriticalImages = async () => {
const chestImages = chestProgressList.value.map((chest) => imageUrl(getChestImageName(chest)))
const criticalImages = [
imageUrl('bg2'),
imageUrl('bg'),
imageUrl('totalRewardBg'),
imageUrl('lvTaskBg'),
imageUrl('LvRewardInfoBg'),
imageUrl('ruleInfoBg'),
imageUrl(`titleLv${currentLevel.value}`),
imageUrl(currentChestImageName.value),
imageUrl('amountShowBt'),
imageUrl('amountHiddenBt'),
imageUrl('done'),
imageUrl('LvActive'),
imageUrl('reward_gift'),
imageUrl('reward_frame'),
imageUrl('reward_ride'),
imageUrl('reward_banner'),
...chestImages,
imageUrl(getImgName('LvRewardTitle')),
imageUrl(getImgName('receiveBt')),
imageUrl(getImgName('receivedBt')),
imageUrl(getImgName('ruleTitle')),
]
await Promise.all([preloadImages(criticalImages), preloadCriticalVap(bgVapUrl)])
}
const completePreloading = async () => {
try {
await initData()
await preloadCriticalImages()
} catch (error) {
console.error('棰勫姞杞借繃绋嬩腑鍙戠敓閿欒:', error)
} finally {
isLoading.value = false
queueRewardVapPreload(activityRewards.value)
}
}
const startPageInitialization = async () => {
if (pageInitializationStarted) {
return
}
pageInitializationStarted = true
await completePreloading()
}
const connectToAppHandler = async () => {
const result = await connectToApp(async () => {
await startPageInitialization()
})
if (!result?.success) {
isLoading.value = false
}
}
onMounted(() => {
if (import.meta.env.DEV && typeof window !== 'undefined') {
window.__setDailyRechargeDevice = setSimulatedDeviceMode
window.__dailyRechargeDeviceMode = simulatedDeviceMode
}
connectToAppHandler()
})
onBeforeUnmount(() => {
if (import.meta.env.DEV && typeof window !== 'undefined') {
delete window.__setDailyRechargeDevice
delete window.__dailyRechargeDeviceMode
}
cleanupRewardVap()
})
</script>
<style lang="scss" scoped>
* {
color: #fff;
}
.fullPage {
height: 100vh;
position: relative;
z-index: 0;
background: #220e08;
}
.loading-container {
width: 100vw;
height: 100vh;
position: fixed;
inset: 0;
z-index: 10;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
overflow: hidden;
background: #220e08;
}
.active-game-shell {
width: 100vw;
height: 100vh;
overflow-y: auto;
}
.active-game-stage {
width: 100vw;
min-height: 100vh;
position: relative;
isolation: isolate;
}
.vap-background-layer {
width: 100vw;
height: auto;
position: absolute;
top: 0;
left: 0;
z-index: 1;
object-fit: cover;
pointer-events: none;
}
.vap-background-vap-layer {
height: 150vw;
}
.page-background-layer {
position: absolute;
top: 120vw;
left: 0;
right: 0;
bottom: 0;
z-index: 0;
overflow: hidden;
pointer-events: none;
}
.page-content {
position: relative;
z-index: 2;
padding: 132vw 2vw 10vw;
}
.chest-progress-scroll {
width: 100%;
overflow-x: auto;
direction: ltr;
}
.chest-progress-scroll-rtl {
direction: rtl;
}
.chest-progress-row {
position: relative;
z-index: 0;
display: flex;
align-items: center;
width: max-content;
direction: ltr;
}
.chest-progress-row-rtl {
margin-left: auto;
}
.chest-progress-track {
position: absolute;
z-index: -1;
height: 3vw;
display: flex;
align-items: center;
overflow: hidden;
background: rgba(103, 41, 0, 0.7);
}
.chest-progress-fill {
height: 3vw;
overflow: hidden;
}
.chest-progress-fill-rtl {
direction: ltr;
}
.chest-progress-bar {
display: block;
width: 100%;
height: 3vw;
object-fit: cover;
transform-origin: center;
}
.chest-progress-bar-rtl {
transform: scaleX(-1) !important;
}
.chest-progress-list {
display: flex;
align-items: center;
gap: 2vw;
}
.chest-progress-list-rtl {
flex-direction: row-reverse;
}
.reward-showcase {
flex: 1;
min-height: 0;
width: 100%;
display: flex;
gap: 3vw;
direction: ltr;
unicode-bidi: isolate;
}
.reward-showcase-main,
.reward-showcase-item {
min-width: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.reward-showcase-main {
width: 33vw;
height: 100%;
}
.reward-showcase-vap-wrap {
width: 90%;
height: 100%;
position: relative;
display: flex;
align-items: center;
justify-content: center;
}
.reward-showcase-vap-wrap-playable {
cursor: pointer;
}
.reward-showcase-play-button {
position: absolute;
left: 50%;
top: 50%;
display: flex;
align-items: center;
justify-content: center;
width: 10vw;
height: 10vw;
min-width: 36px;
min-height: 36px;
padding: 0;
border: 0;
border-radius: 50%;
background: rgba(0, 0, 0, 0.42);
transform: translate(-50%, -50%);
}
.reward-showcase-play-icon {
display: block;
width: 0;
height: 0;
margin-left: 0.4vw;
border-top: 1.8vw solid transparent;
border-bottom: 1.8vw solid transparent;
border-left: 2.8vw solid #fff;
}
.reward-showcase-grid {
flex: 1;
min-width: 0;
display: grid;
grid-template-columns: repeat(2, 1fr);
grid-template-rows: repeat(3, 1fr);
column-gap: 3vw;
direction: ltr;
}
.reward-showcase-text {
color: #fff;
font-size: 1em;
text-align: center;
}
.reward-extra-scroll-viewport {
width: 100%;
max-width: 100%;
min-width: 0;
overflow: hidden;
white-space: nowrap;
direction: ltr;
}
.reward-extra-scroll-viewport-rtl {
direction: rtl;
}
.reward-extra-scroll-wrapper {
display: flex;
width: fit-content;
animation-name: reward-scroll-left;
animation-duration: 6s;
animation-timing-function: linear;
animation-iteration-count: infinite;
will-change: transform;
direction: ltr;
}
.reward-extra-scroll-wrapper-rtl {
animation-name: reward-scroll-right;
direction: rtl;
}
.reward-extra-scroll-content {
flex: 0 0 auto;
padding: 0 2vw;
white-space: nowrap;
}
@keyframes reward-scroll-left {
0% {
transform: translateX(0);
}
100% {
transform: translateX(-50%);
}
}
@keyframes reward-scroll-right {
0% {
transform: translateX(0);
}
100% {
transform: translateX(50%);
}
}
.scroll::-webkit-scrollbar {
display: none;
}
[dir='rtl'] .flipImg {
transform: scaleX(-1);
}
.ruleBt {
width: 5vw;
min-width: 0;
display: block;
position: absolute;
top: -5vw;
right: 0px;
}
[dir='rtl'] .ruleBt {
left: 0px;
right: auto;
}
@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;
}
}
</style>