966 lines
27 KiB
Vue
Executable File
Raw Blame History

This file contains ambiguous Unicode characters

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

<template>
<view class="page">
<!-- 开屏动画 -->
<SplashScreen />
<!-- 品牌级背景装饰系统 - 统一漂浮光球 -->
<view class="bg-decoration"></view>
<!-- 自定义 tabBar (仅抖音小程序) -->
<!-- #ifdef MP-TOUTIAO -->
<customTabBarToutiao />
<!-- #endif -->
<!-- 顶部导航栏 (搜索) -->
<view class="nav-header">
<!-- 品牌标识已按需移除 -->
</view>
<!-- 滚动区域 -->
<scroll-view class="main-content" scroll-y>
<!-- Banner 区域 (现代级浮动设计) -->
<view class="banner-container">
<swiper :key="'banner-' + swiperKey" class="banner-swiper" :current="bannerIndex" :circular="swiperAutoplay" :autoplay="swiperAutoplay" 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="aspectFit" @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-v2" @tap="onNoticeTap">
<view class="notice-icon">📢</view>
<swiper :key="'notice-' + swiperKey" class="notice-swiper" vertical :circular="swiperAutoplay" :autoplay="swiperAutoplay" 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-arrow"></view>
</view>
<!-- 玩法分类专区 -->
<!-- #ifndef MP-TOUTIAO -->
<view class="gameplay-section">
<view class="section-header">
<text class="section-title">玩法分类</text>
</view>
<view class="gameplay-grid-v2">
<!-- 上排两大核心 -->
<view class="grid-row-top">
<view class="game-card-large card-match" @tap="navigateTo('/pages-activity/activity/list/index?category=对对碰')">
<view class="card-content-large">
<text class="card-title-large">对对碰</text>
<view class="card-tag-large">碰一碰消除</view>
<image class="card-mascot-large" src="https://via.placeholder.com/150/FFB6C1/000000?text=Match" mode="aspectFit" />
</view>
</view>
<view class="game-card-large card-wuxian" @tap="navigateTo('/pages-activity/activity/list/index?category=无限赏')">
<view class="card-content-large">
<text class="card-title-large">无限赏</text>
<view class="card-tag-large yellow">一发入魂</view>
<image class="card-mascot-large" src="https://via.placeholder.com/150/FFD700/000000?text=WU" mode="aspectFit" />
</view>
</view>
</view>
<!-- 下排四个功能 -->
<view class="grid-row-bottom">
<view class="game-card-small card-yifan-small" @tap="navigateTo('/pages-activity/activity/list/index?category=一番赏')">
<text class="card-title-small">一番赏</text>
<text class="card-subtitle-small">欧皇擂台</text>
<image class="card-icon-small" src="https://via.placeholder.com/80/90EE90/000000?text=YI" mode="aspectFit" />
</view>
<view class="game-card-small card-tower" @tap="navigateTo('/pages-game/game/minesweeper/index')">
<text class="card-title-small">扫雷</text>
<text class="card-subtitle-small">福利挑战</text>
<image class="card-icon-small" src="https://via.placeholder.com/80/9370DB/000000?text=Mine" mode="aspectFit" />
</view>
<view class="game-card-small card-welfare" @tap="navigateTo('/pages-activity/activity/welfare/index')">
<text class="card-title-small">福利活动</text>
<text class="card-subtitle-small">日周月福利</text>
<image class="card-icon-small" src="https://via.placeholder.com/80/98FB98/000000?text=Gift" mode="aspectFit" />
</view>
<view class="game-card-small card-threshold" @tap="navigateTo('/pages-activity/activity/threshold/index')">
<text class="card-title-small">裂变活动</text>
<text class="card-subtitle-small">拉新达标参与</text>
<image class="card-icon-small" src="https://via.placeholder.com/80/87CEFA/000000?text=Invite" mode="aspectFit" />
</view>
</view>
</view>
</view>
<!-- #endif -->
<!-- 推荐活动列表 -->
<!-- #ifndef MP-TOUTIAO -->
<view class="activity-section">
<view class="section-header">
<text class="section-title">推荐活动</text>
</view>
<view v-if="activeGroupItems.length" class="activity-grid-list">
<view class="activity-item" v-for="a in activeGroupItems" :key="a.id" @tap="onActivityTap(a)">
<view class="activity-thumb-box">
<image v-if="a.image" class="activity-thumb" :src="a.image" mode="aspectFill" />
<view v-else class="banner-fallback mini">
<text class="banner-fallback-text mini">{{ a.title }}</text>
</view>
<!-- 热门标签 -->
<view class="activity-tag-hot">HOT</view>
</view>
<view class="activity-info">
<text class="activity-name">{{ a.title }}</text>
<view class="activity-row">
<text class="activity-desc" v-if="a.subtitle">{{ a.subtitle }}</text>
<view class="activity-btn-go">GO</view>
</view>
</view>
</view>
</view>
<view v-else class="activity-empty">暂无更多活动</view>
</view>
<!-- #endif -->
<!-- 底部垫高 - 避开TabBar -->
<view style="height: 140rpx"></view>
</scroll-view>
<PrizeClaimPopup
v-model:visible="prizeClaimVisible"
:activity="prizeClaimActivity"
:loading="prizeClaimLoading"
@close="handlePrizeClaimClose"
@claim="handlePrizeClaim"
/>
</view>
</template>
<script>
import { authRequest, request } from '../../utils/request.js'
import { getPendingPrizeGrantActivity, claimPrizeGrantActivity } from '@/api/prizeClaim'
import SplashScreen from '@/components/SplashScreen.vue'
import PrizeClaimPopup from '@/components/activity/PrizeClaimPopup.vue'
// #ifdef MP-TOUTIAO
import customTabBarToutiao from '@/components/app-tab-bar-toutiao.vue'
// #endif
export default {
components: {
SplashScreen,
PrizeClaimPopup
// #ifdef MP-TOUTIAO
, customTabBarToutiao
// #endif
},
data() {
return {
notices: [],
banners: [],
activities: [],
selectedGroupName: '',
bannerIndex: 0,
isHomeLoading: false,
swiperAutoplay: true,
swiperKey: 0,
prizeClaimVisible: false,
prizeClaimLoading: false,
prizeClaimActivity: null,
prizeClaimChecking: false,
prizeClaimLastCheckAt: 0
}
},
computed: {
displayNotices() {
if (Array.isArray(this.notices) && this.notices.length) return this.notices
return [
{ id: 'n1', text: '欢迎光临' },
{ id: 'n2', text: '最新活动敬请期待' }
]
},
displayBanners() {
if (Array.isArray(this.banners) && this.banners.length) return this.banners
return [
{ id: 'ph-1', title: '精彩内容即将上线', image: '' },
{ id: 'ph-2', title: '敬请期待', image: '' },
{ id: 'ph-3', title: '更多活动请关注', image: '' }
]
},
activityGroups() {
const list = Array.isArray(this.activities) ? this.activities : []
const map = new Map()
list.forEach(a => {
const key = (a.category_name || '').trim() || '其他'
if (!map.has(key)) map.set(key, [])
map.get(key).push(a)
})
return Array.from(map.entries()).map(([name, items]) => ({ name, items }))
},
activeGroupItems() {
// Return ALL activities without filtering by group
return Array.isArray(this.activities) ? this.activities : []
}
},
onLoad() {
// #ifdef MP-TOUTIAO
// 抖音平台屏蔽首页,自动跳转到盒柜
uni.switchTab({
url: '/pages/cabinet/index'
})
return
// #endif
// 延迟 200ms 首次加载,让 Token/Session 有机会就绪
// 同时避免页面动画卡顿
setTimeout(() => {
this.loadHomeData()
}, 200)
},
onPullDownRefresh() {
this.loadHomeData(true)
},
onShow() {
this.swiperKey++
this.swiperAutoplay = true
if (this.activities.length === 0 && !this.isHomeLoading) {
this.loadHomeData()
}
this.checkPrizeGrantActivity()
},
onHide() {
this.swiperAutoplay = false
},
onUnload() {
this.swiperAutoplay = false
},
beforeUnmount() {
this.swiperAutoplay = false
},
methods: {
onBannerChange(e) {
this.bannerIndex = e.detail.current
},
onSelectGroup(name) {
this.selectedGroupName = String(name || '')
},
updateSelectedGroup() {
// No-op as we now show all
},
toArray(x) { return Array.isArray(x) ? x : [] },
unwrap(list) {
if (Array.isArray(list)) return list
const obj = list || {}
const arr = obj.list || obj.items || obj.data || []
return Array.isArray(arr) ? arr : []
},
cleanUrl(u) {
const s = String(u || '').trim()
const m = s.match(/https?:\/\/[^\s'"`]+/)
if (m && m[0]) return m[0]
return s.replace(/[`'\"]/g, '').trim()
},
apiGet(url) {
const token = uni.getStorageSync('token')
const fn = token ? authRequest : request
return fn({ url })
},
getPrizeClaimSessionKey() {
const userId = uni.getStorageSync('user_id')
const sessionId = uni.getStorageSync('app_session_id')
if (!userId || !sessionId) return ''
return `prize_claim_closed:${userId}:${sessionId}`
},
async checkPrizeGrantActivity(force = false) {
const token = uni.getStorageSync('token')
if (!token || this.prizeClaimChecking || this.prizeClaimVisible) return
const sessionKey = this.getPrizeClaimSessionKey()
if (!force && sessionKey && uni.getStorageSync(sessionKey)) return
const now = Date.now()
if (!force && now - this.prizeClaimLastCheckAt < 10000) return
this.prizeClaimChecking = true
this.prizeClaimLastCheckAt = now
try {
const res = await getPendingPrizeGrantActivity()
if (res && res.has_pending && res.activity) {
this.prizeClaimActivity = res.activity
this.prizeClaimVisible = true
}
} catch (err) {
console.warn('checkPrizeGrantActivity failed', err)
} finally {
this.prizeClaimChecking = false
}
},
handlePrizeClaimClose() {
const sessionKey = this.getPrizeClaimSessionKey()
if (sessionKey) {
try { uni.setStorageSync(sessionKey, 1) } catch (_) {}
}
this.prizeClaimVisible = false
},
async handlePrizeClaim() {
if (!this.prizeClaimActivity?.id || this.prizeClaimLoading) return
this.prizeClaimLoading = true
try {
await claimPrizeGrantActivity(this.prizeClaimActivity.id)
uni.showToast({ title: '领取成功', icon: 'success' })
this.prizeClaimVisible = false
this.prizeClaimActivity = null
this.checkPrizeGrantActivity(true)
} catch (err) {
uni.showToast({ title: err?.message || '领取失败', icon: 'none' })
} finally {
this.prizeClaimLoading = false
}
},
normalizeNotices(list) {
const arr = this.unwrap(list)
return arr.map((i, idx) => ({
id: i.id ?? String(idx),
text: i.content ?? i.text ?? i.title ?? ''
})).filter(i => i.text)
},
normalizeBanners(list) {
const arr = this.unwrap(list)
const mapped = arr.map((i, idx) => ({
id: i.id ?? String(idx),
title: i.title ?? '',
image: this.cleanUrl(i.imageUrl ?? i.image_url ?? i.image ?? i.img ?? i.pic ?? ''),
link: this.cleanUrl(i.linkUrl ?? i.link_url ?? i.link ?? i.url ?? ''),
sort: typeof i.sort === 'number' ? i.sort : 0
})).filter(i => i.image)
mapped.sort((a, b) => a.sort - b.sort)
return mapped
},
normalizeActivities(list) {
const arr = this.unwrap(list)
const mapped = arr.map((i, idx) => ({
id: i.id ?? String(idx),
image: this.cleanUrl(i.image ?? i.banner ?? i.coverUrl ?? i.cover_url ?? i.img ?? i.pic ?? ''),
title: i.title ?? i.name ?? '',
subtitle: this.buildActivitySubtitle(i),
link: this.cleanUrl(i.linkUrl ?? i.link_url ?? i.link ?? i.url ?? ''),
category_name: (i.category_name ?? i.categoryName ?? '').trim(),
category_id: i.activity_category_id ?? i.category_id ?? i.categoryId ?? null
})).filter(i => i.image || i.title)
return mapped
},
buildActivitySubtitle(i) {
const base = i.subTitle ?? i.sub_title ?? i.subtitle ?? i.desc ?? i.description ?? ''
if (base) return base
const cat = i.category_name ?? i.categoryName ?? ''
const price = (i.price_draw !== undefined && i.price_draw !== null) ? `${(Number(i.price_draw || 0) / 100).toFixed(2)}` : ''
const parts = [cat, price].filter(Boolean)
return parts.join(' · ')
},
async loadHomeData(isRefresh = false) {
if (this.isHomeLoading && !isRefresh) return
this.isHomeLoading = true
// 定义重试函数
const fetchWithRetry = async (url, retries = 3) => {
for (let i = 0; i < retries; i++) {
try {
const res = await this.apiGet(url)
if (res) return res
// 如果返回空,认为是失败,尝试重试
if (i < retries - 1) await new Promise(r => setTimeout(r, 1000))
} catch (e) {
console.error(`Fetch ${url} failed, attempt ${i + 1}`, e)
if (i < retries - 1) await new Promise(r => setTimeout(r, 1000))
}
}
return null
}
// 同时发起请求,大幅提升首屏加载速度
try {
const [nData, bData, acData] = await Promise.all([
fetchWithRetry('/api/app/notices'),
fetchWithRetry('/api/app/banners'),
fetchWithRetry('/api/app/activities')
])
if (nData) this.notices = this.normalizeNotices(nData)
if (bData) this.banners = this.normalizeBanners(bData)
// 只有获取到有效活动数据才更新,保留旧数据兜底
if (acData) {
const validActivities = this.normalizeActivities(acData)
if (validActivities.length > 0) {
this.activities = validActivities
}
}
} catch (e) {
console.error('Home data load failed', e)
if (isRefresh) {
uni.showToast({ title: '刷新失败,请稍后重试', icon: 'none' })
}
} finally {
this.isHomeLoading = false
if (isRefresh) {
uni.stopPullDownRefresh()
uni.showToast({ title: '刷新成功', icon: 'none' })
}
}
},
onBannerTap(b) {
const imgs = (Array.isArray(this.banners) ? this.banners : []).map(x => x.image).filter(Boolean)
const current = b && b.image
if (current) {
uni.previewImage({ urls: imgs.length ? imgs : [current], current })
return
}
if (b.link && /^\/.+/.test(b.link)) {
uni.navigateTo({ url: b.link })
}
},
onActivityTap(a) {
const name = (a.category_name || a.categoryName || '').trim()
const id = a.id
let path = ''
if (name.includes('一番赏')) path = '/pages-activity/activity/yifanshang/index'
else if (name.includes('无限赏')) path = '/pages-activity/activity/wuxianshang/index'
else if (name.includes('对对碰')) path = '/pages-activity/activity/duiduipeng/index'
else if (name.includes('爬塔')) path = '/pages-activity/activity/pata/index'
if (path && id) {
uni.navigateTo({ url: `${path}?id=${id}` })
return
}
if (a.link && /^\/.+/.test(a.link)) {
uni.navigateTo({ url: a.link })
}
},
navigateTo(url) {
if(url === '#') return
uni.navigateTo({ url })
},
async openLeaderboard() {
uni.navigateTo({ url: '/pages-game/game/minesweeper/index?from=home&focus=leaderboard' })
},
onNoticeTap() {
const content = this.displayNotices.map(n => n.text).join('\n')
uni.showModal({
title: '系统通知',
content: content || '暂无通知',
showCancel: false,
confirmText: '知道了'
})
}
},
// 分享给好友
onShareAppMessage() {
const inviteCode = uni.getStorageSync('invite_code') || (uni.getStorageSync('user_info') || {}).invite_code || ''
return {
title: '柯大鸭 - 开箱惊喜等你来',
path: `/pages/index/index?invite_code=${inviteCode}`,
imageUrl: '/static/logo.png'
}
},
// 分享到朋友圈
onShareTimeline() {
const inviteCode = uni.getStorageSync('invite_code') || (uni.getStorageSync('user_info') || {}).invite_code || ''
return {
title: '柯大鸭 - 开箱惊喜等你来',
query: `invite_code=${inviteCode}`,
imageUrl: '/static/logo.png'
}
}
}
</script>
<style lang="scss">
/* ============================================
柯大鸭 - 首页样式 (V7.0 Clean Modern Refresh)
============================================ */
.page {
padding: 0;
background: linear-gradient(180deg, #f8fafc 0%, #f3f6fb 55%, #eef3f8 100%);
min-height: 100vh;
display: flex;
flex-direction: column;
position: relative;
overflow: hidden;
}
/* ========== 顶部导航栏 ========== */
.nav-header {
height: env(safe-area-inset-top);
z-index: 10;
position: sticky;
top: 0;
background: rgba(248, 250, 252, 0.78);
backdrop-filter: blur(14rpx);
}
.main-content {
flex: 1;
position: relative;
z-index: 1;
}
/* Banner - 更轻、更干净 */
.banner-container {
padding: 12rpx $spacing-lg 28rpx;
position: relative;
z-index: 2;
}
.banner-swiper {
height: 320rpx;
overflow: visible;
}
.banner-card {
height: 100%;
margin: 0;
border-radius: 32rpx;
overflow: hidden;
position: relative;
transform: scale(0.985);
transition: all 0.35s $ease-out;
box-shadow:
0 14rpx 36rpx rgba(15, 23, 42, 0.08),
0 2rpx 10rpx rgba(15, 23, 42, 0.04);
border: 1px solid rgba(255, 255, 255, 0.72);
background: rgba(255, 255, 255, 0.72);
}
.banner-card.active {
transform: scale(1);
box-shadow:
0 18rpx 44rpx rgba(15, 23, 42, 0.10),
0 4rpx 14rpx rgba(255, 107, 0, 0.08);
}
.banner-image {
width: 100%;
height: 100%;
display: block;
}
.banner-fallback {
width: 100%;
height: 100%;
background: linear-gradient(135deg, #ffb36b 0%, #ff8f6b 45%, #ff735d 100%);
display: flex;
flex-direction: column;
align-items: flex-start;
justify-content: flex-end;
padding: 36rpx 32rpx;
position: relative;
}
.fallback-glow {
position: absolute;
inset: 0;
background: radial-gradient(circle at top right, rgba(255,255,255,0.24) 0%, transparent 55%);
}
.banner-fallback-text {
font-size: 42rpx;
font-weight: 900;
color: #fff;
margin-bottom: 10rpx;
letter-spacing: 1rpx;
z-index: 1;
}
.banner-tag {
background: rgba(255,255,255,0.16);
color: rgba(255,255,255,0.92);
padding: 8rpx 20rpx;
border-radius: $radius-round;
font-size: 20rpx;
font-weight: 700;
backdrop-filter: blur(6px);
z-index: 1;
}
.banner-indicator {
display: flex;
justify-content: center;
gap: 10rpx;
margin-top: 16rpx;
}
.indicator-dot {
width: 14rpx;
height: 8rpx;
background: rgba(148, 163, 184, 0.35);
border-radius: 999rpx;
transition: all 0.25s ease;
}
.indicator-dot.active {
width: 34rpx;
background: $brand-primary;
}
/* Notice Bar */
.notice-bar-v2 {
margin: 0 $spacing-lg 28rpx;
background: rgba(255,255,255,0.78);
border-radius: 28rpx;
padding: 20rpx 24rpx;
display: flex;
align-items: center;
gap: 16rpx;
box-shadow: 0 10rpx 28rpx rgba(15, 23, 42, 0.05);
border: 1px solid rgba(255,255,255,0.85);
}
.notice-icon { font-size: 28rpx; }
.notice-swiper { flex: 1; height: 34rpx; }
.notice-item {
font-size: 24rpx;
color: #334155;
line-height: 34rpx;
font-weight: 600;
}
.notice-arrow {
width: 10rpx;
height: 10rpx;
border-top: 2rpx solid #cbd5e1;
border-right: 2rpx solid #cbd5e1;
transform: rotate(45deg);
}
/* 玩法专区 */
.gameplay-section {
padding: 0 $spacing-lg;
margin-bottom: 36rpx;
position: relative;
z-index: 2;
}
.section-header {
margin-bottom: 20rpx;
}
.section-title {
font-size: 34rpx;
font-weight: 900;
color: #0f172a;
display: flex;
align-items: center;
letter-spacing: 0.5rpx;
}
.section-title::before {
content: '';
width: 8rpx;
height: 28rpx;
background: linear-gradient(180deg, $brand-primary, $brand-secondary);
margin-right: 14rpx;
border-radius: 999rpx;
}
.gameplay-grid-v2 {
display: flex;
flex-direction: column;
gap: 18rpx;
}
.grid-row-top {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 18rpx;
min-height: 180rpx;
}
.game-card-large {
border-radius: 28rpx;
position: relative;
overflow: hidden;
padding: 24rpx;
transition: transform 0.2s ease;
box-shadow: 0 14rpx 34rpx rgba(15, 23, 42, 0.08);
border: 1px solid rgba(255,255,255,0.82);
}
.game-card-large:active {
transform: scale(0.98);
}
.grid-row-bottom {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 14rpx;
min-height: 122rpx;
}
.game-card-small {
min-width: 0;
border-radius: 24rpx;
position: relative;
overflow: hidden;
padding: 18rpx 14rpx;
display: flex;
flex-direction: column;
justify-content: flex-start;
transition: all 0.2s ease;
box-shadow: 0 10rpx 24rpx rgba(15, 23, 42, 0.06);
border: 1px solid rgba(255,255,255,0.85);
}
.game-card-small:active {
transform: translateY(2rpx) scale(0.98);
}
.card-content-large {
position: relative;
z-index: 2;
height: 100%;
display: flex;
flex-direction: column;
justify-content: space-between;
align-items: flex-start;
}
.card-title-large {
font-size: 34rpx;
font-weight: 900;
color: #fff;
margin-bottom: 8rpx;
line-height: 1.15;
}
.card-tag-large {
font-size: 20rpx;
background: rgba(255, 255, 255, 0.92);
color: $text-main;
padding: 6rpx 16rpx;
border-radius: $radius-round;
font-weight: 800;
}
.card-tag-large.yellow { color: #b45309; }
.card-mascot-large {
position: absolute;
right: -4rpx;
bottom: -10rpx;
width: 132rpx;
height: 132rpx;
transform: rotate(8deg);
filter: drop-shadow(0 8rpx 14rpx rgba(0,0,0,0.15));
}
.card-title-small {
font-size: 26rpx;
font-weight: 800;
color: $text-main;
margin-bottom: 6rpx;
z-index: 2;
line-height: 1.18;
}
.card-subtitle-small {
font-size: 20rpx;
color: rgba(15, 23, 42, 0.68);
z-index: 2;
line-height: 1.2;
}
.card-icon-small {
position: absolute;
right: -6rpx;
bottom: -6rpx;
width: 68rpx;
height: 68rpx;
opacity: 0.9;
transform: rotate(-6deg);
}
.card-yifan {
background: linear-gradient(135deg, #ff8a5c 0%, #ff6b4a 100%);
}
.card-wuxian {
background: linear-gradient(135deg, #ffcf6d 0%, #ff9f43 100%);
}
.card-yifan-small {
background: linear-gradient(135deg, #ff9a6e 0%, #ff7a50 100%);
}
.card-yifan-small .card-title-small { color: #fff; }
.card-yifan-small .card-subtitle-small { color: rgba(255,255,255,0.84); }
.card-match {
background: linear-gradient(135deg, #ff9fba 0%, #ffb8d4 100%);
}
.card-match .card-title-large { color: #fff; }
.card-match .card-tag-large { color: #db2777; }
.card-tower {
background: linear-gradient(135deg, #ede9fe 0%, #ddd6fe 100%);
}
.card-tower .card-title-small { color: #5b21b6; }
.card-tower .card-subtitle-small { color: #6d28d9; }
.card-welfare {
background: linear-gradient(135deg, #dcfce7 0%, #bbf7d0 100%);
}
.card-welfare .card-title-small { color: #047857; }
.card-welfare .card-subtitle-small { color: #059669; }
.card-threshold {
background: linear-gradient(135deg, #dbeafe 0%, #c7d2fe 100%);
}
.card-threshold .card-title-small { color: #1d4ed8; }
.card-threshold .card-subtitle-small { color: #4338ca; }
.card-threshold .card-icon-small {
opacity: 0.96;
transform: rotate(-4deg);
}
.activity-section {
padding: 0 $spacing-lg 8rpx;
animation: fadeInUp 0.6s ease-out 0.3s backwards;
position: relative;
z-index: 2;
}
.activity-grid-list {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 18rpx;
}
.activity-item {
background: rgba(255,255,255,0.82);
border-radius: 28rpx;
overflow: hidden;
display: flex;
flex-direction: column;
transition: all 0.25s ease;
box-shadow: 0 12rpx 30rpx rgba(15, 23, 42, 0.06);
border: 1px solid rgba(255,255,255,0.88);
}
.activity-item:active {
transform: translateY(2rpx) scale(0.98);
}
.activity-thumb-box {
position: relative;
width: 100%;
padding-bottom: 96%;
}
.activity-thumb {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
.activity-tag-hot {
position: absolute;
top: 16rpx;
left: 16rpx;
background: rgba(255,255,255,0.86);
color: $brand-primary;
font-size: 18rpx;
padding: 6rpx 14rpx;
border-radius: 999rpx;
font-weight: 800;
}
.activity-info {
padding: 22rpx;
flex: 1;
display: flex;
flex-direction: column;
justify-content: space-between;
gap: 14rpx;
}
.activity-name {
font-size: 28rpx;
font-weight: 800;
color: #0f172a;
line-height: 1.4;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2;
line-clamp: 2;
-webkit-box-orient: vertical;
}
.activity-row {
display: flex;
justify-content: space-between;
align-items: center;
gap: 16rpx;
}
.activity-desc {
flex: 1;
min-width: 0;
font-size: 22rpx;
color: #64748b;
font-weight: 600;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.activity-btn-go {
background: rgba(255, 107, 0, 0.10);
color: $brand-primary;
font-size: 20rpx;
font-weight: 800;
padding: 10rpx 22rpx;
border-radius: 999rpx;
}
@keyframes pulse {
0% { transform: scale(1); opacity: 1; }
50% { transform: scale(1.04); opacity: 0.92; }
100% { transform: scale(1); opacity: 1; }
}
@keyframes fadeInUp {
from { opacity: 0; transform: translateY(32rpx); }
to { opacity: 1; transform: translateY(0); }
}
@keyframes float {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-16rpx); }
}
.banner-container { animation: fadeInUp 0.6s $ease-out; }
.notice-bar-v2 { animation: fadeInUp 0.6s $ease-out 0.12s both; }
.gameplay-section { animation: fadeInUp 0.6s $ease-out 0.24s both; }
.activity-section { animation: fadeInUp 0.6s $ease-out 0.36s both; }
.activity-btn-go:active {
transform: scale(0.96);
}
.brand-text {
background-clip: text;
}
</style>