Files
faceFamilySource/src/components/HomePage.vue
2025-09-17 15:05:05 +08:00

584 lines
15 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<script setup>
import { ref, computed, watch, onMounted } from "vue"
import { useRouter, useRoute } from 'vue-router'
import faceFamily from "../assets/audio/faceFamily.mp3"
import backgroundVideo from "../assets/video/1_stab_chf3_rhea1.mp4"
import MyPhoto from './MyPhoto.vue'
import PhotoSquare from './PhotoSquare.vue'
import globalToastEvent, { ToastType } from '../globalToastEvent';
import { globalStore } from "../globalstore.js";
import Login from '../components/Login.vue'
import { isWeixin, isLogin, getParam, Storage, Request } from "../libs/utils"
defineProps({
show: true
})
const isMusicOn = ref(false);
const audioElement = ref(null);
const videoElement = ref(null);
const videoLoaded = ref(false);
const videoError = ref(false);
// 初始化全局音频实例
const initGlobalAudio = () => {
if (!globalStore.globalAudio) {
globalStore.globalAudio = new Audio(faceFamily);
globalStore.globalAudio.loop = true; // 设置循环播放
globalStore.globalAudio.preload = 'auto';
// 监听音频事件
globalStore.globalAudio.addEventListener('play', () => {
isMusicOn.value = true;
});
globalStore.globalAudio.addEventListener('pause', () => {
isMusicOn.value = false;
});
globalStore.globalAudio.addEventListener('ended', () => {
isMusicOn.value = false;
});
}
audioElement.value = globalStore.globalAudio;
// 同步当前音乐状态
isMusicOn.value = !globalStore.globalAudio.paused;
};
onMounted(() => {
initGlobalAudio();
initVideo();
checkAndPlayAudio();
})
// 检查并播放音频
const checkAndPlayAudio = () => {
if (!audioElement.value) return;
// 如果用户曾经手动静音,则不自动播放
if (globalStore.userMutedMusic) {
console.log('用户曾经手动静音,不自动播放音乐');
isMusicOn.value = false;
return;
}
// 检查音频是否已在播放
if (!audioElement.value.paused) {
console.log('音频已在播放,跳过重复播放');
isMusicOn.value = true;
return;
}
// 只在首次访问或音乐没有被用户手动静音时才尝试自动播放
if (globalStore.isFirstVisitHomePage || !globalStore.userMutedMusic) {
const playPromise = audioElement.value.play();
if (playPromise !== undefined) {
playPromise.then(() => {
// 自动播放成功
isMusicOn.value = true;
globalStore.isFirstVisitHomePage = false;
console.log('自动播放成功');
})
.catch(error => {
// 自动播放被阻止
console.log('自动播放被阻止,需要用户交互:', error);
isMusicOn.value = false;
audioElement.value.pause();
});
}
} else {
// 非首次访问且用户没有手动静音,但音乐当前暂停,则保持暂停状态
isMusicOn.value = false;
}
};
// 初始化视频
const initVideo = () => {
setTimeout(() => {
if (videoElement.value) {
const video = videoElement.value;
// 移动设备视频优化设置
video.muted = true;
video.loop = true;
video.autoplay = true;
video.playsInline = true;
video.setAttribute('webkit-playsinline', 'true');
video.setAttribute('playsinline', 'true');
video.setAttribute('x5-video-player-type', 'h5');
video.setAttribute('x5-video-player-fullscreen', 'false');
// 移动设备特殊处理
const isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
if (isMobile) {
video.preload = 'metadata'; // 移动设备使用较小的预加载
// 尝试在用户交互后播放
const playVideo = () => {
video.play().then(() => {
console.log('移动设备视频播放成功');
videoLoaded.value = true;
}).catch(error => {
console.log('移动设备视频播放失败:', error);
videoError.value = true;
});
};
// 在用户点击时尝试播放视频
document.addEventListener('touchstart', playVideo, { once: true });
document.addEventListener('click', playVideo, { once: true });
}
// 按钮交互触发视频播放(用于 iOS 等严格的自动播放限制)
const buttons = document.querySelectorAll('.scene-item');
buttons.forEach(button => {
button.addEventListener('click', () => {
if (video.paused && !videoError.value) {
video.play().catch(error => {
console.log('点击后视频播放失败:', error);
});
}
}, { once: true });
});
// 监听视频事件
video.addEventListener('loadedmetadata', () => {
console.log('视频元数据加载完成');
if (!isMobile) {
video.play().catch(error => {
console.log('PC设备视频自动播放失败:', error);
videoError.value = true;
});
}
});
video.addEventListener('canplay', () => {
videoLoaded.value = true;
videoError.value = false;
});
}
}, 200);
};
// 播放/暂停切换
const toggleMusicState = () => {
if (!audioElement.value) {
initGlobalAudio();
}
if (isMusicOn.value) {
// 当前在播放,用户点击静音
audioElement.value.pause();
globalStore.userMutedMusic = true; // 记录用户手动静音
isMusicOn.value = false;
console.log('用户手动静音音乐');
} else {
// 当前暂停,用户点击播放
const playPromise = audioElement.value.play();
if (playPromise !== undefined) {
playPromise.then(() => {
globalStore.userMutedMusic = false; // 用户重新开启音乐,清除静音标记
isMusicOn.value = true;
console.log('用户手动开启音乐');
})
.catch(error => {
console.log('播放失败:', error);
isMusicOn.value = false;
});
}
}
};
// 视频事件处理
const onVideoLoadStart = () => {
console.log('视频开始加载');
};
const onVideoCanPlay = () => {
console.log('视频可以播放');
videoLoaded.value = true;
videoError.value = false;
};
const onVideoLoaded = () => {
console.log('视频数据加载完成');
videoLoaded.value = true;
};
const onVideoError = (event) => {
console.error('视频加载错误:', event);
videoError.value = true;
videoLoaded.value = false;
};
const navigateTodoList = () => {
globalToastEvent.emit(ToastType.SHOW_TODO)
}
const handlePrizeList = async () => {
globalToastEvent.emit(ToastType.SHOW_PRIZELIST)
}
const handleRule = () => {
globalToastEvent.emit(ToastType.SHOW_RULE)
}
const handleLottery = () => {
globalToastEvent.emit(ToastType.SHOW_LOTTERY)
if (globalStore.draw_chances <= 0) return;
globalStore.reducerDrawChances();
}
const router = useRouter();
const navigateSelectTemplatePage = () => {
if (globalStore.game_chances <= 0) return;
globalStore.reducerGameChances();
router.push({
name: 'selectTemplateV2'
})
}
const route = useRoute()
const myPhotoValue = route.query.myPhotoValue;
const isMyPhotoVisible = ref(false);
const isPhotoSquareVisible = ref(false);
const showMyPhoto = () => {
isMyPhotoVisible.value = true;
isPhotoSquareVisible.value = false;
}
const showPhotoSquare=()=>{
isMyPhotoVisible.value = false;
isPhotoSquareVisible.value = true;
}
watch(() => myPhotoValue, async (newVal) => {
if (!newVal) {
return
}
isMyPhotoVisible.value = true;
}, { immediate: true })
const loginShow = ref(false)
// 登录状态
const userStatus = async (callback) => {
const pageCode = getParam("code")
const userinfos = Storage.get("userinfos")
if (isWeixin()) {
if (!pageCode) {
weui.alert("未获取到code")
return
}
const weixinResult = await Request("wechat/login", {
code: pageCode
})
Storage.set("userinfos", weixinResult.json)
if (weixinResult.json && weixinResult.json.phone) {
loginShow.value = false
callback && callback()
} else {
loginShow.value = true
}
} else {
loginShow.value = true
}
}
//初始化持久数据
const initUserGameInfos = async (refresh_official, refresh_cap_scan) => {
const result = await Request('game/info', { refresh_official: refresh_official, refresh_cap_scan: refresh_cap_scan }, "GET")
if (result.res.status === 200) {
globalStore.draw_chances = result.json.draw_chances
globalStore.game_chances = result.json.game_chances
globalStore.invitees = result.json.invitees
globalStore.followed_official = result.json.followed_official
globalStore.cap_scan = result.json.cap_scan
globalStore.game_chances_view_recipes = result.json.game_chances_view_recipes
globalStore.MAX_VIEW_RECIPES_DAILY = result.json.constants.MAX_VIEW_RECIPES_DAILY
globalStore.CONSUME_POINT_1_PER_DRAW = result.json.constants.CONSUME_POINT_1_PER_DRAW
globalStore.MAX_CAP_SCAN = result.json.constants.MAX_CAP_SCAN
globalStore.MAX_INVITE_DAILY = result.json.constants.MAX_INVITE_DAILY
globalToastEvent.emit(ToastType.MOUNTED)
}
}
const handleLoginSuccess = async () => {
console.log("已登录")
loginShow.value = false
await initUserGameInfos(true, true)
}
if (isLogin()) {
handleLoginSuccess()
} else {
userStatus(handleLoginSuccess)
}
let mergeId = '';
const urlParams = new URLSearchParams(window.location.search);
mergeId = urlParams.get('merge_id');
watch(() => mergeId, async (newVal) => {
if (!newVal) {
return
}
if (newVal) {
isPhotoSquareVisible.value = true;
}
}, {immediate: true})
</script>
<template>
<div :show="show" class="main">
<div class="home-wrapper">
<!-- 视频背景 -->
<video
ref="videoElement"
class="background-video"
:src="backgroundVideo"
autoplay
loop
muted
playsinline
webkit-playsinline
preload="metadata"
x5-video-player-type="h5"
x5-video-player-fullscreen="false"
x5-video-orientation="portraint"
@loadstart="onVideoLoadStart"
@canplay="onVideoCanPlay"
@error="onVideoError"
@loadeddata="onVideoLoaded"
>
<!-- 为不支持视频的设备提供 fallback -->
<p style="display: none;">您的浏览器不支持视频播放</p>
</video>
<div class="scene-item logo">
<img src="../assets/images/logo.webp" alt="logo">
</div>
<div class="scene-item slogan">
<img src="../assets/images/slogan.webp" alt="slogan">
</div>
<!-- fallback 背景图当视频无法加载时显示 -->
<div v-if="videoError || !videoLoaded" class="fallback-background"></div>
<div class="scene-item item-1" @click="handleLottery" :class="{ 'disabled': globalStore.draw_chances <= 0 }">
<img src="../assets/images/lottery.webp" alt="抽奖">
<div class="lottery-main">
<p class="lottery-value">{{ globalStore.draw_chances }}</p>
</div>
</div>
<div class="scene-item item-2" @click="navigateSelectTemplatePage" :class="{ 'disabled': globalStore.game_chances <= 0 }">
<img src="../assets/images/join.webp" alt="立即参与">
<div class="join-main">
<p class="join-value"><span>X</span>{{ globalStore.game_chances }}</p>
</div>
</div>
<div class="scene-item item-3" @click="navigateTodoList">
<img src="../assets/images/task.webp" alt="任务">
</div>
<div @click="toggleMusicState">
<div v-if="isMusicOn" key="on" class="scene-item item-4">
<img src="../assets/images/music-on.webp" alt="音乐开">
</div>
<div v-else key="off" class="scene-item item-5">
<img src="../assets/images/music-off.webp" alt="音乐关">
</div>
</div>
<div class="scene-item item-6" @click="handleRule">
<img src="../assets/images/rule.webp" alt="规则">
</div>
<div class="scene-item item-7" @click="handlePrizeList">
<img src="../assets/images/award.webp" alt="奖励">
</div>
<div class="scene-item item-8" @click="showMyPhoto">
<img src="../assets/images/my-photo.webp" alt="我的照片">
</div>
<div class="scene-item item-9" @click="showPhotoSquare">
<img src="../assets/images/photos.webp" alt="照片广场">
</div>
</div>
</div>
<MyPhoto @go-photo-square="showPhotoSquare" v-model:show="isMyPhotoVisible" />
<PhotoSquare @go-my-photo="showMyPhoto" v-model:show="isPhotoSquareVisible" />
<Login :show="loginShow" @login-success="handleLoginSuccess" />
</template>
<style scoped>
.logo {
top: 4vw;
width: 24vw;
left: 4vw;
}
.slogan {
top: 20vw;
width: 76vw;
}
.main {
height: 100%;
overflow-y: auto;
}
.home-wrapper {
width: 100vw;
height: 200vw;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
position: relative;
min-height: -webkit-fill-available;
overflow: hidden; /* 防止视频溢出 */
}
/* 视频背景样式 */
.background-video {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
object-fit: cover; /* 保持视频比例并填满容器 */
z-index: 0; /* 置于最底层 */
pointer-events: none; /* 禁止视频交互,避免影响按钮点击 */
}
/* fallback 背景图 */
.fallback-background {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-image: url('../assets/images/home-bg.webp');
background-size: cover;
background-repeat: no-repeat;
background-position: center;
z-index: -1; /* 置于视频下方 */
}
.scene-item {
position: fixed;
cursor: pointer;
transition: all 0.4s ease;
overflow: hidden;
animation: float 4s ease-in-out infinite;
}
.scene-item:hover {
transform: scale(1.05);
}
.scene-item img {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
}
.item-1 {
width: 21vw;
bottom: 5vw;
left: 0;
animation-delay: 0s;
}
.lottery-main {
display: flex;
justify-content: center;
align-items: center;
width: 4vw;
height: 3.5vw;
position: absolute;
top: 1.5vw;
right: 6.6vw;
color: #fff;
}
.lottery-main .lottery-value {
font-size: 10px;
}
.item-2 {
width: 48vw;
bottom: 5vw;
animation-delay: 0s;
}
.join-main {
position: absolute;
top: 3.4vw;
right: -0.4vw;
width: 13vw;
}
.join-main .join-value {
margin: 0;
color: white;
text-stroke: 4px #ff0000;
-webkit-text-stroke: 1px #ff0000;
font-size: 24px;
font-weight: 900;
}
.join-main .join-value span {
padding-right: 0.2vw;
}
.item-3 {
width: 21vw;
bottom: 5vw;
right: 0;
animation-delay: 0s;
}
.item-4 {
width: 11vw;
top: 1.5%;
right: 1.5%;
animation-delay: 0s;
}
.item-5 {
width: 11vw;
top: 1.5%;
right: 1.5%;
animation-delay: 0s;
}
.item-6 {
width: 14.5vw;
top: 8%;
right: 0;
animation-delay: 0s;
}
.item-7 {
width: 14.5vw;
top: 13.5%;
right: 0;
animation-delay: 0s;
}
.item-8 {
width: 11vw;
bottom: 32%;
right: 1%;
animation-delay: 0s;
}
.item-9 {
width: 11vw;
bottom: 23%;
right: 1%;
animation-delay: 0s;
}
@keyframes loginloading {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
</style>