503 lines
12 KiB
Vue
503 lines
12 KiB
Vue
<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>
|