feat(每日充值活动): 根据手机系统类型使用不同的展示方案
This commit is contained in:
parent
e67b130afc
commit
f236293dfb
@ -10,13 +10,34 @@ import {
|
||||
// ==================== 工具函数 ====================
|
||||
|
||||
/**
|
||||
* 检测是否为设备(Android、ios)系统
|
||||
* 检测是否为移动设备(Android、iOS)系统
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function android() {
|
||||
return /Android|iPad|iPhone|iPod/.test(navigator.userAgent)
|
||||
}
|
||||
|
||||
/**
|
||||
* 检测是否为 Android 设备
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function isAndroidDevice() {
|
||||
if (typeof navigator === 'undefined') return false
|
||||
return /Android/i.test(navigator.userAgent || '')
|
||||
}
|
||||
|
||||
/**
|
||||
* 检测是否为 iOS 设备
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function isIosDevice() {
|
||||
if (typeof navigator === 'undefined') return false
|
||||
const userAgent = navigator.userAgent || ''
|
||||
const isIosUserAgent = /iPad|iPhone|iPod/i.test(userAgent)
|
||||
const isIPadOS = /Macintosh/i.test(userAgent) && navigator.maxTouchPoints > 1
|
||||
return isIosUserAgent || isIPadOS
|
||||
}
|
||||
|
||||
/**
|
||||
* 检测是否在APP环境中
|
||||
* @returns {boolean}
|
||||
@ -289,6 +310,21 @@ export function gotoAppPage(pageName) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 通知 APP 调用 VAP 播放
|
||||
*/
|
||||
export function notifyAppPlayVap(sourceUrl) {
|
||||
if (!sourceUrl) return
|
||||
try {
|
||||
const message = `play_vap:${sourceUrl}`
|
||||
infoLog(message)
|
||||
// 直接使用Flutter通道
|
||||
window.FlutterPageControl.postMessage(message)
|
||||
} catch (error) {
|
||||
errorLog('通知APP调用VAP失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 打开app组件
|
||||
*/
|
||||
|
||||
119
src/views/Activities/DailyRecharge/components/vapBg.vue
Normal file
119
src/views/Activities/DailyRecharge/components/vapBg.vue
Normal file
@ -0,0 +1,119 @@
|
||||
<template>
|
||||
<div class="active-game-page">
|
||||
<video
|
||||
v-if="!useVapPlayer"
|
||||
ref="nativeVideoRef"
|
||||
class="native-background-video"
|
||||
:src="resolvedSrc"
|
||||
autoplay
|
||||
muted
|
||||
loop
|
||||
playsinline
|
||||
webkit-playsinline
|
||||
x5-playsinline
|
||||
preload="auto"
|
||||
></video>
|
||||
<VapMp4Player
|
||||
v-else
|
||||
v-model="backgroundVisible"
|
||||
class="vap-background"
|
||||
:src="src"
|
||||
mode="inline"
|
||||
fit="cover"
|
||||
:loop="true"
|
||||
:mute="true"
|
||||
:auto-close="false"
|
||||
:show-status="false"
|
||||
@error="handleVapError"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, nextTick, onMounted, ref, watch } from 'vue'
|
||||
import { showError } from '@/utils/toast.js'
|
||||
import VapMp4Player from '@/components/VapMp4Player.vue'
|
||||
import { preloadVapConfig } from '@/utils/vapMp4Config.js'
|
||||
import { resolveProtectedAssetUrl } from '@/utils/protectedAssets.js'
|
||||
|
||||
const props = defineProps({
|
||||
src: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
})
|
||||
|
||||
const backgroundVisible = ref(true)
|
||||
const nativeVideoRef = ref(null)
|
||||
const useVapPlayer = ref(false)
|
||||
const resolvedSrc = computed(() => resolveProtectedAssetUrl(props.src))
|
||||
|
||||
async function ensureNativeVideoPlayback() {
|
||||
await nextTick()
|
||||
|
||||
const video = nativeVideoRef.value
|
||||
if (!video) {
|
||||
return
|
||||
}
|
||||
|
||||
video.muted = true
|
||||
video.defaultMuted = true
|
||||
video.playsInline = true
|
||||
video.setAttribute('playsinline', '')
|
||||
video.setAttribute('webkit-playsinline', '')
|
||||
video.setAttribute('x5-playsinline', '')
|
||||
|
||||
try {
|
||||
await video.play()
|
||||
} catch (error) {
|
||||
console.warn('Native background video autoplay failed:', error)
|
||||
}
|
||||
}
|
||||
|
||||
async function resolvePlaybackMode() {
|
||||
useVapPlayer.value = false
|
||||
|
||||
if (!resolvedSrc.value) {
|
||||
return
|
||||
}
|
||||
|
||||
ensureNativeVideoPlayback()
|
||||
|
||||
const vapConfig = await preloadVapConfig(resolvedSrc.value)
|
||||
if (vapConfig) {
|
||||
useVapPlayer.value = true
|
||||
}
|
||||
}
|
||||
|
||||
function handleVapError(error) {
|
||||
console.error('VAP playback failed:', error)
|
||||
useVapPlayer.value = false
|
||||
ensureNativeVideoPlayback()
|
||||
|
||||
if (!error?.message?.includes('VAP config')) {
|
||||
showError(error?.message || 'VAP playback failed')
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
resolvePlaybackMode()
|
||||
})
|
||||
|
||||
watch(resolvedSrc, () => {
|
||||
resolvePlaybackMode()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.active-game-page {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.native-background-video {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
pointer-events: none;
|
||||
}
|
||||
</style>
|
||||
@ -15,12 +15,14 @@
|
||||
<div class="active-game-shell scroll">
|
||||
<div class="active-game-stage">
|
||||
<!-- 头部背景图 -->
|
||||
<img
|
||||
v-smart-img.noFade
|
||||
:src="imageUrl('bg2')"
|
||||
alt=""
|
||||
class="vap-background-layer"
|
||||
<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"
|
||||
@ -325,36 +327,35 @@ justify-content: flex-start;
|
||||
<div
|
||||
class="reward-showcase-vap-wrap"
|
||||
:key="mainRewardRenderKey"
|
||||
:class="{ 'reward-showcase-vap-wrap-playable': mainReward?.sourceUrl }"
|
||||
@click="playMainRewardPreview"
|
||||
:class="{
|
||||
'reward-showcase-vap-wrap-playable': isIosDevice && mainReward?.sourceUrl,
|
||||
}"
|
||||
@click="handleMainRewardPreviewClick"
|
||||
>
|
||||
<VapMp4Player
|
||||
v-if="mainReward.sourceUrl && rewardPreviewPlaying"
|
||||
:model-value="rewardPreviewPlaying"
|
||||
v-if="isAndroidDevice && mainReward.sourceUrl"
|
||||
:model-value="true"
|
||||
:src="mainReward.sourceUrl"
|
||||
mode="overlay"
|
||||
mode="inline"
|
||||
fit="contain"
|
||||
:loop="true"
|
||||
:mute="true"
|
||||
:auto-close="false"
|
||||
:show-close="true"
|
||||
:show-status="false"
|
||||
@update:modelValue="rewardPreviewPlaying = $event"
|
||||
@close="rewardPreviewPlaying = false"
|
||||
@error="handleRewardVapError"
|
||||
/>
|
||||
<img
|
||||
v-if="!mainReward.sourceUrl || !rewardPreviewPlaying"
|
||||
v-else
|
||||
:src="mainReward.cover || ''"
|
||||
alt=""
|
||||
style="display: block; width: 90%; object-fit: contain"
|
||||
@error="(e) => handleRewardImageError(e, mainReward.type)"
|
||||
/>
|
||||
<button
|
||||
v-if="mainReward.sourceUrl && !rewardPreviewPlaying"
|
||||
v-if="isIosDevice && mainReward.sourceUrl"
|
||||
type="button"
|
||||
class="reward-showcase-play-button"
|
||||
@click.stop="playMainRewardPreview"
|
||||
@click.stop="handleMainRewardPreviewClick"
|
||||
>
|
||||
<span class="reward-showcase-play-icon"></span>
|
||||
</button>
|
||||
@ -425,11 +426,7 @@ justify-content: flex-start;
|
||||
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"
|
||||
>
|
||||
<div v-for="value in 2" :key="value" class="reward-extra-scroll-content">
|
||||
{{ getRewardDisplayText(reward) }}
|
||||
</div>
|
||||
</div>
|
||||
@ -511,11 +508,16 @@ justify-content: flex-start;
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||
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 { getPngUrl } from '@/config/imagePaths.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'
|
||||
@ -534,7 +536,9 @@ import { useThrottle } from '@/utils/useDebounce.js'
|
||||
import BackgroundLayer from '@/components/BackgroundLayer.vue'
|
||||
import itemCenter from '@/components/itemCenter.vue'
|
||||
import maskLayer from '@/components/MaskLayer.vue'
|
||||
import VapMp4Player from '@/components/VapMp4Player.vue'
|
||||
|
||||
const VapMp4Player = defineAsyncComponent(() => import('@/components/VapMp4Player.vue'))
|
||||
const VapBg = defineAsyncComponent(() => import('./components/vapBg.vue'))
|
||||
|
||||
const isLoading = ref(true)
|
||||
|
||||
@ -547,10 +551,74 @@ const currentLangType = computed(() => {
|
||||
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
|
||||
@ -713,8 +781,8 @@ const rewardGridList = computed(() => {
|
||||
const mainRewardRenderKey = computed(() => {
|
||||
return mainReward.value ? getRewardRenderKey(mainReward.value, 0) : 'empty-main-reward'
|
||||
})
|
||||
const rewardPreviewPlaying = ref(false)
|
||||
const rewardVapPreloadElements = new Map()
|
||||
const criticalVapPreloadElements = new Map()
|
||||
let rewardVapPreloadTimer = null
|
||||
let rewardVapPreloadIdleCallbackId = null
|
||||
let rewardVapPreloadStarted = false
|
||||
@ -749,7 +817,7 @@ const getRewardVapUrls = (rewardTopList) => {
|
||||
}
|
||||
|
||||
const preloadRewardVap = (sourceUrl) => {
|
||||
if (!sourceUrl || rewardVapPreloadElements.has(sourceUrl)) {
|
||||
if (!isAndroidDevice.value || !sourceUrl || rewardVapPreloadElements.has(sourceUrl)) {
|
||||
return
|
||||
}
|
||||
|
||||
@ -785,8 +853,39 @@ const preloadRewardVaps = (rewardTopList) => {
|
||||
getRewardVapUrls(rewardTopList).forEach(preloadRewardVap)
|
||||
}
|
||||
|
||||
const preloadCriticalVap = (sourceUrl) => {
|
||||
if (!isAndroidDevice.value || !sourceUrl) {
|
||||
return Promise.resolve()
|
||||
}
|
||||
|
||||
const resolvedSourceUrl = resolveProtectedAssetUrl(sourceUrl)
|
||||
const configPromise = preloadVapConfig(resolvedSourceUrl)
|
||||
|
||||
if (criticalVapPreloadElements.has(resolvedSourceUrl) || typeof document === 'undefined') {
|
||||
return configPromise
|
||||
}
|
||||
|
||||
const preloadVideo = document.createElement('video')
|
||||
preloadVideo.preload = 'auto'
|
||||
preloadVideo.muted = true
|
||||
preloadVideo.playsInline = true
|
||||
preloadVideo.src = resolvedSourceUrl
|
||||
preloadVideo.style.position = 'fixed'
|
||||
preloadVideo.style.left = '-9999px'
|
||||
preloadVideo.style.top = '-9999px'
|
||||
preloadVideo.style.width = '1px'
|
||||
preloadVideo.style.height = '1px'
|
||||
preloadVideo.style.opacity = '0'
|
||||
preloadVideo.style.pointerEvents = 'none'
|
||||
document.body.appendChild(preloadVideo)
|
||||
preloadVideo.load()
|
||||
criticalVapPreloadElements.set(resolvedSourceUrl, preloadVideo)
|
||||
|
||||
return configPromise
|
||||
}
|
||||
|
||||
const startRewardVapPreload = () => {
|
||||
if (rewardVapPreloadStarted || !activityRewards.value.length) {
|
||||
if (!isAndroidDevice.value || rewardVapPreloadStarted || !activityRewards.value.length) {
|
||||
return
|
||||
}
|
||||
|
||||
@ -795,7 +894,7 @@ const startRewardVapPreload = () => {
|
||||
}
|
||||
|
||||
const queueRewardVapPreload = () => {
|
||||
if (rewardVapPreloadStarted || typeof window === 'undefined') {
|
||||
if (!isAndroidDevice.value || rewardVapPreloadStarted || typeof window === 'undefined') {
|
||||
return
|
||||
}
|
||||
|
||||
@ -912,22 +1011,19 @@ const openPopup = (type) => {
|
||||
|
||||
const handleRewardVapError = (error) => {
|
||||
console.error('Reward VAP playback failed:', error)
|
||||
rewardPreviewPlaying.value = false
|
||||
showError(error?.message || t('active_game_vap_playback_failed'))
|
||||
}
|
||||
|
||||
const playMainRewardPreview = () => {
|
||||
const handleMainRewardPreviewClick = () => {
|
||||
if (!mainReward.value?.sourceUrl) {
|
||||
return
|
||||
}
|
||||
|
||||
rewardPreviewPlaying.value = true
|
||||
if (isIosDevice.value) {
|
||||
notifyAppPlayVap(mainReward.value.sourceUrl)
|
||||
}
|
||||
}
|
||||
|
||||
watch(mainRewardRenderKey, () => {
|
||||
rewardPreviewPlaying.value = false
|
||||
})
|
||||
|
||||
const claimTask1Reward = useThrottle(async () => {
|
||||
if (!canClaimTask1Reward.value) {
|
||||
return
|
||||
@ -1017,9 +1113,7 @@ const preloadCriticalImages = async () => {
|
||||
imageUrl(getImgName('ruleTitle')),
|
||||
]
|
||||
|
||||
await Promise.all([
|
||||
preloadImages(criticalImages),
|
||||
])
|
||||
await Promise.all([preloadImages(criticalImages), preloadCriticalVap(bgVapUrl)])
|
||||
}
|
||||
|
||||
const completePreloading = async () => {
|
||||
@ -1068,10 +1162,18 @@ const connectToAppHandler = async () => {
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
if (connectToAppFallbackTimer !== null && typeof window !== 'undefined') {
|
||||
window.clearTimeout(connectToAppFallbackTimer)
|
||||
connectToAppFallbackTimer = null
|
||||
@ -1095,6 +1197,13 @@ onBeforeUnmount(() => {
|
||||
video.parentNode && video.parentNode.removeChild(video)
|
||||
})
|
||||
rewardVapPreloadElements.clear()
|
||||
criticalVapPreloadElements.forEach((video) => {
|
||||
video.pause()
|
||||
video.removeAttribute('src')
|
||||
video.load()
|
||||
video.parentNode && video.parentNode.removeChild(video)
|
||||
})
|
||||
criticalVapPreloadElements.clear()
|
||||
})
|
||||
</script>
|
||||
|
||||
@ -1144,9 +1253,14 @@ onBeforeUnmount(() => {
|
||||
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;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user