bindbox-mini/components/activity/LotteryResultPopup.vue
2026-01-02 20:07:24 +08:00

503 lines
12 KiB
Vue
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<view v-if="visible" class="lottery-overlay" @touchmove.stop.prevent>
<!-- 背景光效 -->
<view class="bg-glow"></view>
<view class="bg-rays"></view>
<!-- 彩带粒子 -->
<view class="confetti-container">
<view v-for="i in 20" :key="i" class="confetti" :style="getConfettiStyle(i)"></view>
</view>
<!-- 主内容区 -->
<view class="lottery-content">
<!-- 中奖标题 -->
<view class="title-area">
<view class="crown-icon">🎉</view>
<text class="main-title">恭喜获得</text>
</view>
<!-- 奖品展示区 -->
<scroll-view scroll-y class="prizes-scroll">
<view class="prizes-grid">
<view
v-for="(item, index) in groupedResults"
:key="index"
class="prize-card"
:style="{ animationDelay: `${0.2 + index * 0.15}s` }"
>
<!-- 光效边框 -->
<view class="card-glow-border"></view>
<!-- 卡片内容 -->
<view class="card-inner">
<view class="qty-badge" v-if="item.quantity > 1">x{{ item.quantity }}</view>
<view class="image-wrap">
<image
v-if="item.image"
class="prize-img"
:src="item.image"
mode="aspectFill"
@tap="previewImage(item.image)"
/>
<view v-else class="prize-placeholder">🎁</view>
</view>
<view class="prize-details">
<text class="prize-name">{{ item.title }}</text>
</view>
</view>
</view>
</view>
</scroll-view>
<!-- 底部按钮 -->
<view class="action-area">
<!-- 如果使用次数卡显示"再来一次"按钮 -->
<view v-if="showRetryButton" class="retry-buttons">
<view class="retry-btn" @tap="handleRetry">
<view class="btn-glow"></view>
<view class="btn-inner">
<text class="btn-icon">🔄</text>
<text class="btn-text">再来一次</text>
</view>
</view>
<view class="secondary-btn" @tap="handleClose">
<text class="btn-text">知道了</text>
</view>
</view>
<!-- 普通情况显示单个按钮 -->
<view v-else class="claim-btn" @tap="handleClose">
<view class="btn-glow"></view>
<view class="btn-inner">
<text class="btn-icon"></text>
<text class="btn-text">知道了</text>
</view>
</view>
</view>
</view>
</view>
</template>
<script setup>
import { computed } from 'vue'
const props = defineProps({
visible: { type: Boolean, default: false },
results: { type: Array, default: () => [] },
showRetryButton: { type: Boolean, default: false } // 是否显示"再来一次"按钮
})
const emit = defineEmits(['update:visible', 'close', 'retry'])
function cleanUrl(u) {
if (!u) return '/static/logo.png'
let s = String(u).trim()
// 尝试解析 JSON 数组字符串 (针对后端返回的 JSON 字符串图片地址)
if (s.startsWith('[') && s.endsWith(']')) {
try {
const arr = JSON.parse(s)
if (Array.isArray(arr) && arr.length > 0) {
s = arr[0]
}
} catch (e) {
console.warn('JSON parse failed for prize image:', s)
}
}
// 清理反引号、引号和空格
s = s.replace(/[`'"]/g, '').trim()
// 提取 http 链接
const m = s.match(/https?:\/\/[^\s]+/)
if (m && m[0]) return m[0]
return s || '/static/logo.png'
}
const groupedResults = computed(() => {
const map = new Map()
const arr = Array.isArray(props.results) ? props.results : []
arr.forEach(item => {
// 使用reward_id作为唯一key避免同名不同产品被错误合并
const rewardId = item.reward_id || item.rewardId || item.id
const key = rewardId != null ? `rid_${rewardId}` : (item.title || item.name || '神秘奖品')
if (map.has(key)) {
map.get(key).quantity++
} else {
map.set(key, {
title: item.title || item.name || '神秘奖品',
image: cleanUrl(item.image || item.img || item.pic || ''),
reward_id: rewardId,
quantity: 1
})
}
})
return Array.from(map.values())
})
function getConfettiStyle(i) {
const colors = ['#FF6B35', '#FFD93D', '#6BCB77', '#4D96FF', '#FF6B6B', '#C9B1FF']
const left = Math.random() * 100
const delay = Math.random() * 2
const duration = 2 + Math.random() * 2
const size = 8 + Math.random() * 8
return {
left: `${left}%`,
animationDelay: `${delay}s`,
animationDuration: `${duration}s`,
width: `${size}rpx`,
height: `${size * 1.5}rpx`,
background: colors[i % colors.length]
}
}
function handleClose() {
emit('update:visible', false)
emit('close')
}
function handleRetry() {
emit('update:visible', false)
emit('retry')
}
function previewImage(url) {
if (url) uni.previewImage({ urls: [url], current: url })
}
</script>
<style lang="scss" scoped>
.lottery-overlay {
position: fixed;
top: 0; left: 0; right: 0; bottom: 0;
z-index: 2000;
display: flex;
align-items: center;
justify-content: center;
background: radial-gradient(ellipse at center, rgba(30, 20, 50, 0.95) 0%, rgba(10, 5, 20, 0.98) 100%);
animation: fadeIn 0.3s ease-out;
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
/* 背景光效 */
.bg-glow {
position: absolute;
top: 20%; left: 50%;
transform: translateX(-50%);
width: 600rpx; height: 600rpx;
background: radial-gradient(circle, rgba(255, 180, 100, 0.4) 0%, transparent 70%);
filter: blur(60rpx);
animation: pulse 3s ease-in-out infinite;
}
.bg-rays {
position: absolute;
top: 15%; left: 50%;
transform: translateX(-50%);
width: 800rpx; height: 800rpx;
background: conic-gradient(from 0deg, transparent, rgba(255, 200, 100, 0.1), transparent, rgba(255, 200, 100, 0.1), transparent);
animation: rotate 20s linear infinite;
}
@keyframes rotate { to { transform: translateX(-50%) rotate(360deg); } }
@keyframes pulse { 0%, 100% { opacity: 0.6; transform: translateX(-50%) scale(1); } 50% { opacity: 1; transform: translateX(-50%) scale(1.1); } }
/* 彩带 */
.confetti-container {
position: absolute;
top: 0; left: 0; right: 0;
height: 100%;
overflow: hidden;
pointer-events: none;
}
.confetti {
position: absolute;
top: -20rpx;
border-radius: 4rpx;
animation: confettiFall 3s linear infinite;
}
@keyframes confettiFall {
0% { transform: translateY(-20rpx) rotate(0deg); opacity: 1; }
100% { transform: translateY(100vh) rotate(720deg); opacity: 0; }
}
/* 主内容 */
.lottery-content {
position: relative;
width: 88%;
max-height: 80vh;
display: flex;
flex-direction: column;
align-items: center;
animation: contentPop 0.5s cubic-bezier(0.34, 1.56, 0.64, 1);
}
@keyframes contentPop {
from { opacity: 0; transform: scale(0.8) translateY(40rpx); }
to { opacity: 1; transform: scale(1) translateY(0); }
}
/* 标题区 */
.title-area {
position: relative;
text-align: center;
margin-bottom: 40rpx;
z-index: 10;
}
.crown-icon {
font-size: 80rpx;
display: block;
animation: bounce 2s ease-in-out infinite;
}
@keyframes bounce {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-10rpx); }
}
.main-title {
font-size: 56rpx;
font-weight: 900;
color: #fff;
text-shadow: 0 0 40rpx rgba(255, 180, 100, 0.8), 0 4rpx 20rpx rgba(0, 0, 0, 0.5);
display: block;
letter-spacing: 8rpx;
}
/* 奖品滚动区 */
.prizes-scroll {
width: 100%;
max-height: 50vh;
padding: 0 10rpx;
}
.prizes-grid {
display: flex;
flex-wrap: wrap;
gap: 24rpx;
justify-content: center;
padding: 20rpx 0;
}
/* 奖品卡片 */
.prize-card {
position: relative;
width: calc(50% - 12rpx);
max-width: 300rpx;
animation: cardReveal 0.6s ease-out backwards;
}
@keyframes cardReveal {
from { opacity: 0; transform: scale(0.8) rotateY(-30deg); }
to { opacity: 1; transform: scale(1) rotateY(0); }
}
.card-glow-border {
position: absolute;
inset: -4rpx;
background: linear-gradient(135deg, #FFD700, #FF8C00, #FFD700, #FF6347, #FFD700);
background-size: 400% 400%;
border-radius: 28rpx;
animation: borderGlow 3s ease infinite;
z-index: 0;
}
@keyframes borderGlow {
0%, 100% { background-position: 0% 50%; }
50% { background-position: 100% 50%; }
}
.card-inner {
position: relative;
background: linear-gradient(145deg, rgba(255, 255, 255, 0.98), rgba(255, 248, 240, 0.95));
border-radius: 24rpx;
padding: 24rpx;
display: flex;
flex-direction: column;
align-items: center;
z-index: 1;
}
.qty-badge {
position: absolute;
top: -12rpx; right: -12rpx;
background: linear-gradient(135deg, #FF6B35, #FF8C00);
color: #fff;
font-size: 24rpx;
font-weight: 800;
padding: 8rpx 16rpx;
border-radius: 20rpx;
box-shadow: 0 4rpx 16rpx rgba(255, 107, 53, 0.5);
z-index: 10;
}
.image-wrap {
width: 160rpx; height: 160rpx;
border-radius: 16rpx;
overflow: hidden;
background: linear-gradient(145deg, #FFF8F3, #FFE8D1);
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.1);
}
.prize-img {
width: 100%; height: 100%;
}
.prize-placeholder {
font-size: 64rpx;
}
.prize-details {
margin-top: 16rpx;
text-align: center;
width: 100%;
}
.prize-name {
font-size: 24rpx;
font-weight: 700;
color: #333;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
line-height: 1.4;
}
/* 底部按钮 - 重新设计 */
.action-area {
width: 100%;
padding: 40rpx 20rpx 20rpx;
}
.claim-btn {
position: relative;
width: 100%;
height: 110rpx;
display: flex;
align-items: center;
justify-content: center;
&:active .btn-inner {
transform: scale(0.96);
}
}
.retry-buttons {
display: flex;
gap: 16rpx;
width: 100%;
}
.retry-btn {
position: relative;
flex: 2;
height: 110rpx;
display: flex;
align-items: center;
justify-content: center;
&:active .btn-inner {
transform: scale(0.96);
}
}
.secondary-btn {
flex: 1;
height: 110rpx;
background: rgba(255, 255, 255, 0.2);
border: 2rpx solid rgba(255, 255, 255, 0.3);
border-radius: 55rpx;
display: flex;
align-items: center;
justify-content: center;
&:active {
background: rgba(255, 255, 255, 0.3);
transform: scale(0.96);
}
.btn-text {
font-size: 30rpx;
font-weight: 700;
color: #fff;
text-shadow: 0 2rpx 4rpx rgba(0, 0, 0, 0.2);
}
}
.btn-glow {
position: absolute;
inset: 0;
background: linear-gradient(135deg, #FFD700, #FF8C00, #FF6B35);
border-radius: 55rpx;
filter: blur(15rpx);
opacity: 0.6;
animation: btnPulse 2s ease-in-out infinite;
}
@keyframes btnPulse {
0%, 100% { opacity: 0.4; transform: scale(1); }
50% { opacity: 0.7; transform: scale(1.02); }
}
.btn-inner {
position: relative;
width: 100%;
height: 100%;
background: linear-gradient(135deg, #FFD700 0%, #FF8C00 50%, #FF6B35 100%);
border-radius: 55rpx;
display: flex;
align-items: center;
justify-content: center;
gap: 12rpx;
box-shadow:
0 8rpx 32rpx rgba(255, 140, 0, 0.5),
inset 0 2rpx 0 rgba(255, 255, 255, 0.4),
inset 0 -2rpx 0 rgba(0, 0, 0, 0.1);
transition: transform 0.2s ease;
overflow: hidden;
&::before {
content: '';
position: absolute;
top: 0; left: -100%;
width: 100%; height: 100%;
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.3), transparent);
animation: btnShine 2.5s ease-in-out infinite;
}
}
@keyframes btnShine {
0% { left: -100%; }
50%, 100% { left: 100%; }
}
.btn-icon {
font-size: 36rpx;
}
.btn-text {
font-size: 34rpx;
font-weight: 800;
color: #fff;
text-shadow: 0 2rpx 4rpx rgba(0, 0, 0, 0.2);
letter-spacing: 4rpx;
}
</style>