bindbox-mini/components/BoxReveal.vue

339 lines
7.4 KiB
Vue

<template>
<view class="box-reveal-root">
<!-- Stage 1: The Box -->
<view v-if="stage === 'box'" class="box-stage" :class="{ shaking: isShaking }">
<view class="mystery-box">
<image class="box-img" src="/static/images/mystery-box.png" mode="widthFix" />
<view class="box-glow"></view>
</view>
<text class="box-tip">{{ isShaking ? '正在开启...' : '准备开启' }}</text>
</view>
<!-- Stage 2: Reveal Results -->
<view v-else-if="stage === 'result'" class="result-stage">
<view class="result-light-burst"></view>
<!-- Single Reward -->
<view v-if="rewards.length === 1" class="single-reward">
<view class="reward-card large bounce-in">
<image class="reward-img" :src="rewards[0].image" mode="aspectFit" />
<view class="reward-info">
<text class="reward-name">{{ rewards[0].title }}</text>
<text class="reward-desc">恭喜获得</text>
</view>
</view>
</view>
<!-- Multiple Rewards (Horizontal Scroll) -->
<scroll-view v-else scroll-x class="multi-rewards-scroll">
<view class="rewards-track">
<view
v-for="(item, index) in rewards"
:key="index"
class="reward-card small slide-in-right"
:style="{ animationDelay: `${index * 0.1}s` }"
>
<view class="card-inner">
<image class="reward-img" :src="item.image" mode="aspectFit" />
<text class="reward-name">{{ item.title }}</text>
</view>
</view>
</view>
</scroll-view>
<view class="action-area">
<button class="confirm-btn" @tap="onConfirm">收下奖励</button>
</view>
</view>
</view>
</template>
<script setup>
import { ref } from 'vue'
const props = defineProps({
// No specific props needed for now, rewards passed via method
})
const emit = defineEmits(['close'])
const stage = ref('box') // box, result
const isShaking = ref(false)
const rewards = ref([])
// Public method to reset state
function reset() {
stage.value = 'box'
isShaking.value = false
rewards.value = []
}
// Public method to reveal results
function revealResults(list) {
const arr = Array.isArray(list) ? list : (list ? [list] : [])
rewards.value = arr
// Start animation sequence
isShaking.value = true
// Shake for 1.5s then open
setTimeout(() => {
isShaking.value = false
stage.value = 'result'
uni.vibrateLong()
}, 1500)
}
function onConfirm() {
emit('close')
}
defineExpose({ reset, revealResults })
</script>
<style lang="scss" scoped>
.box-reveal-root {
width: 100%;
min-height: 600rpx;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
position: relative;
}
/* Stage 1: Box */
.box-stage {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.mystery-box {
width: 400rpx;
height: 400rpx;
position: relative;
display: flex;
align-items: center;
justify-content: center;
}
.box-img {
width: 100%;
height: 100%;
position: relative;
z-index: 2;
// Fallback if image missing, use a block
min-height: 300rpx;
}
.box-glow {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 500rpx;
height: 500rpx;
background: radial-gradient(circle, rgba($brand-primary, 0.6) 0%, transparent 70%);
z-index: 1;
filter: blur(40rpx);
animation: pulse 2s infinite;
}
.box-tip {
margin-top: 40rpx;
font-size: 32rpx;
color: $text-main;
font-weight: 600;
letter-spacing: 2rpx;
}
.shaking .mystery-box {
animation: shake 0.5s cubic-bezier(.36,.07,.19,.97) infinite both;
}
@keyframes shake {
10%, 90% { transform: translate3d(-2px, 0, 0) rotate(-2deg); }
20%, 80% { transform: translate3d(4px, 0, 0) rotate(2deg); }
30%, 50%, 70% { transform: translate3d(-8px, 0, 0) rotate(-4deg); }
40%, 60% { transform: translate3d(8px, 0, 0) rotate(4deg); }
}
@keyframes pulse {
0% { opacity: 0.5; transform: translate(-50%, -50%) scale(0.9); }
50% { opacity: 0.8; transform: translate(-50%, -50%) scale(1.1); }
100% { opacity: 0.5; transform: translate(-50%, -50%) scale(0.9); }
}
/* Stage 2: Result */
.result-stage {
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
animation: fade-in 0.5s ease-out;
}
.result-light-burst {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 800rpx;
height: 800rpx;
background: radial-gradient(circle, rgba(255, 200, 50, 0.2) 0%, transparent 70%);
animation: rotate-slow 10s linear infinite;
pointer-events: none;
z-index: 0;
}
/* Single Reward */
.single-reward {
position: relative;
z-index: 2;
margin-bottom: 60rpx;
}
.reward-card.large {
width: 460rpx;
background: #fff;
border-radius: 32rpx;
padding: 40rpx;
box-shadow: 0 20rpx 60rpx rgba(0,0,0,0.15);
display: flex;
flex-direction: column;
align-items: center;
border: 4rpx solid $bg-secondary;
}
.reward-card.large .reward-img {
width: 320rpx;
height: 320rpx;
margin-bottom: 30rpx;
}
.reward-card.large .reward-name {
font-size: 36rpx;
font-weight: bold;
color: $text-main;
text-align: center;
margin-bottom: 12rpx;
line-height: 1.4;
}
.reward-card.large .reward-desc {
font-size: 24rpx;
color: $text-tertiary;
}
/* Multiple Rewards */
.multi-rewards-scroll {
width: 100%;
white-space: nowrap;
padding: 20rpx 0;
margin-bottom: 40rpx;
}
.rewards-track {
display: flex;
padding: 0 40rpx;
align-items: center;
}
.reward-card.small {
display: inline-block;
width: 240rpx;
height: 320rpx;
background: #fff;
border-radius: 20rpx;
margin-right: 24rpx;
box-shadow: $shadow-md;
overflow: hidden;
position: relative;
vertical-align: top;
}
.card-inner {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
padding: 20rpx;
box-sizing: border-box;
}
.reward-card.small .reward-img {
width: 160rpx;
height: 160rpx;
margin-bottom: 20rpx;
margin-top: 20rpx;
}
.reward-card.small .reward-name {
font-size: 24rpx;
color: $text-main;
font-weight: 600;
text-align: center;
white-space: normal;
line-height: 1.3;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
overflow: hidden;
}
/* Animations */
.bounce-in {
animation: bounce-in 0.8s cubic-bezier(0.215, 0.61, 0.355, 1);
}
@keyframes bounce-in {
0% { opacity: 0; transform: scale(0.3); }
20% { transform: scale(1.1); }
40% { transform: scale(0.9); }
60% { opacity: 1; transform: scale(1.03); }
80% { transform: scale(0.97); }
100% { opacity: 1; transform: scale(1); }
}
.slide-in-right {
animation: slide-in-right 0.5s cubic-bezier(0.250, 0.460, 0.450, 0.940) both;
}
@keyframes slide-in-right {
0% { transform: translateX(100rpx); opacity: 0; }
100% { transform: translateX(0); opacity: 1; }
}
@keyframes fade-in {
from { opacity: 0; }
to { opacity: 1; }
}
@keyframes rotate-slow {
from { transform: translate(-50%, -50%) rotate(0deg); }
to { transform: translate(-50%, -50%) rotate(360deg); }
}
.confirm-btn {
background: $gradient-brand;
color: #fff;
border-radius: 999rpx;
padding: 0 80rpx;
height: 88rpx;
line-height: 88rpx;
font-size: 32rpx;
font-weight: bold;
box-shadow: $shadow-warm;
border: none;
&:active {
transform: scale(0.96);
}
}
</style>