1.效果
WeChat_20250217192041
2.代码
2.1 index.vue
<template><div class="pages"><TopNavigationYleftTitle="打年兽"ruleIconColor="#fff"backgroundImage=""svgIpcn="backIcon4"gradientBackgroundColor="rgba(128, 76, 104, 0.6)"topNavHeight="56px"howToPlay="newYearEvent":backdropFilter="true"ruleIcon="ruleFFF"><template v-slot:top_l_right><SvgIconclass="custimage"width="34px"height="34px"name="customerIconBW"@click="openService()"/></template></TopNavigationY><div class="nianBeastBox"><transition-group name="fade"><animationv-for="item in JSONanimations":key="item.id":JSONanimations="item.anim"v-show="item.id == animationId && animationId != -1"class="nianBeastImg"></animation><imgv-show="animationId == -1"class="death":src="useImageUrl('newYearEvent', `criticalStrike`, 'develop')"alt=""/></transition-group><shellref="shellRef"class="shell"/><div class="nianBeast"><!-- 动态渲染伤害值 --><divv-for="item in data.elementsHtml":key="item.id"class="damageValueNum"v-show="data.elementsHtml"><divclass="tex"v-show="!item.status"><div class="text1">-</div><div class="text2">-</div><div class="text3">-</div></div><imgv-show="item.status"class="img":src="useImageUrl('newYearEvent', `criticalStrike`, 'develop')"alt=""/><div class="num"><div class="text1">{{ item.num }}</div><div class="text2">{{ item.num }}</div><div class="text3">{{ item.num }}</div></div></div><imgv-show="data.lastStrikesImgShow"class="lastStrike":src="useImageUrl('newYearEvent', `lastStrikes`, 'develop')"alt=""/><div class="Progress"><van-progress:percentage="data.progressNum":show-pivot="false"/><divclass="progress_pivot":style="{ left: data.progressNum + '%' }"></div></div><div class="countdown">剩余时间:<van-count-down:time="data.countdown"@finish="finishCountdown"/></div></div><div class="cannon"><imgclass="cannonLeft":src="useImageUrl('newYearEvent', `barrel1`, 'develop')"alt=""/><imgclass="view":src="useImageUrl('newYearEvent', `view`, 'develop')"alt=""@click="viewYearBeast()"/><imgclass="cannonRight":src="useImageUrl('newYearEvent', `barrel2`, 'develop')"alt=""/></div><div class="shellBox"><div class="shellList"><divv-for="item in 4":key="item"@click="selectProjectile(item)":style="{ opacity: data.shellFrameIndex === item ? '1' : '0.6' }"><div class="shellItem"><img:src="useImageUrl('newYearEvent', `shell${item}`, 'develop')"alt=""/></div><div class="num">x 111</div></div></div><van-stepperv-model="data.shellNum":integer="true"/></div><div class="strikeYearBeast"><imgclass="ranking"@click="goToRanking":src="useImageUrl('newYearEvent', `ranking`, 'develop')"alt=""/><divclass="strikeYearBeastBtn"@click="clickYearBeastBtn"></div><div class="accumulate">{{ convertToWan(data.accumulatedDamage) }}</div></div></div></div><!-- 年兽弹窗 --><van-overlay:show="data.nianBeastPop"z-index="1000"><div class="popCentent"><div class="cententBox"><divclass="cententItem"v-for="item in data.nianBeastList":key="item.id"><div class="time">开始时间:{{ item.startTime }}</div><div class="time">结束时间:{{ item.endTime }}</div><imgclass="img"src="@/assets/image/newYearEvent/nianBeast.png"alt=""/><imgclass="btn":src="useImageUrl('newYearEvent', `btn${item.status}`, 'develop')"alt=""/></div></div><divclass="cancelPop"@click="cancelYearBeast"></div></div></van-overlay><!-- 最后一击弹窗 --><van-overlay:show="data.lastStrikeShow"z-index="1000"><div class="lastStrikeCentent"><div class="box"><text-show:direction="'center'":text="data.jewelry.name"text-id="444"><div class="text textEllipsis">{{ data.jewelry.name }}</div></text-show><imgclass="img"src="https://img.zbt.com/e/steam/item/730/UDkwIHwgQXNpaW1vdiAoRmFjdG9yeSBOZXcp.png"alt=""/><div class="price"><imgclass="coinImg":src="useImageUrl('base', 'conch')"alt=""/>{{ data.jewelry.price }}</div></div><divclass="cancelPop"@click="cancelYearBeast"></div></div></van-overlay>
</template>
<script setup lang="ts">
import { onMounted, onUnmounted, reactive, ref, watch } from "vue"
import { getTextByCode, viewTextByCode } from "@/api/base"
import { showIntroduce } from "@/utils/Introduce"
import { navigateTo } from "@/utils/route"
// import { useImageUrl,joinImgPrefix } from "@/utils/index"
import { useImageUrl, convertToWan } from "@/utils/index"
import animation from "@/components/animation/index.vue"
import nianBeast1 from "@/assets/json/nianBeast1.json"
import nianBeast2 from "@/assets/json/nianBeast2.json"
import shell from "./animation/shell.vue"
import { openService } from "@/utils/userStore"
import config from "@/config"const data = reactive({countdown: 5000000, // 年兽倒计时shellNum: 1, // 炮弹数量progressNum: 50, // 进度条elementsHtml: [],shellFrameIndex: 1,nianBeastPop: false, // 年兽弹窗nianBeastList: [{id: 1,startTime: "2025.12.26 12:52:24",endTime: "2025.12.26 12:52:24",status: 2},{id: 2,startTime: "2025.12.26 12:52:24",endTime: "2025.12.26 12:52:24",status: 1},{id: 3,startTime: "2025.12.26 12:52:24",endTime: "2025.12.26 12:52:24",status: 3},{id: 1,startTime: "2025.12.26 12:52:24",endTime: "2025.12.26 12:52:24",status: 2}],lastStrikeShow: false, // 最后一击弹窗lastStrikesImgShow: false, // 最后一击图片jewelry: {price: 11111,name: "额温额温额温额温额温额温额温额温额温额温"},accumulatedDamage: "2222222" // 累计伤害
})
// 查看年兽弹窗
const viewYearBeast = () => {data.nianBeastPop = trueconsole.log(`output-11111`, 11111)
}
// 关闭查看年兽弹窗
const cancelYearBeast = () => {data.nianBeastPop = falsedata.lastStrikeShow = false
}
// 选择炮弹
const selectProjectile = val => {data.shellFrameIndex = valdata.shellNum = 1
}
// 去排名
const goToRanking = () => {navigateTo({name: "newYearRanking"})
}
// 打年兽
const shellRef = ref(null)
const clickYearBeastBtn = () => {shellRef.value.createAnimation()barrelAnimation()shellDamageValue()
}
// 炮弹动画
const barrelAnimation = () => {const animateElement = (element, scale, origin, duration) => {// 设置动画样式element.style.transform = `scaleX(${scale})`element.style.transition = `transform ${duration}ms ease`element.style.transformOrigin = origin// 动画复原setTimeout(() => {element.style.transform = "scaleX(1)"}, duration)}// 获取左侧炮管并执行动画const left = document.querySelector(".cannonLeft")animateElement(left, 0.8, "left center", 200)// 获取右侧炮管并执行动画const right = document.querySelector(".cannonRight")animateElement(right, 0.8, "right center", 200)config.cannonAudio.currentTime = 0.2 // 设置从第 1 秒开始播放config.cannonAudio.play() // 播放音效
}
// 炮弹伤害值
const shellDamageValue = () => {const obj = {num: Math.floor(Math.random() * 10),status: true}data.elementsHtml.push(obj)
}
// 玩法规则是否第一次弹出
const showRule = async () => {const res = await getTextByCode("newYearEvent")if (res.data.show) {showIntroduce("newYearEvent")await viewTextByCode("newYearEvent")}
}
// 音效播放函数
const playSound = () => {config.newYearAudio.volume = 0.2 // 设置音量config.newYearAudio.loop = true // 设置循环播放// 播放音效config.newYearAudio.play().then(() => {console.log("音效播放成功")}).catch(error => {console.error("音效播放失败:", error)})
}
// 音效停止函数
const stopSound = () => {if (config.newYearAudio) {config.newYearAudio.pause() // 暂停播放config.newYearAudio.currentTime = 0 // 重置播放时间}
}
// 获取年兽动画
const JSONanimations = ref<any>([{id: 1,anim: nianBeast1},{id: 2,anim: nianBeast2},{id: 3,anim: nianBeast2},{id: 4,anim: nianBeast2}
])
const animationId = ref(1)
const finishCountdown = () => {if (data.progressNum <= 0) returndata.nianBeastList.map(item => {if (item.status == 1) {animationId.value = item.id}})
}
watch(() => data.progressNum,val => {if (val <= 0) {animationId.value = -1data.lastStrikesImgShow = true}},{ immediate: true }
)
onMounted(async () => {await playSound()await showRule()
})
onUnmounted(() => {stopSound()
})
</script>
<style lang="scss" scoped>
:deep(.top_navigation) {position: fixed;.top_l {top: 56px !important;transform: translateY(-50%);margin-left: 8px;display: flex;align-items: center;.custimage {transform: translateY(1px);margin-left: 23px;}}
}
.pages {width: 750px;min-height: 1624px;background: #490205;overflow: hidden;.nianBeastBox {width: 750px;height: 1624px;background: url($yjnewYearEventBg) no-repeat bottom;background-size: 100%;position: relative;.fade-enter-active,.fade-leave-active {transition: opacity 0.5s ease;}.fade-enter,.fade-leave-to {opacity: 0;}.nianBeastImg {position: absolute;top: 10px;}.death {position: absolute;left: 50%;transform: translateX(-50%);top: 570px;width: 400px;height: 400px;}.nianBeast {width: 750px;height: 1050px;display: grid;place-items: center;position: absolute;@keyframes float {0% {transform: translateY(0);opacity: 1;}100% {transform: translateY(-30px);opacity: 0;}}.damageValueNum {font-size: 56px;font-weight: bold;display: flex;justify-content: center;align-items: center;position: absolute;font-family: YouSheBiaoTiHei;animation: float 1.7s ease forwards;top: 500px;left: 50%;transform: translateY(-50%);.tex {position: absolute;left: -50px;}.img {width: 222px;height: 107px;margin-top: 60px;margin-right: 10px;margin-left: -150px;}.text1 {font-size: 56px;text-shadow:-2px -2px 0 #ffd700,2px -2px 0 #ffd700,-2px 2px 0 #ffd700,2px 2px 0 #ffd700;position: absolute;top: 80px;-webkit-text-stroke: 20px #ffd700;letter-spacing: 6px;}.text2 {font-size: 56px;text-shadow:-2px -2px 0 #8b0000,2px -2px 0 #8b0000,-2px 2px 0 #8b0000,2px 2px 0 #8b0000;-webkit-text-stroke: 16px #8b0000;position: absolute;top: 80px;letter-spacing: 5px;}.text3 {color: #fff;position: absolute;top: 80px;letter-spacing: 5px;}}.lastStrike {width: 600px;height: 216.846px;background: url($yjprogressHead) no-repeat center;background-size: 100%;position: absolute;top: 700px;animation: float 2.5s ease forwards;}.Progress {position: absolute;top: 820px;left: -40px;width: 312px;transform: rotate(270deg);.progress_pivot {width: 100px;height: 56px;transform: rotate(90deg) !important;background: url($yjprogressHead) no-repeat bottom;background-size: 100%;position: absolute;top: -16px;opacity: 1 !important;margin-left: -46px;filter: brightness(2);}:deep(.van-progress) {height: 32px;border-radius: 4px;opacity: 0.9;background: rgba(0, 0, 0, 0.5);box-shadow: 0px 4px 2px 0px rgba(0, 0, 0, 0.25) inset;display: flex;align-items: center;}:deep(.van-progress__portion) {border-radius: 2px;height: 24px;border-top: 4px solid #ffa45a;border-right: 4px solid #ffa45a;border-bottom: 4px solid #ffa45a;background: linear-gradient(180deg, #ffa45a 0%, #e24129 100%);margin-left: 3px;}}.countdown {position: absolute;bottom: 0;left: 50%;transform: translateX(-50%);width: 320px;height: 60px;background: rgba(73, 2, 4, 0.8);display: flex;justify-content: center;align-items: center;color: #fff;font-family: "AP700";font-size: 32px;.van-count-down {color: #fff;font-family: "AP700";font-size: 32px;}}}}.cannon {width: 750px;height: 170px;margin-top: 50px;display: flex;justify-content: space-between;align-items: center;position: absolute;top: 1045px;z-index: 1;.view {width: 300px;height: 80px;}.cannonLeft {width: 148px;height: 170px;}.cannonRight {@extend .cannonLeft;}}.shellBox {position: absolute;top: 1255px;left: 50%;transform: translateX(-50%);text-align: center;.shellList {width: 460px;margin: 0 auto;display: grid;grid-template-columns: repeat(4, 1fr);gap: 20px;margin-bottom: 20px;.shellItem {width: 100px;height: 100px;background: url($yjnewYearlastshellFrame) no-repeat bottom;background-size: 100%;img {width: 73px;height: 67px;margin-left: 13px;margin-top: 15px;}}.num {color: #f5b142;text-align: center;font-family: "AP500";font-size: 20px;margin-top: 4px;}}}.strikeYearBeast {display: flex;justify-content: center;position: absolute;top: 1485px;left: 50%;transform: translateX(-50%);.ranking {width: 102px;height: 102px;}.strikeYearBeastBtn {width: 360px;height: 106px;margin: 0 26px;background: url($yjnewYearstrikeYearBeastBtn) no-repeat bottom;background-size: 100%;&:active {background: url($yjnewYearstrikeYearBeastBtnA) no-repeat bottom;background-size: 100%;}}.accumulate {width: 142px;height: 102px;background: url($yjnewYearaccumulate) no-repeat bottom;background-size: 100%;color: #ffc337;text-align: center;font-family: "AP700";font-size: 36px;}}
}
.popCentent {width: 688px;height: 903px;background: url($yjnewYearviewPop) no-repeat bottom;background-size: 100%;position: absolute;top: 50%;left: 50%;transform: translate(-50%, -50%);.cententBox {display: grid; /* 外层容器 */grid-template-columns: repeat(2, 1fr);gap: 20px;padding: 0 74px;margin-top: 200px;.cententItem {color: #fff7c4;font-family: "AP500";font-size: 14px;width: 254px;height: 254px;text-align: center;padding: 12px;.time {line-height: 20px;}.img {width: 139px;height: 139px;}.btn {width: 156px;height: 48px;}}}
}
.cancelPop {width: 50px;height: 50px;position: absolute;bottom: 0;left: 50%;transform: translateX(-50%);
}
.lastStrikeCentent {width: 688px;height: 898px;background: url($yjnewYearlastStrike) no-repeat bottom;background-size: 100%;position: absolute;top: 50%;left: 50%;transform: translate(-50%, -50%);.box {position: absolute;left: 50%;top: 270px;transform: translateX(-50%);}.text {width: 240px;color: #fad16c;font-family: "AP600";font-size: 22px;text-align: center;padding: 0 10px;}.img {width: 241px;height: 159px;transform: rotate(20deg);margin-top: 40px;margin-left: -8px;}.price {@extend .text;display: flex;justify-content: center;margin-top: 33px;img {width: 26px;height: 26px;transform: translateY(-2px);}}
}
:deep(.van-stepper__minus) {background: rgba(0, 0, 0, 0);color: #fff;font-family: "AP700";font-size: 32px;
}
:deep(.van-stepper__minus--disabled) {color: #ccc; /* 设置为变暗的颜色 */cursor: not-allowed; /* 修改鼠标样式 */opacity: 0.5; /* 设置透明度 */
}
/* 覆盖减号图标 */
:deep(.van-stepper__minus:before) {width: 0px;height: 0px;content: "-";transform: translateY(-15px);
}
:deep(.van-stepper__input) {width: 64px;height: 40px;border-radius: 2px;color: #fff;font-family: "AP700";font-size: 28px;background: rgba(0, 0, 0, 0);border: 2px solid #fcc651;
}
:deep(.van-stepper__plus) {background: rgba(0, 0, 0, 0);color: #fff;font-family: "AP700";font-size: 32px;transform: translate(-15px, -15px);
}
// /* 覆盖加号图标 */
:deep(.van-stepper__plus:before) {width: 0px;height: 0px;content: "+";
}
:deep(.van-stepper__plus:after) {width: 0px;height: 0px;
}
</style>
2.2 newYearRanking.vue
<template><div class="allPages"><TopNavigationY title="排行榜" /><imgclass="title":src="useImageUrl('newYearEvent', `rankingTitle`, 'develop')"alt=""/><swiperclass="mySwiper":slides-per-view="3":space-between="10"><swiper-slide:class="['titleItem', { active: item == activeIndex }]"v-for="item in 4":key="item"@click="handleToggle(item)">年兽{{ item }}号</swiper-slide></swiper><div class="list"><divv-for="item in rankingInfo":key="item.id"class="rankingInfo"><imgv-if="item.id <= 3"class="img":src="useImageUrl('newYearEvent', `rank${item.id}`, 'develop')"alt=""/><divv-if="item.id > 3"class="imgs">{{ item.id }}</div><imgclass="profilePicture":src="useImageUrl('newYearEvent', `rank3`, 'develop')"alt=""/><text-show:direction="'center'":text="ProhibitedWords(item.name)":text-id="item.id"><div class="name textEllipsis">{{ ProhibitedWords(item.name) }}</div></text-show><div class="damageValue">伤害值:<imgclass="shellIcon":src="useImageUrl('newYearEvent', `shellIcon`, 'develop')"alt=""/>x{{ convertToWan(item.damageValue) }}</div></div><touchGroundclass="touchGround":total="pageData.total":size="pageData.size":currentpage="pageData.current"@touchGroundFun="touchGroundFun"></touchGround></div><div class="personalInfo"><div class="userName"><imgclass="userIcon":src="useImageUrl('newYearEvent', `shellIcon`, 'develop')"alt=""/><text-show:direction="'center'":text="myInfo.name":text-id="3 + myInfo.ranking"><div class="name textEllipsis">{{ myInfo.name }}</div></text-show></div><div class="ranking">排名: {{ myInfo.ranking }}</div><div class="damageValueC">伤害值:<imgclass="shellIcon":src="useImageUrl('newYearEvent', `shellIcon`, 'develop')"alt=""/>{{ convertToWan(myInfo.damageValue) }}</div></div></div>
</template>
<script setup lang="ts">
import { ref } from "vue"
import { useImageUrl,ProhibitedWords,convertToWan } from "@/utils/index"
import "swiper/css"
import { Swiper, SwiperSlide } from "swiper/vue"
const rankingInfo = ref([{id: 1,name: "111111",damageValue: "222"},{id: 2,name: "小仙女",damageValue: "222"},{id: 3,name: "111111",damageValue: "222"},{id: 4,name: "111111",damageValue: "222"},{id: 5,name: "11111wwwwwwwww1",damageValue: "222"},{id: 6,name: "111111",damageValue: "222"},{id: 1,name: "111111",damageValue: "222"},{id: 2,name: "111111",damageValue: "222"},{id: 3,name: "111111",damageValue: "222"},{id: 4,name: "111111",damageValue: "222"},{id: 5,name: "11111wwwwwwwww1",damageValue: "222"},{id: 6,name: "111111",damageValue: "222"}
])
const pageData = ref({current: 1,size: 15,total: 1
})
const myInfo = ref({name: "eee",ranking: 111111,damageValue: "331133"
})
const activeIndex = ref(1)
// 选择年兽
const handleToggle = val => {activeIndex.value = val
}
const getlistData = async () => {}
// 触底加载
const touchGroundFun = () => {pageData.value.size += 15getlistData()
}
</script><style lang="scss" scoped>
.allPages {width: 750px;height: 1624px;background: url($yjnewYeartheChartsBg) no-repeat center;background-size: 100% 100%;.title {width: 570px;height: 137px;margin: 30px 90px 20px;}.mySwiper {height: 64px;margin: 0 26px 50px;.titleItem {width: 186px;height: 64px;background: url($yjnewYearcheckedState) no-repeat center;background-size: 100% 100%;color: #bc2811;text-align: center;font-family: "AP600";font-size: 32px;line-height: 64px;&.active {background-image: url($yjnewYearcheckedStates);background-size: 100% 100%;color: #ffe7bb;}}}.list {height: 1100px;overflow: auto;padding-bottom: 50px;.rankingInfo {width: 694px;height: 120px;background: url($yjnewYearrankingBg) no-repeat center;background-size: 100% 100%;margin: 0 auto 24px;display: flex;align-items: center;.img {width: 64px;height: 64px;margin-left: 42px;}.imgs {@extend .img;background: url($yjnewYearrank4) no-repeat center;background-size: 100% 100%;color: #f1bc7a;text-align: center;font-family: "AP600";font-size: 34px;line-height: 66px;}.profilePicture {width: 50px;height: 48px;border-radius: 50%;background: lightgray 50% / cover no-repeat;flex-shrink: 0;margin-left: 32px;}.text {color: #ffe7bb;font-family: "AP500";font-size: 28px;}.name {@extend .text;width: 200px;margin: 0 18px;}.damageValue {@extend .text;float: right;margin-top: -10px;.shellIcon {width: 38px;height: 38px;transform: translateY(5px);}}}}.personalInfo {width: 750px;height: 108px;background: url($yjnewYearpersonalInfoBg) no-repeat center;background-size: 100% 100%;position: fixed;bottom: 0px;display: flex;align-items: center;.text {color: #fff7c4;font-family: "AP400";font-size: 28px;}.userName {width: 210px;display: flex;align-items: center;justify-content: center;.userIcon {width: 50px;height: 48px;flex-shrink: 0;margin-right: 10px;}.name {max-width: 130px;transform: translateY(3px);}}.ranking {@extend .text;width: 180px;margin-left: 54px;transform: translateY(4px);}.damageValueC {@extend .text;transform: translateY(-4px);position: absolute;right: 30px;.shellIcon {width: 38px;height: 38px;transform: translateY(4px);}}}
}
</style>
2.3 animation/shell.vue 设置炮弹
<template><div><divv-for="(animationData, index) in animations":key="index"class="animation-container"></div></div>
</template><script setup lang="ts">
import { ref, onBeforeUnmount } from "vue"
import lottie from "lottie-web"
import JSONanimations from "@/assets/json/shell.json"// 响应式数据,用于跟踪动画实例
const animations = ref<{ id: number; container: HTMLElement }[]>([])
const animationInstances = ref<any>([]) // 存储 Lottie 动画实例// 创建动画实例
const createAnimation = () => {const container = document.createElement("div")container.className = "animation-container"document.body.appendChild(container)const animation = lottie.loadAnimation({container,renderer: "svg",loop: true,autoplay: true,animationData: JSONanimations})animationInstances.value.push(animation)animations.value.push({ id: animationInstances.value.length - 1, container })// 设置定时器,3秒后删除动画setTimeout(() => {removeAnimation(animation, container)}, 500)
}// 移除动画实例和 DOM 元素
const removeAnimation = (animationToRemove, container) => {const index = animationInstances.value.indexOf(animationToRemove)if (index > -1) {animationToRemove.destroy() // 销毁动画实例animationInstances.value.splice(index, 1) // 从数组中移除动画实例animations.value.splice(index, 1) // 从响应式数据中移除container.remove() // 从 DOM 中移除容器元素}
}// 在组件销毁前清理动画和 DOM 元素
onBeforeUnmount(() => {animationInstances.value.forEach((animation, index) => {animation.destroy() // 销毁动画实例animations.value[index]?.container.remove() // 从 DOM 中移除容器元素})animationInstances.value.length = 0 // 清空实例数组animations.value = [] // 清空响应式数据
})
// 暴露方法给父组件
defineExpose({createAnimation
})
</script><style>
.animation-container {width: 750px;height: 1435px;position: absolute;top: 0;
}
</style>