界面如下

代码如下
<template>
<view class="puzzle-container">
<view class="puzzle-title">任务进度 {{completedCount}}/{{totalPieces}}</view>
<view class="puzzle-grid">
<view
v-for="(piece, index) in puzzlePieces"
:key="index"
class="puzzle-piece"
:class="{
'unlocked': piece.unlocked,
'locked': !piece.unlocked,
'unlock-animation': animatingIndex === index
}"
@click="onPieceTap(index)"
>
<image
v-if="piece.unlocked"
:src="piece.imageUrl"
mode="aspectFill"
class="piece-image"
></image>
<image
v-else
src="@/static/images/form/close-image.png"
class="lock-icon"
></image>
<text class="piece-number">{{index + 1}}</text>
<view class="particle-container" v-if="animatingIndex === index">
<view
class="particle"
v-for="particle in particles"
:key="particle.id"
:style="{
left: particle.left + '%',
top: particle.top + '%',
backgroundColor: particle.color
}"
></view>
</view>
</view>
</view>
<uni-popup ref="completePopup" type="center">
<view class="celebration-popup">
<image src="/static/celebration.png" mode="widthFix"></image>
<text class="celebration-text">恭喜完成所有任务!</text>
<button @click="viewCompletePuzzle">查看完整拼图</button>
</view>
</uni-popup>
</view>
</template>
<script>
export default {
data() {
return {
totalPieces: 20,
puzzlePieces: [],
completedCount: 0,
animatingIndex: -1,
particles: [],
allCompleted: false
}
},
created() {
this.initPuzzle();
},
methods: {
initPuzzle() {
this.puzzlePieces = Array(this.totalPieces).fill().map((_, index) => ({
index,
unlocked: false,
imageUrl: `../../static/pt/pt${index + 1}.png`
}));
this.unlockPiece(0);
this.unlockPiece(1);
this.unlockPiece(2);
this.unlockPiece(3);
this.unlockPiece(4);
this.completedCount = 5;
},
unlockPiece(index) {
if (index >= this.totalPieces || this.puzzlePieces[index].unlocked) return;
this.$set(this.puzzlePieces, index, {
...this.puzzlePieces[index],
unlocked: true
});
this.animatingIndex = index;
this.completedCount++;
this.createParticles();
if (this.completedCount === this.totalPieces) {
this.allCompleted = true;
setTimeout(() => {
this.$refs.completePopup.open();
}, 1000);
}
setTimeout(() => {
this.animatingIndex = -1;
}, 1000);
},
createParticles() {
this.particles = [];
const particles = [];
for (let i = 0; i < 12; i++) {
particles.push({
id: i,
left: Math.random() * 60 + 20,
top: Math.random() * 60 + 20,
color: `hsl(${Math.random() * 360}, 100%, 50%)`,
'--tx': Math.random() * 100,
'--ty': Math.random() * 100
});
}
this.particles = particles;
setTimeout(() => {
this.particles = [];
}, 1000);
},
onPieceTap(index) {
if (!this.puzzlePieces[index].unlocked) {
uni.showToast({
title: `完成任务${index + 1}后解锁`,
icon: 'none'
});
}
},
completeRandomTask() {
const firstLockedIndex = this.puzzlePieces.findIndex(p => !p.unlocked);
if (firstLockedIndex !== -1) {
this.unlockPiece(firstLockedIndex);
} else {
uni.showToast({
title: '所有拼图已解锁!',
icon: 'success'
});
}
},
viewCompletePuzzle() {
this.$refs.completePopup.close();
uni.navigateTo({
url: '/pages/puzzle-preview/puzzle-preview'
});
}
}
}
</script>
<style lang="scss" scoped>
.puzzle-container {
padding: 20rpx;
box-sizing: border-box;
}
.puzzle-title {
text-align: center;
font-size: 36rpx;
margin-bottom: 30rpx;
color: #333;
}
.puzzle-grid {
display: grid;
grid-template-columns: repeat(5, 1fr);
gap: 10rpx;
}
.puzzle-piece {
position: relative;
aspect-ratio: 1;
border-radius: 10rpx;
overflow: hidden;
display: flex;
justify-content: center;
align-items: center;
background-color: #f5f5f5;
&.unlocked {
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.1);
}
.piece-image, .lock-icon {
width: 100%;
height: 100%;
}
.lock-icon {
width: 60% !important;
height: 60% !important;
opacity: 0.6;
}
.piece-number {
position: absolute;
bottom: 8rpx;
right: 8rpx;
font-size: 24rpx;
color: #999;
background-color: rgba(255, 255, 255, 0.7);
border-radius: 50%;
width: 36rpx;
height: 36rpx;
display: flex;
justify-content: center;
align-items: center;
}
}
.unlock-animation {
animation: unlockScale 0.6s ease-out;
}
@keyframes unlockScale {
0% { transform: scale(0.8); opacity: 0; }
50% { transform: scale(1.1); }
100% { transform: scale(1); opacity: 1; }
}
.particle-container {
position: absolute;
width: 100%;
height: 100%;
pointer-events: none;
}
.particle {
position: absolute;
width: 10rpx;
height: 10rpx;
border-radius: 50%;
animation: particleFly 1s ease-out forwards;
}
@keyframes particleFly {
to {
transform: translate(
calc((var(--tx, 0) - 50) * 1rpx),
calc((var(--ty, 0) - 50) * 1rpx)
);
opacity: 0;
}
}
.celebration-popup {
background: white;
padding: 40rpx;
border-radius: 20rpx;
display: flex;
flex-direction: column;
align-items: center;
image {
width: 200rpx;
margin-bottom: 20rpx;
}
.celebration-text {
font-size: 36rpx;
margin-bottom: 30rpx;
color: #ff6b81;
font-weight: bold;
}
button {
background: #ff6b81;
color: white;
border: none;
}
}
</style>