vue3开发打年兽功能
1.效果
WeChat_20250217192041
2.代码
2.1 index.vue
<template>
<div class="pages">
<TopNavigationY
leftTitle="打年兽"
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>
<SvgIcon
class="custimage"
width="34px"
height="34px"
name="customerIconBW"
@click="openService()"
/>
</template>
</TopNavigationY>
<div class="nianBeastBox">
<transition-group name="fade">
<animation
v-for="item in JSONanimations"
:key="item.id"
:JSONanimations="item.anim"
v-show="item.id == animationId && animationId != -1"
class="nianBeastImg"
></animation>
<img
v-show="animationId == -1"
class="death"
:src="useImageUrl('newYearEvent', `criticalStrike`, 'develop')"
alt=""
/>
</transition-group>
<shell
ref="shellRef"
class="shell"
/>
<div class="nianBeast">
<!-- 动态渲染伤害值 -->
<div
v-for="item in data.elementsHtml"
:key="item.id"
class="damageValueNum"
v-show="data.elementsHtml"
>
<div
class="tex"
v-show="!item.status"
>
<div class="text1">-</div>
<div class="text2">-</div>
<div class="text3">-</div>
</div>
<img
v-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>
<img
v-show="data.lastStrikesImgShow"
class="lastStrike"
:src="useImageUrl('newYearEvent', `lastStrikes`, 'develop')"
alt=""
/>
<div class="Progress">
<van-progress
:percentage="data.progressNum"
:show-pivot="false"
/>
<div
class="progress_pivot"
:style="{ left: data.progressNum + '%' }"
></div>
</div>
<div class="countdown">
剩余时间:
<van-count-down
:time="data.countdown"
@finish="finishCountdown"
/>
</div>
</div>
<div class="cannon">
<img
class="cannonLeft"
:src="useImageUrl('newYearEvent', `barrel1`, 'develop')"
alt=""
/>
<img
class="view"
:src="useImageUrl('newYearEvent', `view`, 'develop')"
alt=""
@click="viewYearBeast()"
/>
<img
class="cannonRight"
:src="useImageUrl('newYearEvent', `barrel2`, 'develop')"
alt=""
/>
</div>
<div class="shellBox">
<div class="shellList">
<div
v-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-stepper
v-model="data.shellNum"
:integer="true"
/>
</div>
<div class="strikeYearBeast">
<img
class="ranking"
@click="goToRanking"
:src="useImageUrl('newYearEvent', `ranking`, 'develop')"
alt=""
/>
<div
class="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">
<div
class="cententItem"
v-for="item in data.nianBeastList"
:key="item.id"
>
<div class="time">开始时间:{{ item.startTime }}</div>
<div class="time">结束时间:{{ item.endTime }}</div>
<img
class="img"
src="@/assets/image/newYearEvent/nianBeast.png"
alt=""
/>
<img
class="btn"
:src="useImageUrl('newYearEvent', `btn${item.status}`, 'develop')"
alt=""
/>
</div>
</div>
<div
class="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>
<img
class="img"
src="https://img.zbt.com/e/steam/item/730/UDkwIHwgQXNpaW1vdiAoRmFjdG9yeSBOZXcp.png"
alt=""
/>
<div class="price">
<img
class="coinImg"
:src="useImageUrl('base', 'conch')"
alt=""
/>
{{ data.jewelry.price }}
</div>
</div>
<div
class="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 = true
console.log(`output-11111`, 11111)
}
// 关闭查看年兽弹窗
const cancelYearBeast = () => {
data.nianBeastPop = false
data.lastStrikeShow = false
}
// 选择炮弹
const selectProjectile = val => {
data.shellFrameIndex = val
data.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) return
data.nianBeastList.map(item => {
if (item.status == 1) {
animationId.value = item.id
}
})
}
watch(
() => data.progressNum,
val => {
if (val <= 0) {
animationId.value = -1
data.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="排行榜" />
<img
class="title"
:src="useImageUrl('newYearEvent', `rankingTitle`, 'develop')"
alt=""
/>
<swiper
class="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">
<div
v-for="item in rankingInfo"
:key="item.id"
class="rankingInfo"
>
<img
v-if="item.id <= 3"
class="img"
:src="useImageUrl('newYearEvent', `rank${item.id}`, 'develop')"
alt=""
/>
<div
v-if="item.id > 3"
class="imgs"
>
{{ item.id }}
</div>
<img
class="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">
伤害值:
<img
class="shellIcon"
:src="useImageUrl('newYearEvent', `shellIcon`, 'develop')"
alt=""
/>
x{{ convertToWan(item.damageValue) }}
</div>
</div>
<touchGround
class="touchGround"
:total="pageData.total"
:size="pageData.size"
:currentpage="pageData.current"
@touchGroundFun="touchGroundFun"
></touchGround>
</div>
<div class="personalInfo">
<div class="userName">
<img
class="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">
伤害值:
<img
class="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 += 15
getlistData()
}
</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>
<div
v-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>