feat: 新增开奖加载弹窗组件,统一活动等级显示逻辑,并优化柜子库存加载性能。
This commit is contained in:
parent
e19ec06d74
commit
75638f895b
319
components/activity/DrawLoadingPopup.vue
Normal file
319
components/activity/DrawLoadingPopup.vue
Normal file
@ -0,0 +1,319 @@
|
||||
<template>
|
||||
<view v-if="visible" class="draw-loading-overlay" @touchmove.stop.prevent>
|
||||
<!-- 背景渐变 -->
|
||||
<view class="bg-gradient"></view>
|
||||
|
||||
<!-- 光圈效果 -->
|
||||
<view class="light-ring"></view>
|
||||
<view class="light-ring ring-2"></view>
|
||||
|
||||
<!-- 主内容 -->
|
||||
<view class="loading-content">
|
||||
<!-- 3D礼盒动画 -->
|
||||
<view class="gift-container">
|
||||
<view class="gift-box">
|
||||
<view class="gift-lid">
|
||||
<view class="lid-top"></view>
|
||||
<view class="lid-ribbon"></view>
|
||||
</view>
|
||||
<view class="gift-body">
|
||||
<view class="body-ribbon"></view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 闪光粒子 -->
|
||||
<view class="sparkle sparkle-1">✨</view>
|
||||
<view class="sparkle sparkle-2">⭐</view>
|
||||
<view class="sparkle sparkle-3">✨</view>
|
||||
<view class="sparkle sparkle-4">💫</view>
|
||||
</view>
|
||||
|
||||
<!-- 文字区域 -->
|
||||
<view class="text-area">
|
||||
<text class="loading-title">{{ title }}</text>
|
||||
<view class="loading-dots">
|
||||
<view class="dot"></view>
|
||||
<view class="dot"></view>
|
||||
<view class="dot"></view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 进度条(当有多次抽奖时显示) -->
|
||||
<view v-if="total > 1" class="progress-area">
|
||||
<view class="progress-bar">
|
||||
<view class="progress-fill" :style="{ width: progressPercent + '%' }"></view>
|
||||
</view>
|
||||
<text class="progress-text">{{ progress }} / {{ total }}</text>
|
||||
</view>
|
||||
|
||||
<!-- 提示文字 -->
|
||||
<text class="tip-text">请稍候,好运即将到来...</text>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
visible: { type: Boolean, default: false },
|
||||
title: { type: String, default: '正在开奖中' },
|
||||
progress: { type: Number, default: 0 },
|
||||
total: { type: Number, default: 1 }
|
||||
})
|
||||
|
||||
const progressPercent = computed(() => {
|
||||
if (props.total <= 0) return 0
|
||||
return Math.min(100, Math.round((props.progress / props.total) * 100))
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.draw-loading-overlay {
|
||||
position: fixed;
|
||||
top: 0; left: 0; right: 0; bottom: 0;
|
||||
z-index: 2000;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
animation: fadeIn 0.3s ease-out;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
|
||||
/* 背景 */
|
||||
.bg-gradient {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: radial-gradient(ellipse at center,
|
||||
rgba(255, 140, 0, 0.15) 0%,
|
||||
rgba(30, 20, 50, 0.98) 50%,
|
||||
rgba(10, 5, 20, 0.99) 100%
|
||||
);
|
||||
}
|
||||
|
||||
/* 光圈 */
|
||||
.light-ring {
|
||||
position: absolute;
|
||||
width: 500rpx; height: 500rpx;
|
||||
border: 4rpx solid rgba(255, 200, 100, 0.3);
|
||||
border-radius: 50%;
|
||||
animation: ringExpand 2s ease-out infinite;
|
||||
}
|
||||
|
||||
.ring-2 {
|
||||
animation-delay: 1s;
|
||||
}
|
||||
|
||||
@keyframes ringExpand {
|
||||
0% {
|
||||
transform: scale(0.5);
|
||||
opacity: 0.8;
|
||||
border-width: 8rpx;
|
||||
}
|
||||
100% {
|
||||
transform: scale(2);
|
||||
opacity: 0;
|
||||
border-width: 2rpx;
|
||||
}
|
||||
}
|
||||
|
||||
/* 主内容 */
|
||||
.loading-content {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
z-index: 10;
|
||||
animation: contentPop 0.5s cubic-bezier(0.34, 1.56, 0.64, 1);
|
||||
}
|
||||
|
||||
@keyframes contentPop {
|
||||
from { opacity: 0; transform: scale(0.8); }
|
||||
to { opacity: 1; transform: scale(1); }
|
||||
}
|
||||
|
||||
/* 礼盒容器 */
|
||||
.gift-container {
|
||||
position: relative;
|
||||
width: 240rpx;
|
||||
height: 240rpx;
|
||||
margin-bottom: 60rpx;
|
||||
}
|
||||
|
||||
/* 礼盒动画 */
|
||||
.gift-box {
|
||||
position: absolute;
|
||||
left: 50%; top: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
animation: boxBounce 1.5s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes boxBounce {
|
||||
0%, 100% { transform: translate(-50%, -50%); }
|
||||
50% { transform: translate(-50%, -60%); }
|
||||
}
|
||||
|
||||
.gift-lid {
|
||||
position: relative;
|
||||
animation: lidShake 1.5s ease-in-out infinite;
|
||||
transform-origin: center bottom;
|
||||
}
|
||||
|
||||
@keyframes lidShake {
|
||||
0%, 100% { transform: rotate(0deg) translateY(0); }
|
||||
25% { transform: rotate(-5deg) translateY(-10rpx); }
|
||||
75% { transform: rotate(5deg) translateY(-10rpx); }
|
||||
}
|
||||
|
||||
.lid-top {
|
||||
width: 140rpx; height: 30rpx;
|
||||
background: linear-gradient(135deg, #FF6B35, #FF8C00);
|
||||
border-radius: 8rpx 8rpx 0 0;
|
||||
box-shadow: 0 -4rpx 16rpx rgba(255, 107, 53, 0.5);
|
||||
}
|
||||
|
||||
.lid-ribbon {
|
||||
position: absolute;
|
||||
left: 50%; top: -20rpx;
|
||||
transform: translateX(-50%);
|
||||
width: 40rpx; height: 50rpx;
|
||||
background: linear-gradient(135deg, #FFD700, #FFA500);
|
||||
border-radius: 8rpx;
|
||||
|
||||
&::before, &::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 36rpx;
|
||||
width: 30rpx; height: 30rpx;
|
||||
background: linear-gradient(135deg, #FFD700, #FFA500);
|
||||
border-radius: 50%;
|
||||
}
|
||||
&::before { left: -20rpx; }
|
||||
&::after { right: -20rpx; }
|
||||
}
|
||||
|
||||
.gift-body {
|
||||
width: 120rpx; height: 100rpx;
|
||||
background: linear-gradient(135deg, #FF8C00, #FF6B35);
|
||||
border-radius: 0 0 12rpx 12rpx;
|
||||
margin: 0 auto;
|
||||
margin-top: -2rpx;
|
||||
box-shadow:
|
||||
0 12rpx 32rpx rgba(255, 107, 53, 0.4),
|
||||
inset 0 -10rpx 20rpx rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.body-ribbon {
|
||||
width: 30rpx; height: 100%;
|
||||
background: linear-gradient(180deg, #FFD700, #FFA500);
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
/* 闪光粒子 */
|
||||
.sparkle {
|
||||
position: absolute;
|
||||
font-size: 32rpx;
|
||||
animation: sparkleFloat 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.sparkle-1 { top: 10rpx; left: 20rpx; animation-delay: 0s; }
|
||||
.sparkle-2 { top: 30rpx; right: 20rpx; animation-delay: 0.5s; }
|
||||
.sparkle-3 { bottom: 40rpx; left: 0; animation-delay: 1s; }
|
||||
.sparkle-4 { bottom: 20rpx; right: 10rpx; animation-delay: 1.5s; }
|
||||
|
||||
@keyframes sparkleFloat {
|
||||
0%, 100% {
|
||||
opacity: 0.4;
|
||||
transform: translateY(0) scale(0.8);
|
||||
}
|
||||
50% {
|
||||
opacity: 1;
|
||||
transform: translateY(-20rpx) scale(1.2);
|
||||
}
|
||||
}
|
||||
|
||||
/* 文字区域 */
|
||||
.text-area {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16rpx;
|
||||
margin-bottom: 40rpx;
|
||||
}
|
||||
|
||||
.loading-title {
|
||||
font-size: 40rpx;
|
||||
font-weight: 800;
|
||||
color: #FFF;
|
||||
text-shadow: 0 0 30rpx rgba(255, 180, 100, 0.8);
|
||||
letter-spacing: 4rpx;
|
||||
}
|
||||
|
||||
.loading-dots {
|
||||
display: flex;
|
||||
gap: 8rpx;
|
||||
}
|
||||
|
||||
.dot {
|
||||
width: 12rpx; height: 12rpx;
|
||||
background: #FFD700;
|
||||
border-radius: 50%;
|
||||
animation: dotBounce 1.4s ease-in-out infinite;
|
||||
|
||||
&:nth-child(1) { animation-delay: 0s; }
|
||||
&:nth-child(2) { animation-delay: 0.2s; }
|
||||
&:nth-child(3) { animation-delay: 0.4s; }
|
||||
}
|
||||
|
||||
@keyframes dotBounce {
|
||||
0%, 80%, 100% {
|
||||
transform: scale(0.6);
|
||||
opacity: 0.5;
|
||||
}
|
||||
40% {
|
||||
transform: scale(1);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
/* 进度条 */
|
||||
.progress-area {
|
||||
width: 400rpx;
|
||||
margin-bottom: 30rpx;
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
height: 16rpx;
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
border-radius: 8rpx;
|
||||
overflow: hidden;
|
||||
box-shadow: inset 0 2rpx 4rpx rgba(0,0,0,0.2);
|
||||
}
|
||||
|
||||
.progress-fill {
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, #FFD700, #FF8C00, #FF6B35);
|
||||
border-radius: 8rpx;
|
||||
transition: width 0.3s ease-out;
|
||||
box-shadow: 0 0 16rpx rgba(255, 200, 0, 0.6);
|
||||
}
|
||||
|
||||
.progress-text {
|
||||
display: block;
|
||||
text-align: center;
|
||||
font-size: 24rpx;
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
margin-top: 12rpx;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* 提示文字 */
|
||||
.tip-text {
|
||||
font-size: 26rpx;
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
letter-spacing: 2rpx;
|
||||
}
|
||||
</style>
|
||||
@ -3,6 +3,7 @@
|
||||
*/
|
||||
import { ref } from 'vue'
|
||||
import { getIssueDrawLogs } from '@/api/appUser'
|
||||
import { levelToAlpha } from '@/utils/activity'
|
||||
|
||||
/**
|
||||
* 购买记录管理
|
||||
@ -55,9 +56,8 @@ export function useRecords() {
|
||||
|
||||
function getLevelName(level) {
|
||||
if (!level) return ''
|
||||
// 尝试映射 1->A, 2->B ...
|
||||
const map = { 1: 'A', 2: 'B', 3: 'C', 4: 'D', 5: 'E', 6: 'F', 7: 'G', 8: 'H', 9: 'I', 10: 'J' }
|
||||
return map[level] || (level + '赏')
|
||||
const alpha = levelToAlpha(level)
|
||||
return alpha + '赏'
|
||||
}
|
||||
|
||||
function clearRecords() {
|
||||
|
||||
@ -187,6 +187,7 @@ import RecordsList from '@/components/activity/RecordsList.vue'
|
||||
import RulesPopup from '@/components/activity/RulesPopup.vue'
|
||||
import CabinetPreviewPopup from '@/components/activity/CabinetPreviewPopup.vue'
|
||||
import { getActivityDetail, getActivityIssues, getActivityIssueRewards, getUserCoupons, getItemCards, createWechatOrder, getMatchingCardTypes, createMatchingPreorder, checkMatchingGame, getIssueDrawLogs, getMatchingGameCards } from '../../../api/appUser'
|
||||
import { levelToAlpha } from '@/utils/activity'
|
||||
|
||||
const detail = ref({})
|
||||
const statusText = ref('')
|
||||
@ -634,13 +635,6 @@ function formatPercent(v) {
|
||||
return `${n}%`
|
||||
}
|
||||
|
||||
function levelToAlpha(level) {
|
||||
if (level === 'BOSS') return 'BOSS'
|
||||
const n = Number(level)
|
||||
if (isNaN(n) || n <= 0) return String(level || '赏')
|
||||
// 1 -> A, 2 -> B ... 26 -> Z
|
||||
return String.fromCharCode(64 + n)
|
||||
}
|
||||
|
||||
function openRewardsPopup() {
|
||||
rewardsVisible.value = true
|
||||
|
||||
@ -103,6 +103,13 @@
|
||||
v-model:visible="cabinetVisible"
|
||||
:activity-id="activityId"
|
||||
/>
|
||||
|
||||
<!-- 开奖加载弹窗 -->
|
||||
<DrawLoadingPopup
|
||||
:visible="showDrawLoading"
|
||||
:progress="drawProgress"
|
||||
:total="drawTotal"
|
||||
/>
|
||||
</template>
|
||||
</ActivityPageLayout>
|
||||
</template>
|
||||
@ -120,6 +127,7 @@ import RecordsList from '@/components/activity/RecordsList.vue'
|
||||
import RulesPopup from '@/components/activity/RulesPopup.vue'
|
||||
import CabinetPreviewPopup from '@/components/activity/CabinetPreviewPopup.vue'
|
||||
import LotteryResultPopup from '@/components/activity/LotteryResultPopup.vue'
|
||||
import DrawLoadingPopup from '@/components/activity/DrawLoadingPopup.vue'
|
||||
import PaymentPopup from '@/components/PaymentPopup.vue'
|
||||
// Composables
|
||||
import { useActivity, useIssues, useRewards, useRecords } from '@/composables'
|
||||
@ -164,6 +172,9 @@ const cabinetVisible = ref(false)
|
||||
const showResultPopup = ref(false)
|
||||
const drawResults = ref([])
|
||||
const drawLoading = ref(false)
|
||||
const showDrawLoading = ref(false)
|
||||
const drawProgress = ref(0)
|
||||
const drawTotal = ref(1)
|
||||
|
||||
const isDevMode = ref(false)
|
||||
const customDrawCount = ref(1)
|
||||
@ -353,6 +364,11 @@ async function onMachineDraw(count) {
|
||||
})
|
||||
})
|
||||
|
||||
// 支付成功后立即显示开奖加载弹窗
|
||||
drawTotal.value = times
|
||||
drawProgress.value = 0
|
||||
showDrawLoading.value = true
|
||||
|
||||
// 轮询等待开奖完成
|
||||
let resultRes = await getLotteryResult(orderNo)
|
||||
let pollCount = 0
|
||||
@ -361,16 +377,22 @@ async function onMachineDraw(count) {
|
||||
while (resultRes?.status === 'paid_waiting' &&
|
||||
resultRes?.completed < resultRes?.count &&
|
||||
pollCount < maxPolls) {
|
||||
// 更新进度
|
||||
drawProgress.value = resultRes?.completed || 0
|
||||
await new Promise(r => setTimeout(r, resultRes?.nextPollMs || 2000))
|
||||
resultRes = await getLotteryResult(orderNo)
|
||||
pollCount++
|
||||
}
|
||||
|
||||
// 隐藏加载弹窗
|
||||
showDrawLoading.value = false
|
||||
|
||||
const items = mapResultsToFlipItems(resultRes, currentIssueRewards.value)
|
||||
|
||||
drawResults.value = items
|
||||
showResultPopup.value = true
|
||||
} catch (e) {
|
||||
showDrawLoading.value = false
|
||||
uni.showToast({ title: e.message || '操作失败', icon: 'none' })
|
||||
} finally {
|
||||
drawLoading.value = false
|
||||
|
||||
@ -249,7 +249,7 @@ onShow(() => {
|
||||
if (currentTab.value === 1) {
|
||||
loadShipments(uid)
|
||||
} else {
|
||||
loadAllInventory(uid)
|
||||
loadInventory(uid) // 改为只加载第一页,后续由 onReachBottom 触发
|
||||
}
|
||||
})
|
||||
|
||||
@ -265,6 +265,7 @@ onReachBottom(() => {
|
||||
})
|
||||
|
||||
function switchTab(index) {
|
||||
if (loading.value) return // 防止切换过快导致并发加载冲突
|
||||
currentTab.value = index
|
||||
// 切换时重新加载数据
|
||||
page.value = 1
|
||||
@ -275,7 +276,7 @@ function switchTab(index) {
|
||||
if (currentTab.value === 1) {
|
||||
loadShipments(uid)
|
||||
} else {
|
||||
loadAllInventory(uid)
|
||||
loadInventory(uid) // 改为按需加载
|
||||
}
|
||||
}
|
||||
|
||||
@ -498,6 +499,7 @@ async function loadInventory(uid) {
|
||||
original_ids: [item.id], // 初始化 id 数组
|
||||
name: (item.product_name || item.name || '').trim(),
|
||||
image: imageUrl,
|
||||
price: item.product_price ? item.product_price / 100 : null, // 直接使用后端返回的价格
|
||||
count: 1,
|
||||
selected: false,
|
||||
selectedCount: 1,
|
||||
@ -594,33 +596,6 @@ async function loadInventory(uid) {
|
||||
}
|
||||
}
|
||||
|
||||
async function loadAllInventory(uid) {
|
||||
try {
|
||||
while (hasMore.value) {
|
||||
await loadInventory(uid)
|
||||
}
|
||||
fetchProductPrices()
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
async function fetchProductPrices() {
|
||||
const currentList = currentTab.value === 1 ? shippedList : aggregatedList
|
||||
const list = currentList.value
|
||||
for (let i = 0; i < list.length; i++) {
|
||||
const item = list[i]
|
||||
if (item.id && !item.price) {
|
||||
try {
|
||||
const meta = await fetchProductMeta(item.id)
|
||||
if (meta) {
|
||||
if (!item.price && meta.price !== null) item.price = meta.price
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Fetch price failed for:', item.id, e)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function toggleSelect(item) {
|
||||
uni.vibrateShort({ type: 'light' })
|
||||
item.selected = !item.selected
|
||||
|
||||
@ -54,7 +54,7 @@
|
||||
<text class="tag-text">已开启</text>
|
||||
</view>
|
||||
<view class="level-tag" v-if="order.reward_level">
|
||||
<text class="tag-text">{{ order.reward_level }}赏</text>
|
||||
<text class="tag-text">{{ getLevelLabel(order.reward_level) }}赏</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@ -234,6 +234,7 @@
|
||||
import { ref } from 'vue'
|
||||
import { onLoad } from '@dcloudio/uni-app'
|
||||
import { getOrderDetail, cancelOrder, createWechatOrder } from '../../api/appUser'
|
||||
import { levelToAlpha } from '@/utils/activity'
|
||||
|
||||
const orderId = ref('')
|
||||
const order = ref(null)
|
||||
@ -433,6 +434,10 @@ function getSourceTypeText(type) {
|
||||
return '其他'
|
||||
}
|
||||
|
||||
function getLevelLabel(level) {
|
||||
return levelToAlpha(level)
|
||||
}
|
||||
|
||||
function showProofHelp() {
|
||||
uni.showModal({
|
||||
title: '抽奖凭证说明',
|
||||
|
||||
@ -39,14 +39,32 @@ export function detectBoss(item) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 等级数字转字母 (1 -> A, 2 -> B, ...)
|
||||
* 奖品等级映射 (与管理端保持一致)
|
||||
*/
|
||||
export const PRIZE_LEVEL_LABELS = {
|
||||
1: 'S',
|
||||
2: 'A',
|
||||
3: 'B',
|
||||
4: 'C',
|
||||
5: 'D',
|
||||
6: 'E',
|
||||
7: 'F',
|
||||
8: 'G',
|
||||
9: 'H',
|
||||
11: 'Last'
|
||||
}
|
||||
|
||||
/**
|
||||
* 等级数字转字母/标签
|
||||
* @param {number|string} level - 等级
|
||||
* @returns {string}
|
||||
*/
|
||||
export function levelToAlpha(level) {
|
||||
if (level === 'BOSS') return 'BOSS'
|
||||
const n = Number(level)
|
||||
if (PRIZE_LEVEL_LABELS[n]) return PRIZE_LEVEL_LABELS[n]
|
||||
if (isNaN(n) || n <= 0) return String(level || '赏')
|
||||
// 兜底逻辑:如果超出定义的映射,使用 A, B, C...
|
||||
return String.fromCharCode(64 + n)
|
||||
}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user