feat: 优化抽奖活动页面UI,新增奖池分级展示和购买记录功能。

This commit is contained in:
邹方成 2025-12-25 19:17:57 +08:00
parent a18845c849
commit 148c62a983
8 changed files with 1310 additions and 629 deletions

View File

@ -74,6 +74,10 @@ export function getActivityIssueRewards(activity_id, issue_id) {
return authRequest({ url: `/api/app/activities/${activity_id}/issues/${issue_id}/rewards`, method: 'GET' })
}
export function getIssueDrawLogs(activity_id, issue_id) {
return authRequest({ url: `/api/app/activities/${activity_id}/issues/${issue_id}/draw_logs`, method: 'GET' })
}
export function drawActivityIssue(activity_id, issue_id) {
return authRequest({ url: `/api/app/activities/${activity_id}/issues/${issue_id}/draw`, method: 'POST' })
}

View File

@ -476,39 +476,40 @@ async function onPaymentConfirm(paymentData) {
text-shadow: 0 2rpx 4rpx rgba(0,0,0,0.1);
}
/* ============= 底部操作栏 ============= */
/* ============= 底部操作栏 - 高级重置 ============= */
.action-bar {
position: fixed;
bottom: calc(40rpx + env(safe-area-inset-bottom));
left: 30rpx;
right: 30rpx;
background: rgba($bg-card, 0.9);
backdrop-filter: blur(20rpx);
padding: 20rpx 30rpx;
box-shadow: $shadow-lg;
background: rgba(255, 255, 255, 0.85);
backdrop-filter: blur(30rpx);
padding: 24rpx 40rpx;
box-shadow: 0 16rpx 48rpx rgba(0, 0, 0, 0.12);
border-radius: 999rpx;
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
z-index: 100;
border: 1rpx solid rgba($bg-card, 0.5);
animation: slideUp 0.4s ease-out backwards;
border: 1rpx solid rgba(255, 255, 255, 0.6);
animation: slideUp 0.4s cubic-bezier(0.23, 1, 0.32, 1) backwards;
}
/* 选择信息行 */
.selection-info {
font-size: 26rpx;
font-size: 28rpx;
color: $text-main;
display: flex;
align-items: center;
font-weight: 600;
align-items: baseline;
font-weight: 800;
}
.highlight {
color: $brand-primary;
font-weight: 800;
font-size: 36rpx;
font-weight: 900;
font-size: 40rpx;
margin: 0 8rpx;
font-family: 'DIN Alternate', sans-serif;
}
/* 按钮组 */
@ -519,54 +520,75 @@ async function onPaymentConfirm(paymentData) {
/* 通用按钮样式 */
.btn-common {
height: 80rpx;
line-height: 80rpx;
padding: 0 48rpx;
height: 88rpx;
line-height: 88rpx;
padding: 0 56rpx;
border-radius: 999rpx;
font-size: 28rpx;
font-weight: 700;
font-size: 30rpx;
font-weight: 900;
margin: 0;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s;
transition: all 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275);
border: none;
&:active {
transform: scale(0.96);
transform: scale(0.92);
}
}
/* 购买按钮 */
/* 购买按钮 - 品牌渐变 + 流光 */
.btn-buy {
background: $gradient-brand !important;
color: #FFFFFF !important;
box-shadow: 0 8rpx 20rpx rgba($brand-primary, 0.3);
box-shadow: 0 12rpx 32rpx rgba($brand-primary, 0.35);
position: relative;
overflow: hidden;
/* 脉冲动画 */
animation: pulse 2s infinite;
}
/* 随机按钮 */
.btn-random {
background: $bg-secondary !important;
color: $text-main !important;
box-shadow: none;
border: 1rpx solid transparent;
&:active {
background: #E5E7EB !important;
&::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: btnShine 4s infinite cubic-bezier(0.19, 1, 0.22, 1);
pointer-events: none;
}
}
/* 随机按钮 - 轻量化设计 */
.btn-random {
background: #1A1A1A !important;
color: $accent-gold !important;
box-shadow: 0 8rpx 20rpx rgba(0, 0, 0, 0.15);
&:active {
background: #333 !important;
}
}
@keyframes btnShine {
0% { left: -150%; }
100% { left: 150%; }
}
@keyframes slideUp {
from { transform: translateY(100%); opacity: 0; }
from { transform: translateY(120rpx); opacity: 0; }
to { transform: translateY(0); opacity: 1; }
}
@keyframes pulse {
0% { box-shadow: 0 0 0 0 rgba($brand-primary, 0.4); }
70% { box-shadow: 0 0 0 20rpx rgba($brand-primary, 0); }
100% { box-shadow: 0 0 0 0 rgba($brand-primary, 0); }
@keyframes scaleIn {
from { transform: scale(0.95); opacity: 0; }
to { transform: scale(1); opacity: 1; }
}
@keyframes float {

View File

@ -25,6 +25,16 @@
<view class="tag-item">理性消费</view>
</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>
</view>
<view class="section-container animate-enter stagger-1">
@ -51,18 +61,6 @@
</view>
<view class="section-container animate-enter stagger-2">
<view class="issue-header">
<view class="issue-switch-btn" @tap="prevIssue">
<text class="arrow"></text>
</view>
<view class="issue-info-center">
<text class="issue-current-text">{{ currentIssueTitle }}</text>
<text class="issue-status-badge">{{ currentIssueStatusText }}</text>
</view>
<view class="issue-switch-btn" @tap="nextIssue">
<text class="arrow"></text>
</view>
</view>
<view class="modern-tabs">
<view class="tab-item" :class="{ active: tabActive === 'pool' }" @tap="tabActive = 'pool'">
@ -80,13 +78,21 @@
<text class="section-title">奖池配置</text>
<text class="section-more" @tap="openRewardsPopup">查看全部</text>
</view>
<scroll-view v-if="currentIssueRewards.length > 0" 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 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>
</scroll-view>
</view>
<view v-else class="empty-state">
<text class="empty-icon">📭</text>
<text class="empty-text">暂无奖励配置</text>
@ -101,7 +107,6 @@
<view class="record-title">{{ it.title }}</view>
<view class="record-meta">
<text class="record-count">x{{ it.count }}</text>
<text v-if="it.percent !== undefined">占比 {{ it.percent }}%</text>
</view>
</view>
</view>
@ -185,17 +190,25 @@
<text class="rewards-close" @tap="closeRewardsPopup">×</text>
</view>
<scroll-view scroll-y class="rewards-list">
<view v-for="(item, idx) in currentIssueRewards" :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 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>
<text class="rewards-percent">概率 {{ formatPercent(item.percent) }}</text>
</view>
</view>
<view v-if="!currentIssueRewards.length" class="rewards-empty">暂无奖品数据</view>
<view v-else class="rewards-empty">暂无奖品数据</view>
</scroll-view>
</view>
</view>
@ -214,7 +227,7 @@
import { ref, computed } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import PaymentPopup from '../../../components/PaymentPopup.vue'
import { getActivityDetail, getActivityIssues, getActivityIssueRewards, getUserCoupons, getItemCards, createWechatOrder, getMatchingCardTypes, createMatchingPreorder, checkMatchingGame } from '../../../api/appUser'
import { getActivityDetail, getActivityIssues, getActivityIssueRewards, getUserCoupons, getItemCards, createWechatOrder, getMatchingCardTypes, createMatchingPreorder, checkMatchingGame, getIssueDrawLogs } from '../../../api/appUser'
const detail = ref({})
const statusText = ref('')
@ -328,10 +341,33 @@ const currentIssueRewards = computed(() => {
const m = rewardsMap.value || {}
return (iid && Array.isArray(m[iid])) ? m[iid] : []
})
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
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 currentIssueTitle = computed(() => {
const arr = issues.value || []
const cur = arr[selectedIssueIndex.value]
return (cur && (cur.title || ('第' + (cur.no || '-') + '期'))) || '-'
//
return (cur && (cur.title || '奖池')) || '-'
})
const currentIssueStatusText = computed(() => {
const arr = issues.value || []
@ -502,7 +538,8 @@ 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)
boss: detectBoss(i),
level: levelToAlpha(i.prize_level ?? i.level ?? (detectBoss(i) ? 'BOSS' : '赏'))
}))
const total = items.reduce((acc, it) => acc + (it.weight > 0 ? it.weight : 0), 0)
const enriched = items.map(it => ({
@ -555,6 +592,10 @@ async function fetchIssues(id) {
const latestId = pickLatestIssueId(issues.value)
setSelectedById(latestId)
await fetchRewardsForIssues(id)
//
if (currentIssueId.value) {
fetchWinRecords(id, currentIssueId.value)
}
}
function pickLatestIssueId(list) {
@ -596,12 +637,50 @@ function nextIssue() {
syncResumeGame(activityId.value)
}
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 formatPercent(v) {
const n = Number(v)
if (!Number.isFinite(n)) return '0%'
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
}
@ -609,6 +688,18 @@ function closeRewardsPopup() {
rewardsVisible.value = false
}
function showRules() {
uni.showModal({
title: '活动规则',
content: detail.value.rules || '1. 选择卡牌类型进行对对碰\\n2. 每次抽取随机获得奖品\\n3. 奖池与概率以页面展示为准',
showCancel: false
})
}
function goCabinet() {
uni.switchTab({ url: '/pages/cabinet/index' })
}
function onPreviewBanner() {
const url = detail.value.banner || ''
if (url) uni.previewImage({ urls: [url], current: url })
@ -1266,10 +1357,10 @@ onLoad((opts) => {
}
.header-info {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
justify-content: flex-start;
min-height: 180rpx;
justify-content: center;
padding: 6rpx 0;
}
.header-title {
@ -1306,6 +1397,52 @@ onLoad((opts) => {
border: 1rpx solid rgba($brand-primary, 0.1);
}
.header-actions {
display: flex;
flex-direction: column;
gap: 28rpx;
margin-left: 16rpx;
padding-left: 24rpx;
border-left: 2rpx solid #E8E8E8;
justify-content: center;
align-self: stretch;
}
.action-btn {
display: flex;
flex-direction: column;
align-items: center;
&:active {
opacity: 0.6;
}
}
.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;
}
.section-container {
margin: 0 $spacing-lg $spacing-lg;
background: rgba(255, 255, 255, 0.9);
@ -1384,14 +1521,12 @@ onLoad((opts) => {
.preview-scroll {
white-space: nowrap;
margin: 0 -$spacing-lg;
padding: 0 $spacing-lg;
width: calc(100% + 40rpx);
width: 100%;
}
.preview-item {
display: inline-block;
width: 160rpx;
margin-right: $spacing-lg;
width: 180rpx;
margin-right: $spacing-md;
vertical-align: top;
position: relative;
transition: transform 0.2s;
@ -1401,12 +1536,12 @@ onLoad((opts) => {
}
&:last-child {
margin-right: 40rpx;
margin-right: 0;
}
}
.preview-img {
width: 160rpx;
height: 160rpx;
width: 180rpx;
height: 180rpx;
border-radius: $radius-lg;
background: $bg-secondary;
margin-bottom: $spacing-sm;
@ -1414,7 +1549,7 @@ onLoad((opts) => {
border: 1rpx solid rgba(0,0,0,0.03);
}
.preview-name {
font-size: $font-sm;
font-size: $font-xs;
color: $text-secondary;
width: 100%;
overflow: hidden;
@ -1423,6 +1558,47 @@ onLoad((opts) => {
text-align: center;
font-weight: 500;
}
.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;
&.badge-boss {
background: $gradient-gold;
color: #78350F;
border-color: rgba(217, 119, 6, 0.3);
}
}
.level-prob {
font-size: 22rpx;
color: $brand-primary;
font-weight: 800;
opacity: 0.9;
}
.prize-tag {
position: absolute;
top: 10rpx;
@ -1542,6 +1718,7 @@ onLoad((opts) => {
font-weight: 900;
background: $gradient-brand;
-webkit-background-clip: text;
background-clip: text;
-webkit-text-fill-color: transparent;
display: inline-block;
}
@ -1831,24 +2008,28 @@ onLoad((opts) => {
.empty-icon { font-size: 80rpx; margin-bottom: $spacing-lg; opacity: 0.5; }
.empty-text { font-size: $font-md; }
/* Float Bar */
/* 底部悬浮操作栏 - 高级重置 */
.float-bar {
position: fixed;
left: 0; right: 0; bottom: 0;
padding: $spacing-lg $spacing-xl;
padding-bottom: calc($spacing-lg + env(safe-area-inset-bottom));
background: rgba(255, 255, 255, 0.9);
backdrop-filter: blur(20px);
box-shadow: 0 -8rpx 30rpx rgba(0, 0, 0, 0.05);
left: 32rpx;
right: 32rpx;
bottom: calc(40rpx + env(safe-area-inset-bottom));
z-index: 100;
animation: slideUp 0.4s ease-out backwards;
animation: slideUp 0.6s cubic-bezier(0.23, 1, 0.32, 1) backwards;
}
.float-bar-inner {
background: rgba(255, 255, 255, 0.85);
backdrop-filter: blur(30rpx);
padding: 24rpx 40rpx;
border-radius: 999rpx;
display: flex;
align-items: center;
justify-content: space-between;
gap: $spacing-lg;
box-shadow: 0 16rpx 48rpx rgba(0, 0, 0, 0.12);
border: 1rpx solid rgba(255, 255, 255, 0.6);
}
.float-price {
display: flex;
align-items: baseline;
@ -1856,9 +2037,9 @@ onLoad((opts) => {
font-weight: 800;
min-width: 0;
}
.currency { font-size: $font-md; color: $brand-primary; }
.amount { font-size: 44rpx; margin: 0 6rpx; color: $brand-primary; font-family: 'DIN Alternate', sans-serif; }
.unit { font-size: $font-sm; color: $text-tertiary; font-weight: 600; }
.float-price .currency { font-size: 26rpx; margin-right: 4rpx; color: $brand-primary; }
.float-price .amount { font-size: 44rpx; font-weight: 900; font-family: 'DIN Alternate', sans-serif; color: $brand-primary; }
.float-price .unit { font-size: 24rpx; color: $text-sub; margin-left: 4rpx; font-weight: 600; }
.rewards-overlay { position: fixed; left: 0; right: 0; top: 0; bottom: 0; z-index: 9000; }
.rewards-mask {
@ -1901,6 +2082,43 @@ onLoad((opts) => {
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;
@ -1978,44 +2196,57 @@ onLoad((opts) => {
to { transform: translateY(0); opacity: 1; }
}
.action-btn {
height: 96rpx;
border-radius: $radius-round;
height: 88rpx;
line-height: 88rpx;
padding: 0 56rpx;
border-radius: 999rpx;
font-size: 30rpx;
font-weight: 900;
display: flex;
align-items: center;
justify-content: center;
font-size: $font-xl;
font-weight: 800;
transition: all 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275);
position: relative;
overflow: hidden;
transition: all 0.2s;
&.primary {
background: $gradient-brand;
color: #fff;
box-shadow: $shadow-warm;
}
&.secondary {
background: rgba($bg-card, 0.9);
color: $text-main;
border: 2rpx solid rgba($brand-primary, 0.25);
box-shadow: $shadow-sm;
padding: 0 40rpx;
background: $gradient-brand !important;
color: #fff !important;
box-shadow: 0 12rpx 32rpx rgba($brand-primary, 0.35);
&::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: btnShine 4s infinite cubic-bezier(0.19, 1, 0.22, 1);
pointer-events: none;
}
}
&:active { transform: scale(0.98); }
}
.btn-shine {
position: absolute;
top: 0; left: -100%; width: 50%; height: 100%;
background: linear-gradient(90deg, transparent, rgba(255,255,255,0.3), transparent);
transform: skewX(-20deg);
animation: shine 3s infinite;
&.secondary {
background: #1A1A1A !important;
color: $accent-gold !important;
box-shadow: 0 8rpx 20rpx rgba(0, 0, 0, 0.15);
}
&:active {
transform: scale(0.92);
}
}
@keyframes shine {
0% { left: -100%; }
50%, 100% { left: 200%; }
@keyframes btnShine {
0% { left: -150%; }
100% { left: 150%; }
}
.flip-overlay {

View File

@ -214,6 +214,7 @@ $local-gold: #FFD700; // 特殊金色,比全局更亮
text-shadow: 0 4rpx 16rpx rgba(0,0,0,0.6);
background: linear-gradient(180deg, #fff, #b3b3b3);
-webkit-background-clip: text;
background-clip: text;
color: transparent;
letter-spacing: 2rpx;
}
@ -327,59 +328,71 @@ $local-gold: #FFD700; // 特殊金色,比全局更亮
}
.action-area {
background: $bg-dark-card;
backdrop-filter: blur(40rpx);
padding: 24rpx 32rpx;
border-radius: 100rpx;
position: fixed;
left: 40rpx;
right: 40rpx;
bottom: calc(40rpx + env(safe-area-inset-bottom));
background: rgba(26, 26, 26, 0.85);
backdrop-filter: blur(30rpx);
padding: 24rpx 40rpx;
border-radius: 999rpx;
display: flex;
align-items: center;
justify-content: center;
border: 1px solid $border-dark;
box-shadow: 0 20rpx 60rpx rgba(0,0,0,0.5);
margin-bottom: calc(env(safe-area-inset-bottom) + 20rpx);
animation: slideUp 0.6s ease-out backwards;
animation-delay: 0.3s;
border: 1rpx solid rgba(255, 255, 255, 0.1);
box-shadow: 0 16rpx 48rpx rgba(0, 0, 0, 0.4);
z-index: 100;
animation: slideUp 0.6s cubic-bezier(0.23, 1, 0.32, 1) backwards;
}
.challenge-btn {
background: $gradient-brand;
color: #fff;
background: $gradient-brand !important;
color: #fff !important;
font-weight: 900;
border-radius: 100rpx;
border-radius: 999rpx;
padding: 0 60rpx;
height: 88rpx;
line-height: 88rpx;
font-size: 32rpx;
box-shadow: 0 8rpx 24rpx rgba(255, 107, 0, 0.3);
box-shadow: 0 12rpx 32rpx rgba(255, 107, 0, 0.3);
border: none;
position: relative;
overflow: hidden;
transition: all 0.2s;
transition: all 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275);
width: 100%;
&::after {
&::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: shimmer 3s infinite;
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: btnShine 4s infinite cubic-bezier(0.19, 1, 0.22, 1);
pointer-events: none;
}
&:active {
transform: scale(0.96);
box-shadow: 0 4rpx 12rpx rgba(255, 107, 0, 0.2);
transform: scale(0.94);
}
&.disabled {
background: #555;
color: #999;
background: #333 !important;
color: #666 !important;
box-shadow: none;
&::after { display: none; }
&::before { display: none; }
}
}
@keyframes shimmer {
0% { left: -100%; }
50%, 100% { left: 200%; }
@keyframes btnShine {
0% { left: -150%; }
100% { left: 150%; }
}
</style>

View File

@ -27,28 +27,71 @@
</view>
<view class="header-actions">
<view class="action-btn" @tap="showRules">
<text class="icon">📋</text>
<text>规则</text>
<view class="action-icon rules-icon"></view>
<text class="action-label">规则</text>
</view>
<view class="action-btn" @tap="goCabinet">
<text class="icon">📦</text>
<text>盒柜</text>
<view class="action-icon cabinet-icon"></view>
<text class="action-label">盒柜</text>
</view>
</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>
<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 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>
</scroll-view>
<view class="tab-item" :class="{ active: tabActive === 'records' }" @tap="tabActive = 'records'">
购买记录
<view v-if="tabActive === 'records'" class="active-dot"></view>
</view>
</view>
<view v-show="tabActive === 'pool'">
<view class="section-header">
<text class="section-title">奖池配置</text>
<text class="section-more" @tap="openRewardsPopup">查看全部</text>
</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>
</view>
<view style="height: 220rpx;"></view>
@ -83,17 +126,25 @@
<text class="rewards-close" @tap="closeRewardsPopup">×</text>
</view>
<scroll-view scroll-y class="rewards-list">
<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 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>
<text class="rewards-percent">概率 {{ formatPercent(item.percent) }}</text>
</view>
</view>
<view v-if="!rewardsForPopup.length" class="rewards-empty">暂无奖池数据</view>
<view v-else class="rewards-empty">暂无奖池数据</view>
</scroll-view>
</view>
</view>
@ -120,11 +171,13 @@ 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 } from '../../../api/appUser'
import { getActivityDetail, getActivityIssues, getActivityIssueRewards, joinLottery, createWechatOrder, getLotteryResult, getItemCards, getUserCoupons, getIssueDrawLogs } 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('')
@ -137,8 +190,8 @@ const coverUrl = computed(() => cleanUrl(detail.value && (detail.value.image ||
const currentIssueTitle = computed(() => {
const arr = issues.value || []
const cur = arr[selectedIssueIndex.value]
const t = (cur && (cur.title || ('第' + (cur.no || '-') + '期'))) || '-'
return t
//
return (cur && (cur.title || '奖池')) || '-'
})
const points = ref(0)
const flipRef = ref(null)
@ -147,6 +200,28 @@ 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([])
@ -169,6 +244,15 @@ 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
}
@ -235,7 +319,8 @@ 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)
boss: detectBoss(i),
level: levelToAlpha(i.prize_level ?? i.level ?? (detectBoss(i) ? 'BOSS' : '赏'))
}))
const total = items.reduce((acc, it) => acc + (it.weight > 0 ? it.weight : 0), 0)
const enriched = items.map(it => ({
@ -288,6 +373,10 @@ async function fetchIssues(id) {
const latestId = pickLatestIssueId(issues.value)
setSelectedById(latestId)
await fetchRewardsForIssues(id)
//
if (currentIssueId.value) {
fetchWinRecords(id, currentIssueId.value)
}
}
function pickLatestIssueId(list) {
@ -328,6 +417,36 @@ 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 })
@ -618,10 +737,10 @@ function closeFlip() { showFlip.value = false }
}
.header-info {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
justify-content: flex-start;
min-height: 180rpx;
justify-content: center;
padding: 6rpx 0;
}
.header-title {
@ -660,29 +779,47 @@ function closeFlip() { showFlip.value = false }
.header-actions {
display: flex;
flex-direction: column;
gap: $spacing-lg;
margin-left: 20rpx;
padding-left: $spacing-lg;
border-left: 1rpx solid rgba(0,0,0,0.06);
gap: 28rpx;
margin-left: 16rpx;
padding-left: 24rpx;
border-left: 2rpx solid #E8E8E8;
justify-content: center;
height: 140rpx;
align-self: stretch;
}
.action-btn {
display: flex;
flex-direction: column;
align-items: center;
font-size: $font-xs;
color: $text-sub;
transition: all 0.2s;
&:active {
transform: scale(0.9);
color: $text-main;
opacity: 0.6;
}
}
.action-btn .icon {
font-size: $font-xl;
margin-bottom: 6rpx;
filter: grayscale(0.2);
.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;
}
.section-container {
@ -693,6 +830,39 @@ 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;
@ -733,31 +903,29 @@ function closeFlip() { showFlip.value = false }
.preview-scroll {
white-space: nowrap;
margin: 0 -$spacing-lg;
padding: 0 $spacing-lg;
width: calc(100% + 40rpx);
width: 100%;
}
.preview-item {
display: inline-block;
width: 200rpx;
margin-right: $spacing-lg;
width: 180rpx;
margin-right: $spacing-md;
vertical-align: top;
position: relative;
transition: transform 0.2s;
&:active { transform: scale(0.96); }
&:last-child { margin-right: 40rpx; }
&:last-child { margin-right: 0; }
}
.preview-img {
width: 200rpx;
height: 200rpx;
width: 180rpx;
height: 180rpx;
border-radius: $radius-lg;
background: $bg-secondary;
margin-bottom: $spacing-md;
margin-bottom: $spacing-sm;
box-shadow: $shadow-sm;
border: 1rpx solid rgba(0,0,0,0.03);
}
.preview-name {
font-size: $font-sm;
font-size: $font-xs;
color: $text-secondary;
width: 100%;
overflow: hidden;
@ -768,16 +936,16 @@ function closeFlip() { showFlip.value = false }
}
.prize-tag {
position: absolute;
top: 10rpx;
left: 10rpx;
top: 8rpx;
left: 8rpx;
background: rgba(0,0,0,0.6);
color: #fff;
font-size: $font-xs;
padding: 4rpx $spacing-sm;
border-radius: $radius-sm;
font-size: $font-xxs;
padding: 2rpx $spacing-xs;
border-radius: 4rpx;
z-index: 10;
font-weight: 700;
backdrop-filter: blur(4rpx);
backdrop-filter: blur(4px);
transform: scale(0.9);
transform-origin: top left;
}
@ -786,6 +954,47 @@ 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;
@ -893,6 +1102,7 @@ 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;
@ -997,23 +1207,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: $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);
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);
z-index: 999;
animation: slideUp $transition-slow $ease-out backwards;
border-top: 1rpx solid rgba(0,0,0,0.05);
border-top: 1rpx solid rgba(255, 255, 255, 0.8);
}
.tier-btn {
flex: 1;
min-width: 0;
@ -1021,92 +1231,80 @@ function closeFlip() { showFlip.value = false }
flex-direction: column;
align-items: center;
justify-content: center;
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;
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;
&:active {
transform: scale(0.95);
background: $bg-page;
transform: scale(0.92);
background: #F9F9F9;
box-shadow: none;
}
}
.tier-price {
font-size: $font-lg;
font-weight: 800;
color: $text-main;
font-family: 'DIN Alternate', sans-serif;
}
.tier-label {
font-size: $font-xs;
color: $text-sub;
margin-top: 4rpx;
font-weight: 500;
}
.tier-price {
font-size: 34rpx;
font-weight: 900;
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;
}
/* 热门/最高档位 - 高级动效 */
.tier-hot {
background: $gradient-red;
border: none;
box-shadow: 0 6rpx 14rpx rgba($accent-red, 0.12);
background: $gradient-brand !important;
border: none !important;
box-shadow: 0 12rpx 32rpx rgba($brand-primary, 0.35) !important;
position: relative;
overflow: hidden;
border-radius: $radius-lg;
transform: translateZ(0);
.tier-price, .tier-label {
color: #fff !important;
position: relative;
z-index: 2;
.tier-price {
color: #FFF !important;
text-shadow: 0 2rpx 4rpx rgba(0, 0, 0, 0.1);
}
.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;
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;
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;
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 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; }
@keyframes hotSweep {
0% { left: -150%; }
100% { left: 150%; }
}
.rewards-overlay { position: fixed; left: 0; right: 0; top: 0; bottom: 0; z-index: 9000; }
@ -1150,6 +1348,43 @@ 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;
@ -1257,4 +1492,67 @@ 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,30 +38,67 @@
</view>
</view>
<view class="header-actions">
<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 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>
</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>
<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 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>
</scroll-view>
<view class="tab-item" :class="{ active: tabActive === 'records' }" @tap="tabActive = 'records'">
购买记录
<view v-if="tabActive === 'records'" class="active-dot"></view>
</view>
</view>
<view v-show="tabActive === 'pool'">
<view class="section-header">
<text class="section-title">奖品配置</text>
<text class="section-more" @tap="openRewardsPopup">查看全部</text>
</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>
</view>
<!-- 选号区域 -->
@ -142,7 +179,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 } from '../../../api/appUser'
import { getActivityDetail, getActivityIssues, getActivityIssueRewards, getIssueDrawLogs } from '../../../api/appUser'
const detail = ref({})
const issues = ref([])
@ -379,6 +416,10 @@ async function fetchIssues(id) {
const latestId = pickLatestIssueId(issues.value)
setSelectedById(latestId)
await fetchRewardsForIssues(id)
//
if (currentIssueId.value) {
fetchWinRecords(id, currentIssueId.value)
}
}
function pickLatestIssueId(list) {
@ -419,7 +460,35 @@ 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 || ''
@ -612,10 +681,10 @@ onUnload(() => {
}
.header-info {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
justify-content: flex-start;
min-height: 180rpx;
justify-content: center;
padding: 6rpx 0;
}
.header-title {
@ -672,30 +741,47 @@ onUnload(() => {
.header-actions {
display: flex;
flex-direction: column;
gap: $spacing-lg;
margin-left: 20rpx;
padding-left: $spacing-lg;
border-left: 1rpx solid rgba(0,0,0,0.06);
gap: 28rpx;
margin-left: 16rpx;
padding-left: 24rpx;
border-left: 2rpx solid #E8E8E8;
justify-content: center;
height: 140rpx;
align-self: stretch;
}
.action-btn {
display: flex;
flex-direction: column;
align-items: center;
font-size: $font-xs;
color: $text-sub;
transition: all 0.2s;
&:active {
transform: scale(0.9);
color: $text-main;
opacity: 0.6;
}
}
.action-btn .icon {
font-size: $font-xl;
margin-bottom: 6rpx;
filter: grayscale(0.2);
.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;
}
/* 通用板块容器 */
@ -1044,4 +1130,99 @@ 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>

View File

@ -115,13 +115,16 @@ export default {
}
})
// URL
// URL
const gameBaseUrl = 'https://game.1024tool.vip'
const gameUrl = `${gameBaseUrl}?ticket=${res.ticket_token}`
const gameToken = encodeURIComponent(res.game_token)
const nakamaServer = encodeURIComponent(res.nakama_server)
const nakamaKey = encodeURIComponent(res.nakama_key)
const gameUrl = `${gameBaseUrl}?game_token=${gameToken}&nakama_server=${nakamaServer}&nakama_key=${nakamaKey}`
// webview
uni.navigateTo({
url: `/pages/game/webview?url=${encodeURIComponent(gameUrl)}&ticket=${res.ticket_token}`
url: `/pages/game/webview?url=${encodeURIComponent(gameUrl)}`
})
} catch (e) {
uni.showToast({

View File

@ -1,7 +1,11 @@
<template>
<view class="page">
<!-- 背景装饰 -->
<view class="bg-decoration"></view>
<!-- 品牌级背景装饰系统 -->
<view class="premium-bg">
<view class="bg-shape circle-1"></view>
<view class="bg-shape circle-2"></view>
<view class="bg-shape mesh-gradient"></view>
</view>
<!-- 顶部导航栏 (搜索) -->
<view class="nav-header">
@ -13,28 +17,36 @@
<!-- 滚动区域 -->
<scroll-view class="main-content" scroll-y>
<!-- Banner 区域 -->
<view class="banner-box">
<swiper class="banner-swiper" circular autoplay interval="4000" duration="500">
<swiper-item v-for="b in displayBanners" :key="b.id">
<image v-if="b.image" class="banner-image" :src="b.image" mode="aspectFill" @tap="onBannerTap(b)" />
<view v-else class="banner-fallback">
<text class="banner-fallback-text">{{ b.title || '柯大鸭潮玩 V6.0' }}</text>
<text class="banner-tag">功能更新UI优化全面来袭</text>
<!-- Banner 区域 (现代级浮动设计) -->
<view class="banner-container">
<swiper class="banner-swiper" circular autoplay interval="5000" duration="600" :indicator-dots="false" @change="onBannerChange">
<swiper-item v-for="(b, index) in displayBanners" :key="b.id">
<view class="banner-card" :class="{ 'active': bannerIndex === index }">
<image v-if="b.image" class="banner-image" :src="b.image" mode="aspectFill" @tap="onBannerTap(b)" />
<view v-else class="banner-fallback">
<view class="fallback-glow"></view>
<text class="banner-fallback-text">{{ b.title || 'KE DAYA TOYS' }}</text>
<view class="banner-tag">UI/UX PREMIUM 6.0</view>
</view>
</view>
</swiper-item>
</swiper>
<!-- 自定义指示器 -->
<view class="banner-indicator">
<view v-for="(b, index) in displayBanners" :key="'dot' + index"
class="indicator-dot" :class="{ 'active': bannerIndex === index }"></view>
</view>
</view>
<!-- 通知栏 -->
<view class="notice-bar" @tap="onNoticeTap">
<view class="notice-tag">通知</view>
<swiper class="notice-swiper" vertical circular autoplay interval="3000" duration="300">
<!-- 品牌动态栏 (极简风格) -->
<view class="notice-bar-v2" @tap="onNoticeTap">
<view class="notice-icon">📢</view>
<swiper class="notice-swiper" vertical circular autoplay interval="3500">
<swiper-item v-for="n in displayNotices" :key="n.id">
<view class="notice-item">{{ n.text }}</view>
</swiper-item>
</swiper>
<view class="notice-more">查看详情</view>
<view class="notice-arrow"></view>
</view>
@ -129,7 +141,8 @@ export default {
notices: [],
banners: [],
activities: [],
selectedGroupName: ''
selectedGroupName: '',
bannerIndex: 0
}
},
computed: {
@ -182,6 +195,9 @@ export default {
this.loadHomeData()
},
methods: {
onBannerChange(e) {
this.bannerIndex = e.detail.current
},
onSelectGroup(name) {
this.selectedGroupName = String(name || '')
},
@ -341,10 +357,54 @@ export default {
.page {
padding: 0;
background-color: $bg-page;
background-color: #F8F9FB;
min-height: 100vh;
display: flex;
flex-direction: column;
position: relative;
overflow-x: hidden;
}
/* ========== 品牌级背景系统 ========== */
.premium-bg {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 0;
pointer-events: none;
overflow: hidden;
}
.bg-shape {
position: absolute;
filter: blur(100rpx);
opacity: 0.6;
}
.circle-1 {
width: 600rpx;
height: 600rpx;
background: radial-gradient(circle, rgba(255, 107, 0, 0.15) 0%, transparent 70%);
top: -200rpx;
right: -100rpx;
animation: float 10s ease-in-out infinite alternate;
}
.circle-2 {
width: 500rpx;
height: 500rpx;
background: radial-gradient(circle, rgba(255, 193, 7, 0.1) 0%, transparent 70%);
bottom: 200rpx;
left: -100rpx;
animation: float 12s ease-in-out infinite alternate-reverse;
}
.mesh-gradient {
width: 100%;
height: 100%;
background: linear-gradient(135deg, rgba(255, 107, 0, 0.03) 0%, rgba(255, 255, 255, 0) 50%, rgba(255, 193, 7, 0.03) 100%);
}
/* ========== 顶部导航栏 ========== */
@ -352,192 +412,203 @@ export default {
display: flex;
align-items: center;
padding: $spacing-md $spacing-lg;
background: transparent; /* 透明背景,依靠内容区的渐变 */
padding-top: calc($spacing-md + env(safe-area-inset-top));
gap: $spacing-lg;
z-index: 10;
}
.brand-logo {
display: flex;
align-items: center;
position: sticky;
top: 0;
}
.brand-text {
font-size: 40rpx;
font-size: 44rpx;
font-weight: 900;
color: $text-main;
font-style: italic;
letter-spacing: -1rpx;
text-shadow: 0 2rpx 4rpx rgba(255, 255, 255, 0.5);
letter-spacing: -2rpx;
background: linear-gradient(135deg, #1A1A1A 0%, #444 100%);
-webkit-background-clip: text;
background-clip: text;
-webkit-text-fill-color: transparent;
}
.brand-star {
font-size: 24rpx;
margin-left: 4rpx;
margin-top: -16rpx;
animation: pulse 2s infinite;
font-size: 28rpx;
margin-left: 6rpx;
}
.search-bar {
flex: 1;
height: 72rpx;
background: rgba(255, 255, 255, 0.8);
backdrop-filter: blur(10px);
border-radius: $radius-round;
display: flex;
align-items: center;
padding: 0 $spacing-lg;
border: 1rpx solid rgba(255, 255, 255, 0.6);
box-shadow: $shadow-sm;
transition: all 0.3s ease;
}
.search-bar:active {
background: #fff;
transform: scale(0.98);
}
.search-icon { margin-right: 12rpx; font-size: 32rpx; }
.search-placeholder { color: $text-tertiary; font-size: 28rpx; font-weight: 500; }
/* ========== 滚动主内容区 ========== */
.main-content {
flex: 1;
/* background: linear-gradient(180deg, $bg-secondary 0%, $bg-page 400rpx); */ /* 移除原有背景,使用全局背景装饰 */
position: relative;
z-index: 1; /* 确保内容在背景装饰之上 */
z-index: 1;
padding-top: $spacing-sm;
}
/* Logo Banner */
.banner-box {
margin: 0 $spacing-lg $spacing-lg;
border-radius: $radius-lg;
overflow: hidden;
box-shadow: $shadow-float;
transform: translateZ(0); /* 开启硬件加速 */
animation: fadeInUp 0.6s ease-out;
/* Banner Container (Modern Floating) */
.banner-container {
padding: 0 $spacing-lg $spacing-xl;
position: relative;
z-index: 2;
}
.banner-swiper, .banner-image, .banner-fallback {
.banner-swiper {
height: 360rpx;
overflow: visible; /* 让阴影不被切断 */
}
.banner-card {
height: 100%;
margin: 0 4rpx;
border-radius: 32rpx;
overflow: hidden;
position: relative;
transform: scale(0.96);
transition: all 0.5s $ease-out;
box-shadow: 0 16rpx 48rpx rgba(255, 107, 0, 0.1);
}
.banner-card.active {
transform: scale(1);
box-shadow: $shadow-float;
}
.banner-image {
width: 100%;
height: 340rpx; /* 略微增高 */
height: 100%;
display: block;
}
.banner-fallback {
background: linear-gradient(135deg, $bg-secondary, #FFF0D6);
width: 100%;
height: 100%;
background: linear-gradient(135deg, $brand-primary, $brand-secondary);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
border: 4rpx dashed $brand-primary-light;
position: relative;
}
.fallback-glow {
position: absolute;
width: 100%;
height: 100%;
background: radial-gradient(circle at center, rgba(255,255,255,0.2) 0%, transparent 80%);
}
.banner-fallback-text {
font-size: 48rpx;
font-size: 52rpx;
font-weight: 900;
color: $brand-primary;
color: #fff;
font-style: italic;
margin-bottom: 12rpx;
text-shadow: 0 2rpx 0 rgba(255,255,255,1);
margin-bottom: 12rpx;
letter-spacing: 2rpx;
z-index: 1;
}
.banner-tag {
background: #1A1A1A;
color: $accent-gold;
padding: 6rpx 20rpx;
background: rgba(0,0,0,0.2);
color: #fff;
padding: 8rpx 24rpx;
border-radius: $radius-round;
font-size: 24rpx;
font-weight: 700;
font-size: 22rpx;
font-weight: 700;
backdrop-filter: blur(4px);
z-index: 1;
}
/* 通知栏 */
.notice-bar {
/* Indicator */
.banner-indicator {
display: flex;
justify-content: center;
gap: 12rpx;
margin-top: -16rpx;
}
.indicator-dot {
width: 12rpx;
height: 6rpx;
background: rgba(0,0,0,0.1);
border-radius: 4rpx;
transition: all 0.3s ease;
}
.indicator-dot.active {
width: 32rpx;
background: $brand-primary;
}
/* Notice Bar V2 (Minimalist) */
.notice-bar-v2 {
margin: 0 $spacing-lg $spacing-xl;
background: #FFFFFF;
border-radius: $radius-round;
padding: 16rpx 24rpx;
border-radius: 32rpx;
padding: 24rpx 32rpx;
display: flex;
align-items: center;
gap: 16rpx;
gap: 20rpx;
box-shadow: $shadow-sm;
animation: fadeInUp 0.6s ease-out 0.1s backwards;
}
.notice-tag {
background: $gradient-brand;
color: #fff;
font-size: 20rpx;
padding: 4rpx 12rpx;
border-radius: 8rpx;
font-weight: 700;
box-shadow: 0 2rpx 6rpx rgba($brand-primary, 0.3);
border: 1rpx solid rgba(0,0,0,0.02);
}
.notice-icon { font-size: 32rpx; }
.notice-swiper { flex: 1; height: 36rpx; }
.notice-item {
font-size: 26rpx;
color: $text-main;
line-height: 36rpx;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
font-weight: 500;
font-weight: 600;
}
.notice-arrow {
width: 12rpx;
height: 12rpx;
border-top: 3rpx solid #DDD;
border-right: 3rpx solid #DDD;
transform: rotate(45deg);
}
.notice-more { font-size: 24rpx; color: $text-sub; display: flex; align-items: center; }
.notice-more::after { content: ''; margin-left: 4rpx; font-size: 32rpx; line-height: 24rpx; }
/* 玩法专区 - 方案B2+3 杂志风布局 */
/* 玩法专区 - 极质设计 */
.gameplay-section {
padding: 0 $spacing-lg;
margin-bottom: $spacing-xl;
animation: fadeInUp 0.6s ease-out 0.2s backwards;
position: relative;
z-index: 2;
}
.section-header {
margin-bottom: 20rpx;
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 24rpx;
}
.section-title {
font-size: 36rpx;
font-size: 38rpx;
font-weight: 900;
color: $text-main;
position: relative;
z-index: 1;
padding-left: 12rpx;
font-style: italic;
display: flex;
align-items: center;
}
/* 标题装饰竖线 */
.section-title::before {
content: '';
position: absolute;
left: 0;
top: 50%;
transform: translateY(-50%) skewX(-10deg);
width: 8rpx;
height: 32rpx;
background: $brand-primary;
border-radius: 2rpx;
margin-right: 16rpx;
border-radius: 4rpx;
transform: skewX(-15deg);
}
.gameplay-grid-v2 {
display: flex;
flex-direction: column;
gap: 20rpx;
gap: 24rpx;
}
/* 上排 */
.grid-row-top {
display: flex;
gap: 20rpx;
height: 220rpx; /* 增加高度,展示更多细节 */
gap: 24rpx;
height: 190rpx;
}
.game-card-large {
@ -545,7 +616,7 @@ export default {
border-radius: $radius-lg;
position: relative;
overflow: hidden;
padding: 28rpx;
padding: 22rpx;
box-shadow: $shadow-card;
transition: transform 0.2s;
}
@ -558,7 +629,7 @@ export default {
.grid-row-bottom {
display: flex;
gap: 20rpx;
height: 160rpx;
height: 130rpx;
}
.game-card-small {
@ -566,7 +637,7 @@ export default {
border-radius: $radius-md;
position: relative;
overflow: hidden;
padding: 20rpx;
padding: 16rpx;
display: flex;
flex-direction: column;
justify-content: center;
@ -591,19 +662,19 @@ export default {
}
.card-title-large {
font-size: 40rpx;
font-size: 34rpx;
font-weight: 900;
color: #FFF;
font-style: italic;
margin-bottom: 16rpx;
margin-bottom: 12rpx;
text-shadow: 0 4rpx 8rpx rgba(0,0,0,0.1);
}
.card-tag-large {
font-size: 22rpx;
font-size: 20rpx;
background: rgba(255, 255, 255, 0.9);
color: $text-main;
padding: 6rpx 16rpx;
padding: 4rpx 14rpx;
border-radius: $radius-round;
font-weight: 800;
box-shadow: 0 4rpx 12rpx rgba(0,0,0,0.1);
@ -613,10 +684,10 @@ export default {
.card-mascot-large {
position: absolute;
right: -20rpx;
bottom: -30rpx;
width: 180rpx;
height: 180rpx;
right: -10rpx;
bottom: -20rpx;
width: 140rpx;
height: 140rpx;
transform: rotate(10deg);
filter: drop-shadow(0 8rpx 16rpx rgba(0,0,0,0.2));
}
@ -640,8 +711,8 @@ export default {
position: absolute;
right: -10rpx;
bottom: -10rpx;
width: 100rpx;
height: 100rpx;
width: 80rpx;
height: 80rpx;
opacity: 0.9;
transform: rotate(-10deg);
}
@ -675,7 +746,10 @@ export default {
.activity-section {
padding: 0 $spacing-lg;
animation: fadeInUp 0.6s ease-out 0.3s backwards;
position: relative;
z-index: 2;
}
.activity-grid-list {
display: grid;
grid-template-columns: 1fr 1fr;
@ -684,28 +758,29 @@ export default {
.activity-item {
background: #FFFFFF;
border-radius: $radius-lg;
border-radius: 32rpx;
overflow: hidden;
box-shadow: $shadow-card;
box-shadow: 0 4rpx 20rpx rgba(0,0,0,0.03);
display: flex;
flex-direction: column;
transition: transform 0.2s;
transition: all 0.3s ease;
}
.activity-item:active {
transform: scale(0.98);
transform: translateY(4rpx);
}
.activity-thumb-box {
position: relative;
width: 100%;
padding-bottom: 100%; /* 1:1 正方形 */
padding-bottom: 100%;
}
.activity-thumb {
position: absolute;
top: 0; left: 0;
width: 100%;
height: 100%;
background: #EEE;
}
.activity-tag-hot {
@ -715,11 +790,10 @@ export default {
background: $gradient-brand;
color: #fff;
font-size: 20rpx;
padding: 6rpx 14rpx;
border-radius: 8rpx;
padding: 6rpx 16rpx;
border-radius: 12rpx;
font-weight: 800;
box-shadow: 0 4rpx 10rpx rgba($brand-primary, 0.3);
animation: pulse 2s infinite;
box-shadow: 0 4rpx 12rpx rgba(255,107,0,0.3);
}
.activity-info {
@ -740,6 +814,7 @@ export default {
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2;
line-clamp: 2;
-webkit-box-orient: vertical;
}
@ -751,27 +826,23 @@ export default {
.activity-desc {
font-size: 26rpx;
color: $accent-red; /* 价格/热度颜色 */
color: $accent-red;
font-weight: 800;
}
.activity-btn-go {
background: $text-main;
color: #FFD700;
font-size: 20rpx;
background: #1A1A1A;
color: $accent-gold;
font-size: 22rpx;
font-weight: 900;
padding: 8rpx 24rpx;
padding: 10rpx 28rpx;
border-radius: $radius-round;
box-shadow: 0 4rpx 12rpx rgba(0,0,0,0.2);
box-shadow: 0 6rpx 16rpx rgba(0,0,0,0.1);
}
/* 空状态 */
.activity-empty {
text-align: center;
padding: 80rpx 0;
color: $text-tertiary;
font-size: 28rpx;
}
/* ============================================
🌌 动画与高级动效
============================================ */
@keyframes pulse {
0% { transform: scale(1); opacity: 1; }
@ -780,170 +851,28 @@ export default {
}
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(30rpx);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes scaleIn {
from {
opacity: 0;
transform: scale(0.95);
}
to {
opacity: 1;
transform: scale(1);
}
}
@keyframes shimmer {
0% { background-position: -200% 0; }
100% { background-position: 200% 0; }
from { opacity: 0; transform: translateY(40rpx); }
to { opacity: 1; transform: translateY(0); }
}
@keyframes float {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-8rpx); }
50% { transform: translateY(-20rpx); }
}
/* Banner 入场动画 */
.banner-box {
animation: scaleIn 0.5s ease-out;
}
.banner-container { animation: fadeInUp 0.6s $ease-out; }
.notice-bar-v2 { animation: fadeInUp 0.6s $ease-out 0.15s both; }
.gameplay-section { animation: fadeInUp 0.6s $ease-out 0.3s both; }
.activity-section { animation: fadeInUp 0.6s $ease-out 0.45s both; }
/* 通知栏滑入 */
.notice-bar {
animation: fadeInUp 0.4s ease-out 0.1s both;
}
.brand-star { animation: pulse 2s infinite; }
/* 玩法卡片交错入场 */
.game-card-large:first-child {
animation: fadeInUp 0.4s ease-out 0.2s both;
}
.game-card-large:last-child {
animation: fadeInUp 0.4s ease-out 0.3s both;
}
.game-card-small:nth-child(1) {
animation: fadeInUp 0.4s ease-out 0.35s both;
}
.game-card-small:nth-child(2) {
animation: fadeInUp 0.4s ease-out 0.4s both;
}
.game-card-small:nth-child(3) {
animation: fadeInUp 0.4s ease-out 0.45s both;
}
/* 活动卡片交错入场 */
.activity-item:nth-child(1) { animation: fadeInUp 0.4s ease-out 0.2s both; }
.activity-item:nth-child(2) { animation: fadeInUp 0.4s ease-out 0.25s both; }
.activity-item:nth-child(3) { animation: fadeInUp 0.4s ease-out 0.3s both; }
.activity-item:nth-child(4) { animation: fadeInUp 0.4s ease-out 0.35s both; }
.activity-item:nth-child(n+5) { animation: fadeInUp 0.4s ease-out 0.4s both; }
/* 玻璃态搜索栏 */
.search-bar {
background: rgba(255, 255, 255, 0.85);
backdrop-filter: blur(20rpx);
-webkit-backdrop-filter: blur(20rpx);
border: 1rpx solid rgba(255, 255, 255, 0.6);
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.04);
transition: all 0.3s ease;
}
.search-bar:active {
transform: scale(0.98);
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.08);
}
/* 卡片点击效果 */
.game-card-large,
.game-card-small {
transition: all 0.25s cubic-bezier(0.25, 0.46, 0.45, 0.94);
cursor: pointer;
}
.game-card-large:active {
transform: scale(0.97);
box-shadow: 0 2rpx 8rpx rgba(0,0,0,0.1);
}
.game-card-small:active {
transform: scale(0.95);
box-shadow: 0 2rpx 6rpx rgba(0,0,0,0.08);
}
/* 活动卡片悬浮效果 */
.activity-item {
transition: all 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94);
}
.activity-item:active {
transform: scale(0.97) translateY(4rpx);
box-shadow: 0 4rpx 12rpx rgba(0,0,0,0.08);
}
/* GO 按钮脉冲动画 */
.activity-btn-go {
animation: pulse 2s infinite;
transition: all 0.2s ease;
}
.activity-btn-go:active {
animation: none;
transform: scale(0.9);
}
/* 图片加载动画 */
.activity-thumb {
background: linear-gradient(90deg, #f0f0f0 25%, #e8e8e8 50%, #f0f0f0 75%);
background-size: 200% 100%;
animation: shimmer 1.5s infinite;
}
/* 热门标签浮动效果 */
.activity-tag-hot {
animation: float 3s ease-in-out infinite;
}
/* Banner 图片悬浮效果 */
.banner-image {
transition: transform 0.4s ease;
}
.banner-box:active .banner-image {
transform: scale(1.02);
}
/* 通知栏点击效果 */
.notice-bar {
transition: all 0.2s ease;
}
.notice-bar:active {
background: #F0F0F0;
transform: scale(0.99);
}
/* 品牌标志动画 */
.brand-star {
animation: float 2s ease-in-out infinite;
}
/* Section 标题增强 */
.section-header {
animation: fadeInUp 0.4s ease-out 0.15s both;
}
/* 增强阴影效果 */
.banner-box {
box-shadow: 0 12rpx 40rpx rgba(0,0,0,0.08);
}
.activity-item {
box-shadow: 0 8rpx 32rpx rgba(0,0,0,0.06);
}
/* 热门标签增强 */
.activity-tag-hot {
background: linear-gradient(135deg, rgba(255,77,79,0.9), rgba(255,107,53,0.9));
color: #fff;
text-shadow: 0 1rpx 2rpx rgba(0,0,0,0.2);
/* 兼容性修复 */
.brand-text {
background-clip: text;
}
</style>