feat: 新增裂变活动小程序页面与首页调整

This commit is contained in:
Zuncle 2026-06-09 21:45:17 +08:00
parent ece6ce9740
commit 4fb2536c64
8 changed files with 1035 additions and 252 deletions

25
api/thresholdActivity.js Normal file
View File

@ -0,0 +1,25 @@
import { request, authRequest } from '../utils/request'
export function listThresholdActivities(params = {}) {
return request({ url: '/api/app/threshold-activities', method: 'GET', data: params })
}
export function getThresholdActivity(id) {
return request({ url: `/api/app/threshold-activities/${id}`, method: 'GET' })
}
export function getMyThresholdActivity(id) {
return authRequest({ url: `/api/app/threshold-activities/${id}/my`, method: 'GET', suppressAuthModal: true })
}
export function joinThresholdActivity(id) {
return authRequest({ url: `/api/app/threshold-activities/${id}/join`, method: 'POST' })
}
export function listThresholdParticipants(id, page = 1, page_size = 20) {
return request({ url: `/api/app/threshold-activities/${id}/participants`, method: 'GET', data: { page, page_size } })
}
export function listThresholdWinners(id, page = 1, page_size = 100) {
return request({ url: `/api/app/threshold-activities/${id}/winners`, method: 'GET', data: { page, page_size } })
}

View File

@ -0,0 +1,586 @@
<template>
<view class="threshold-detail-page" v-if="detail">
<image v-if="detail.cover_image" class="detail-cover" :src="detail.cover_image" mode="aspectFill" />
<view v-else class="detail-cover empty">暂无活动图片</view>
<view class="detail-panel hero-panel">
<view class="hero-head">
<text class="detail-title">{{ detail.title }}</text>
<text class="detail-type" :class="typeClass(detail.type)">{{ typeLabel(detail.type) }}</text>
</view>
<text class="detail-status" :class="statusClass(detail.status)">{{ statusText(detail.status) }}</text>
<text class="detail-time">开奖时间{{ formatTime(detail.draw_time) }}</text>
<text class="detail-desc" v-if="detail.description">{{ detail.description }}</text>
</view>
<view class="detail-panel qualification-panel">
<view class="panel-head">
<text class="panel-title">我的参与资格</text>
<text class="panel-side">{{ qualificationModeLabel(detail.qualification_mode) }}</text>
</view>
<view class="share-guide-card" v-if="showInviteGuide">
<view class="share-guide-head">
<text class="share-guide-title">邀请好友一起冲开奖</text>
<text class="share-guide-badge">裂变加速</text>
</view>
<text class="share-guide-desc">当前还差 {{ crowdGapCount }} 人达到开奖标准分享活动给好友可更快凑齐人数</text>
<view class="share-guide-actions">
<button class="share-action-btn primary" open-type="share">立即邀请好友</button>
<view class="share-action-btn secondary" @tap="copyInviteTip">复制邀请口令</view>
<view class="share-action-btn secondary" @tap="goMyInvites">我的邀请记录</view>
</view>
</view>
<view class="metric-card">
<view class="metric-head">
<text class="metric-title">消费进度</text>
<text class="metric-state" :class="qualification?.spend_qualified ? 'ok' : 'pending'">
{{ qualification?.spend_qualified ? '已达标' : '未达标' }}
</text>
</view>
<text class="metric-value">{{ money(qualification?.current_paid || 0) }} / {{ money(detail.spend_threshold_amount) }}</text>
<view class="progress-bar">
<view class="progress-fill spend" :style="{ width: spendProgressPercent + '%' }"></view>
</view>
</view>
<view class="metric-card">
<view class="metric-head">
<text class="metric-title">邀请进度</text>
<text class="metric-state" :class="qualification?.invite_qualified ? 'ok' : 'pending'">
{{ qualification?.invite_qualified ? '已达标' : '未达标' }}
</text>
</view>
<text class="metric-value">{{ qualification?.effective_invite_count || 0 }} / {{ detail.invite_threshold_count || 0 }} </text>
<view class="progress-bar">
<view class="progress-fill invite" :style="{ width: inviteProgressPercent + '%' }"></view>
</view>
<text class="metric-help" v-if="detail.invite_effective_amount > 0">每位活动开始后新邀请的用户消费满 {{ money(detail.invite_effective_amount) }} 才算 1 位有效邀请</text>
</view>
<view class="qualification-summary">
<text class="qualification-text">{{ qualificationSummary }}</text>
<view class="join-btn" :class="joinButtonClass" @tap="handleJoin">{{ joinButtonText }}</view>
</view>
</view>
<view class="detail-panel crowd-panel">
<view class="panel-head compact">
<text class="panel-title">开奖进度</text>
<text class="panel-side">{{ detail.participant_count || 0 }} / {{ detail.min_participants || 0 }} </text>
</view>
<view class="crowd-progress-bar">
<view class="crowd-progress-fill" :style="{ width: crowdProgressPercent + '%' }"></view>
</view>
<view class="crowd-summary">
<text class="crowd-summary-main">{{ crowdSummary }}</text>
<text class="crowd-summary-sub">{{ crowdHint }}</text>
</view>
</view>
<view class="detail-panel">
<view class="panel-head">
<text class="panel-title">奖品</text>
<text class="panel-side">参与人数 {{ detail.participant_count || 0 }} / 最低开奖 {{ detail.min_participants || 0 }}</text>
</view>
<scroll-view v-if="sortedPrizes.length" scroll-x class="prize-scroll" show-scrollbar="false">
<view class="prize-track">
<view v-for="prize in sortedPrizes" :key="prize.id" class="prize-item">
<image v-if="prize.image" class="prize-image" :src="prize.image" mode="aspectFill" />
<view v-else class="prize-image empty">{{ prizePlaceholderText(prize) }}</view>
<text class="prize-name">{{ prize.name }}</text>
<text class="prize-price">参考价 ¥{{ formatAmount(prizePrice(prize)) }}</text>
<text class="prize-count">x{{ prize.quantity }}</text>
</view>
</view>
</scroll-view>
<view v-else class="empty-text">暂无奖品</view>
</view>
<view class="detail-panel">
<view class="panel-head participant-head">
<text class="panel-title">参与玩家</text>
<view class="participant-head-actions">
<view class="head-action-btn overview" @tap="openWinnerOverview">中奖概览</view>
<view v-if="showMoreParticipants" class="head-action-btn" @tap="openParticipantsPopup">查看更多</view>
</view>
</view>
<view class="participant-meta-row">
<text class="panel-side"> {{ participantCount }} </text>
<text class="panel-side emphasis">{{ crowdGapText }}</text>
</view>
<view class="avatar-row" v-if="previewParticipants.length">
<view v-for="player in previewParticipants" :key="player.user_id" class="avatar-item" :class="{ mine: isSelfParticipant(player) }">
<image class="participant-avatar" :src="player.avatar || '/static/logo.png'" mode="aspectFill" />
</view>
</view>
<view v-else class="empty-text">暂无参与玩家</view>
</view>
<view class="detail-panel history-entry" @tap="goHistory">
<view>
<text class="panel-title">查看往期裂变活动</text>
<text class="history-subtitle">浏览历史开奖与流产活动</text>
</view>
</view>
<view v-if="winnerPopupVisible" class="winner-popup-overlay" @touchmove.stop.prevent>
<view class="winner-popup-mask" @tap="winnerPopupVisible = false"></view>
<view class="winner-popup-panel" @tap.stop>
<view class="winner-popup-head">
<text class="winner-popup-title">当前活动中奖概览</text>
<text class="winner-popup-close" @tap="winnerPopupVisible = false">×</text>
</view>
<scroll-view scroll-y class="winner-popup-list">
<view v-if="winnerOverviewList.length">
<view v-for="item in winnerOverviewList" :key="item.id" class="winner-popup-item">
<image v-if="item.prize_image" class="winner-popup-image" :src="item.prize_image" mode="aspectFill" />
<view v-else class="winner-popup-image empty">{{ rewardPlaceholderText(item) }}</view>
<view class="winner-popup-info">
<text class="winner-popup-name">{{ item.nickname || ('用户' + item.user_id) }} · {{ item.prize_name }}</text>
<text class="winner-popup-price">参考价 ¥{{ formatAmount(winnerPrice(item)) }}</text>
<text class="winner-popup-time">{{ formatTime(item.created_at) }}</text>
</view>
</view>
</view>
<view v-else class="empty-text">还未开奖</view>
</scroll-view>
</view>
</view>
<view v-if="participantsPopupVisible" class="winner-popup-overlay" @touchmove.stop.prevent>
<view class="winner-popup-mask" @tap="participantsPopupVisible = false"></view>
<view class="winner-popup-panel" @tap.stop>
<view class="winner-popup-head">
<text class="winner-popup-title">全部参与玩家</text>
<text class="winner-popup-close" @tap="participantsPopupVisible = false">×</text>
</view>
<view class="winner-popup-meta">
<text class="winner-popup-meta-main">当前 {{ participantCount }} / {{ detail.min_participants || 0 }} </text>
<text class="winner-popup-meta-sub">{{ crowdGapText }}</text>
</view>
<scroll-view scroll-y class="winner-popup-list">
<view v-if="allParticipants.length">
<view v-for="player in allParticipants" :key="player.user_id" class="winner-popup-item participant-popup-item">
<image class="winner-popup-image participant-popup-avatar" :src="player.avatar || '/static/logo.png'" mode="aspectFill" />
<view class="winner-popup-info">
<text class="winner-popup-name">{{ player.nickname || ('用户' + player.user_id) }}</text>
<text class="winner-popup-time" :class="{ 'self-tag': isSelfParticipant(player) }">{{ isSelfParticipant(player) ? '我已参与' : '活动参与玩家' }}</text>
</view>
</view>
</view>
<view v-else class="empty-text">暂无参与玩家</view>
</scroll-view>
</view>
</view>
</view>
</template>
<script>
import {
getMyThresholdActivity,
getThresholdActivity,
joinThresholdActivity,
listThresholdParticipants,
listThresholdWinners
} from '@/api/thresholdActivity'
export default {
data() {
return {
detail: null,
winners: [],
participants: [],
participantsPage: 1,
participantsPageSize: 20,
participantsLoading: false,
winnerPopupVisible: false,
participantsPopupVisible: false,
currentUserId: 0
}
},
computed: {
qualification() {
return this.detail?.qualification_progress || {}
},
sortedPrizes() {
const prizes = Array.isArray(this.detail?.prizes) ? [...this.detail.prizes] : []
return prizes.sort((a, b) => {
const aPrice = this.prizePrice(a)
const bPrice = this.prizePrice(b)
if (aPrice !== bPrice) return bPrice - aPrice
return Number(a.sort || 0) - Number(b.sort || 0)
})
},
spendProgressPercent() {
const current = Number(this.qualification?.current_paid || 0)
const target = Number(this.detail?.spend_threshold_amount || 0)
if (target <= 0) return 0
return Math.max(0, Math.min(100, Math.round((current / target) * 100)))
},
inviteProgressPercent() {
const current = Number(this.qualification?.effective_invite_count || 0)
const target = Number(this.detail?.invite_threshold_count || 0)
if (target <= 0) return 0
return Math.max(0, Math.min(100, Math.round((current / target) * 100)))
},
crowdProgressPercent() {
const current = Number(this.detail?.participant_count || 0)
const target = Number(this.detail?.min_participants || 0)
if (target <= 0) return 0
return Math.max(0, Math.min(100, Math.round((current / target) * 100)))
},
crowdGapText() {
const target = Number(this.detail?.min_participants || 0)
const current = Number(this.detail?.participant_count || 0)
const gap = Math.max(0, target - current)
if (this.detail?.status === 'aborted') return '本期参与人数未达标,活动已流产'
if (this.detail?.status === 'finished') return '本期已结束'
if (gap <= 0) return '已满足开奖人数条件'
return `还差 ${gap} 人达到开奖标准`
},
crowdGapCount() {
return Math.max(0, Number(this.detail?.min_participants || 0) - Number(this.detail?.participant_count || 0))
},
showInviteGuide() {
return this.detail?.status === 'active' && this.crowdGapCount > 0
},
crowdSummary() {
const current = Number(this.detail?.participant_count || 0)
const target = Number(this.detail?.min_participants || 0)
return `当前已参与 ${current} 人 / 开奖至少需要 ${target}`
},
crowdHint() {
const gap = Math.max(0, Number(this.detail?.min_participants || 0) - Number(this.detail?.participant_count || 0))
if (this.detail?.status === 'aborted') return '本期人数不足,未开奖'
if (this.detail?.status === 'finished') return '本期已开奖结束'
if (gap <= 0) return '已满足开奖人数条件,等待开奖'
return `还差 ${gap} 人即可开奖`
},
qualificationSummary() {
if (this.detail?.joined) return '您已成功参加本期活动'
if (this.detail?.status === 'aborted') return '本期人数不足,活动已流产'
if (this.detail?.status === 'finished') return '活动已开奖结束'
if (this.detail?.can_join) return '已达到参与门槛,可立即报名'
return this.buildPendingHint()
},
joinButtonText() {
if (this.detail?.joined) return '已参加'
if (this.detail?.status === 'aborted') return '已流产'
if (this.detail?.status === 'finished') return '已结束'
const startTime = this.detail?.start_time ? new Date(this.detail.start_time).getTime() : 0
if (startTime && startTime > Date.now()) return '未开始'
if (this.detail?.can_join) return '参加活动'
return '未达门槛'
},
joinButtonClass() {
if (this.detail?.can_join && !this.detail?.joined) return 'primary'
if (this.detail?.joined) return 'joined'
if (this.detail?.status === 'aborted') return 'aborted'
return 'muted'
},
participantCount() {
return Number(this.detail?.participant_count || this.participants.length || 0)
},
previewParticipants() {
return this.sortedParticipants.slice(0, 6)
},
sortedParticipants() {
const list = Array.isArray(this.participants) ? [...this.participants] : []
if (!this.currentUserId) return list
return list.sort((a, b) => {
const aSelf = Number(a?.user_id || 0) === this.currentUserId ? 1 : 0
const bSelf = Number(b?.user_id || 0) === this.currentUserId ? 1 : 0
return bSelf - aSelf
})
},
showMoreParticipants() {
return this.participantCount > this.previewParticipants.length
},
allParticipants() {
return this.sortedParticipants
},
winnerOverviewList() {
return Array.isArray(this.winners) ? this.winners : []
}
},
onLoad(options) {
this.id = Number(options?.id || 0)
const inviteCode = options?.invite_code || options?.inviteCode || ''
if (inviteCode) {
try { uni.setStorageSync('inviter_code', inviteCode) } catch (_) {}
}
this.loadData()
},
onShareAppMessage() {
const inviteCode = uni.getStorageSync('invite_code') || (uni.getStorageSync('user_info') || {}).invite_code || ''
const title = this?.detail?.title || '拉新裂变活动'
const crowd = Number(this?.detail?.participant_count || 0)
const min = Number(this?.detail?.min_participants || 0)
const gap = Math.max(0, min - crowd)
return {
title: gap > 0 ? `${title}|还差 ${gap} 人开奖,快来一起冲!` : `${title}|已达开奖人数,快来参加!`,
path: `/pages-user/invite/landing?invite_code=${inviteCode}`,
imageUrl: this?.detail?.cover_image || '/static/logo.png'
}
},
onShareTimeline() {
const inviteCode = uni.getStorageSync('invite_code') || (uni.getStorageSync('user_info') || {}).invite_code || ''
const title = this?.detail?.title || '拉新裂变活动'
return {
title,
query: `invite_code=${inviteCode}`,
imageUrl: this?.detail?.cover_image || '/static/logo.png'
}
},
methods: {
async loadData() {
if (!this.id) return
try {
try {
this.detail = await getMyThresholdActivity(this.id)
} catch (_) {
this.detail = await getThresholdActivity(this.id)
}
if (!this.detail.cover_image && this.detail.prizes && this.detail.prizes.length) {
this.detail.cover_image = this.detail.prizes.find((item) => item.image)?.image || ''
}
this.participants = Array.isArray(this.detail?.participants) ? this.detail.participants : []
this.currentUserId = Number(uni.getStorageSync('user_id') || 0)
this.participantsPage = 1
const winnersRes = await listThresholdWinners(this.id, 1, 100)
this.winners = winnersRes?.list || []
} catch (e) {
uni.showToast({ title: e.message || '加载失败', icon: 'none' })
}
},
async handleJoin() {
if (this.detail?.joined || !this.detail?.can_join) return
try {
await joinThresholdActivity(this.id)
uni.showToast({ title: '参加成功', icon: 'success' })
await this.loadData()
} catch (e) {
uni.showToast({ title: e.message || '参加失败', icon: 'none' })
}
},
goMyInvites() {
uni.navigateTo({ url: '/pages-user/invites/index' })
},
copyInviteTip() {
const title = this.detail?.title || '裂变活动'
const inviteCode = uni.getStorageSync('invite_code') || (uni.getStorageSync('user_info') || {}).invite_code || ''
const text = inviteCode ? `一起参加${title},用我的邀请码 ${inviteCode} 登录后更快凑齐开奖人数!` : `一起参加${title},快来帮我凑齐开奖人数!`
uni.setClipboardData({
data: text,
success: () => {
uni.showToast({ title: '邀请口令已复制', icon: 'success' })
},
fail: () => {
uni.showToast({ title: '复制失败,请手动分享', icon: 'none' })
}
})
},
async loadMoreParticipants() {
if (this.participantsLoading) return
if (this.participants.length >= this.participantCount) return
this.participantsLoading = true
try {
const nextPage = this.participantsPage + 1
const res = await listThresholdParticipants(this.id, nextPage, this.participantsPageSize)
const list = Array.isArray(res?.list) ? res.list : []
const seen = new Set(this.participants.map(item => String(item.user_id)))
list.forEach(item => {
const key = String(item.user_id)
if (!seen.has(key)) {
seen.add(key)
this.participants.push(item)
}
})
this.participantsPage = nextPage
} catch (e) {
uni.showToast({ title: e.message || '加载参与玩家失败', icon: 'none' })
} finally {
this.participantsLoading = false
}
},
async openParticipantsPopup() {
this.participantsPopupVisible = true
await this.loadAllParticipants()
},
async loadAllParticipants() {
if (this.participantsLoading) return
while (this.participants.length < this.participantCount) {
const prevLength = this.participants.length
await this.loadMoreParticipants()
if (this.participants.length === prevLength) break
}
},
openWinnerOverview() {
this.winnerPopupVisible = true
},
goHistory() {
uni.navigateTo({ url: '/pages-activity/activity/threshold/index?mode=history' })
},
buildPendingHint() {
const qualificationMode = this.detail?.qualification_mode
const spendTarget = Number(this.detail?.spend_threshold_amount || 0)
const spendCurrent = Number(this.qualification?.current_paid || 0)
const inviteTarget = Number(this.detail?.invite_threshold_count || 0)
const inviteCurrent = Number(this.qualification?.effective_invite_count || 0)
if (qualificationMode === 'spend_only') {
return `消费还差 ${this.money(Math.max(0, spendTarget - spendCurrent))} 即可参加`
}
if (qualificationMode === 'invite_only') {
return `有效邀请还差 ${Math.max(0, inviteTarget - inviteCurrent)} 人即可参加`
}
const parts = []
if (spendTarget > spendCurrent) parts.push(`消费还差 ${this.money(spendTarget - spendCurrent)}`)
if (inviteTarget > inviteCurrent) parts.push(`有效邀请还差 ${inviteTarget - inviteCurrent}`)
return parts.length ? `${parts.join('')} 即可参加` : '当前未满足参加条件'
},
statusText(status) {
return { active: '进行中', finished: '已结束', aborted: '已流产' }[status] || status || '-'
},
statusClass(status) {
return { active: 'status-active', finished: 'status-finished', aborted: 'status-aborted' }[status] || 'status-finished'
},
typeLabel(type) {
return { daily: '每日裂变', weekly: '每周裂变', monthly: '每月裂变' }[type] || '裂变活动'
},
typeClass(type) {
return { daily: 'type-daily', weekly: 'type-weekly', monthly: 'type-monthly' }[type] || 'type-default'
},
qualificationModeLabel(mode) {
return { spend_only: '消费达标', invite_only: '邀请达标', either: '任一达标' }[mode] || '门槛活动'
},
formatTime(v) {
if (!v) return '-'
return String(v).replace('T', ' ').slice(0, 16)
},
formatAmount(cents) {
return (Number(cents || 0) / 100).toFixed(2)
},
money(v) {
return `¥${(Number(v || 0) / 100).toFixed(2)}`
},
prizePrice(prize) {
return Number(prize?.price_cents ?? prize?.price ?? prize?.product_price ?? prize?.price_snapshot_cents ?? 0)
},
winnerPrice(item) {
return Number(item?.price_cents ?? item?.price ?? item?.product_price ?? item?.price_snapshot_cents ?? 0)
},
rewardPlaceholderText(item) {
const type = String(item?.reward_type || '').toLowerCase()
if (type === 'coupon') return '优惠券'
if (type === 'item_card') return '道具卡'
return '奖品'
},
prizePlaceholderText(prize) {
return this.rewardPlaceholderText(prize)
},
isSelfParticipant(player) {
return Number(player?.user_id || 0) > 0 && Number(player?.user_id || 0) === this.currentUserId
}
}
}
</script>
<style lang="scss">
.threshold-detail-page { min-height: 100vh; padding-bottom: 40rpx; background: #fff7ed; }
.detail-cover { width: 100%; height: 420rpx; display: block; background: #f3f4f6; }
.detail-cover.empty { display: flex; align-items: center; justify-content: center; color: #9ca3af; }
.detail-panel { margin: 24rpx 28rpx 0; padding: 28rpx; border-radius: 28rpx; background: #fff; box-shadow: 0 12rpx 30rpx rgba(0,0,0,.06); }
.hero-head { display: flex; align-items: center; justify-content: space-between; gap: 20rpx; }
.detail-title { display: block; font-size: 38rpx; font-weight: 900; color: #1f2937; flex: 1; }
.detail-type { display: inline-flex; align-items: center; padding: 10rpx 20rpx; border-radius: 999rpx; font-size: 22rpx; font-weight: 800; }
.type-daily { background: rgba(249, 115, 22, .12); color: #f97316; }
.type-weekly { background: rgba(239, 68, 68, .12); color: #ef4444; }
.type-monthly { background: linear-gradient(135deg, #a855f7, #ec4899, #f59e0b); color: #fff; }
.type-default { background: rgba(148, 163, 184, .12); color: #64748b; }
.detail-status { display: inline-block; margin-top: 16rpx; padding: 8rpx 20rpx; border-radius: 999rpx; font-size: 22rpx; font-weight: 800; }
.status-active { background: #dcfce7; color: #16a34a; }
.status-finished { background: #e2e8f0; color: #64748b; }
.status-aborted { background: #fee2e2; color: #dc2626; }
.detail-time, .detail-desc { display: block; margin-top: 16rpx; font-size: 24rpx; color: #4b5563; }
.panel-head { margin-bottom: 18rpx; display: flex; align-items: center; justify-content: space-between; gap: 16rpx; }
.panel-title { font-size: 28rpx; font-weight: 900; color: #1f2937; }
.panel-side { font-size: 22rpx; color: #9ca3af; }
.share-guide-card { margin-top: 18rpx; padding: 24rpx; border-radius: 24rpx; background: linear-gradient(135deg, #fff7ed, #ffedd5); border: 2rpx solid rgba(249, 115, 22, 0.14); }
.share-guide-head { display: flex; align-items: center; justify-content: space-between; gap: 16rpx; }
.share-guide-title { font-size: 28rpx; font-weight: 900; color: #7c2d12; }
.share-guide-badge { padding: 8rpx 18rpx; border-radius: 999rpx; background: rgba(249, 115, 22, 0.12); color: #f97316; font-size: 22rpx; font-weight: 800; }
.share-guide-desc { display: block; margin-top: 14rpx; font-size: 24rpx; line-height: 1.6; color: #9a3412; }
.share-guide-actions { display: flex; gap: 16rpx; margin-top: 20rpx; }
.share-action-btn { flex: 1; min-height: 80rpx; display: flex; align-items: center; justify-content: center; border-radius: 999rpx; font-size: 24rpx; font-weight: 800; box-sizing: border-box; }
button.share-action-btn { padding: 0 24rpx; line-height: 80rpx; }
button.share-action-btn::after { border: none; }
.share-action-btn.primary { background: linear-gradient(135deg, #fb923c, #f97316); color: #fff; }
.share-action-btn.secondary { background: #fff; color: #ea580c; border: 2rpx solid rgba(249, 115, 22, 0.18); }
.metric-card { padding: 22rpx; border-radius: 22rpx; background: #fff7ed; margin-top: 18rpx; }
.crowd-panel { background: linear-gradient(180deg, #fff 0%, #fff7ed 100%); }
.crowd-progress-bar { height: 18rpx; border-radius: 999rpx; background: #fde7cf; overflow: hidden; margin-top: 18rpx; }
.crowd-progress-fill { height: 100%; border-radius: 999rpx; background: linear-gradient(90deg, #f59e0b, #f97316); }
.crowd-summary { display: flex; flex-direction: column; gap: 10rpx; margin-top: 18rpx; }
.crowd-summary-main { font-size: 28rpx; font-weight: 800; color: #1f2937; }
.crowd-summary-sub { font-size: 22rpx; color: #9a3412; }
.metric-head { display: flex; align-items: center; justify-content: space-between; gap: 16rpx; }
.metric-title { font-size: 26rpx; font-weight: 800; color: #1f2937; }
.metric-state { font-size: 22rpx; font-weight: 800; }
.metric-state.ok { color: #16a34a; }
.metric-state.pending { color: #f97316; }
.metric-value { display: block; margin-top: 14rpx; font-size: 30rpx; font-weight: 800; color: #1f2937; }
.metric-help { display: block; margin-top: 12rpx; font-size: 22rpx; color: #6b7280; }
.progress-bar { height: 18rpx; border-radius: 999rpx; background: #fed7aa; overflow: hidden; margin-top: 14rpx; }
.progress-fill { height: 100%; border-radius: 999rpx; }
.progress-fill.spend { background: linear-gradient(90deg, #f97316, #fb7185); }
.progress-fill.invite { background: linear-gradient(90deg, #0ea5e9, #22c55e); }
.qualification-summary { display: flex; align-items: center; justify-content: space-between; gap: 20rpx; margin-top: 22rpx; }
.qualification-text { flex: 1; font-size: 22rpx; color: #6b7280; }
.join-btn { min-width: 180rpx; padding: 18rpx 28rpx; text-align: center; border-radius: 999rpx; font-size: 24rpx; font-weight: 800; }
.join-btn.primary { background: linear-gradient(135deg, #fb923c, #f97316); color: #fff; }
.join-btn.joined { background: #dcfce7; color: #16a34a; }
.join-btn.aborted { background: #fee2e2; color: #dc2626; }
.join-btn.muted { background: #f3f4f6; color: #9ca3af; }
.prize-scroll { white-space: nowrap; }
.prize-track { display: inline-flex; gap: 20rpx; }
.prize-item { width: 260rpx; flex-shrink: 0; }
.prize-image { width: 260rpx; height: 180rpx; border-radius: 20rpx; background: #f3f4f6; }
.prize-image.empty { display: flex; align-items: center; justify-content: center; color: #9ca3af; }
.prize-name { display: -webkit-box; margin-top: 12rpx; min-height: 68rpx; font-size: 24rpx; font-weight: 700; color: #1f2937; white-space: normal; word-break: break-all; overflow: hidden; text-overflow: ellipsis; -webkit-line-clamp: 2; -webkit-box-orient: vertical; }
.prize-price { display: block; margin-top: 8rpx; font-size: 22rpx; color: #f97316; line-height: 1.4; }
.prize-count { display: block; margin-top: 6rpx; font-size: 22rpx; color: #6b7280; line-height: 1.4; }
.participant-head { align-items: flex-start; }
.participant-head-actions { display: flex; align-items: center; gap: 12rpx; flex-shrink: 0; }
.participant-meta-row { margin-bottom: 16rpx; display: flex; align-items: center; justify-content: space-between; gap: 16rpx; flex-wrap: wrap; }
.panel-side.emphasis { color: #f97316; font-weight: 800; }
.avatar-row { display: flex; align-items: center; gap: 12rpx; overflow-x: auto; padding-bottom: 4rpx; }
.avatar-item { position: relative; flex-shrink: 0; }
.avatar-item.mine .participant-avatar { border-color: #fb923c; box-shadow: 0 0 0 4rpx rgba(251, 146, 60, 0.16); }
.participant-avatar { width: 68rpx; height: 68rpx; border-radius: 50%; background: #f3f4f6; border: 4rpx solid #fff; box-shadow: 0 8rpx 18rpx rgba(0,0,0,.08); }
.head-action-btn { flex-shrink: 0; padding: 14rpx 20rpx; border-radius: 999rpx; background: #fff7ed; color: #f97316; font-size: 22rpx; font-weight: 800; border: 2rpx solid rgba(249,115,22,.18); }
.head-action-btn.overview { background: linear-gradient(135deg, #fff7ed, #ffedd5); }
.empty-text { font-size: 24rpx; color: #9ca3af; }
.history-entry { display: flex; justify-content: space-between; align-items: center; }
.history-subtitle { display: block; margin-top: 10rpx; font-size: 22rpx; color: #9ca3af; }
.winner-popup-overlay { position: fixed; inset: 0; z-index: 1000; display: flex; align-items: center; justify-content: center; }
.winner-popup-mask { position: absolute; inset: 0; background: rgba(0,0,0,.48); }
.winner-popup-panel { position: relative; width: 88%; max-height: 72vh; background: #fff; border-radius: 28rpx; overflow: hidden; box-shadow: 0 18rpx 50rpx rgba(0,0,0,.18); }
.winner-popup-head { display: flex; align-items: center; justify-content: space-between; gap: 16rpx; padding: 26rpx 28rpx; border-bottom: 2rpx solid #f3f4f6; }
.winner-popup-meta { padding: 16rpx 28rpx 0; display: flex; flex-direction: column; gap: 8rpx; }
.winner-popup-meta-main { font-size: 24rpx; font-weight: 800; color: #1f2937; }
.winner-popup-meta-sub { font-size: 22rpx; color: #f97316; }
.winner-popup-title { flex: 1; font-size: 28rpx; font-weight: 900; color: #1f2937; }
.winner-popup-close { font-size: 42rpx; color: #9ca3af; line-height: 1; }
.winner-popup-list { max-height: 54vh; padding: 24rpx 28rpx; }
.winner-popup-item { display: flex; gap: 16rpx; align-items: center; padding: 18rpx 0; }
.participant-popup-item { align-items: center; }
.participant-popup-avatar { border-radius: 50%; }
.self-tag { color: #f97316; font-weight: 800; }
.winner-popup-image { width: 96rpx; height: 96rpx; border-radius: 20rpx; background: #f3f4f6; flex-shrink: 0; }
.winner-popup-image.empty { display: flex; align-items: center; justify-content: center; color: #9ca3af; }
.winner-popup-info { flex: 1; min-width: 0; }
.winner-popup-name { display: block; font-size: 26rpx; font-weight: 800; color: #1f2937; }
.winner-popup-price, .winner-popup-time { display: block; margin-top: 8rpx; font-size: 22rpx; color: #6b7280; }
</style>

View File

@ -0,0 +1,130 @@
<template>
<view class="threshold-list-page">
<view class="page-head">
<text class="page-title">{{ mode === 'history' ? '往期裂变活动' : '拉新裂变活动' }}</text>
<text class="page-subtitle">{{ mode === 'history' ? '查看历史开奖与流产活动' : '完成消费或邀请门槛即可参与' }}</text>
</view>
<view class="toolbar">
<view class="toolbar-btn" :class="{ active: mode === 'active' }" @tap="switchMode('active')">进行中</view>
<view class="toolbar-btn" :class="{ active: mode === 'history' }" @tap="switchMode('history')">往期</view>
</view>
<view v-if="loading" class="state">加载中...</view>
<view v-else-if="activities.length === 0" class="state">{{ mode === 'history' ? '暂无往期活动' : '暂无进行中的活动' }}</view>
<view v-else class="activity-grid">
<view v-for="item in activities" :key="item.id" class="activity-card" @tap="goDetail(item.id)">
<image v-if="item.cover_image" class="activity-cover" :src="item.cover_image" mode="aspectFill" />
<view v-else class="activity-cover empty">暂无图片</view>
<view class="activity-meta">
<view class="title-row">
<text class="activity-name">{{ item.title }}</text>
<text class="activity-status" :class="statusClass(item.status)">{{ statusLabel(item.status) }}</text>
</view>
<view class="tag-row">
<text class="activity-type" :class="typeClass(item.type)">{{ typeLabel(item.type) }}</text>
<text class="activity-mode">{{ qualificationModeLabel(item.qualification_mode) }}</text>
</view>
<view class="stat-row">
<text>消费门槛 {{ money(item.spend_threshold_amount) }}</text>
<text>最低开奖 {{ item.min_participants || 0 }} </text>
</view>
<view class="stat-row" v-if="Number(item.invite_threshold_count || 0) > 0">
<text>邀请门槛 {{ item.invite_threshold_count || 0 }} </text>
<text>有效消费 {{ money(item.invite_effective_amount) }}</text>
</view>
</view>
</view>
</view>
</view>
</template>
<script>
import { listThresholdActivities } from '@/api/thresholdActivity'
export default {
data() {
return {
loading: false,
mode: 'active',
activities: []
}
},
onLoad(options) {
this.mode = options?.mode === 'history' ? 'history' : 'active'
this.loadData()
},
methods: {
async loadData() {
this.loading = true
try {
const params = { page: 1, page_size: 50 }
if (this.mode === 'active') params.status = 'active'
const res = await listThresholdActivities(params)
const list = Array.isArray(res?.list) ? res.list : []
this.activities = this.mode === 'history' ? list.filter(item => item.status !== 'active') : list
} catch (e) {
uni.showToast({ title: e.message || '加载失败', icon: 'none' })
} finally {
this.loading = false
}
},
switchMode(mode) {
if (this.mode === mode) return
this.mode = mode
this.loadData()
},
goDetail(id) {
uni.navigateTo({ url: `/pages-activity/activity/threshold/detail?id=${id}` })
},
typeLabel(type) {
return { daily: '每日裂变', weekly: '每周裂变', monthly: '每月裂变' }[type] || '裂变活动'
},
typeClass(type) {
return { daily: 'type-daily', weekly: 'type-weekly', monthly: 'type-monthly' }[type] || 'type-default'
},
qualificationModeLabel(mode) {
return { spend_only: '消费达标', invite_only: '邀请达标', either: '任一达标' }[mode] || '门槛活动'
},
statusLabel(status) {
return { active: '进行中', finished: '已结束', aborted: '已流产' }[status] || status || '-'
},
statusClass(status) {
return { active: 'status-active', finished: 'status-finished', aborted: 'status-aborted' }[status] || 'status-finished'
},
money(v) {
return `¥${(Number(v || 0) / 100).toFixed(2)}`
}
}
}
</script>
<style lang="scss">
.threshold-list-page { min-height: 100vh; padding: 28rpx; background: #fff7ed; }
.page-head { margin-bottom: 28rpx; }
.page-title { display: block; font-size: 46rpx; font-weight: 900; color: #1f2937; }
.page-subtitle { display: block; margin-top: 10rpx; font-size: 24rpx; color: #9ca3af; }
.toolbar { display: flex; gap: 16rpx; margin-bottom: 28rpx; }
.toolbar-btn { flex: 1; text-align: center; padding: 20rpx 0; background: #fff; border-radius: 999rpx; color: #9a5b24; font-weight: 800; }
.toolbar-btn.active { background: #ff8a3d; color: #fff; }
.state { padding: 120rpx 0; text-align: center; color: #9ca3af; }
.activity-grid { display: flex; flex-direction: column; gap: 24rpx; }
.activity-card { overflow: hidden; border-radius: 28rpx; background: #fff; box-shadow: 0 12rpx 30rpx rgba(0,0,0,.06); }
.activity-cover { width: 100%; height: 260rpx; display: block; background: #f3f4f6; }
.activity-cover.empty { display: flex; align-items: center; justify-content: center; color: #9ca3af; }
.activity-meta { padding: 24rpx 22rpx; display: flex; flex-direction: column; gap: 14rpx; }
.title-row, .tag-row, .stat-row { display: flex; align-items: center; justify-content: space-between; gap: 16rpx; flex-wrap: wrap; }
.activity-name { flex: 1; min-width: 0; font-size: 30rpx; font-weight: 800; color: #1f2937; }
.activity-status { font-size: 22rpx; font-weight: 800; }
.status-active { color: #10b981; }
.status-finished { color: #94a3b8; }
.status-aborted { color: #ef4444; }
.activity-type, .activity-mode { display: inline-flex; align-items: center; padding: 8rpx 18rpx; border-radius: 999rpx; font-size: 22rpx; font-weight: 800; }
.type-daily { background: rgba(249, 115, 22, .12); color: #f97316; }
.type-weekly { background: rgba(239, 68, 68, .12); color: #ef4444; }
.type-monthly { background: linear-gradient(135deg, #a855f7, #ec4899, #f59e0b); color: #fff; }
.type-default { background: rgba(148, 163, 184, .12); color: #64748b; }
.activity-mode { background: rgba(14, 165, 233, .12); color: #0284c7; }
.stat-row { font-size: 22rpx; color: #6b7280; }
</style>

View File

@ -43,19 +43,21 @@
<!-- 底部按钮 -->
<view class="footer">
<view
class="btn-primary"
<view
class="btn-primary"
:class="{ disabled: ticketCount <= 0 || entering }"
@tap="enterGame('minesweeper')"
>
<text class="enter-btn-text">{{ entering ? '正在进入...' : (ticketCount > 0 ? '立即开局' : '资格不足') }}</text>
</view>
<view
class="btn-secondary"
@tap="goRoomList"
>
<text class="secondary-btn-text">📡 对战列表 / 围观</text>
<view class="minesweeper-actions">
<view class="btn-secondary" @tap="goRoomList">
<text class="secondary-btn-text">📡 对战列表 / 围观</text>
</view>
<view class="btn-secondary leaderboard-btn" @tap="openLeaderboard">
<text class="secondary-btn-text">🏆 扫雷排行榜</text>
</view>
</view>
</view>
</view>
@ -77,6 +79,9 @@ export default {
this.loadTickets()
},
methods: {
async openLeaderboard() {
uni.navigateTo({ url: '/pages-game/game/minesweeper/leaderboard' })
},
async loadTickets() {
this.loading = true
@ -304,6 +309,12 @@ export default {
padding-bottom: calc(40rpx + env(safe-area-inset-bottom));
}
.minesweeper-actions {
display: flex;
gap: 20rpx;
margin-top: 24rpx;
}
.btn-primary {
height: 110rpx;
width: 100%;
@ -316,8 +327,8 @@ export default {
}
}
.btn-secondary {
margin-top: 24rpx;
btn-secondary {
flex: 1;
background: rgba(255,255,255,0.05);
border: 1px solid rgba(255,255,255,0.1);
border-radius: 55rpx;
@ -327,6 +338,11 @@ export default {
justify-content: center;
}
.leaderboard-btn {
background: rgba($brand-primary, 0.10);
border-color: rgba($brand-primary, 0.18);
}
.secondary-btn-text {
color: #94a3b8;
font-size: 32rpx;

View File

@ -185,13 +185,18 @@ function onGetPhoneNumber(e) {
} catch(e) {}
uni.showToast({ title: '欢迎加入!', icon: 'success' })
const backTarget = '/pages-activity/activity/threshold/index'
setTimeout(() => {
// #ifdef MP-TOUTIAO
//
uni.switchTab({ url: '/pages/shop/index' })
// #endif
// #ifndef MP-TOUTIAO
uni.switchTab({ url: '/pages/index/index' })
const pages = getCurrentPages()
if (pages.length > 1) {
uni.navigateBack({ delta: 1 })
} else {
uni.switchTab({ url: '/pages/index/index' })
}
// #endif
}, 500)

View File

@ -19,6 +19,16 @@
<text class="stat-num">{{ getRewardsTotal() }}</text>
<text class="stat-label">累计奖励</text>
</view>
<view class="stat-divider"></view>
<view class="stat-item">
<text class="stat-num">{{ shareCount }}</text>
<text class="stat-label">分享引导</text>
</view>
</view>
<view class="action-row">
<button class="share-btn" open-type="share">继续邀请好友</button>
<view class="copy-btn" @tap="copyInviteLink">复制邀请码</view>
</view>
<!-- 内容区 -->
@ -83,6 +93,7 @@ const isRefreshing = ref(false)
const page = ref(1)
const pageSize = 20
const hasMore = ref(true)
const shareCount = ref(0)
// ID
function getUserId() {
@ -126,6 +137,16 @@ function getRewardsTotal() {
return list.value.length * rewardPerInvite
}
function copyInviteLink() {
const inviteCode = uni.getStorageSync('invite_code') || (uni.getStorageSync('user_info') || {}).invite_code || ''
const text = inviteCode ? `快来和我一起参加裂变活动,邀请码:${inviteCode}` : '快来和我一起参加裂变活动'
uni.setClipboardData({
data: text,
success: () => uni.showToast({ title: '邀请码已复制', icon: 'success' }),
fail: () => uni.showToast({ title: '复制失败', icon: 'none' })
})
}
//
async function onRefresh() {
isRefreshing.value = true
@ -172,8 +193,19 @@ async function fetchData(append = false) {
}
onLoad(() => {
const inviteCode = uni.getStorageSync('invite_code') || (uni.getStorageSync('user_info') || {}).invite_code || ''
shareCount.value = inviteCode ? 1 : 0
fetchData()
})
onShareAppMessage(() => {
const inviteCode = uni.getStorageSync('invite_code') || (uni.getStorageSync('user_info') || {}).invite_code || ''
return {
title: '邀请好友一起参加裂变活动,达标就能开奖!',
path: `/pages-user/invite/landing?invite_code=${inviteCode}`,
imageUrl: '/static/logo.png'
}
})
</script>
<style lang="scss" scoped>
@ -248,6 +280,39 @@ onLoad(() => {
}
/* 内容滚动区 */
.action-row {
display: flex;
gap: 20rpx;
margin: 0 $spacing-lg $spacing-lg;
}
.share-btn, .copy-btn {
flex: 1;
min-height: 84rpx;
display: flex;
align-items: center;
justify-content: center;
border-radius: 999rpx;
font-size: 24rpx;
font-weight: 800;
}
.share-btn {
background: linear-gradient(135deg, $brand-primary, $brand-secondary);
color: #fff;
border: none;
}
.share-btn::after {
border: none;
}
.copy-btn {
background: rgba($brand-primary, 0.08);
color: $brand-primary;
border: 2rpx solid rgba($brand-primary, 0.16);
}
.content-scroll {
height: calc(100vh - 400rpx);
padding: 0 $spacing-lg $spacing-lg;

View File

@ -77,6 +77,18 @@
"style": {
"navigationBarTitleText": "活动详情"
}
},
{
"path": "activity/threshold/index",
"style": {
"navigationBarTitleText": "拉新裂变活动"
}
},
{
"path": "activity/threshold/detail",
"style": {
"navigationBarTitleText": "活动详情"
}
}
]
},

View File

@ -77,7 +77,7 @@
</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>
@ -97,10 +97,10 @@
<image class="card-icon-small" src="https://via.placeholder.com/80/98FB98/000000?text=Gift" mode="aspectFit" />
</view>
<view class="game-card-small card-more" @tap="openLeaderboard">
<text class="card-title-small">排行榜</text>
<text class="card-subtitle-small">扫雷战绩榜</text>
<image class="card-icon-small" src="https://via.placeholder.com/80/E0E0E0/000000?text=More" mode="aspectFit" />
<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>
@ -452,7 +452,7 @@ export default {
uni.navigateTo({ url })
},
async openLeaderboard() {
uni.navigateTo({ url: '/pages-game/game/minesweeper/leaderboard' })
uni.navigateTo({ url: '/pages-game/game/minesweeper/index?from=home&focus=leaderboard' })
},
onNoticeTap() {
const content = this.displayNotices.map(n => n.text).join('\n')
@ -487,12 +487,12 @@ export default {
<style lang="scss">
/* ============================================
柯大鸭 - 首页样式 (V6.0 Pro Refined)
柯大鸭 - 首页样式 (V7.0 Clean Modern Refresh)
============================================ */
.page {
padding: 0;
background-color: $bg-page;
background: linear-gradient(180deg, #f8fafc 0%, #f3f6fb 55%, #eef3f8 100%);
min-height: 100vh;
display: flex;
flex-direction: column;
@ -506,57 +506,48 @@ export default {
z-index: 10;
position: sticky;
top: 0;
background: rgba($bg-page, 0.8);
backdrop-filter: blur(10rpx);
background: rgba(248, 250, 252, 0.78);
backdrop-filter: blur(14rpx);
}
/* ========== 滚动主内容区 ========== */
/* ========== 滚动主内容区 ========== */
.main-content {
flex: 1;
position: relative;
z-index: 1;
}
/* Banner Container - Claymorphism Style */
/* Banner - 更轻、更干净 */
.banner-container {
padding: $spacing-sm $spacing-lg $spacing-xl;
padding: 12rpx $spacing-lg 28rpx;
position: relative;
z-index: 2;
}
.banner-swiper {
height: 360rpx;
height: 320rpx;
overflow: visible;
}
.banner-card {
height: 100%;
margin: 0;
border-radius: 40rpx;
border-radius: 32rpx;
overflow: hidden;
position: relative;
transform: scale(0.96);
transition: all 0.5s $ease-out;
/* Claymorphism 双阴影效果 */
transform: scale(0.985);
transition: all 0.35s $ease-out;
box-shadow:
12rpx 12rpx 24rpx rgba(0, 0, 0, 0.08),
-12rpx -12rpx 24rpx rgba(255, 255, 255, 0.7),
inset 4rpx 4rpx 8rpx rgba(255, 255, 255, 0.5),
inset -4rpx -4rpx 8rpx rgba(0, 0, 0, 0.05);
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:
16rpx 16rpx 32rpx rgba(255, 107, 0, 0.12),
-16rpx -16rpx 32rpx rgba(255, 255, 255, 0.6),
inset 6rpx 6rpx 12rpx rgba(255, 255, 255, 0.4),
inset -6rpx -6rpx 12rpx rgba(0, 0, 0, 0.06);
0 18rpx 44rpx rgba(15, 23, 42, 0.10),
0 4rpx 14rpx rgba(255, 107, 0, 0.08);
}
.banner-image {
@ -568,314 +559,280 @@ export default {
.banner-fallback {
width: 100%;
height: 100%;
background: linear-gradient(135deg, $brand-primary, $brand-secondary);
background: linear-gradient(135deg, #ffb36b 0%, #ff8f6b 45%, #ff735d 100%);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
align-items: flex-start;
justify-content: flex-end;
padding: 36rpx 32rpx;
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%);
inset: 0;
background: radial-gradient(circle at top right, rgba(255,255,255,0.24) 0%, transparent 55%);
}
.banner-fallback-text {
font-size: 52rpx;
font-weight: 900;
color: #fff;
font-style: italic;
margin-bottom: 12rpx;
letter-spacing: 2rpx;
.banner-fallback-text {
font-size: 42rpx;
font-weight: 900;
color: #fff;
margin-bottom: 10rpx;
letter-spacing: 1rpx;
z-index: 1;
}
.banner-tag {
background: rgba(0,0,0,0.2);
color: #fff;
padding: 8rpx 24rpx;
border-radius: $radius-round;
font-size: 22rpx;
.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(4px);
backdrop-filter: blur(6px);
z-index: 1;
}
/* Indicator */
.banner-indicator {
display: flex;
justify-content: center;
gap: 12rpx;
margin-top: -16rpx;
gap: 10rpx;
margin-top: 16rpx;
}
.indicator-dot {
width: 12rpx;
height: 6rpx;
background: rgba(0,0,0,0.1);
border-radius: 4rpx;
transition: all 0.3s ease;
width: 14rpx;
height: 8rpx;
background: rgba(148, 163, 184, 0.35);
border-radius: 999rpx;
transition: all 0.25s ease;
}
.indicator-dot.active {
width: 32rpx;
width: 34rpx;
background: $brand-primary;
}
/* Notice Bar - Claymorphism Style */
/* Notice Bar */
.notice-bar-v2 {
margin: 0 $spacing-lg $spacing-xl;
background: linear-gradient(145deg, #ffffff, #f5f5f5);
border-radius: 40rpx;
padding: 24rpx 32rpx;
margin: 0 $spacing-lg 28rpx;
background: rgba(255,255,255,0.78);
border-radius: 28rpx;
padding: 20rpx 24rpx;
display: flex;
align-items: center;
gap: 20rpx;
/* Claymorphism 双阴影 */
box-shadow:
8rpx 8rpx 16rpx rgba(0, 0, 0, 0.06),
-8rpx -8rpx 16rpx rgba(255, 255, 255, 0.8),
inset 2rpx 2rpx 4rpx rgba(255, 255, 255, 0.9),
inset -2rpx -2rpx 4rpx rgba(0, 0, 0, 0.03);
border: 1px solid rgba(255, 255, 255, 0.6);
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: 32rpx; }
.notice-swiper { flex: 1; height: 36rpx; }
.notice-item {
font-size: 26rpx;
color: $text-main;
line-height: 36rpx;
.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: 12rpx;
height: 12rpx;
border-top: 3rpx solid #DDD;
border-right: 3rpx solid #DDD;
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: $spacing-xl;
margin-bottom: 36rpx;
position: relative;
z-index: 2;
}
.section-header {
margin-bottom: 24rpx;
margin-bottom: 20rpx;
}
.section-title {
font-size: 38rpx;
font-size: 34rpx;
font-weight: 900;
color: $text-main;
font-style: italic;
color: #0f172a;
display: flex;
align-items: center;
text-shadow: 0 2rpx 8rpx rgba(0,0,0,0.05);
letter-spacing: 0.5rpx;
}
.section-title::before {
content: '';
width: 8rpx;
height: 32rpx;
background: $brand-primary;
margin-right: 16rpx;
border-radius: 4rpx;
transform: skewX(-15deg);
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: 24rpx;
gap: 18rpx;
}
.grid-row-top {
display: flex;
gap: 24rpx;
height: 190rpx;
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 18rpx;
min-height: 180rpx;
}
/* 玩法卡片 - Claymorphism Style */
.game-card-large {
flex: 1;
border-radius: $radius-xl;
border-radius: 28rpx;
position: relative;
overflow: hidden;
padding: 22rpx;
transition: transform 0.2s;
/* Claymorphism 阴影 */
box-shadow:
12rpx 12rpx 24rpx rgba(0, 0, 0, 0.1),
-12rpx -12rpx 24rpx rgba(255, 255, 255, 0.6),
inset 4rpx 4rpx 8rpx rgba(255, 255, 255, 0.3),
inset -4rpx -4rpx 8rpx rgba(0, 0, 0, 0.1);
border: 1px solid rgba(255, 255, 255, 0.2);
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.96);
box-shadow:
6rpx 6rpx 12rpx rgba(0, 0, 0, 0.12),
-6rpx -6rpx 12rpx rgba(255, 255, 255, 0.4),
inset 6rpx 6rpx 12rpx rgba(0, 0, 0, 0.15),
inset -6rpx -6rpx 12rpx rgba(255, 255, 255, 0.2);
transform: scale(0.98);
}
/* 下排 */
.grid-row-bottom {
display: flex;
gap: 20rpx;
height: 130rpx;
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 14rpx;
min-height: 122rpx;
}
.game-card-small {
flex: 1;
border-radius: $radius-lg;
min-width: 0;
border-radius: 24rpx;
position: relative;
overflow: hidden;
padding: 16rpx;
padding: 18rpx 14rpx;
display: flex;
flex-direction: column;
justify-content: center;
transition: all 0.2s;
/* Claymorphism 阴影 */
box-shadow:
8rpx 8rpx 16rpx rgba(0, 0, 0, 0.08),
-8rpx -8rpx 16rpx rgba(255, 255, 255, 0.7),
inset 2rpx 2rpx 4rpx rgba(255, 255, 255, 0.5),
inset -2rpx -2rpx 4rpx rgba(0, 0, 0, 0.05);
background: linear-gradient(145deg, #ffffff, #f8f8f8);
border: 1px solid rgba(255, 255, 255, 0.6);
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: scale(0.94);
box-shadow:
4rpx 4rpx 8rpx rgba(0, 0, 0, 0.1),
-4rpx -4rpx 8rpx rgba(255, 255, 255, 0.5),
inset 4rpx 4rpx 8rpx rgba(0, 0, 0, 0.08),
inset -4rpx -4rpx 8rpx rgba(255, 255, 255, 0.3);
transform: translateY(2rpx) scale(0.98);
}
/* 内容样式 - 大卡片 */
.card-content-large {
position: relative;
z-index: 2;
height: 100%;
display: flex;
flex-direction: column;
justify-content: center;
justify-content: space-between;
align-items: flex-start;
}
.card-title-large {
font-size: 34rpx;
font-weight: 900;
color: #FFF;
font-style: italic;
margin-bottom: 12rpx;
text-shadow: 0 4rpx 8rpx rgba(0,0,0,0.1);
color: #fff;
margin-bottom: 8rpx;
line-height: 1.15;
}
.card-tag-large {
font-size: 20rpx;
background: rgba(255, 255, 255, 0.9);
background: rgba(255, 255, 255, 0.92);
color: $text-main;
padding: 4rpx 14rpx;
padding: 6rpx 16rpx;
border-radius: $radius-round;
font-weight: 800;
box-shadow: 0 4rpx 12rpx rgba(0,0,0,0.1);
backdrop-filter: blur(4px);
}
.card-tag-large.yellow { color: #D97706; }
.card-tag-large.yellow { color: #b45309; }
.card-mascot-large {
position: absolute;
right: -10rpx;
bottom: -20rpx;
width: 140rpx;
height: 140rpx;
transform: rotate(10deg);
filter: drop-shadow(0 8rpx 16rpx rgba(0,0,0,0.2));
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: 30rpx;
font-size: 26rpx;
font-weight: 800;
color: $text-main;
margin-bottom: 6rpx;
z-index: 2;
line-height: 1.18;
}
.card-subtitle-small {
font-size: 22rpx;
color: $text-sub;
font-size: 20rpx;
color: rgba(15, 23, 42, 0.68);
z-index: 2;
line-height: 1.2;
}
.card-icon-small {
position: absolute;
right: -10rpx;
bottom: -10rpx;
width: 80rpx;
height: 80rpx;
right: -6rpx;
bottom: -6rpx;
width: 68rpx;
height: 68rpx;
opacity: 0.9;
transform: rotate(-10deg);
transform: rotate(-6deg);
}
/* 背景配色 - 优化后的渐变 */
.card-yifan {
background: linear-gradient(135deg, $brand-primary 0%, $brand-secondary 100%); /* 品牌橙渐变 */
background: linear-gradient(135deg, #ff8a5c 0%, #ff6b4a 100%);
}
.card-wuxian {
background: $gradient-gold; /* 质感金渐变 */
background: linear-gradient(135deg, #ffcf6d 0%, #ff9f43 100%);
}
.card-yifan-small {
background: linear-gradient(135deg, $brand-primary 0%, $brand-secondary 100%);
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,.85); }
.card-yifan-small .card-subtitle-small { color: rgba(255,255,255,0.84); }
.card-match {
background: linear-gradient(135deg, #FF9A9E 0%, #FECFEF 100%);
background: linear-gradient(135deg, #ff9fba 0%, #ffb8d4 100%);
}
.card-match .card-title-large { color: #fff; }
.card-match .card-tag-large { color: $accent-pink; }
.card-match .card-tag-large { color: #db2777; }
.card-tower {
background: linear-gradient(135deg, #FFE0CC 0%, #FFCBA4 100%); /* 品牌橙暖色 */
background: linear-gradient(135deg, #ede9fe 0%, #ddd6fe 100%);
}
.card-tower .card-title-small { color: $brand-primary; }
.card-tower .card-title-small { color: #5b21b6; }
.card-tower .card-subtitle-small { color: #6d28d9; }
.card-welfare {
background: linear-gradient(135deg, #D1FAE5 0%, #A7F3D0 100%);
background: linear-gradient(135deg, #dcfce7 0%, #bbf7d0 100%);
}
.card-welfare .card-title-small { color: #047857; }
.card-welfare .card-subtitle-small { color: #059669; }
.card-more {
background: linear-gradient(135deg, $bg-secondary 0%, #E5E7EB 100%); /* 金属灰 */
.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);
}
.card-more .card-title-small { color: $text-sub; }
/* 推荐活动列表 - Claymorphism Style */
.activity-section {
padding: 0 $spacing-lg;
padding: 0 $spacing-lg 8rpx;
animation: fadeInUp 0.6s ease-out 0.3s backwards;
position: relative;
z-index: 2;
@ -884,44 +841,34 @@ export default {
.activity-grid-list {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 24rpx;
gap: 18rpx;
}
.activity-item {
background: linear-gradient(145deg, #ffffff, #f8f8f8);
border-radius: $radius-xl;
background: rgba(255,255,255,0.82);
border-radius: 28rpx;
overflow: hidden;
display: flex;
flex-direction: column;
transition: all 0.3s ease;
/* Claymorphism 阴影 */
box-shadow:
10rpx 10rpx 20rpx rgba(0, 0, 0, 0.06),
-10rpx -10rpx 20rpx rgba(255, 255, 255, 0.7),
inset 3rpx 3rpx 6rpx rgba(255, 255, 255, 0.8),
inset -3rpx -3rpx 6rpx rgba(0, 0, 0, 0.03);
border: 1px solid rgba(255, 255, 255, 0.6);
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(4rpx) scale(0.98);
box-shadow:
5rpx 5rpx 10rpx rgba(0, 0, 0, 0.08),
-5rpx -5rpx 10rpx rgba(255, 255, 255, 0.5),
inset 5rpx 5rpx 10rpx rgba(0, 0, 0, 0.05),
inset -5rpx -5rpx 10rpx rgba(255, 255, 255, 0.3);
transform: translateY(2rpx) scale(0.98);
}
.activity-thumb-box {
position: relative;
width: 100%;
padding-bottom: 100%;
padding-bottom: 96%;
}
.activity-thumb {
position: absolute;
top: 0; left: 0;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
@ -930,28 +877,27 @@ export default {
position: absolute;
top: 16rpx;
left: 16rpx;
background: $gradient-brand;
color: #fff;
font-size: 20rpx;
padding: 6rpx 16rpx;
border-radius: 12rpx;
background: rgba(255,255,255,0.86);
color: $brand-primary;
font-size: 18rpx;
padding: 6rpx 14rpx;
border-radius: 999rpx;
font-weight: 800;
box-shadow: 0 4rpx 12rpx rgba(255,107,0,0.3);
}
.activity-info {
padding: 24rpx;
padding: 22rpx;
flex: 1;
display: flex;
flex-direction: column;
justify-content: space-between;
gap: 14rpx;
}
.activity-name {
font-size: 28rpx;
font-weight: 700;
color: $text-main;
margin-bottom: 20rpx;
font-weight: 800;
color: #0f172a;
line-height: 1.4;
overflow: hidden;
text-overflow: ellipsis;
@ -965,56 +911,54 @@ export default {
display: flex;
justify-content: space-between;
align-items: center;
gap: 16rpx;
}
.activity-desc {
font-size: 26rpx;
color: $accent-red;
font-weight: 800;
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: #1A1A1A;
color: $accent-gold;
font-size: 22rpx;
font-weight: 900;
padding: 10rpx 28rpx;
border-radius: $radius-round;
box-shadow: 0 6rpx 16rpx rgba(0,0,0,0.1);
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.05); opacity: 0.9; }
50% { transform: scale(1.04); opacity: 0.92; }
100% { transform: scale(1); opacity: 1; }
}
@keyframes fadeInUp {
from { opacity: 0; transform: translateY(40rpx); }
from { opacity: 0; transform: translateY(32rpx); }
to { opacity: 1; transform: translateY(0); }
}
@keyframes float {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-20rpx); }
50% { transform: translateY(-16rpx); }
}
.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; }
.brand-star { animation: pulse 2s infinite; }
.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.9);
transform: scale(0.96);
}
/* 兼容性修复 */
.brand-text {
background-clip: text;
}