This commit is contained in:
yixu
2025-12-19 11:21:04 +08:00
commit 258e14e27c
82 changed files with 5371 additions and 0 deletions

71
src/components/Ad.vue Normal file
View File

@@ -0,0 +1,71 @@
<template>
<ModalTransition class="ad" :show="show" position="center">
<div class="ad-wrapper">
<div class="btn-ad" v-html="adHtml" @click="adGoto"></div>
<div class="close" @click="emit('close')"></div>
</div>
</ModalTransition>
</template>
<script setup>
import ModalTransition from "./ModalTransition.vue"
import { ref } from "vue"
import { isWeixinPlatform, miniJumpToActive, getMiniPageBtnHack } from "../libs/utils"
const props = defineProps({
show: false,
})
const emit = defineEmits(['close'])
const adHtml = ref('')
//TODO: 这个地址后面要改的
adHtml.value = getMiniPageBtnHack("/pages/retail-act/landing-page/ordinary?id=897432916524553363&orgId=200282401019674482&programId=84796583983972352")
const adGoto = () => {
if (!isWeixinPlatform()) {
weui.alert("请前往「泸州老窖会员中心」小程序进行查询")
}else{
miniJumpToActive()
}
}
</script>
<style>
.ad-wrapper {
position: relative;
width: 100vw;
height: 190vw;
background-image: url("../assets/images/ad-bg.webp");
background-repeat: no-repeat;
background-size: 100%;
}
.btn-ad {
width: 54.351852vw;
height: 20.092593vw;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, 180%);
background-image: url("../assets/images/btn-ad.webp");
background-repeat: no-repeat;
background-size: 100%;
}
.btn-ad wx-open-launch-weapp#launch-btn{
width: 100%;
height: 100%;
display: block !important;
}
.ad .close {
position: absolute;
width: 8.148148vw;
height: 8.148148vw;
top: 50%;
left: 50%;
transform: translate(-50%, 0);
margin-top: 58vw;
background-image: url("../assets/images/close-btn.webp");
background-repeat: no-repeat;
background-size: 100%;
}
</style>

255
src/components/Address.vue Normal file
View File

@@ -0,0 +1,255 @@
<template>
<ModalTransition :show="show">
<div class="address-wrapper">
<div class="address-title">收货地址填写</div>
<div class="address-content">
<div class="address-item">
<div class="address-item-label">收货人</div>
<input type="text" class="i-name" v-model="name" placeholder="请填写收货人姓名" />
</div>
<div class="address-item">
<div class="address-item-label">联系方式</div>
<input type="number" class="i-phone" v-model="phone" placeholder="请填写手机号码" />
</div>
<div class="address-item">
<div class="address-item-label">所在地区</div>
<div class="address-area" @click="changeArea">{{ area ? area : '请选择地理位置' }}</div>
</div>
<div class="address-item address-textarea">
<div class="address-item-label">详细地址</div>
<textarea placeholder="请填写详细地址" v-model="address"></textarea>
</div>
</div>
<div class="btn-submit" :class="{ disable: btnDisableClass }" @click="handleSubmit">保存</div>
<div class="address-close" @click="$emit('addressClose')"></div>
</div>
</ModalTransition>
</template>
<script setup>
import { ref, computed } from "vue"
import { Request } from "../libs/utils"
import AREA from "../libs/area"
import ModalTransition from "./ModalTransition.vue"
const props = defineProps({
show: false,
prizeId: Number
})
const emit = defineEmits(['addressSubmit','addressClose'])
// const addressShow = computed(() => props.prizeId ? props.show : false)
const btnDisableClass = ref(false)
const name = ref('')
const phone = ref('')
const province = ref('')
const city = ref('')
const county = ref('')
const area = computed(() => `${province.value}${city.value}${county.value}`)
const address = ref('')
const changeArea = () => {
weui.picker(AREA, {
defaultValue: ["110000", "110000", '110101'],
onConfirm: (result) => {
province.value = result[0].label
city.value = result[1].label
county.value = result[2].label
}
})
}
const checkForm = () => {
if (name.value == "") {
weui.alert("请输入联系人")
return false
} else if (phone.value === "" || phone.value.toString().length !== 11) {
weui.alert("请输入正确的手机号")
return false
} else if (!province.value || !city.value || !county.value) {
weui.alert("请选择地区")
return false
} else if (address.value == "") {
weui.alert("请输入详细地址")
return false
} else {
return true
}
}
const handleSubmit = async () => {
if (btnDisableClass.value) {
return
}
if (!checkForm()) {
return
}
btnDisableClass.value = true
const result = await Request(`user/address`, {
name: name.value,
phone: phone.value,
province: province.value,
city: city.value,
county: county.value,
address: address.value,
lottery_log_id: props.prizeId,
})
if (result.res.status == 200 || result.res.status == 201) {
emit("addressSubmit", { id: props.prizeId })
}else{
emit('addressClose')
}
btnDisableClass.value = false
}
</script>
<style scoped>
.address-wrapper {
width: 100%;
background-color: #f2f3f8;
border-radius: 2vw 2vw 0 0;
animation: transitionIn ease 0.3s forwards;
position: relative;
padding: 0 4vw;
padding-bottom: 14vw;
position: relative;
}
.address-title {
font-size: 3.703704vw;
font-weight: 700;
padding: 4vw 0;
}
.address-content {
display: flex;
flex-direction: column;
align-items: flex-end;
margin-bottom: 10vw;
}
.address-content input,
.address-content textarea {
padding: 0;
border: none;
width: 100%;
height: 100%;
resize: none;
background: none;
line-height: 1.2;
}
.address-area,
.address-content input,
.address-content textarea {
font-size: 2.962963vw;
}
.address-content textarea {
margin-top: .1vw;
}
.address-area {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.address-content input::placeholder,
.address-content textarea::placeholder,
.address-area {
color: #86746e;
}
.address-item {
width: 100%;
position: relative;
display: flex;
padding: 3vw 0;
align-items: center;
}
.address-item::after {
content: "";
box-sizing: border-box;
position: absolute;
left: 0;
top: 0;
width: 200%;
height: 200%;
border-bottom: 1px solid gray;
transform: scale(0.5);
transform-origin: 0 0;
pointer-events: none;
}
.address-item-label {
min-width: 20vw;
font-size: 3vw;
line-height: 1.2;
color: #000;
}
.address-item input {
flex: 1;
line-height: 100%;
height: 6vw;
}
.btn-submit {
text-align: center;
border-radius: 1vw;
background-color: #70b2e2;
margin-top: 4vw;
padding: 2vw;
color: #fff;
position: relative;
}
.btn-submit.disable {
opacity: .7;
}
.btn-submit.disable::before {
content: "";
position: absolute;
border: .5vw solid #fff;
border-color: rgba(255, 255, 255, .8) transparent transparent transparent;
border-radius: 50%;
width: 3vw;
height: 3vw;
top: 30%;
left: calc(50% - 14vw);
animation: loginloading 1s linear infinite;
}
.address-area {
flex: 1;
font-weight: 700;
}
.address-textarea {
height: 10vw;
align-items: normal;
}
.address-textarea::after {
display: none;
}
.address-close {
display: block;
position: absolute;
right: 4vw;
top: 4vw;
width: 8.425926vw;
height: 8.703704vw;
background-repeat: no-repeat;
background-position: 0 0;
background: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciICB2aWV3Qm94PSIwIDAgNTAgNTAiIHdpZHRoPSIxMDBweCIgaGVpZ2h0PSIxMDBweCI+PHBhdGggZD0iTTI1LDJDMTIuMzE5LDIsMiwxMi4zMTksMiwyNXMxMC4zMTksMjMsMjMsMjNzMjMtMTAuMzE5LDIzLTIzUzM3LjY4MSwyLDI1LDJ6IE0zMy43MSwzMi4yOWMwLjM5LDAuMzksMC4zOSwxLjAzLDAsMS40MglDMzMuNTEsMzMuOSwzMy4yNiwzNCwzMywzNHMtMC41MS0wLjEtMC43MS0wLjI5TDI1LDI2LjQybC03LjI5LDcuMjlDMTcuNTEsMzMuOSwxNy4yNiwzNCwxNywzNHMtMC41MS0wLjEtMC43MS0wLjI5CWMtMC4zOS0wLjM5LTAuMzktMS4wMywwLTEuNDJMMjMuNTgsMjVsLTcuMjktNy4yOWMtMC4zOS0wLjM5LTAuMzktMS4wMywwLTEuNDJjMC4zOS0wLjM5LDEuMDMtMC4zOSwxLjQyLDBMMjUsMjMuNThsNy4yOS03LjI5CWMwLjM5LTAuMzksMS4wMy0wLjM5LDEuNDIsMGMwLjM5LDAuMzksMC4zOSwxLjAzLDAsMS40MkwyNi40MiwyNUwzMy43MSwzMi4yOXoiLz48L3N2Zz4=) center center no-repeat;
background-size: 80%;
}
</style>

View File

@@ -0,0 +1,67 @@
<template>
<ModalTransition :show="show">
<div class="agreement">
<div class="agreement-wrapper">
<div class="agreement-content" v-html="agreeHtml"></div>
</div>
<div class="agreement-close" @click="$emit('close')"></div>
</div>
</ModalTransition>
</template>
<script setup>
import { ref } from "vue"
import ModalTransition from "./ModalTransition.vue";
const show = ref(false)
const agreeHtml = ref("")
const loading = weui.loading("加载中……")
fetch(`./agreement.html`, {
method: "GET",
headers: new Headers({
"Content-Type": "text/html"
})
}).then(res => res.text())
.catch(error => {
loading.hide()
console.log(error)
})
.then(res => {
loading.hide()
agreeHtml.value = res
show.value = true
})
</script>
<style scoped>
.agreement {
background-color: #fff;
border-radius: 2vw 2vw 0 0;
}
.agreement-wrapper {
width: 100%;
height: 90vh;
overflow: auto;
}
.agreement-content {
padding: 6vw;
}
.agreement-close {
z-index: 1;
position: absolute;
top: 25vw;
right: 3vw;
border-radius: 50%;
width: 8.425926vw;
height: 8.703704vw;
background-repeat: no-repeat;
background-position: center center;
background-color: rgba(255,255,255,.5);
background-size: 80% auto;
background-image: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA3MiA3MiIgd2lkdGg9IjEyOHB4IiBoZWlnaHQ9IjEyOHB4Ij48cGF0aCBkPSJNIDE5IDE1IEMgMTcuOTc3IDE1IDE2Ljk1MTg3NSAxNS4zOTA4NzUgMTYuMTcxODc1IDE2LjE3MTg3NSBDIDE0LjYwOTg3NSAxNy43MzM4NzUgMTQuNjA5ODc1IDIwLjI2NjEyNSAxNi4xNzE4NzUgMjEuODI4MTI1IEwgMzAuMzQzNzUgMzYgTCAxNi4xNzE4NzUgNTAuMTcxODc1IEMgMTQuNjA5ODc1IDUxLjczMzg3NSAxNC42MDk4NzUgNTQuMjY2MTI1IDE2LjE3MTg3NSA1NS44MjgxMjUgQyAxNi45NTE4NzUgNTYuNjA4MTI1IDE3Ljk3NyA1NyAxOSA1NyBDIDIwLjAyMyA1NyAyMS4wNDgxMjUgNTYuNjA5MTI1IDIxLjgyODEyNSA1NS44MjgxMjUgTCAzNiA0MS42NTYyNSBMIDUwLjE3MTg3NSA1NS44MjgxMjUgQyA1MS43MzE4NzUgNTcuMzkwMTI1IDU0LjI2NzEyNSA1Ny4zOTAxMjUgNTUuODI4MTI1IDU1LjgyODEyNSBDIDU3LjM5MTEyNSA1NC4yNjUxMjUgNTcuMzkxMTI1IDUxLjczNDg3NSA1NS44MjgxMjUgNTAuMTcxODc1IEwgNDEuNjU2MjUgMzYgTCA1NS44MjgxMjUgMjEuODI4MTI1IEMgNTcuMzkwMTI1IDIwLjI2NjEyNSA1Ny4zOTAxMjUgMTcuNzMzODc1IDU1LjgyODEyNSAxNi4xNzE4NzUgQyA1NC4yNjgxMjUgMTQuNjEwODc1IDUxLjczMTg3NSAxNC42MDk4NzUgNTAuMTcxODc1IDE2LjE3MTg3NSBMIDM2IDMwLjM0Mzc1IEwgMjEuODI4MTI1IDE2LjE3MTg3NSBDIDIxLjA0ODEyNSAxNS4zOTE4NzUgMjAuMDIzIDE1IDE5IDE1IHoiLz48L3N2Zz4=);
}
</style>

133
src/components/GameDemo.vue Normal file
View File

@@ -0,0 +1,133 @@
<script setup>
import { Request, Storage } from "../libs/utils"
import ModalTransition from "./ModalTransition.vue"
import { globalStore } from "../globalstore.js";
defineProps({
show: false
})
const emit = defineEmits(['close'])
// const emit = defineEmits(['update:show']);
const cancelBtn = () => {
emit('close')
}
</script>
<template>
<ModalTransition class="popup" :show="show">
<div class="home-bg">
<div class="popup-bg">
<div class="scene-item item-1">
<img src="../assets/images/new/start.png" @click="cancelBtn" alt="开始">
</div>
<div class="scene-item item-2">
<img src="../assets/images/new/start-1.png" @click="cancelBtn" alt="立即挑战">
</div>
<div class="scene-item item-3">
<img src="../assets/images/new/close-btn.png" @click="cancelBtn" alt="关闭">
</div>
</div>
</div>
</ModalTransition>
</template>
<style scoped>
.home-bg {
position: absolute;
top: 0;
left: 0;
}
.message {
width: 60vw;
position: absolute;
top: 42%;
color: #774107;
text-align: center;
}
.popup-bg {
width: 100vw;
height: 204vw;
background-image: url('../assets/images/new/demo-popup.png');
background-size: 100%;
background-repeat: no-repeat;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
position: relative;
}
.scene-item {
position: absolute;
cursor: pointer;
transition: all 0.4s ease;
border-radius: 8px;
overflow: hidden;
border: 3px solid transparent;
animation: float 4s ease-in-out infinite;
}
.scene-item:hover {
transform: scale(1.05);
}
.btn-login {
text-align: center;
border-radius: 1vw;
background-color: #70b2e2;
margin-top: 4vw;
padding: 3vw;
color: #fff;
position: relative;
}
.item-1 {
top: 114vw;
width: 20vw;
}
.item-2 {
top: 134vw;
width: 54vw;
}
.item-3 {
top: 152vw;
width: 12vw;
}
.scene-item img {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
}
.btn-login.disable {
opacity: .6;
}
.btn-login.subloading {
opacity: .6;
}
.btn-login.subloading::before {
content: "";
position: absolute;
border: .5vw solid #fff;
border-color: rgba(255, 255, 255, .8) transparent transparent transparent;
border-radius: 50%;
width: 3vw;
height: 3vw;
top: 30%;
left: calc(50% - 14vw);
animation: loginloading 1s linear infinite;
}
@keyframes loginloading {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
</style>

363
src/components/GamePage.vue Normal file
View File

@@ -0,0 +1,363 @@
<template>
<ModalTransition class="game-swiper" :show="show">
<div class="scene-item item-1" @click="handleGoBack">
<img src="../assets/images/new/back.png" alt="后退">
</div>
<div class="scene-item item-2">
<img src="../assets/images/new/music.png" alt="音乐">
</div>
<!-- 隐藏的圆形点击区域 -->
<div
class="hidden-circular-area"
@click="handleShowLottery"
:style="{
top: circularArea.top,
left: circularArea.left,
width: circularArea.size,
height: circularArea.size
}"
></div>
<div class="game-page">
</div>
</ModalTransition>
<Lottery :show="lotteryShow" @close="lotteryShow = false" :type="lotteryType" :data="lotteryNoticeData" />
</template>
<script setup>
import { ref, onMounted, onUnmounted } from 'vue'
import ModalTransition from "./ModalTransition.vue"
import Lottery from './Lottery.vue'
import globalToastEvent, { ToastType } from '../globalToastEvent';
const props = defineProps({
show: false,
})
const emit = defineEmits(['close'])
// const emit = defineEmits(['update:show'])
// Lottery 相关状态
const lotteryShow = ref(false)
const lotteryType = ref('draw')
const lotteryNoticeData = ref(null)
// 隐藏圆形点击区域的配置(可调整坐标和大小)
const circularArea = ref({
top: '78vw', // 可配置:距离顶部的位置
left: '50vw', // 可配置:距离左边的位置
size: '25vw' // 可配置:圆形区域的大小
})
// const handleGoHome = () => {
// emit('update:show', false)
// }
const handleGoBack = () => {
emit('close')
}
// 点击隐藏区域显示抽奖弹窗
const handleShowLottery = () => {
globalToastEvent.emit(ToastType.SHOW_LOTTERY)
}
// 轮播图数据
const slides = ref([
{
image: new URL('../assets/images/new/slider1.png', import.meta.url).href,
title: '轮播图1',
link: '/home' // 点击跳转的路由
},
{
image: new URL('../assets/images/new/slider1.png', import.meta.url).href,
title: '轮播图2',
link: '/lottery'
},
{
image: new URL('../assets/images/new/slider1.png', import.meta.url).href,
title: '轮播图3',
link: '/address'
}
])
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 handleSlideClick = (slide) => {
}
// 自动播放
const startAutoPlay = () => {
autoPlayTimer = setInterval(() => {
nextSlide()
}, 3000) // 每3秒切换一次
}
// 停止自动播放
const stopAutoPlay = () => {
if (autoPlayTimer) {
clearInterval(autoPlayTimer)
autoPlayTimer = null
}
}
// 生命周期
onMounted(() => {
// startAutoPlay()
})
onUnmounted(() => {
stopAutoPlay()
})
</script>
<style scoped>
.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: 8vw;
left: 2vw;
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;
}
/* 隐藏的圆形点击区域 */
.hidden-circular-area {
position: fixed;
border-radius: 50%;
cursor: pointer;
transform: translate(-50%, -50%);
/* 隐藏样式 - 完全透明且不可见 */
background: transparent;
opacity: 0;
z-index: 100;
/* 开发调试时可以取消注释下面这行来查看区域位置 */
/* background: rgba(255, 0, 0, 0.3); */
}
.game-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/game-1-bg.png');
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;
z-index: 10;
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>

View File

@@ -0,0 +1,359 @@
<template>
<ModalTransition class="game-swiper" :show="show">
<div class="scene-item item-1" @click="handleGoHome">
<img src="../assets/images/new/go-home.png" alt="回到首页">
</div>
<div class="scene-item item-2">
<img src="../assets/images/new/music.png" alt="音乐">
</div>
<div class="confirm-layout">
<div class="scene-item item-3" @click="handleConfirmClick">
<img src="../assets/images/new/confirm-btn.png" 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 * 100}% + ${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 :src="slide.image" :alt="slide.title" />
</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 } from 'vue'
import ModalTransition from "./ModalTransition.vue"
import globalToastEvent, { ToastType } from '../globalToastEvent';
const props = defineProps({
show: false,
})
const emit = defineEmits(['update:show'])
const handleGoHome = () => {
emit('close')
}
// 轮播图数据
const slides = ref([
{
image: new URL('../assets/images/new/slider1.png', import.meta.url).href,
title: '轮播图1',
link: '/home' // 点击跳转的路由
},
{
image: new URL('../assets/images/new/slider1.png', import.meta.url).href,
title: '轮播图2',
link: '/lottery'
},
{
image: new URL('../assets/images/new/slider1.png', import.meta.url).href,
title: '轮播图3',
link: '/address'
}
])
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 = () => {
const slide = slides.value?.[currentIndex.value]
if (!slide) return
handleSlideClick(slide)
}
const handleSlideClick = (slide) => {
if (!hasVisitedBefore) {
localStorage.setItem('hasVisitedGameSwiper', 'true');
globalToastEvent.emit(ToastType.SHOW_GAMEDEMO)
globalToastEvent.emit(ToastType.SHOW_GAMEPAGE)
} else {
globalToastEvent.emit(ToastType.SHOW_GAMEPAGE)
}
}
// 自动播放
const startAutoPlay = () => {
autoPlayTimer = setInterval(() => {
nextSlide()
}, 3000) // 每3秒切换一次
}
// 停止自动播放
const stopAutoPlay = () => {
if (autoPlayTimer) {
clearInterval(autoPlayTimer)
autoPlayTimer = null
}
}
// 生命周期
onMounted(() => {
// startAutoPlay()
})
onUnmounted(() => {
stopAutoPlay()
})
</script>
<style scoped>
.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: 8vw;
left: 2vw;
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.png');
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>

663
src/components/HomePage.vue Normal file
View File

@@ -0,0 +1,663 @@
<script setup>
import { ref, computed, watch, onMounted } from "vue"
import bgm from "../assets/audio/bgm.mp3"
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"
import Lottery from '../components/Lottery.vue';
import PrizeList from "./PrizeList.vue"
import Rule from "./Rule.vue";
import GameSwiper from "./GameSwiper.vue";
import Todolist from "./TodoList.vue";
import SharePage from "./SharePage.vue";
import GamePage from "./GamePage.vue";
import GameDemo from "./GameDemo.vue";
import Address from "./Address.vue";
import PopupMore from "./PopupMore.vue";
const props = defineProps({
show: true
})
const isMusicOn = ref(false);
const audioElement = ref(null);
const videoElement = ref(null);
const videoLoaded = ref(false);
const videoError = ref(false);
const lotteryShow = ref(false)
const lotteryType = ref("draw")
const lotteryNoticeData = ref(null)
// 初始化全局音频实例
const initGlobalAudio = () => {
if (!globalStore.globalAudio) {
globalStore.globalAudio = new Audio(bgm);
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 = () => {
document.addEventListener('WeixinJSBridgeReady',()=>{
videoElement.value && videoElement.value.play()
})
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 popupMore = async () => {
globalToastEvent.emit(ToastType.SHOW_POPUPMORE)
}
const handleRule = () => {
globalToastEvent.emit(ToastType.SHOW_RULE)
}
const handleLottery = () => {
// if (globalStore.draw_chances <= 0) {
// return weui.alert("还没有抽奖机会,快去参加活动吧")
// };
lotteryType.value = 'draw'
lotteryNoticeData.value = null
globalToastEvent.emit(ToastType.SHOW_LOTTERY);
}
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.first_share_today = result.json.first_share_today
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 getNotice = async () => {
const result = await Request('notice/latest', {}, "GET")
if (result?.json?.notification) {
lotteryType.value = 'notice'
lotteryNoticeData.value = result.json.notification
lotteryShow.value = true
}
}
const handleLoginSuccess = async () => {
console.log("已登录")
loginShow.value = false
await initUserGameInfos(true, true)
await getNotice()
}
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 })
globalToastEvent.on(ToastType.SHOW_LOTTERY, async () => {
// await initUserGameInfos(false, false);
lotteryShow.value = true
})
globalToastEvent.on(ToastType.INFO_UPDATE, async () => {
initUserGameInfos(false, false)
})
watch(() => lotteryShow.value, async (newVal) => {
initUserGameInfos(true, true);
}, { immediate: true })
const gameSwiperShow = ref(false);
const navigateGamePage = () => {
gameSwiperShow.value = true;
}
globalToastEvent.on(ToastType.SHOW_SWIPER, () => {
gameSwiperShow.value = true;
})
const ruleShow = ref(false)
globalToastEvent.on(ToastType.SHOW_RULE, () => {
ruleShow.value = true
})
const prizeListShow = ref(false)
const handlePrizeList = async () => {
await getUserLottery()
prizeListShow.value = true
}
const prizelist = ref([])
const getUserLottery = async () => {
const result = await Request("lottery", { pool: "all" }, "GET")
if (result.res.status === 200) {
prizelist.value = result.json.lottery_logs.length > 0 ? result.json.lottery_logs : []
}
//TODO 上线换成上面的
// prizelist.value = [
// { id: 1, prize_code: "FIRST", prize_name: "一等奖", coupon_type: "scene", pushed: 0 },
// { id: 2, prize_code: "FIRST1", prize_name: "二等奖", coupon_type: "scene", pushed: 1 }
// ]
}
globalToastEvent.on(ToastType.SHOW_PRIZELIST, async () => {
await getUserLottery()
prizeListShow.value = true
})
const todolistShow = ref(false)
globalToastEvent.on(ToastType.SHOW_TODO, () => {
todolistShow.value = true
})
const sharePageShow = ref(false)
globalToastEvent.on(ToastType.SHOW_SHAREPAGE, () => {
sharePageShow.value = true
})
const popupMoreShow = ref(false)
globalToastEvent.on(ToastType.SHOW_POPUPMORE, () => {
popupMoreShow.value = true
})
const gamePageShow = ref(false)
globalToastEvent.on(ToastType.SHOW_GAMEPAGE, () => {
gamePageShow.value = true
})
const gameDemoShow = ref(false)
globalToastEvent.on(ToastType.SHOW_GAMEDEMO, () => {
gameDemoShow.value = true
})
globalToastEvent.on(ToastType.CLOSE_ALL, () => {
gameSwiperShow.value = false;
gamePageShow.value = false;
gameDemoShow.value = false;
lotteryShow.value = false;
})
const activePrizeId = ref(0)
const addressShow = ref(false)
const handleAddressSubmitAfter = (data) => {
const targetItem = prizelist.value.find(item => item.id === data.id)
targetItem.pushed = 1
addressShow.value = false
}
const handleAddress = (id) => {
activePrizeId.value = id
addressShow.value = true
}
</script>
<template>
<div :show="show" class="main">
<div class="home-wrapper">
<div class="fallback-background"></div>
<div class="scene-item item-1" @click="handleLottery" :class="{ 'disabled': globalStore.draw_chances <= 0 }">
<img src="../assets/images/new/lottery.png" alt="抽奖">
</div>
<div class="scene-item item-2" @click="navigateGamePage">
<img src="../assets/images/new/start-btn.png" alt="开始探索">
</div>
<div class="scene-item item-3" @click="navigateTodoList">
<img src="../assets/images/new/task.png" alt="任务">
</div>
<div @click="toggleMusicState">
<div v-if="isMusicOn" key="on" class="scene-item item-4">
<img src="../assets/images/new/music.png" alt="音乐开">
</div>
<!-- <div v-else key="off" class="scene-item item-5">
<img src="../assets/images/music-off.png" alt="音乐关">
</div> -->
</div>
<div class="scene-item item-6" @click="handleRule">
<img src="../assets/images/new/rule.png" alt="规则">
</div>
<div class="scene-item item-7" @click="handlePrizeList">
<img src="../assets/images/new/award.png" alt="奖励">
</div>
<div class="scene-item item-8" @click="popupMore">
<img src="../assets/images/new/learn-more.png" alt="更多金喜">
</div>
<Rule :show="ruleShow" @close="ruleShow = false"></Rule>
<PrizeList :show="prizeListShow" @close="prizeListShow = false" :prizelist="prizelist" @address="handleAddress">
</PrizeList>
<Address :show="addressShow" :prizeId="activePrizeId" @address-submit="handleAddressSubmitAfter"
@address-close="addressShow = false"></Address>
</div>
</div>
<Login :show="loginShow" @login-success="handleLoginSuccess" />
<GameSwiper v-model:show="gameSwiperShow" @close="gameSwiperShow = false" />
<Todolist :show="todolistShow" @close="todolistShow = false"></Todolist>
<GamePage :show="gamePageShow" @close="gamePageShow = false" />
<GameDemo :show="gameDemoShow" @close="gameDemoShow = false" />
<Lottery :show="lotteryShow" @close="lotteryShow = false" :type="lotteryType" :data="lotteryNoticeData"></Lottery>
<SharePage :show="sharePageShow" @close="sharePageShow = false"></SharePage>
<PopupMore :show="popupMoreShow" @close="popupMoreShow = false"></PopupMore>
</template>
<style scoped>
.logo {
top: 4vw;
width: 24vw;
left: 4vw;
}
.slogan {
top: 20vw;
width: 76vw;
}
.home-title {
width: 72vw;
top: 144vw;
}
.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/new/home-bg.png');
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: 7vw;
left: 0;
animation-delay: 0s;
}
.lottery-main {
display: flex;
justify-content: center;
align-items: center;
width: 4vw;
height: 3.5vw;
position: absolute;
top: 1.7vw;
right: 7.3vw;
color: #fff;
}
.lottery-main .lottery-value {
font-size: 4vw;
}
.item-2 {
width: 48vw;
bottom: 5vw;
animation-delay: 0s;
}
.join-main {
position: absolute;
top: 3.4vw;
right: -3.5vw;
width: 13vw;
}
.join-main .join-value {
margin: 0;
color: white;
text-shadow: -1px -1px 0 #ff0000, 1px -1px 0 #ff0000, -1px 1px 0 #ff0000, 1px 1px 0 #ff0000, 0 0 2px #ff0000;
font-size: 5.4vw;
font-weight: 900;
}
.item-3 {
width: 21vw;
bottom: 7vw;
right: 0;
animation-delay: 0s;
}
.item-4 {
width: 11vw;
top: 1.5%;
right: 7%;
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: 12%;
right: 0;
animation-delay: 0s;
}
.item-8 {
width: 27vw;
bottom: 27vw;
right: 1vw;
animation-delay: 0s;
}
@keyframes loginloading {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
</style>

282
src/components/Login.vue Normal file
View File

@@ -0,0 +1,282 @@
<script setup>
import { ref, computed, watch } from "vue"
import { Request, Storage } from "../libs/utils"
import ModalTransition from "./ModalTransition.vue"
import Agreement from "./Agreement.vue"
defineProps({
show: false
})
const emit = defineEmits(['loginSuccess'])
const phone = ref('')
const code = ref('')
const timedown = ref(60)
const codeText = ref('获取验证码')
const selected = ref(false)
const agreementShow = ref(false)
const subloading = ref(false)
const codeStyleDisable = ref(false)
watch(phone, (newPhone) => {
phone.value = newPhone.replace(/\D/g, '').slice(0, 11).replace(/(\d{3})(\d{4})(\d{4})/, '$1 $2 $3')
})
const codelock = computed(() => { return !/^\d{11}$/.test(phone.value.replace(/\s+/g, '')) })
const selectChange = () => { selected.value = !selected.value }
const loginBtnClass = computed(() => ({
disable: codelock.value || code.value === '' || !selected.value, subloading: subloading.value
}))
const checkData = () => {
if (codelock.value) {
weui.alert("请输入正确的手机号码")
return false
} else if (code.value === '') {
weui.alert("请输入验证码")
return false
} else if (!selected.value) {
weui.alert("请阅读并同意协议")
return false
} else if (subloading.value) {
return false
} else {
return true
}
}
const countDown = () => {
const timer = setTimeout(() => {
if (timedown.value <= 0) {
timedown.value = 60
codeText.value = '获取验证码'
codeStyleDisable.value = false
} else {
timedown.value = timedown.value - 1
codeText.value = `${timedown.value}秒后重新获取`
countDown()
}
clearTimeout(timer)
}, 1000)
}
const handleCode = async () => {
if (codelock.value) {
weui.alert("请输入正确的手机号码")
return
}
if (codeStyleDisable.value) {
return
}
codeStyleDisable.value = true
const result = await Request("sms/sendCode", { phone: phone.value.replace(/\s+/g, ''), test: 'uNeverN0WhatTh1s' })
if (result.res.status === 200) {
timedown.value = 60
codeText.value = `${timedown.value}秒后重新获取`
countDown()
} else {
codeStyleDisable.value = false
}
}
const emitAgreementClose = () => {
agreementShow.value = false
}
const loginSubmit = async () => {
if (!checkData()) {
return
}
subloading.value = true
const result = await Request("sms/authPhone", { code: code.value, phone: phone.value.replace(/\s+/g, ''), agree: 1 })
if (result.res.status === 200) {
const userData = Object.assign({}, Storage.get("userinfos"), { ...result.json })
Storage.set("userinfos", userData)
emit("loginSuccess", userData)
} else {
subloading.value = false
}
}
</script>
<template>
<ModalTransition class="login" :show="show">
<div class="login-modal-content">
<div class="login-wrapper">
<div class="login-title">泸州老窖会员登录</div>
<div class="login-content">
<div class="login-item">
<input type="tel" class="i-phone" v-model="phone" placeholder="请输入手机号">
</div>
<div class="login-item login-code">
<input type="tel" class="i-code" v-model="code" placeholder="请输入验证码" />
<div class="login-code-send" :class="{ lock: codelock, disable: codeStyleDisable }" @click="handleCode">
{{
codeText }}</div>
</div>
<div class="login-agreement">
<div class="login-agreement-select" :class="{ selected: selected }" @click="selectChange"></div>
<div class="login-agreement-text">我已查看并同意<span class="login-agreement-link"
@click="() => agreementShow = true">泸州老窖会员中心隐私协议</span></div>
</div>
</div>
<div class="btn-login" :class="loginBtnClass" @click="loginSubmit">点击登录</div>
</div>
</div>
</ModalTransition>
<Agreement v-if="agreementShow" @close="emitAgreementClose"></Agreement>
</template>
<style scoped>
.login-modal-content {
width: 100%;
background-color: #f2f3f8;
border-radius: 2vw 2vw 0 0;
animation: transitionIn ease 0.3s forwards;
position: relative;
}
.login-wrapper {
width: calc(100% - 8vw);
margin-left: 4vw;
padding-bottom: 14vw;
}
.login-item {
display: flex;
}
.login-title {
font-size: 3.703704vw;
font-weight: 700;
padding: 4vw 0;
}
.login-item {
margin-top: 3vw;
position: relative;
}
.login-content input {
border-radius: 1vw;
background-color: #fff;
font-size: 5.555556vw;
padding: 3vw;
font-weight: 700;
color: #000;
}
.login-content input::placeholder {
color: #9e9e9e;
font-size: 3.703704vw;
line-height: 1;
transform: translateY(-.5vw);
}
.login-code-send {
position: absolute;
right: 0;
top: 0;
height: 100%;
display: flex;
align-items: center;
padding: 0 3vw;
font-size: 3.703704vw;
line-height: 1;
}
.login-code-send.lock,
.login-code-send.disable {
color: #9e9e9e;
}
.login-agreement {
display: flex;
flex-direction: row;
font-size: 2.777778vw;
margin-top: 4vw;
color: #9e9e9e;
align-items: center;
}
.login-agreement-select {
width: 8vw;
height: 8vw;
position: relative;
}
.login-agreement-select::after {
content: "";
position: absolute;
width: 4vw;
height: 4vw;
border-radius: 50%;
border: 1px solid #000;
top: 50%;
transform: translate3d(0, -50%, 0);
}
.login-agreement-select::before {
content: "";
position: absolute;
width: 4vw;
height: 4vw;
top: 50%;
transform: translate3d(0, -50%, 0) scale(0);
background-image: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciICB2aWV3Qm94PSIwIDAgNTAgNTAiIHdpZHRoPSI1MHB4IiBoZWlnaHQ9IjUwcHgiPjxwYXRoIGQ9Ik0gNDEuOTM3NSA4LjYyNSBDIDQxLjI3MzQzOCA4LjY0ODQzOCA0MC42NjQwNjMgOSA0MC4zMTI1IDkuNTYyNSBMIDIxLjUgMzguMzQzNzUgTCA5LjMxMjUgMjcuODEyNSBDIDguNzg5MDYzIDI3LjI2OTUzMSA4LjAwMzkwNiAyNy4wNjY0MDYgNy4yODEyNSAyNy4yOTI5NjkgQyA2LjU2MjUgMjcuNTE1NjI1IDYuMDI3MzQ0IDI4LjEyNSA1LjkwMjM0NCAyOC44NjcxODggQyA1Ljc3NzM0NCAyOS42MTMyODEgNi4wNzgxMjUgMzAuMzYzMjgxIDYuNjg3NSAzMC44MTI1IEwgMjAuNjI1IDQyLjg3NSBDIDIxLjA2MjUgNDMuMjQ2MDk0IDIxLjY0MDYyNSA0My40MTAxNTYgMjIuMjA3MDMxIDQzLjMyODEyNSBDIDIyLjc3NzM0NCA0My4yNDIxODggMjMuMjgxMjUgNDIuOTE3OTY5IDIzLjU5Mzc1IDQyLjQzNzUgTCA0My42ODc1IDExLjc1IEMgNDQuMTE3MTg4IDExLjEyMTA5NCA0NC4xNTIzNDQgMTAuMzA4NTk0IDQzLjc4MTI1IDkuNjQ0NTMxIEMgNDMuNDEwMTU2IDguOTg0Mzc1IDQyLjY5NTMxMyA4LjU4OTg0NCA0MS45Mzc1IDguNjI1IFoiLz48L3N2Zz4=");
background-size: 80%;
background-position: 60% center;
transition: transform .3s;
}
.login-agreement-select.selected::before {
transform: translate3d(0, -50%, 0) scale(1);
transition: transform .3s ease-in;
}
.login-agreement-link {
color: #000;
border-bottom: 1px solid #000;
}
.btn-login {
text-align: center;
border-radius: 1vw;
background-color: #70b2e2;
margin-top: 4vw;
padding: 3vw;
color: #fff;
position: relative;
}
.btn-login.disable {
opacity: .6;
}
.btn-login.subloading {
opacity: .6;
}
.btn-login.subloading::before {
content: "";
position: absolute;
border: .5vw solid #fff;
border-color: rgba(255, 255, 255, .8) transparent transparent transparent;
border-radius: 50%;
width: 3vw;
height: 3vw;
top: 30%;
left: calc(50% - 14vw);
animation: loginloading 1s linear infinite;
}
@keyframes loginloading {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
</style>

642
src/components/Lottery.vue Normal file
View File

@@ -0,0 +1,642 @@
<template>
<ModalTransition class="lottery" :show="show" name="opacity" position="center">
<div class="lottery-wrapper">
<div class="guang" ref="guangRef"></div>
<div class="guangyun" ref="guangyunRef"></div>
<div class="lottery-scene" ref="sceneRef">
<div class="lottery-card" ref="cardRef">
<div class="lottery-prize">
<div class="lottery-layer" ref="layerRef">
<!-- 底层中奖图片被遮罩层盖住主要用于语义与对齐 -->
<div class="lottery-image" :class="'USER_' + activePrizeData.prize_code" />
<!-- 中层遮罩层背景默认完全遮住中奖图 -->
<div class="lottery-mask-image" ref="maskBgRef" />
<!-- 顶层通过 mask-img.png 显示中奖图片的一小块并自动放大揭晓 -->
<div
class="lottery-reveal lottery-image"
:class="[
'USER_' + activePrizeData.prize_code,
revealDone ? 'reveal-done' : ''
]"
ref="revealRef"
v-show="activePrizeData.prize_code !== 'NO'"
/>
<!-- 可视化的 mask否则用户看不到 mask-img.png 本身 -->
<div
class="lottery-mask-visual"
:class="revealDone ? 'reveal-done' : ''"
v-show="activePrizeData.prize_code !== 'NO'"
/>
</div>
<!-- 标题保持原逻辑 -->
<div
class="lottery-title"
:class="props.type === 'notice' && 'notice'"
ref="titleRef"
v-show="activePrizeData.prize_code !== 'NO'"
></div>
<div class="lottery-btngroup" v-show="btngroupShow">
<div class="btn-more" @click="navigatePopupMore"></div>
<div class="btn-kaixin" @click="handleLotteryAction" v-if="activePrizeData.prize_code !== 'NO'"></div>
</div>
</div>
</div>
</div>
</div>
</ModalTransition>
</template>
<script setup>
import { gsap } from "gsap";
import { ref, watch, onMounted, onUnmounted } from "vue"
import globalToastEvent, { ToastType } from "../globalToastEvent";
import ModalTransition from "./ModalTransition.vue"
import confetti from "canvas-confetti";
import { Request, Sleep } from "../libs/utils"
import { globalStore } from "@/globalstore";
import getUserPicture from "../libs/getUserPicture";
const navigatePopupMore = () => {
globalToastEvent.emit(ToastType.SHOW_POPUPMORE)
// globalToastEvent.emit(ToastType.SHOW_SHAREPAGE)
}
// const lotteryFaceUrl = ref('')
const cardRef = ref(null)
const sceneRef = ref(null)
const guangRef = ref(null)
const guangyunRef = ref(null)
// 旧的 lotteryImageRef 仍保留(避免影响其他逻辑),但遮罩/揭晓现在用下面两个 ref
const lotteryImageRef = ref(null)
const maskBgRef = ref(null)
const layerRef = ref(null)
const revealRef = ref(null)
const titleRef = ref(null)
const activePrizeData = ref({})
const btngroupShow = ref(false)
const revealDone = ref(false)
gsap.registerPlugin()
const props = defineProps({
show: false,
type: "draw",
data: null
})
const emit = defineEmits(['close'])
let interval = null
let gsapCtx = null
let PRIZEDATA = null
const closeThis = () => {
initAnimateStyle()
emit("close")
}
// const handleBtnMore = () => {
// closeThis()
// globalToastEvent.emit(ToastType.SHOW_AD)
// }
const readNotice = async () => {
await Request(`notice/read`, { id: activePrizeData.value.noticeId })
}
const handleLotteryAction = async () => {
//实物
// closeThis()
globalToastEvent.emit(ToastType.CLOSE_ALL);
if (props.type !== 'draw') {
await readNotice()
}
if (activePrizeData.value.coupon_type === 'scene') {
globalToastEvent.emit(ToastType.SHOW_ADDRESS, activePrizeData.value.id)
}
}
onMounted(() => {
gsap.registerPlugin()
// initAnimateStyle()
})
const initAnimateStyle = () => {
interval && clearInterval(interval)
interval = null
let keyframes = [];
const frames = 20; // 关键帧数量
const strength = 5; // 抖动强度
// 生成关键帧
for (let i = 0; i < frames; i++) {
keyframes.push({
x: gsap.utils.random(-strength, strength),
y: gsap.utils.random(-strength, strength),
duration: 0.05 // 每帧的持续时间
});
}
revealDone.value = false
btngroupShow.value = false
// 重置遮罩层/揭晓层的初始状态
if (maskBgRef.value) {
gsap.set(maskBgRef.value, { opacity: 1 })
}
// 以 50x50px 为起点(按你的假设),把变量放在容器上,便于同时驱动 reveal + 可视化 mask
if (layerRef.value) {
gsap.set(layerRef.value, { '--maskSize': '50px' })
}
}
watch(() => props.show, async (newVal) => {
if (!newVal) {
return
}
const loading = weui.loading()
let lottteryResult = null
// if (props.type === 'draw') {
// lottteryResult = await Request("lottery/draw", { pool: 'game', consume_type: 'points' })
// // 监听请求失败,关闭组件
// if (!lottteryResult || !lottteryResult.res || (lottteryResult.res.status !== 200 && lottteryResult.res.status !== 201)) {
// emit('close')
// loading.hide()
// return
// }
// } else {
// if (!props.data) {
// weui.alert("获取排名奖励出错,请刷新页面重试")
// emit('close')
// loading.hide()
// return
// }
// lottteryResult = {
// res: { status: 200 },
// json: {
// noticeId: props.data.id,
// id: props.data.data.prize.id,
// prize_code: props.data.data.prize.prize_code,
// prize_name: props.data.data.prize.name,
// coupon_type: props.data.data.prize.scene,
// rank: props.data.data.rank
// }
// }
// }
// const userPicture = await getUserPicture()
// const preimg = async() => {
// new Promise((resolve) => {
// const img = new Image()
// img.onload = () => resolve(img)
// img.src = userPicture
// })
// }
// await preimg()
// lotteryFaceUrl.value = userPicture
// TODO: 延时测试,上限删掉
// await Sleep(20000)
// TODO: 测试数据
lottteryResult = {
res: { status: 200 },
json: {
id: 0,
code: "xxx",
// NOTE: 测试用,确保能命中现有的样式映射,便于看到揭晓效果
prize_code: 'LZ_ZQ_DZJ',
coupon_type: "scene",
name: "泸州老窖的一瓶酒"
}
}
loading.hide()
if (lottteryResult.res.status !== 200) {
emit('close')
return
} else {
globalStore.lotteryCut()
activePrizeData.value = lottteryResult.json
// 未中奖的情况
if (lottteryResult.json.code === 204) {
activePrizeData.value = { prize_code: 'NO', coupon_type: 'NO' }
}
// kapianCover.value = PRIZEDATA.prize_code === '66_POINTS' ? 'POINTS' : PRIZEDATA.prize_code
}
gsapCtx = gsap.context(() => {
initAnimateStyle()
const prizeshowTime = gsap.timeline({
onComplete: () => {
btngroupShow.value = true
}
});
prizeshowTime.to([sceneRef.value], {
duration: 2.5,
scale: 1,
opacity: 1,
ease: "power4.inOut"
})
// 中奖揭晓:先显示 2 秒,再用更慢、线性的放大过程(更清晰地看到“逐渐放大”)
if (layerRef.value && activePrizeData.value.prize_code !== 'NO') {
// 保证起始大小50x50px
gsap.set(layerRef.value, { '--maskSize': '50px' })
// 先停留 2 秒,让用户看清初始“窗口”
prizeshowTime.to({}, { duration: 0.5 })
// 再逐渐放大到 50 倍2500px
prizeshowTime.to(layerRef.value, {
duration: 5.0,
ease: "none",
'--maskSize': '2500px',
onComplete: () => {
revealDone.value = true
// 动画结束后把遮罩背景淡出,让中奖图完全可见
maskBgRef.value && gsap.to(maskBgRef.value, { duration: 0.3, opacity: 0 })
}
})
}
// 兼容旧动画(如果外部还在依赖 lotteryImageRef
if (lotteryImageRef.value) {
prizeshowTime.to(lotteryImageRef.value, {
duration: 1,
scale: 1,
ease: "power4.inOut"
}, "-=1.5")
}
prizeshowTime.to(guangyunRef.value, {
duration: 1.5,
scale: 2,
}, "-=1")
prizeshowTime.to(titleRef.value, {
scaleY: 1,
ease: "elastic.out(1,0.3)"
})
var duration = 4 * 1000;
var animationEnd = Date.now() + duration;
var defaults = { startVelocity: 30, spread: 360, ticks: 60, zIndex: 0 };
function randomInRange (min, max) {
return Math.random() * (max - min) + min;
}
interval = setInterval(function () {
var timeLeft = animationEnd - Date.now();
if (timeLeft <= 0) {
return clearInterval(interval);
}
var particleCount = 50 * (timeLeft / duration);
confetti({ ...defaults, particleCount, origin: { x: randomInRange(0.1, 0.3), y: Math.random() - 0.2 } });
confetti({ ...defaults, particleCount, origin: { x: randomInRange(0.7, 0.9), y: Math.random() - 0.2 } });
}, 250);
})
})
onUnmounted(() => {
gsapCtx && gsapCtx.revert()
})
</script>
<style scoped>
.lottery-layer {
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
/* 叠加区域尺寸:按需求宽度 80vw并水平/垂直居中 */
width: 80vw;
height: 80vw;
max-height: 80vh;
}
/* 中层遮罩背景(默认完全盖住中奖图) */
.lottery-mask-image {
height: 110vw;
width: 82vw;
margin-top: -30vw;
position: absolute;
inset: 0;
background-image: url("../assets/images/new/lottery-mask-bg.png");
background-repeat: no-repeat;
/* 保持图片比例:区域 80vw 内居中显示 */
background-size: contain;
background-position: center;
z-index: 2;
}
/* 底层/顶层复用:中奖图 */
.lottery-image {
height: 110vw;
width: 82vw;
margin-top: -30vw;
position: absolute;
inset: 0;
background-position: center;
background-repeat: no-repeat;
/* 保持图片比例:区域 80vw 内居中显示 */
background-size: contain;
z-index: 1;
}
/* 顶层:通过 mask-img.png 露出中奖图,并放大 mask 直到全露出 */
.lottery-reveal {
z-index: 3;
/* CSS MaskWebKit + 标准) */
/* -webkit-mask-image: url("../assets/images/new/mask-img.png");
-webkit-mask-repeat: no-repeat;
-webkit-mask-position: center;
-webkit-mask-size: var(--maskSize) var(--maskSize);
mask-image: url("../assets/images/new/mask-img.png");
mask-repeat: no-repeat;
mask-position: center;
mask-size: var(--maskSize) var(--maskSize); */
-webkit-clip-path: circle(calc(var(--maskSize) * 0.5) at 50% 50%);
clip-path: circle(calc(var(--maskSize) * 0.5) at 50% 50%);
}
/* 可视化的 mask-img.png让用户能看到“遮罩在放大”的过程 */
.lottery-mask-visual {
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
width: var(--maskSize);
height: var(--maskSize);
/* background-image: url("../assets/images/new/mask-img.png"); */
background-repeat: no-repeat;
background-size: 100% 100%;
background-position: center;
z-index: 4;
pointer-events: none;
}
.lottery-reveal.reveal-done {
-webkit-clip-path: none;
clip-path: none;
}
.lottery-mask-visual.reveal-done {
opacity: 0;
}
/* 动画结束后:移除 mask避免某些机型在超大 mask-size 时渲染抖动 */
.lottery-reveal.reveal-done {
-webkit-mask-image: none;
mask-image: none;
}
.lottery-image.USER_LZ_ZQ_DZJ {
background-image: url("../assets/images/new/zhongjiang1.png");
}
.lottery-image.USER_HG_42_GPJ_500ML {
background-image: url("../assets/images/USER_HG_42_GPJ_500ML.webp");
}
.lottery-image.USER_QLL_MH {
background-image: url("../assets/images/USER_QLL_MH.webp");
}
.lottery-image.USER_DZSKSJ {
background-image: url("../assets/images/USER_DZSKSJ.webp");
}
.lottery-image.USER_LZ_ZQ_DZJ_LJW {
background-image: url("../assets/images/USER_LZ_ZQ_DZJ_LJW.webp");
}
.lottery-image.USER_NO {
background-size: 60% auto;
background-position: center center;
background-image: url("../assets/images/USER_NO.webp");
}
.lottery-image.USER_LZ_XS_SPZ_30ML {
background-image: url("../assets/images/USER_LZ_XS_SPZ_30ML.webp");
}
.lottery-image.USER_LZ_DZ_JBGJ {
background-image: url("../assets/images/USER_LZ_DZ_JBGJ.webp");
}
.lottery-image.USER_LZ_DZ_JBGJ_LZ_ZQ_DZJ {
background-image: url("../assets/images/USER_LZ_DZ_JBGJ_LZ_ZQ_DZJ.webp");
}
.guang,
.guangyun {
position: absolute;
left: 50%;
margin-left: -66.2vw;
top: 50%;
margin-top: -64.5vw;
width: 129.62963vw;
height: 129.62963vw;
background-repeat: no-repeat;
background-size: 100%;
}
.guang {
background-image: url("../assets/images/guang.webp");
animation: rotateGuang 20s linear infinite;
}
.guangyun {
background-image: url("../assets/images/guangyun.webp");
/* animation: rotateGuangyun 3s linear infinite; */
}
.lottery-title {
position: absolute;
top: 24vw;
left: 50%;
transform: translateX(-50%) !important;
width: 65.462963vw;
height: 19.907407vw;
background-image: url("../assets/images/new/lottery-title.png");
background-repeat: no-repeat;
background-size: 100%;
z-index: 10;
}
.lottery-title.notice {
width: 39.259259vw;
height: 7.12963vw;
background-image: url("../assets/images/notice-title.webp");
}
.lottery-desc {
position: absolute;
top: 32vw;
left: 50%;
transform: translateX(-50%) !important;
width: 81vw;
font-size: 5vw;
font-weight: 700;
text-align: center;
color: rgb(255, 255, 255);
text-shadow:
0 0 0.37037vw #ff0000,
0 0 0.740741vw #ff0000,
0 0 1.111111vw #ff0000,
0.092593vw 0.092593vw 0 #ff0000,
-0.092593vw -0.092593vw 0 #ff0000;
}
.lottery-name {
position: absolute;
bottom: 18vw;
text-align: center;
width: 80%;
left: 10%;
font-size: 4.3vw;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-weight: 700;
color: rgb(255, 255, 255);
text-shadow:
0 0 0.37037vw #ff0000,
0 0 0.740741vw #ff0000,
0 0 1.111111vw #ff0000,
0.092593vw 0.092593vw 0 #ff0000,
-0.092593vw -0.092593vw 0 #ff0000;
}
.lottery-btngroup {
position: absolute;
bottom: 56vw;
width: 100%;
display: flex;
justify-content: space-evenly;
z-index: 10;
}
.btn-more {
width: 48.703704vw;
height: 18.240741vw;
background-image: url("../assets/images/new/more-btn.png");
background-repeat: no-repeat;
background-size: 100%;
}
.btn-kaixin {
width: 48.703704vw;
height: 18.240741vw;
background-image: url("../assets/images/new/btn-kaixin.png");
background-repeat: no-repeat;
background-size: 100%;
}
@keyframes rotateGuangyun {
0% {
transform: scale(.3);
opacity: 0;
}
50% {
transform: scale(1);
opacity: 1;
}
100% {
transform: scale(1.4);
opacity: 0;
}
}
@keyframes rotateGuang {
from {
transform: rotate(0deg);
}
to {
transform: rotate(-360deg);
}
}
.lottery-scene {
position: relative;
width: 88.055556vw;
height: 148.796296vw;
}
.lottery-card {
position: absolute;
width: 100%;
height: 100%;
top: -14vw;
}
.lottery-face {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
backface-visibility: hidden;
}
.lottery-card img {
width: 100%;
display: block;
}
.lottery-prize {
position: absolute;
/* top: 0;
left: 0;
width: 100%;
height: 100%; */
top: -20vw;
left: -6vw;
width: 100vw;
height: 244vw;
background-image: url("../assets/images/new/lottery-bg.png");
background-repeat: no-repeat;
background-size: 100%;
}
.close {
position: absolute;
width: 8.148148vw;
height: 8.055556vw;
right: 5vw;
top: 0;
background-image: url("../assets/images/icon-close.webp");
background-repeat: no-repeat;
background-size: 100%;
}
</style>

View File

@@ -0,0 +1,130 @@
<template>
<Transition :duration="duration" :name="animateName">
<div class="global-modal" :class="`position-${position}`" v-show="show">
<div class="global-modal-mask"></div>
<div class="global-modal-content">
<div class="global-modal-wrapper">
<slot></slot>
</div>
</div>
</div>
</Transition>
</template>
<script setup>
import { ref } from "vue"
const props = defineProps({
name: {
default:"bottom",
type:String
},
position:{
default:"bottom",
type:String
},
duration: {
default: 300,
type: Number
},
show: false,
noAnimation: {
default: false,
type: Boolean
}
})
const animateName = ref('')
const directionClass = ref('')
if (!props.noAnimation) {
switch (props.name) {
case 'top':
animateName.value = 'transTop'
break;
case 'opacity':
animateName.value = 'opacity'
break;
case 'scale':
animateName.value = 'scale'
break;
default:
animateName.value = 'transBottom'
break;
}
}
</script>
<style>
.global-modal-content {
z-index: 10;
}
.transBottom-enter-active .global-modal-wrapper,
.transBottom-leave-active .global-modal-wrapper {
transform: translate3d(0, 0, 0);
transition: transform .3s ease;
}
.transBottom-enter-from .global-modal-wrapper,
.transBottom-leave-to .global-modal-wrapper {
transform: translate3d(0, 100%, 0);
}
.transBottom-enter-active .global-modal-mask,
.transBottom-leave-active .global-modal-mask {
opacity: 1;
transition: opacity .3s ease;
}
.transBottom-enter-from .global-modal-mask,
.transBottom-leave-to .global-modal-mask {
opacity: 0;
}
.opacity-enter-active .global-modal-wrapper,
.opacity-leave-active .global-modal-wrapper {
opacity:1;
transition: opacity .3s ease;
}
.opacity-enter-from .global-modal-wrapper,
.opacity-leave-to .global-modal-wrapper {
opacity:0;
}
.opacity-enter-active .global-modal-mask,
.opacity-leave-active .global-modal-mask {
opacity: 1;
transition: opacity .3s ease;
}
.opacity-enter-from .global-modal-mask,
.opacity-leave-to .global-modal-mask {
opacity: 0;
}
.scale-enter-active .global-modal-wrapper,
.scale-leave-active .global-modal-wrapper {
opacity:1;
transform: scale(1);
transition: all .6s cubic-bezier(.58,.05,.07,1.4);
}
.scale-enter-from .global-modal-wrapper,
.scale-leave-to .global-modal-wrapper {
opacity:0;
transform: scale(.3);
}
.scale-enter-active .global-modal-mask,
.scale-leave-active .global-modal-mask {
opacity: 1;
transition: opacity .3s ease;
}
.scale-enter-from .global-modal-mask,
.scale-leave-to .global-modal-mask {
opacity: 0;
}
</style>

159
src/components/Popup.vue Normal file
View File

@@ -0,0 +1,159 @@
<script setup>
import { Request, Storage } from "../libs/utils"
import ModalTransition from "./ModalTransition.vue"
import { globalStore } from "../globalstore.js";
import { ElMessage } from 'element-plus';
defineProps({
show: false
})
const emit = defineEmits(['update:show']);
const cancelBtn = () => {
emit('update:show', false);
}
const confirmBtn = () => {
fetch(`https://huodong2.lzlj.com/api/faceFamily/face/publish/${globalStore.mergeId}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Accept: "application/json",
'Authorization': `Bearer ${Storage.get("userinfos").api_token}`
},
body: {}
})
.then(async response => {
const data = await response.json()
if (response.status == 200 || response.status == 201) {
if (data.message === '您已经有一张发布的照片,不能再次发布') {
weui.alert('您已经有一张发布的照片,不能再次发布')
return
}
ElMessage.success('打榜成功!');
console.log('Success:', data);
return { success: true, data };
} else {
ElMessage.error(data.message);
}
})
.catch((error) => {
ElMessage.error('打榜失败!');
return { success: false, error };
});
emit('update:show', false);
}
</script>
<template>
<ModalTransition class="popup" :show="show">
<div class="home-bg">
<div class="popup-bg">
<div class="scene-item item-1">
<img src="../assets/images/confirm.webp" @click="cancelBtn" alt="确认">
</div>
<p class="message">活动已结束</p>
<!-- <div class="scene-item item-2">
<img src="../assets/images/cancel.webp" @click="cancelBtn" alt="取消">
</div> -->
</div>
</div>
</ModalTransition>
</template>
<style scoped>
.home-bg {
position: absolute;
top: 60vw;
left: 12vw;
}
.message {
width: 60vw;
position: absolute;
top: 42%;
color: #774107;
text-align: center;
}
.popup-bg {
width: 77vw;
height: 60vw;
background-image: url('../assets/images/popup.webp');
background-size: 100%;
background-repeat: no-repeat;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
position: relative;
}
.scene-item {
position: absolute;
cursor: pointer;
transition: all 0.4s ease;
border-radius: 8px;
overflow: hidden;
border: 3px solid transparent;
animation: float 4s ease-in-out infinite;
}
.scene-item:hover {
transform: scale(1.05);
}
.btn-login {
text-align: center;
border-radius: 1vw;
background-color: #70b2e2;
margin-top: 4vw;
padding: 3vw;
color: #fff;
position: relative;
}
.item-1 {
top: 50vw;
width: 40vw;
}
.item-2 {
top: 50vw;
width: 40vw;
left: 0;
}
.scene-item img {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
}
.btn-login.disable {
opacity: .6;
}
.btn-login.subloading {
opacity: .6;
}
.btn-login.subloading::before {
content: "";
position: absolute;
border: .5vw solid #fff;
border-color: rgba(255, 255, 255, .8) transparent transparent transparent;
border-radius: 50%;
width: 3vw;
height: 3vw;
top: 30%;
left: calc(50% - 14vw);
animation: loginloading 1s linear infinite;
}
@keyframes loginloading {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
</style>

View File

@@ -0,0 +1,161 @@
<script setup>
import { isWeixinPlatform, miniJumpToScene, getMiniPageBtnHack } from "../libs/utils"
import ModalTransition from "./ModalTransition.vue"
import { globalStore } from "../globalstore.js";
defineProps({
show: false
})
const emit = defineEmits(['close'])
// const emit = defineEmits(['update:show']);
const cancelBtn = () => {
emit('close')
}
const startNow = () => {
if (isWeixinPlatform()) {
miniJumpToScene()
} else {
weui.alert("请前往「泸州老窖会员中心」小程序进行查询")
}
}
</script>
<template>
<ModalTransition class="popup" :show="show">
<div class="home-wrapper">
<div class="fullsection">
<div class="haibao">
</div>
</div>
<div class="scene-item item-2">
<img src="../assets/images/new/start-now.png" @click="startNow" alt="立即参与">
</div>
<div class="scene-item item-3">
<img src="../assets/images/new/close-btn.png" @click="cancelBtn" alt="关闭">
</div>
</div>
</ModalTransition>
</template>
<style scoped>
.haibao {
width: 70vw;
height: 119.444444vw;
background-image: url("../assets/images/new/sanchongli.png");
background-repeat: no-repeat;
background-size: 100%;
}
.home-wrapper {
width: 100vw;
height: 217vw;
background-image: url("../assets/images/new/share-bg.png");
background-size: cover;
background-repeat: no-repeat;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
position: relative;
min-height: -webkit-fill-available;
}
.home-bg {
position: absolute;
top: 0;
left: 0;
}
.message {
width: 60vw;
position: absolute;
top: 42%;
color: #774107;
text-align: center;
}
.popup-bg {
width: 100vw;
height: 204vw;
/* background-image: url('../assets/images/new/sanchongli.png'); */
background-size: 100%;
background-repeat: no-repeat;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
position: relative;
}
.scene-item {
position: absolute;
cursor: pointer;
transition: all 0.4s ease;
border-radius: 8px;
overflow: hidden;
border: 3px solid transparent;
animation: float 4s ease-in-out infinite;
}
.scene-item:hover {
transform: scale(1.05);
}
.btn-login {
text-align: center;
border-radius: 1vw;
background-color: #70b2e2;
margin-top: 4vw;
padding: 3vw;
color: #fff;
position: relative;
}
.item-1 {
top: 18vw;
width: 86vw;
}
.item-2 {
top: 147vw;
width: 46vw;
}
.item-3 {
top: 162vw;
width: 12vw;
}
.scene-item img {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
}
.btn-login.disable {
opacity: .6;
}
.btn-login.subloading {
opacity: .6;
}
.btn-login.subloading::before {
content: "";
position: absolute;
border: .5vw solid #fff;
border-color: rgba(255, 255, 255, .8) transparent transparent transparent;
border-radius: 50%;
width: 3vw;
height: 3vw;
top: 30%;
left: calc(50% - 14vw);
animation: loginloading 1s linear infinite;
}
@keyframes loginloading {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
</style>

View File

@@ -0,0 +1,203 @@
<template>
<div>
<ModalTransition :show="show">
<div class="prizelist">
<div class="prizelist-wrapper" v-show="prizelist.length > 0">
<div class="prizelist-wrapper-scroll">
<div class="prizelist-item" v-for="item in prizelist" :key="item.id">
<div class="prizelist-cover" :class="'USER_' + item.prize_code"></div>
<div class="prizelist-title">{{ item.prize_name }}</div>
<div class="prizelist-btngroup">
<template v-if="item.coupon_type === 'scene'">
<div class="btn-goto" :class="{ noaddress: item.pushed !== 1 }" v-html="sceneBtnHtml"
@click="handleItemBtn(item.id, $event)">
</div>
</template>
<template v-else="item.coupon_type === 'coupon'">
<div class="btn-goto" v-html="couponBtnHtml"></div>
</template>
</div>
</div>
</div>
</div>
<div class="prizelist-close" @click="$emit('close')"></div>
</div>
</ModalTransition>
</div>
</template>
<script setup>
import { ref, watch } from "vue"
import { isWeixinPlatform, miniJumpToScene, getMiniPageBtnHack } from "../libs/utils"
import ModalTransition from "./ModalTransition.vue";
const props = defineProps({
show: false,
prizelist: {
type: Array,
default: () => []
}
})
const emit = defineEmits(['address', 'close'])
const sceneBtnHtml = ref('')
const couponBtnHtml = ref('')
sceneBtnHtml.value = getMiniPageBtnHack("/pages/unify/unify?orgId=200282401019674482&targetUrl=%2Fpages%2Fretail%2Forder%2Forder-list%3Ftab%3DAll%26topTab%3D1")
couponBtnHtml.value = getMiniPageBtnHack("/pages/unify/unify?orgId=200282401019674482&targetUrl=%2Fpages%2Fcoupon%2Fcoupons-list")
const handleItemBtn = (id, event) => {
const target = event.currentTarget
if (target.classList.contains("noaddress")) {
emit("address", id)
} else {
if (isWeixinPlatform()) {
miniJumpToScene()
} else {
weui.alert("请前往「泸州老窖会员中心」小程序进行查询")
}
}
}
</script>
<style>
.prizelist {
position: relative;
width: 100vw;
height: 110vw;
background-image: url("../assets/images/new/prizelist-bg.png");
background-repeat: no-repeat;
background-size: 100%;
display: flex;
align-items: flex-end;
}
.prizelist-wrapper {
width: 100%;
height: 74vw;
overflow: auto;
}
.prizelist-wrapper-scroll {
display: flex;
flex-direction: column;
}
.prizelist-item {
display: flex;
margin-bottom: 4vw;
margin-left: 2.5vw;
padding-left: 3vw;
width: 95.833333vw;
height: 20.648148vw;
flex-direction: row;
justify-content: space-between;
align-items: center;
background-image: url("../assets/images/prizelist-item-bg.webp");
background-repeat: no-repeat;
background-size: 100% 100%;
}
.prizelist-cover {
width: 15vw;
margin-right: 3vw;
height: 15vw;
background-size: 100% 100%;
background-repeat: no-repeat;
}
.prizelist-title {
font-size: 4.444444vw;
flex: 1;
display: flex;
font-weight: 700;
color: #48260a;
overflow: hidden;
text-overflow: ellipsis;
-webkit-line-clamp: 2;
display: -webkit-box;
-webkit-box-orient: vertical;
}
.prizelist-close {
position: absolute;
width: 11vw;
height: 11vw;
right: 3vw;
top: -14vw;
background-image: url("../assets/images/new/close-btn.png");
background-repeat: no-repeat;
background-size: 100%;
}
.btn-goto {
margin-right: 4vw;
width: 23.981481vw;
height: 9.074074vw;
background-size: 100% 100%;
background-image: url("../assets/images/new/btn-goto.png");
background-repeat: no-repeat;
background-size: 100%;
}
wx-open-launch-weapp{
width: 100%;
height: 100%;
}
.btn-goto wx-open-launch-weapp {
display: block;
}
wx-open-launch-weapp,
.btn-goto.noaddress wx-open-launch-weapp {
display: none;
}
.btn-goto.noaddress {
background-image: url("../assets/images/new/btn-address.png");
}
.prizelist-cover.USER_LZ_ZQ_DZJ {
background-image: url("../assets/images/generate/USER_LZ_ZQ_DZJ.webp");
}
.prizelist-cover.USER_HG_42_GPJ_500ML {
background-image: url("../assets/images/USER_HG_42_GPJ_500ML.webp");
}
.prizelist-cover.USER_QLL_MH {
background-image: url("../assets/images/USER_QLL_MH.webp");
}
.prizelist-cover.USER_DZSKSJ {
background-image: url("../assets/images/generate/USER_DZSKSJ.webp");
}
.prizelist-cover.USER_LZ_ZQ_DZJ_LJW {
background-image: url("../assets/images/USER_LZ_ZQ_DZJ_LJW.webp");
}
.prizelist-cover.USER_LZ_XS_SPZ_30ML {
background-image: url("../assets/images/generate/USER_LZ_XS_SPZ_30ML.webp");
}
.prizelist-cover.USER_LZ_DZ_JBGJ {
background-image: url("../assets/images/USER_LZ_DZ_JBGJ.webp");
}
.prizelist-cover.USER_LZ_DZ_JBGJ_LZ_ZQ_DZJ {
background-image: url("../assets/images/generate/USER_LZ_DZ_JBGJ_LZ_ZQ_DZJ.webp");
}
.prizelist-cover.USER_DZCZ {
background-image: url("../assets/images/USER_DZCZ.webp");
}
.prizelist-cover.USER_DZSCZ {
background-image: url("../assets/images/USER_DZSCZ.webp");
}
.prizelist-cover.USER_66_POINTS {
background-image: url("../assets/images/USER_66_POINTS.webp");
}
</style>

59
src/components/Rule.vue Normal file
View File

@@ -0,0 +1,59 @@
<template>
<ModalTransition class="rule" :show="show">
<div class="rule-wrapper">
<div class="rule-content">
<img src="../assets/images/rule-1.webp" alt="">
<img src="../assets/images/rule-2.webp" alt="">
<img src="../assets/images/rule-3.webp" alt="">
<img src="../assets/images/rule-4.webp" alt="">
<img src="../assets/images/rule-5.webp" alt="">
</div>
<div class="close" @click="emit('close')"></div>
</div>
</ModalTransition>
</template>
<script setup>
import ModalTransition from "./ModalTransition.vue"
const props = defineProps({
show: false,
})
const emit = defineEmits(['close'])
//TODO 上线更换rule的图片
</script>
<style scoped>
.rule-wrapper {
position: relative;
width: 100%;
height: 182vw;
background-image: url("../assets/images/rule-bg.webp");
background-repeat: no-repeat;
background-size: 100%;
}
.rule-content{
position: absolute;
top: 50vw;
left: 10vw;
width: 83.981481vw;
height: 150vw;
overflow: auto;
padding-right: 4vw;
padding-bottom: 10vw;
}
.rule-content img {
width: 100%;
height: auto;
display: block;
}
.close {
position: absolute;
width: 11vw;
height: 11vw;
right: 3vw;
top: 11vw;
background-image: url("../assets/images/new/close-btn.png");
background-repeat: no-repeat;
background-size: 100%;
}
</style>

View File

@@ -0,0 +1,207 @@
<script setup>
import { Request, Storage, generateQR } from "../libs/utils.js"
import ModalTransition from "./ModalTransition.vue"
import { ref, onMounted, computed } from "vue"
import Haibao from "../libs/haibao.js"
import ruleBg from "../assets/images/new/rule-share.png"
import globalToastEvent, { ToastType } from '../globalToastEvent';
const props = defineProps({
show: false,
})
const emit = defineEmits(['close'])
// const emit = defineEmits(['update:show'])
const gotoLottery = () => {
globalToastEvent.emit(ToastType.SHOW_LOTTERY);
}
const cancelBtn = () => {
emit('close')
}
// 海报
const haibaoShow = ref(false);
const haibaoUrl = ref('');
const openHaibao = (e) => {
haibaoShow.value = true
handleHaibao()
}
onMounted(openHaibao)
const handleHaibao = async () => {
const loading = weui.loading()
const infos = Storage.get("userinfos")
const haibaoCover = new Haibao(1080, 2160);
haibaoCover.add(ruleBg, 0, 0)
const qrcode = await generateQR(`fromid=${infos.invite_code}&merge_id=${infos.mergeId}`, 200, 200);
haibaoCover.add(qrcode, 742, 1870);
haibaoCover.draw().then(() => {
haibaoCover.generate({ mimeType: 'image/png' }).then(async (url) => {
haibaoUrl.value = url
loading.hide()
}).catch(err => {
console.log(err)
weui.alert("海报生成失败,请重新生成")
loading.hide()
})
})
}
</script>
<template>
<ModalTransition class="popup" :show="show">
<div class="home-wrapper">
<div class="fullsection">
<div class="haibao">
<img :src="haibaoUrl" alt="">
</div>
</div>
<!-- <div class="scene-item item-4">
<img src="../assets/images/new/choujiang.png" @click="gotoLottery" alt="立即抽奖">
</div> -->
<div class="scene-item item-5">
<img src="../assets/images/new/close-btn.png" @click="cancelBtn" alt="关闭">
</div>
</div>
</ModalTransition>
</template>
<style scoped>
.home-wrapper {
width: 100vw;
height: 217vw;
background-image: url("../assets/images/new/share-bg.png");
background-size: cover;
background-repeat: no-repeat;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
position: relative;
min-height: -webkit-fill-available;
}
.haibao {
width: 70vw;
height: 119.444444vw;
background-image: url("../assets/images/new/share-popup-demo.png");
background-repeat: no-repeat;
background-size: 100%;
}
.haibao img {
width: 100%;
height: 100%;
display: block;
opacity: 0;
}
.home-bg {
position: absolute;
top: 0;
left: 0;
}
.message {
width: 60vw;
position: absolute;
top: 42%;
color: #774107;
text-align: center;
}
.share-bg {
width: 100vw;
height: 204vw;
background-image: url('../assets/images/new/share-bg.png');
background-size: 100%;
background-repeat: no-repeat;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
position: relative;
}
.scene-item {
position: absolute;
cursor: pointer;
transition: all 0.4s ease;
border-radius: 8px;
overflow: hidden;
border: 3px solid transparent;
animation: float 4s ease-in-out infinite;
}
.scene-item:hover {
transform: scale(1.05);
}
.btn-login {
text-align: center;
border-radius: 1vw;
background-color: #70b2e2;
margin-top: 4vw;
padding: 3vw;
color: #fff;
position: relative;
}
.item-1 {
top: 114vw;
width: 20vw;
}
.item-2 {
top: 134vw;
width: 54vw;
}
.item-3 {
top: 164vw;
width: 13vw;
}
.item-4 {
top: 166vw;
width: 40vw;
}
.item-5 {
top: 166vw;
width: 14vw;
}
.scene-item img {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
}
.btn-login.disable {
opacity: .6;
}
.btn-login.subloading {
opacity: .6;
}
.btn-login.subloading::before {
content: "";
position: absolute;
border: .5vw solid #fff;
border-color: rgba(255, 255, 255, .8) transparent transparent transparent;
border-radius: 50%;
width: 3vw;
height: 3vw;
top: 30%;
left: calc(50% - 14vw);
animation: loginloading 1s linear infinite;
}
@keyframes loginloading {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
</style>

241
src/components/TodoList.vue Normal file
View File

@@ -0,0 +1,241 @@
<template>
<ModalTransition class="todolist" :show="show">
<div class="todolist-wrapper">
<div class="btn-group">
<div class="btn-share" @click="openHaibao($event)"></div>
<div class="btn-qiwei" :class="globalStore.followed_official && 'has'" @click="openQiwei($event)"></div>
</div>
<div class="close" @click="$emit('close')"></div>
</div>
<div class="fullsection" v-show="haibaoShow">
<div class="haibao" :style="userhaibaoCover">
<img :src="haibaoUrl" alt="">
<div class="close" @click="haibaoShow = false"></div>
</div>
</div>
<div class="fullsection" v-show="qiweiShow">
<div class="qiwei">
<img src="../assets/images/new/qiwei.png" alt="">
<div class="close" @click="qiweiShow = false"></div>
</div>
</div>
</ModalTransition>
</template>
<script setup>
import { ref, computed } from "vue"
import { globalStore } from "@/globalstore";
import ModalTransition from "./ModalTransition.vue";
import globalToastEvent, { ToastType } from '../globalToastEvent';
const props = defineProps({
show: false,
})
const emit = defineEmits(['close', 'open'])
const qiweiShow = ref(false)
const haibaoShow = ref(false)
const haibaoUrl = ref('')
const userHaibaoUrl = ref('')
const userhaibaoCover = computed(() => {
return { backgroundImage: `url(${userHaibaoUrl.value})` }
})
const openHaibao = async (e) => {
globalToastEvent.emit(ToastType.SHOW_SHAREPAGE);
}
const openQiwei = (e) => {
const target = e.currentTarget
if (target.classList.contains("has")) {
return
}
qiweiShow.value = true
}
</script>
<style scoped>
.home-wrapper {
width: 100%;
position: relative;
min-height: -webkit-fill-available;
}
.home-wrapper img {
width: 100%;
background-size: cover;
background-repeat: no-repeat;
}
.scene-item {
position: absolute;
cursor: pointer;
transition: all 0.4s ease;
border-radius: 8px;
overflow: hidden;
border: 3px solid transparent;
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: 10vw;
bottom: 54%;
right: 3%;
animation-delay: 0s;
}
.item-1 {
width: 10vw;
bottom: 54%;
right: 3%;
animation-delay: 0s;
}
.item-2 {
width: 110px;
bottom: 36%;
right: 6%;
animation-delay: 0s;
}
.item-3 {
width: 110px;
bottom: 25%;
right: 6%;
animation-delay: 0s;
}
.item-4 {
width: 110px;
bottom: 14%;
right: 6%;
animation-delay: 0s;
}
.qiwei {
position: relative;
width: 80.740741vw;
/* height: 89.259259vw; */
}
.qiwei img {
width: 100%;
height: 100%;
display: block;
}
.fullsection {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
background-color: rgba(0, 0, 0, .7);
}
.haibao {
position: relative;
width: 65.37037vw;
height: 119.444444vw;
background-image: url("../assets/images/haibao-cover.webp");
background-repeat: no-repeat;
background-size: 100%;
}
.haibao img {
width: 100%;
height: 100%;
display: block;
opacity: 0;
}
.close {
position: absolute;
width: 11vw;
height: 11vw;
right: 3vw;
top: -14vw;
background-image: url("../assets/images/new/close-btn.png");
background-repeat: no-repeat;
background-size: 100%;
}
.haibao .close {
left: 50%;
bottom: -5vw;
top: initial;
transform: translate3d(-50%, 0, 0);
}
.qiwei .close {
right: 50%;
top: 126vw;
margin-right: -6vw;
}
.todolist-wrapper {
position: relative;
width: 100vw;
height: 94vw;
background-image: url("../assets/images/new/todo-bg.png");
background-repeat: no-repeat;
background-size: 100%;
}
.btn-group {
position: absolute;
right: 6vw;
top: 35vw;
width: 24.907407vw;
height: 46vw;
display: flex;
flex-direction: column;
justify-content: space-around;
}
.btn-group div {
width: 24.907407vw;
height: 10vw;
background-repeat: no-repeat;
background-size: 100%;
}
.btn-share {
background-image: url("../assets/images/new/share-link.png");
}
.btn-share.has {
background-image: url("../assets/images/new/limit-btn.png");
}
.btn-qiwei {
background-image: url("../assets/images/new/add-wx.png");
}
.btn-qiwei.has {
background-image: url("../assets/images/btn-added.webp");
}
.btn-scan {
background-image: url("../assets/images/scan-code.webp");
}
.btn-scan.has {
background-image: url("../assets/images/new/limit-btn.png");
}
</style>