Files
zhaoma/src/components/GameSwiper.vue
2025-12-25 15:55:57 +08:00

516 lines
11 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

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

<template>
<ModalTransition class="game-swiper" :show="show">
<div class="scene-item item-1" @click="handleGoHome">
<img src="../assets/images/new/go-home.webp" alt="回到首页">
</div>
<div class="confirm-layout" v-if="isConfirmBtnDisplay">
<div class="scene-item item-3" @click="handleConfirmClick">
<img src="../assets/images/new/confirm-btn.webp" alt="确定">
</div>
</div>
<div class="swiper-page">
<div class="carousel-container">
<div
class="carousel-wrapper"
@touchstart="handleTouchStart"
@touchmove="handleTouchMove"
@touchend="handleTouchEnd"
>
<div
class="carousel-track"
:style="{ transform: `translateX(calc(-${currentIndex * 104}% + ${currentIndex * 60}px))`, transition: isTransitioning ? 'transform 0.3s ease' : 'none' }"
>
<div
v-for="(slide, index) in slides"
:key="index"
:class="['carousel-slide', { active: currentIndex === index }]"
>
<img v-show="slide.show" class="complete-icon" src="../assets/images/new/complete-icon.webp" alt="已完成">
<img :src="slide.image" :alt="slide.title" />
<div class="shou-dev">
<div class="show-position">
<div class="action-btn">
<img class="shou-click" src="../assets/images/new/pre-icon.webp" alt="左" />
<img class="shou-click" src="../assets/images/new/next-icon.webp" alt="右" />
</div>
<img class="shou-btn" src="../assets/images/new/shouzhi.webp" alt="手" />
</div>
</div>
</div>
</div>
</div>
<!-- 指示器 -->
<div class="carousel-indicators">
<span
v-for="(slide, index) in slides"
:key="index"
:class="['indicator', { active: currentIndex === index }]"
@click="goToSlide(index)"
></span>
</div>
</div>
</div>
</ModalTransition>
</template>
<script setup>
import { ref, onMounted, onUnmounted, watch, computed } from 'vue'
import ModalTransition from "./ModalTransition.vue"
import globalToastEvent, { ToastType } from '../globalToastEvent';
import { Request } from "../libs/utils"
import slider1 from '../assets/images/new/slider1.webp';
import slider2 from '../assets/images/new/slider2.webp';
import slider3 from '../assets/images/new/slider3.webp';
import slider4 from '../assets/images/new/slider4.webp';
import slider5 from '../assets/images/new/slider5.webp';
import { globalStore } from "../globalstore.js";
const props = defineProps({
show: false,
})
const emit = defineEmits(['close'])
// 轮播图数据
const slides = ref([
{
image: slider1,
title: '轮播图1',
id: 1,
show: true,
},
{
image: slider2,
title: '轮播图2',
id: 2,
show: true,
},
{
image: slider3,
title: '轮播图3',
id: 3,
show: true,
},
{
image: slider4,
title: '轮播图4',
id: 4,
show: true,
},
{
image: slider5,
title: '轮播图5',
id: 5,
show: true,
}
])
const isConfirmBtnDisplay = computed(() => {
const slide = slides.value?.[currentIndex.value];
return !globalStore.completed_games_today.includes(`game${slide.id}`);
})
const getGameInfo = async () => {
slides.value = slides.value.map(slide => ({
...slide,
show: globalStore.completed_games_today.includes(`game${slide.id}`)
}));
}
watch(() => props.show, async (newVal) => {
if (!newVal) {
return
}
getGameInfo();
}, { immediate: true })
const handleGoHome = () => {
emit('close')
}
const currentIndex = ref(0)
const startX = ref(0)
const moveX = ref(0)
const isDragging = ref(false)
const isTransitioning = ref(true)
let autoPlayTimer = null
// 触摸开始
const handleTouchStart = (e) => {
startX.value = e.touches[0].clientX
isDragging.value = true
isTransitioning.value = false
stopAutoPlay()
}
// 触摸移动
const handleTouchMove = (e) => {
if (!isDragging.value) return
moveX.value = e.touches[0].clientX - startX.value
}
// 触摸结束
const handleTouchEnd = () => {
if (!isDragging.value) return
isTransitioning.value = true
const threshold = 50 // 滑动阈值
if (moveX.value > threshold) {
// 向右滑动 - 上一张
prevSlide()
} else if (moveX.value < -threshold) {
// 向左滑动 - 下一张
nextSlide()
}
isDragging.value = false
moveX.value = 0
// startAutoPlay()
}
// 上一张
const prevSlide = () => {
currentIndex.value = currentIndex.value > 0
? currentIndex.value - 1
: slides.value.length - 1
}
// 下一张
const nextSlide = () => {
currentIndex.value = currentIndex.value < slides.value.length - 1
? currentIndex.value + 1
: 0
}
// 跳转到指定幻灯片
const goToSlide = (index) => {
isTransitioning.value = true
currentIndex.value = index
stopAutoPlay()
// startAutoPlay()
}
const hasVisitedBefore = localStorage.getItem('hasVisitedGameSwiper');
const handleConfirmClick = async () => {
const loading = weui.loading()
const slide = slides.value?.[currentIndex.value]
if (!slide) return
handleSlideClick(slide)
// 点击确定按钮开始游戏调用一次game log接口
const gameSource = `game${slide.id}`;
globalStore.game_id = gameSource;
let gameLogRes = await Request("game/log", { source: globalStore.game_id }, "POST");
if (!gameLogRes || !gameLogRes.res || (gameLogRes.res.status !== 200 && gameLogRes.res.status !== 201)) {
emit('close')
loading.hide()
return
} else {
globalStore.current_game_log_id = gameLogRes.json.log_id;
}
}
const handleSlideClick = (slide) => {
if (!hasVisitedBefore) {
localStorage.setItem('hasVisitedGameSwiper', 'true');
globalToastEvent.emit(ToastType.SHOW_GAMEDEMO)
globalToastEvent.emit(ToastType.SHOW_GAMEPAGE, slide)
} else {
globalToastEvent.emit(ToastType.SHOW_GAMEPAGE, slide)
}
}
// 自动播放
const startAutoPlay = () => {
autoPlayTimer = setInterval(() => {
nextSlide()
}, 3000) // 每3秒切换一次
}
// 停止自动播放
const stopAutoPlay = () => {
if (autoPlayTimer) {
clearInterval(autoPlayTimer)
autoPlayTimer = null
}
}
// 生命周期
onMounted(() => {
// startAutoPlay()
})
onUnmounted(() => {
stopAutoPlay()
})
</script>
<style scoped>
/* 手指轻微上下浮动 */
.shou-dev .shou-btn {
width: 25vw;
position: absolute;
top: 7vw;
left: 20vw;
animation: hand-bounce 1.2s ease-in-out infinite;
transform-origin: center bottom;
}
@keyframes hand-bounce {
0%, 100% {
transform: translateY(0);
}
50% {
transform: translateY(-6px); /* 上浮一点点 */
}
}
/* 确保按钮容器居中,方便做左右位移动画 */
.action-btn {
display: flex;
flex-flow: row;
justify-content: center;
align-items: center;
position: relative;
}
/* 左箭头 向左轻微移动 */
.action-btn .shou-click:first-child {
width: 24vw;
animation: arrow-left-move 1.2s ease-in-out infinite;
}
@keyframes arrow-left-move {
0%, 100% {
transform: translateX(0);
opacity: 0.6;
}
50% {
transform: translateX(-6px); /* 往左移动一点 */
opacity: 1;
}
}
/* 右箭头 向右轻微移动 */
.action-btn .shou-click:last-child {
width: 24vw;
animation: arrow-right-move 1.2s ease-in-out infinite;
}
@keyframes arrow-right-move {
0%, 100% {
transform: translateX(0);
opacity: 0.6;
}
50% {
transform: translateX(6px); /* 往右移动一点 */
opacity: 1;
}
}
.show-position {
position: relative;
}
.shou-dev {
position: absolute;
top: 50vw;
left: 11vw;
}
.shou-dev .shou-btn {
width: 25vw;
position: absolute;
top: 7vw;
left: 20vw;
}
.action-btn {
display: flex;
flex-flow: row;
justify-content: center;
}
.action-btn .shou-click {
width: 24vw;
}
.complete-icon {
width: 14vw !important;
position: absolute;
left: 2vw;
top: -2vw;
}
.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: 14vw;
height: 14vw;
top: 6.8vw;
left: 3vw;
animation-delay: 0s;
}
.item-2 {
width: 12vw;
height: 12vw;
top: 8vw;
right: 2vw;
animation-delay: 0s;
}
.item-3 {
width: 46vw;
bottom: 14vw;
animation-delay: 0s;
}
.confirm-layout {
display: flex;
justify-content: center;
}
.swiper-page {
width: 100%;
min-height: 100vh;
background: #f5f5f5;
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
box-sizing: border-box;
background-image: url('../assets/images/new/swiper-page-bg.webp');
background-size: cover;
background-repeat: no-repeat;
background-position: center;
}
.carousel-container {
width: 100%;
max-width: 100vw;
position: relative;
padding: 0 20px;
box-sizing: border-box;
}
.carousel-wrapper {
width: 80vw;
overflow: visible;
position: relative;
touch-action: pan-y;
}
.carousel-track {
display: flex;
flex-direction: row;
width: 100%;
padding: 20px 0;
}
.carousel-slide {
min-width: calc(100% - 160px);
width: calc(100% - 160px);
margin: 0 20px;
flex-shrink: 0;
cursor: pointer;
user-select: none;
transition: all 0.3s ease;
border-radius: 12px;
overflow: hidden;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
transform: scale(0.85);
opacity: 0.5;
filter: grayscale(60%) brightness(0.7);
}
.carousel-slide.active {
transform: scale(1);
opacity: 1;
filter: grayscale(0%) brightness(1);
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.25);
z-index: 2;
}
.carousel-slide img {
width: 100%;
height: auto;
display: block;
object-fit: cover;
pointer-events: none;
}
/* 指示器 */
.carousel-indicators {
position: absolute;
bottom: -8vw;
left: 50%;
transform: translateX(-50%);
display: flex;
gap: 2vw;
padding: 2vw 4vw;
background: rgba(0, 0, 0, 0.3);
border-radius: 4vw;
backdrop-filter: blur(4px);
}
.indicator {
width: 10px;
height: 10px;
border-radius: 50%;
background: rgba(255, 255, 255, 0.6);
cursor: pointer;
transition: all 0.3s ease;
border: 2px solid transparent;
}
.indicator:hover {
background: rgba(255, 255, 255, 0.8);
transform: scale(1.2);
}
.indicator.active {
background: #fff;
width: 28px;
border-radius: 5px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
}
/* 移动端适配 */
@media (max-width: 768px) {
.game-page {
padding: 10px;
}
.carousel-container {
padding: 0 10px;
}
.carousel-slide {
min-width: calc(100% - 12vw);
width: calc(100% - 12vw);
margin: 0 10px;
border-radius: 8px;
}
.carousel-track {
padding: 10px 0;
}
}
</style>