refactor: 重构活动页面,提取通用组件和组合式函数,并更新一番赏等页面以使用新组件

This commit is contained in:
邹方成 2025-12-25 20:35:12 +08:00
parent 148c62a983
commit 97cfe3f3da
2 changed files with 166 additions and 645 deletions

View File

@ -27,71 +27,28 @@
</view>
<view class="header-actions">
<view class="action-btn" @tap="showRules">
<view class="action-icon rules-icon"></view>
<text class="action-label">规则</text>
<text class="icon">📋</text>
<text>规则</text>
</view>
<view class="action-btn" @tap="goCabinet">
<view class="action-icon cabinet-icon"></view>
<text class="action-label">盒柜</text>
<text class="icon">📦</text>
<text>盒柜</text>
</view>
</view>
</view>
<view class="section-container animate-enter stagger-1">
<view class="modern-tabs">
<view class="tab-item" :class="{ active: tabActive === 'pool' }" @tap="tabActive = 'pool'">
本机奖池
<view v-if="tabActive === 'pool'" class="active-dot"></view>
</view>
<view class="tab-item" :class="{ active: tabActive === 'records' }" @tap="tabActive = 'records'">
购买记录
<view v-if="tabActive === 'records'" class="active-dot"></view>
</view>
<view class="section-container animate-enter stagger-1" v-if="currentIssueRewards.length > 0">
<view class="section-header">
<text class="section-title">奖池一览</text>
<text class="section-more" @tap="openRewardsPopup">查看全部</text>
</view>
<view v-show="tabActive === 'pool'">
<view class="section-header">
<text class="section-title">奖池配置</text>
<text class="section-more" @tap="openRewardsPopup">查看全部</text>
<scroll-view class="preview-scroll" scroll-x>
<view class="preview-item" v-for="(item, idx) in currentIssueRewards" :key="item.id || idx">
<view class="prize-tag" :class="{ 'tag-boss': item.boss }">{{ item.boss ? 'BOSS' : '赏' }}</view>
<image class="preview-img" :src="item.image" mode="aspectFill" />
<view class="preview-name">{{ item.title }}</view>
</view>
<view v-if="rewardGroups.length > 0">
<view class="prize-level-row" v-for="group in rewardGroups" :key="group.level">
<view class="level-header-row">
<view class="level-badge" :class="{ 'badge-boss': group.level === 'BOSS' }">{{ group.level }}</view>
<text class="level-prob">总概率 {{ group.totalPercent }}%</text>
</view>
<scroll-view class="preview-scroll" scroll-x>
<view class="preview-item" v-for="(item, idx) in group.rewards" :key="item.id || idx">
<view class="prize-tag" :class="{ 'tag-boss': item.boss }">{{ item.boss ? 'BOSS' : group.level }}</view>
<image class="preview-img" :src="item.image" mode="aspectFill" />
<view class="preview-name">{{ item.title }}</view>
</view>
</scroll-view>
</view>
</view>
<view v-else class="empty-state">
<text class="empty-icon">📭</text>
<text class="empty-text">暂无奖池配置</text>
</view>
</view>
<view v-show="tabActive === 'records'">
<view class="records-list" v-if="winRecords.length">
<view v-for="(it, idx) in winRecords" :key="it.id" class="record-item">
<image class="record-img" :src="it.image" mode="aspectFill" />
<view class="record-info">
<view class="record-title">{{ it.title }}</view>
<view class="record-meta">
<text class="record-count">x{{ it.count }}</text>
</view>
</view>
</view>
</view>
<view class="empty-state" v-else>
<text class="empty-icon">📝</text>
<text class="empty-text">暂无购买记录</text>
</view>
</view>
</scroll-view>
</view>
<view style="height: 220rpx;"></view>
@ -126,25 +83,17 @@
<text class="rewards-close" @tap="closeRewardsPopup">×</text>
</view>
<scroll-view scroll-y class="rewards-list">
<view v-if="rewardGroups.length > 0">
<view class="rewards-group-v2" v-for="group in rewardGroups" :key="group.level">
<view class="group-header-row">
<text class="group-badge" :class="{ 'badge-boss': group.level === 'BOSS' }">{{ group.level }}</text>
<text class="group-total-prob">该档总概率 {{ group.totalPercent }}%</text>
</view>
<view v-for="(item, idx) in group.rewards" :key="item.id || idx" class="rewards-item">
<image class="rewards-thumb" :src="item.image" mode="aspectFill" />
<view class="rewards-info">
<view class="rewards-name-row">
<text class="rewards-name">{{ item.title || '-' }}</text>
<view class="rewards-tag" v-if="item.boss">BOSS</view>
</view>
<text class="rewards-percent">单项概率 {{ formatPercent(item.percent) }}</text>
</view>
<view v-for="(item, idx) in rewardsForPopup" :key="item.id || idx" class="rewards-item">
<image class="rewards-thumb" :src="item.image" mode="aspectFill" />
<view class="rewards-info">
<view class="rewards-name-row">
<text class="rewards-name">{{ item.title || '-' }}</text>
<view class="rewards-tag" v-if="item.boss">BOSS</view>
</view>
<text class="rewards-percent">概率 {{ formatPercent(item.percent) }}</text>
</view>
</view>
<view v-else class="rewards-empty">暂无奖池数据</view>
<view v-if="!rewardsForPopup.length" class="rewards-empty">暂无奖池数据</view>
</scroll-view>
</view>
</view>
@ -171,13 +120,11 @@ import { ref, computed, nextTick } from 'vue'
import FlipGrid from '../../../components/FlipGrid.vue'
import { onLoad } from '@dcloudio/uni-app'
import PaymentPopup from '../../../components/PaymentPopup.vue'
import { getActivityDetail, getActivityIssues, getActivityIssueRewards, joinLottery, createWechatOrder, getLotteryResult, getItemCards, getUserCoupons, getIssueDrawLogs } from '../../../api/appUser'
import { getActivityDetail, getActivityIssues, getActivityIssueRewards, joinLottery, createWechatOrder, getLotteryResult, getItemCards, getUserCoupons } from '../../../api/appUser'
const detail = ref({})
const statusText = ref('')
const rewardsVisible = ref(false)
const tabActive = ref('pool')
const winRecords = ref([])
const issues = ref([])
const rewardsMap = ref({})
const currentIssueId = ref('')
@ -190,8 +137,8 @@ const coverUrl = computed(() => cleanUrl(detail.value && (detail.value.image ||
const currentIssueTitle = computed(() => {
const arr = issues.value || []
const cur = arr[selectedIssueIndex.value]
//
return (cur && (cur.title || '奖池')) || '-'
const t = (cur && (cur.title || ('第' + (cur.no || '-') + '期'))) || '-'
return t
})
const points = ref(0)
const flipRef = ref(null)
@ -200,28 +147,6 @@ const rewardsForPopup = computed(() => {
const arr = currentIssueRewards.value || []
return Array.isArray(arr) ? arr : []
})
const rewardGroups = computed(() => {
const groups = {}
currentIssueRewards.value.forEach(item => {
const level = item.level || '赏'
if (!groups[level]) groups[level] = []
groups[level].push(item)
})
return Object.keys(groups).sort((a, b) => {
if (a === 'BOSS') return -1
if (b === 'BOSS') return 1
// Alphabetical sort (A, B, C...)
return a.localeCompare(b)
}).map(key => {
const rewards = groups[key]
const total = rewards.reduce((sum, item) => sum + (Number(item.percent) || 0), 0)
return {
level: key,
rewards: rewards,
totalPercent: total.toFixed(1)
}
})
})
const paymentVisible = ref(false)
const paymentAmount = ref('0.00')
const coupons = ref([])
@ -244,15 +169,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
// Only handle up to 26 levels for now as it's rare to have more
return String.fromCharCode(64 + n)
}
function openRewardsPopup() {
rewardsVisible.value = true
}
@ -319,8 +235,7 @@ function normalizeRewards(list) {
title: i.name ?? i.title ?? '',
image: cleanUrl(i.product_image ?? i.image ?? i.img ?? i.pic ?? i.banner ?? ''),
weight: Number(i.weight) || 0,
boss: detectBoss(i),
level: levelToAlpha(i.prize_level ?? i.level ?? (detectBoss(i) ? 'BOSS' : '赏'))
boss: detectBoss(i)
}))
const total = items.reduce((acc, it) => acc + (it.weight > 0 ? it.weight : 0), 0)
const enriched = items.map(it => ({
@ -373,10 +288,6 @@ async function fetchIssues(id) {
const latestId = pickLatestIssueId(issues.value)
setSelectedById(latestId)
await fetchRewardsForIssues(id)
//
if (currentIssueId.value) {
fetchWinRecords(id, currentIssueId.value)
}
}
function pickLatestIssueId(list) {
@ -417,36 +328,6 @@ function nextIssue() {
currentIssueId.value = (cur && cur.id) || ''
}
async function fetchWinRecords(actId, issId) {
if (!actId || !issId) return
try {
const res = await getIssueDrawLogs(actId, issId)
const list = (res && res.list) || (Array.isArray(res) ? res : [])
//
const aggregate = {}
list.forEach(it => {
const key = it.reward_id || it.id
if (!aggregate[key]) {
aggregate[key] = {
id: key,
title: it.reward_name || it.title || it.name || '-',
image: it.reward_image || it.image || '',
count: 0
}
}
aggregate[key].count += 1
})
const total = list.length || 1
winRecords.value = Object.values(aggregate).map(it => ({
...it,
percent: ((it.count / total) * 100).toFixed(1)
}))
} catch (e) {
console.error('fetchWinRecords error', e)
winRecords.value = []
}
}
function onPreviewBanner() {
const url = detail.value.banner || ''
if (url) uni.previewImage({ urls: [url], current: url })
@ -737,10 +618,10 @@ function closeFlip() { showFlip.value = false }
}
.header-info {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
justify-content: center;
justify-content: flex-start;
min-height: 180rpx;
padding: 6rpx 0;
}
.header-title {
@ -779,47 +660,29 @@ function closeFlip() { showFlip.value = false }
.header-actions {
display: flex;
flex-direction: column;
gap: 28rpx;
margin-left: 16rpx;
padding-left: 24rpx;
border-left: 2rpx solid #E8E8E8;
gap: $spacing-lg;
margin-left: 20rpx;
padding-left: $spacing-lg;
border-left: 1rpx solid rgba(0,0,0,0.06);
justify-content: center;
align-self: stretch;
height: 140rpx;
}
.action-btn {
display: flex;
flex-direction: column;
align-items: center;
font-size: $font-xs;
color: $text-sub;
transition: all 0.2s;
&:active {
opacity: 0.6;
transform: scale(0.9);
color: $text-main;
}
}
.action-icon {
width: 44rpx;
height: 44rpx;
margin-bottom: 8rpx;
background-size: contain;
background-repeat: no-repeat;
background-position: center;
}
.rules-icon {
background-color: #999;
mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='currentColor'%3E%3Cpath d='M19 3H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm-5 14H7v-2h7v2zm3-4H7v-2h10v2zm0-4H7V7h10v2z'/%3E%3C/svg%3E");
-webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='currentColor'%3E%3Cpath d='M19 3H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm-5 14H7v-2h7v2zm3-4H7v-2h10v2zm0-4H7V7h10v2z'/%3E%3C/svg%3E");
mask-size: cover;
-webkit-mask-size: cover;
}
.cabinet-icon {
background-color: #999;
mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='currentColor'%3E%3Cpath d='M20 3H4c-1.1 0-2 .9-2 2v16l4-4h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm-9 11H7v-2h4v2zm6-4H7V8h10v2z'/%3E%3C/svg%3E");
-webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='currentColor'%3E%3Cpath d='M20 3H4c-1.1 0-2 .9-2 2v16l4-4h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm-9 11H7v-2h4v2zm6-4H7V8h10v2z'/%3E%3C/svg%3E");
mask-size: cover;
-webkit-mask-size: cover;
}
.action-label {
font-size: 22rpx;
color: #666;
letter-spacing: 1rpx;
.action-btn .icon {
font-size: $font-xl;
margin-bottom: 6rpx;
filter: grayscale(0.2);
}
.section-container {
@ -830,39 +693,6 @@ function closeFlip() { showFlip.value = false }
box-shadow: $shadow-sm;
backdrop-filter: blur(10rpx);
}
.modern-tabs {
display: flex;
background: $bg-secondary;
padding: 8rpx;
border-radius: $radius-lg;
margin-bottom: $spacing-lg;
}
.tab-item {
flex: 1;
text-align: center;
padding: $spacing-md 0;
font-size: $font-md;
color: $text-sub;
border-radius: $radius-md;
font-weight: 600;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
position: relative;
&.active {
background: #FFFFFF;
color: $brand-primary;
box-shadow: $shadow-sm;
}
}
.active-dot {
width: 8rpx; height: 8rpx;
background: $brand-primary;
border-radius: 50%;
position: absolute;
bottom: 8rpx; left: 50%; transform: translateX(-50%);
}
.section-header {
display: flex;
justify-content: space-between;
@ -903,29 +733,31 @@ function closeFlip() { showFlip.value = false }
.preview-scroll {
white-space: nowrap;
width: 100%;
margin: 0 -$spacing-lg;
padding: 0 $spacing-lg;
width: calc(100% + 40rpx);
}
.preview-item {
display: inline-block;
width: 180rpx;
margin-right: $spacing-md;
width: 200rpx;
margin-right: $spacing-lg;
vertical-align: top;
position: relative;
transition: transform 0.2s;
&:active { transform: scale(0.96); }
&:last-child { margin-right: 0; }
&:last-child { margin-right: 40rpx; }
}
.preview-img {
width: 180rpx;
height: 180rpx;
width: 200rpx;
height: 200rpx;
border-radius: $radius-lg;
background: $bg-secondary;
margin-bottom: $spacing-sm;
margin-bottom: $spacing-md;
box-shadow: $shadow-sm;
border: 1rpx solid rgba(0,0,0,0.03);
}
.preview-name {
font-size: $font-xs;
font-size: $font-sm;
color: $text-secondary;
width: 100%;
overflow: hidden;
@ -936,16 +768,16 @@ function closeFlip() { showFlip.value = false }
}
.prize-tag {
position: absolute;
top: 8rpx;
left: 8rpx;
top: 10rpx;
left: 10rpx;
background: rgba(0,0,0,0.6);
color: #fff;
font-size: $font-xxs;
padding: 2rpx $spacing-xs;
border-radius: 4rpx;
font-size: $font-xs;
padding: 4rpx $spacing-sm;
border-radius: $radius-sm;
z-index: 10;
font-weight: 700;
backdrop-filter: blur(4px);
backdrop-filter: blur(4rpx);
transform: scale(0.9);
transform-origin: top left;
}
@ -954,47 +786,6 @@ function closeFlip() { showFlip.value = false }
box-shadow: 0 4rpx 12rpx rgba($brand-primary, 0.4);
}
/* 新增:等级分组样式 */
.prize-level-row {
margin-bottom: $spacing-lg;
background: rgba(0,0,0,0.02);
padding: $spacing-md;
border-radius: $radius-lg;
&:last-child { margin-bottom: 0; }
}
.level-header-row {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: $spacing-md;
}
.level-badge {
display: inline-block;
font-size: $font-xs;
font-weight: 900;
color: $text-main;
background: #F0F0F0;
padding: 4rpx 16rpx;
border-radius: 8rpx;
font-style: italic;
border: 1rpx solid rgba(0,0,0,0.05);
box-shadow: $shadow-xs;
}
.level-prob {
font-size: 22rpx;
color: $brand-primary;
font-weight: 800;
}
.level-badge.badge-boss {
background: $gradient-brand;
color: #fff;
border: none;
}
.selector-container {
display: flex;
flex-direction: column;
@ -1102,7 +893,6 @@ function closeFlip() { showFlip.value = false }
margin-bottom: $spacing-sm;
display: -webkit-box;
-webkit-line-clamp: 2;
line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
line-height: 1.4;
@ -1207,23 +997,23 @@ function closeFlip() { showFlip.value = false }
border: 1rpx solid rgba(255, 255, 255, 0.2);
}
/* 底部多档位抽赏按钮 - 高级重置 */
/* 底部多档位抽赏按钮 */
.bottom-actions {
position: fixed;
left: 0;
right: 0;
bottom: 0;
display: flex;
gap: 20rpx;
padding: 32rpx 32rpx;
padding-bottom: calc(32rpx + env(safe-area-inset-bottom));
background: rgba(255, 255, 255, 0.85);
backdrop-filter: blur(30rpx);
box-shadow: 0 -12rpx 40rpx rgba(0, 0, 0, 0.08);
gap: $spacing-md;
padding: $spacing-lg $spacing-lg;
padding-bottom: calc($spacing-lg + env(safe-area-inset-bottom));
background: rgba(255, 255, 255, 0.9);
backdrop-filter: blur(20rpx);
box-shadow: 0 -8rpx 24rpx rgba(0, 0, 0, 0.08);
z-index: 999;
border-top: 1rpx solid rgba(255, 255, 255, 0.8);
animation: slideUp $transition-slow $ease-out backwards;
border-top: 1rpx solid rgba(0,0,0,0.05);
}
.tier-btn {
flex: 1;
min-width: 0;
@ -1231,80 +1021,92 @@ function closeFlip() { showFlip.value = false }
flex-direction: column;
align-items: center;
justify-content: center;
padding: 24rpx 10rpx;
background: #FFF;
border: 2rpx solid rgba($brand-primary, 0.1);
border-radius: 28rpx;
box-shadow: 0 4rpx 12rpx rgba(0,0,0,0.03);
transition: all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1);
margin: 0;
line-height: normal;
padding: $spacing-md $spacing-xs;
background: $bg-card;
border: 1rpx solid $border-color-light;
border-radius: $radius-lg;
box-shadow: $shadow-sm;
transition: all $transition-fast;
&:active {
transform: scale(0.92);
background: #F9F9F9;
box-shadow: none;
transform: scale(0.95);
background: $bg-page;
}
}
.tier-price {
font-size: 34rpx;
font-weight: 900;
font-size: $font-lg;
font-weight: 800;
color: $text-main;
font-family: 'DIN Alternate', sans-serif;
letter-spacing: -1rpx;
}
.tier-label {
font-size: 22rpx;
color: $brand-primary;
margin-top: 6rpx;
font-weight: 800;
font-style: italic;
font-size: $font-xs;
color: $text-sub;
margin-top: 4rpx;
font-weight: 500;
}
/* 热门/最高档位 - 高级动效 */
.tier-hot {
background: $gradient-brand !important;
border: none !important;
box-shadow: 0 12rpx 32rpx rgba($brand-primary, 0.35) !important;
background: $gradient-red;
border: none;
box-shadow: 0 6rpx 14rpx rgba($accent-red, 0.12);
position: relative;
overflow: hidden;
border-radius: $radius-lg;
transform: translateZ(0);
.tier-price {
color: #FFF !important;
text-shadow: 0 2rpx 4rpx rgba(0, 0, 0, 0.1);
.tier-price, .tier-label {
color: #fff !important;
position: relative;
z-index: 2;
}
.tier-label {
color: rgba(255, 255, 255, 0.9) !important;
text-shadow: 0 1rpx 2rpx rgba(0, 0, 0, 0.1);
}
/* 流光效果 */
&::before {
content: '';
position: absolute;
top: -50%;
left: -150%;
width: 200%;
height: 200%;
background: linear-gradient(
120deg,
rgba(255, 255, 255, 0) 30%,
rgba(255, 255, 255, 0.4) 50%,
rgba(255, 255, 255, 0) 70%
);
transform: rotate(25deg);
animation: hotSweep 4s infinite cubic-bezier(0.19, 1, 0.22, 1);
pointer-events: none;
left: -40%;
top: 0;
width: 60%;
height: 100%;
background: linear-gradient(90deg, rgba(255,255,255,0) 0%, rgba(255,255,255,0.22) 50%, rgba(255,255,255,0) 100%);
transform: skewX(-18deg);
opacity: 0.9;
z-index: 1;
pointer-events: none;
animation: hotShine 2.6s ease-in-out infinite;
}
&::after {
content: 'HOT';
position: absolute;
top: 8rpx;
right: 8rpx;
background: linear-gradient(135deg, rgba(255,255,255,0.25), rgba(255,255,255,0.05));
color: #fff;
font-size: 20rpx;
font-weight: 800;
padding: 3rpx 10rpx;
border-radius: 999rpx;
border: 1rpx solid rgba(255,255,255,0.35);
text-shadow: 0 1rpx 2rpx rgba(0,0,0,0.18);
z-index: 3;
pointer-events: none;
}
&:active {
opacity: 0.9;
transform: scale(0.96);
}
}
.tier-hot .tier-price, .tier-hot .tier-label {
color: #FFFFFF;
}
@keyframes hotSweep {
0% { left: -150%; }
100% { left: 150%; }
@keyframes hotShine {
0% { transform: translateX(-10%) skewX(-18deg); opacity: 0; }
15% { opacity: 0.9; }
55% { transform: translateX(220%) skewX(-18deg); opacity: 0.35; }
100% { transform: translateX(220%) skewX(-18deg); opacity: 0; }
}
.rewards-overlay { position: fixed; left: 0; right: 0; top: 0; bottom: 0; z-index: 9000; }
@ -1348,43 +1150,6 @@ function closeFlip() { showFlip.value = false }
max-height: 60vh;
padding: $spacing-lg;
}
.rewards-group-v2 {
margin-bottom: $spacing-xl;
&:last-child { margin-bottom: 0; }
}
.group-header-row {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: $spacing-md;
padding: 0 4rpx;
}
.group-badge {
font-size: $font-xs;
font-weight: 900;
color: $text-main;
background: #F0F0F0;
padding: 4rpx 16rpx;
border-radius: 8rpx;
font-style: italic;
border: 1rpx solid rgba(0,0,0,0.05);
box-shadow: $shadow-xs;
&.badge-boss {
background: $gradient-gold;
color: #78350F;
border-color: rgba(217, 119, 6, 0.3);
}
}
.group-total-prob {
font-size: 24rpx;
color: $brand-primary;
font-weight: 800;
}
.rewards-item {
display: flex;
align-items: center;
@ -1492,67 +1257,4 @@ function closeFlip() { showFlip.value = false }
transform: scale(0.95);
}
}
/* ============================================
Purchase Records Styles
============================================ */
.records-list {
display: flex;
flex-direction: column;
gap: $spacing-lg;
}
.record-item {
display: flex;
background: #FFFFFF;
padding: $spacing-lg;
border-radius: $radius-lg;
box-shadow: $shadow-sm;
align-items: center;
}
.record-img {
width: 100rpx; height: 100rpx;
border-radius: $radius-md;
background: $bg-secondary;
margin-right: $spacing-lg;
}
.record-info {
flex: 1;
}
.record-title {
font-size: $font-md;
font-weight: 600;
color: $text-main;
margin-bottom: $spacing-xs;
}
.record-meta {
display: flex;
gap: $spacing-md;
font-size: $font-sm;
color: $text-sub;
}
.record-count {
background: rgba($brand-primary, 0.1);
color: $brand-primary;
padding: 2rpx $spacing-sm;
border-radius: $radius-sm;
}
/* Empty State */
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: $spacing-xl * 2;
color: $text-tertiary;
}
.empty-icon {
font-size: 80rpx;
margin-bottom: $spacing-md;
opacity: 0.6;
}
.empty-text {
font-size: $font-md;
color: $text-sub;
}
</style>

View File

@ -38,67 +38,30 @@
</view>
</view>
<view class="header-actions">
<view class="action-btn" @tap="showRules">
<view class="action-icon rules-icon"></view>
<text class="action-label">规则</text>
</view>
<view class="action-btn" @tap="goCabinet">
<view class="action-icon cabinet-icon"></view>
<text class="action-label">盒柜</text>
</view>
<view class="action-btn" @tap="showRules">
<text class="icon">📋</text>
<text>规则</text>
</view>
<view class="action-btn" @tap="goCabinet">
<text class="icon">📦</text>
<text>盒柜</text>
</view>
</view>
</view>
<!-- 赏品概览 -->
<view class="section-container animate-enter stagger-1">
<view class="modern-tabs">
<view class="tab-item" :class="{ active: tabActive === 'pool' }" @tap="tabActive = 'pool'">
本机奖池
<view v-if="tabActive === 'pool'" class="active-dot"></view>
</view>
<view class="tab-item" :class="{ active: tabActive === 'records' }" @tap="tabActive = 'records'">
购买记录
<view v-if="tabActive === 'records'" class="active-dot"></view>
</view>
<view class="section-container animate-enter stagger-1" v-if="currentIssueRewards.length > 0">
<view class="section-header">
<text class="section-title">赏品一览</text>
<text class="section-more" @tap="openRewardsPopup">查看全部</text>
</view>
<view v-show="tabActive === 'pool'">
<view class="section-header">
<text class="section-title">奖品配置</text>
<text class="section-more" @tap="openRewardsPopup">查看全部</text>
<scroll-view class="preview-scroll" scroll-x>
<view class="preview-item" v-for="(item, idx) in currentIssueRewards" :key="idx">
<view class="prize-tag" :class="{ 'tag-boss': item.boss }">{{ item.boss ? 'BOSS' : (item.grade || '赏') }}</view>
<image class="preview-img" :src="item.image" mode="aspectFill" />
<view class="preview-name">{{ item.title }}</view>
</view>
<view v-if="currentIssueRewards.length > 0">
<scroll-view class="preview-scroll" scroll-x>
<view class="preview-item" v-for="(item, idx) in currentIssueRewards" :key="idx">
<view class="prize-tag" :class="{ 'tag-boss': item.boss }">{{ item.boss ? 'BOSS' : (item.grade || '赏') }}</view>
<image class="preview-img" :src="item.image" mode="aspectFill" />
<view class="preview-name">{{ item.title }}</view>
</view>
</scroll-view>
</view>
<view v-else class="empty-state">
<text class="empty-icon">📭</text>
<text class="empty-text">暂无奖品配置</text>
</view>
</view>
<view v-show="tabActive === 'records'">
<view class="records-list" v-if="winRecords.length">
<view v-for="(it, idx) in winRecords" :key="it.id" class="record-item">
<image class="record-img" :src="it.image" mode="aspectFill" />
<view class="record-info">
<view class="record-title">{{ it.title }}</view>
<view class="record-meta">
<text class="record-count">x{{ it.count }}</text>
</view>
</view>
</view>
</view>
<view class="empty-state" v-else>
<text class="empty-icon">📝</text>
<text class="empty-text">暂无购买记录</text>
</view>
</view>
</scroll-view>
</view>
<!-- 选号区域 -->
@ -179,7 +142,7 @@ import { ref, computed } from 'vue'
import { onLoad, onUnload } from '@dcloudio/uni-app'
import FlipGrid from '../../../components/FlipGrid.vue'
import YifanSelector from '@/components/YifanSelector.vue'
import { getActivityDetail, getActivityIssues, getActivityIssueRewards, getIssueDrawLogs } from '../../../api/appUser'
import { getActivityDetail, getActivityIssues, getActivityIssueRewards } from '../../../api/appUser'
const detail = ref({})
const issues = ref([])
@ -416,10 +379,6 @@ async function fetchIssues(id) {
const latestId = pickLatestIssueId(issues.value)
setSelectedById(latestId)
await fetchRewardsForIssues(id)
//
if (currentIssueId.value) {
fetchWinRecords(id, currentIssueId.value)
}
}
function pickLatestIssueId(list) {
@ -460,35 +419,7 @@ function nextIssue() {
currentIssueId.value = (cur && cur.id) || ''
}
async function fetchWinRecords(actId, issId) {
if (!actId || !issId) return
try {
const res = await getIssueDrawLogs(actId, issId)
const list = (res && res.list) || (Array.isArray(res) ? res : [])
//
const aggregate = {}
list.forEach(it => {
const key = it.reward_id || it.id
if (!aggregate[key]) {
aggregate[key] = {
id: key,
title: it.reward_name || it.title || it.name || '-',
image: it.reward_image || it.image || '',
count: 0
}
}
aggregate[key].count += 1
})
const total = list.length || 1
winRecords.value = Object.values(aggregate).map(it => ({
...it,
percent: ((it.count / total) * 100).toFixed(1)
}))
} catch (e) {
console.error('fetchWinRecords error', e)
winRecords.value = []
}
}
function onPreviewBanner() {
const url = coverUrl.value || ''
@ -681,10 +612,10 @@ onUnload(() => {
}
.header-info {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
justify-content: center;
justify-content: flex-start;
min-height: 180rpx;
padding: 6rpx 0;
}
.header-title {
@ -741,47 +672,30 @@ onUnload(() => {
.header-actions {
display: flex;
flex-direction: column;
gap: 28rpx;
margin-left: 16rpx;
padding-left: 24rpx;
border-left: 2rpx solid #E8E8E8;
gap: $spacing-lg;
margin-left: 20rpx;
padding-left: $spacing-lg;
border-left: 1rpx solid rgba(0,0,0,0.06);
justify-content: center;
align-self: stretch;
height: 140rpx;
}
.action-btn {
display: flex;
flex-direction: column;
align-items: center;
font-size: $font-xs;
color: $text-sub;
transition: all 0.2s;
&:active {
opacity: 0.6;
transform: scale(0.9);
color: $text-main;
}
}
.action-icon {
width: 44rpx;
height: 44rpx;
margin-bottom: 8rpx;
background-size: contain;
background-repeat: no-repeat;
background-position: center;
}
.rules-icon {
background-color: #999;
mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='currentColor'%3E%3Cpath d='M19 3H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm-5 14H7v-2h7v2zm3-4H7v-2h10v2zm0-4H7V7h10v2z'/%3E%3C/svg%3E");
-webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='currentColor'%3E%3Cpath d='M19 3H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm-5 14H7v-2h7v2zm3-4H7v-2h10v2zm0-4H7V7h10v2z'/%3E%3C/svg%3E");
mask-size: cover;
-webkit-mask-size: cover;
}
.cabinet-icon {
background-color: #999;
mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='currentColor'%3E%3Cpath d='M20 3H4c-1.1 0-2 .9-2 2v16l4-4h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm-9 11H7v-2h4v2zm6-4H7V8h10v2z'/%3E%3C/svg%3E");
-webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='currentColor'%3E%3Cpath d='M20 3H4c-1.1 0-2 .9-2 2v16l4-4h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm-9 11H7v-2h4v2zm6-4H7V8h10v2z'/%3E%3C/svg%3E");
mask-size: cover;
-webkit-mask-size: cover;
}
.action-label {
font-size: 22rpx;
color: #666;
letter-spacing: 1rpx;
.action-btn .icon {
font-size: $font-xl;
margin-bottom: 6rpx;
filter: grayscale(0.2);
}
/* 通用板块容器 */
@ -1130,99 +1044,4 @@ onUnload(() => {
color: $text-tertiary;
font-size: $font-sm;
}
/* ============================================
Tabs & Purchase Records Styles
============================================ */
.modern-tabs {
display: flex;
background: $bg-secondary;
padding: 8rpx;
border-radius: $radius-lg;
margin-bottom: $spacing-lg;
}
.tab-item {
flex: 1;
text-align: center;
padding: $spacing-md 0;
font-size: $font-md;
color: $text-sub;
border-radius: $radius-md;
font-weight: 600;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
position: relative;
&.active {
background: #FFFFFF;
color: $brand-primary;
box-shadow: $shadow-sm;
}
}
.active-dot {
width: 8rpx; height: 8rpx;
background: $brand-primary;
border-radius: 50%;
position: absolute;
bottom: 8rpx; left: 50%; transform: translateX(-50%);
}
.records-list {
display: flex;
flex-direction: column;
gap: $spacing-lg;
}
.record-item {
display: flex;
background: #FFFFFF;
padding: $spacing-lg;
border-radius: $radius-lg;
box-shadow: $shadow-sm;
align-items: center;
}
.record-img {
width: 100rpx; height: 100rpx;
border-radius: $radius-md;
background: $bg-secondary;
margin-right: $spacing-lg;
}
.record-info {
flex: 1;
}
.record-title {
font-size: $font-md;
font-weight: 600;
color: $text-main;
margin-bottom: $spacing-xs;
}
.record-meta {
display: flex;
gap: $spacing-md;
font-size: $font-sm;
color: $text-sub;
}
.record-count {
background: rgba($brand-primary, 0.1);
color: $brand-primary;
padding: 2rpx $spacing-sm;
border-radius: $radius-sm;
}
/* Empty State */
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: $spacing-xl * 2;
color: $text-tertiary;
}
.empty-icon {
font-size: 80rpx;
margin-bottom: $spacing-md;
opacity: 0.6;
}
.empty-text {
font-size: $font-md;
color: $text-sub;
}
</style>