fix(activity): 完善福利活动前台展示与参与体验
补齐福利活动前台页面与请求接入,优化奖品展示、参与进度、中奖概览与无图占位文案。 同时修复奖品文字重叠问题,提升福利活动详情页的可读性。
This commit is contained in:
parent
e0a1d6e934
commit
2895c2d5b7
417
pages-activity/activity/welfare/detail.vue
Normal file
417
pages-activity/activity/welfare/detail.vue
Normal file
@ -0,0 +1,417 @@
|
|||||||
|
<template>
|
||||||
|
<view class="welfare-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">{{ 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 join-panel">
|
||||||
|
<view class="panel-head">
|
||||||
|
<text class="panel-title">我的参与进度</text>
|
||||||
|
<text class="panel-side">{{ progressText }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="progress-bar">
|
||||||
|
<view class="progress-fill" :style="{ width: progressPercent + '%' }"></view>
|
||||||
|
</view>
|
||||||
|
<view class="progress-foot">
|
||||||
|
<text class="progress-hint">{{ progressHint }}</text>
|
||||||
|
<view
|
||||||
|
class="join-btn"
|
||||||
|
:class="joinButtonClass"
|
||||||
|
@tap="handleJoin"
|
||||||
|
>
|
||||||
|
{{ joinButtonText }}
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="detail-panel">
|
||||||
|
<view class="panel-head">
|
||||||
|
<text class="panel-title">奖品</text>
|
||||||
|
<text v-if="sortedPrizes.length > 1" class="panel-side">按价格从高到低</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>
|
||||||
|
</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>
|
||||||
|
<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 { request, authRequest } from '@/utils/request.js'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
detail: null,
|
||||||
|
winners: [],
|
||||||
|
participants: [],
|
||||||
|
participantsPage: 1,
|
||||||
|
participantsPageSize: 20,
|
||||||
|
participantsLoading: false,
|
||||||
|
winnerPopupVisible: false,
|
||||||
|
participantsPopupVisible: false,
|
||||||
|
currentUserId: 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
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)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
progressPercent() {
|
||||||
|
const current = Number(this.detail?.current_paid || 0)
|
||||||
|
const target = Number(this.detail?.threshold_amount || 0)
|
||||||
|
if (target <= 0) return this.detail?.joined ? 100 : 0
|
||||||
|
return Math.max(0, Math.min(100, Math.round((current / target) * 100)))
|
||||||
|
},
|
||||||
|
progressText() {
|
||||||
|
const current = this.formatAmount(this.detail?.current_paid || 0)
|
||||||
|
const target = this.formatAmount(this.detail?.threshold_amount || 0)
|
||||||
|
return `${this.windowLabel(this.detail?.type)}消费 ${current}/${target}`
|
||||||
|
},
|
||||||
|
progressHint() {
|
||||||
|
if (this.detail?.joined) return '您已成功参加本期活动'
|
||||||
|
if (this.detail?.can_join) return '已达参与门槛,可立即参加活动'
|
||||||
|
const target = Number(this.detail?.threshold_amount || 0)
|
||||||
|
const current = Number(this.detail?.current_paid || 0)
|
||||||
|
if (target > current) {
|
||||||
|
return `还差 ¥${this.formatAmount(target - current)} 即可参加`
|
||||||
|
}
|
||||||
|
return '当前未满足参加条件'
|
||||||
|
},
|
||||||
|
joinButtonText() {
|
||||||
|
if (this.detail?.joined) return '已参加'
|
||||||
|
if (this.detail?.can_join) return '参加活动'
|
||||||
|
return '未达门槛'
|
||||||
|
},
|
||||||
|
joinButtonClass() {
|
||||||
|
if (this.detail?.joined) return 'disabled'
|
||||||
|
if (this.detail?.can_join) return 'primary'
|
||||||
|
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)
|
||||||
|
this.loadData()
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
async loadData() {
|
||||||
|
if (!this.id) return
|
||||||
|
try {
|
||||||
|
try {
|
||||||
|
this.detail = await authRequest({ url: `/api/app/welfare-activities/${this.id}/my`, suppressAuthModal: true })
|
||||||
|
} catch (_) {
|
||||||
|
this.detail = await request({ url: `/api/app/welfare-activities/${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 request({ url: `/api/app/welfare-activities/${this.id}/winners?page=1&page_size=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 authRequest({ url: `/api/app/welfare-activities/${this.id}/join`, method: 'POST' })
|
||||||
|
uni.showToast({ title: '参加成功', icon: 'success' })
|
||||||
|
await this.loadData()
|
||||||
|
} catch (e) {
|
||||||
|
uni.showToast({ title: e.message || '参加失败', 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 request({
|
||||||
|
url: `/api/app/welfare-activities/${this.id}/participants?page=${nextPage}&page_size=${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/welfare/index?mode=finished' })
|
||||||
|
},
|
||||||
|
statusText(status) {
|
||||||
|
return { active: '进行中', finished: '已结束' }[status] || status || '-'
|
||||||
|
},
|
||||||
|
typeLabel(type) {
|
||||||
|
return { daily: '每日福利', weekly: '每周福利', monthly: '每月福利' }[type] || '福利活动'
|
||||||
|
},
|
||||||
|
typeClass(type) {
|
||||||
|
return {
|
||||||
|
daily: 'type-daily',
|
||||||
|
weekly: 'type-weekly',
|
||||||
|
monthly: 'type-monthly'
|
||||||
|
}[type] || 'type-default'
|
||||||
|
},
|
||||||
|
windowLabel(type) {
|
||||||
|
return { daily: '每日', weekly: '每周', monthly: '每月' }[type] || '活动'
|
||||||
|
},
|
||||||
|
formatTime(v) {
|
||||||
|
if (!v) return '-'
|
||||||
|
return String(v).replace('T', ' ').slice(0, 16)
|
||||||
|
},
|
||||||
|
formatAmount(cents) {
|
||||||
|
return (Number(cents || 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">
|
||||||
|
.welfare-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; background: #fef3c7; color: #b45309; font-size: 22rpx; font-weight: 800; }
|
||||||
|
.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-head.compact { margin-bottom: 10rpx; }
|
||||||
|
.panel-title { font-size: 28rpx; font-weight: 900; color: #1f2937; }
|
||||||
|
.panel-side { font-size: 22rpx; color: #9ca3af; }
|
||||||
|
.progress-bar { height: 18rpx; border-radius: 999rpx; background: #fed7aa; overflow: hidden; }
|
||||||
|
.progress-fill { height: 100%; border-radius: 999rpx; background: linear-gradient(90deg, #f97316, #fb7185); }
|
||||||
|
.progress-foot { display: flex; align-items: center; justify-content: space-between; gap: 20rpx; margin-top: 20rpx; }
|
||||||
|
.progress-hint { 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.disabled { background: #dcfce7; color: #16a34a; }
|
||||||
|
.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-preview { display: flex; align-items: center; gap: 12rpx; overflow: hidden; }
|
||||||
|
.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; }
|
||||||
|
.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); }
|
||||||
|
.more-count { min-width: 68rpx; height: 68rpx; padding: 0 16rpx; border-radius: 34rpx; background: #fff7ed; color: #f97316; display: flex; align-items: center; justify-content: center; font-size: 22rpx; font-weight: 800; }
|
||||||
|
.participant-list { display: none; }
|
||||||
|
.participant-overview-card, .participant-card { display: flex; align-items: center; justify-content: space-between; gap: 16rpx; padding: 20rpx; border-radius: 22rpx; background: #fff7ed; }
|
||||||
|
.overview-icon { width: 76rpx; height: 76rpx; border-radius: 50%; background: linear-gradient(135deg, #fde68a, #fb7185); display: flex; align-items: center; justify-content: center; font-size: 34rpx; flex-shrink: 0; }
|
||||||
|
.participant-main { display: flex; align-items: center; gap: 14rpx; flex: 1; min-width: 0; }
|
||||||
|
.participant-card-avatar { width: 76rpx; height: 76rpx; border-radius: 50%; background: #f3f4f6; }
|
||||||
|
.participant-card-info { flex: 1; min-width: 0; }
|
||||||
|
.participant-name { display: block; font-size: 26rpx; font-weight: 800; color: #1f2937; }
|
||||||
|
.participant-sub { display: block; margin-top: 8rpx; font-size: 22rpx; color: #6b7280; }
|
||||||
|
.winner-btn { padding: 14rpx 20rpx; border-radius: 999rpx; background: #fff; color: #f97316; font-size: 22rpx; font-weight: 800; flex-shrink: 0; }
|
||||||
|
.overview-btn { min-width: 112rpx; text-align: center; }
|
||||||
|
.expand-btn { display: none; }
|
||||||
|
.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-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>
|
||||||
108
pages-activity/activity/welfare/index.vue
Normal file
108
pages-activity/activity/welfare/index.vue
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
<template>
|
||||||
|
<view class="welfare-list-page">
|
||||||
|
<view class="page-head">
|
||||||
|
<text class="page-title">{{ mode === 'finished' ? '往期活动' : '福利活动' }}</text>
|
||||||
|
<text class="page-subtitle">{{ mode === 'finished' ? '查看已结束活动' : '当前仅展示进行中的活动' }}</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="toolbar">
|
||||||
|
<view class="toolbar-btn" :class="{ active: mode === 'active' }" @tap="switchMode('active')">进行中</view>
|
||||||
|
<view class="toolbar-btn" :class="{ active: mode === 'finished' }" @tap="switchMode('finished')">往期活动</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view v-if="loading" class="state">加载中...</view>
|
||||||
|
<view v-else-if="activities.length === 0" class="state">{{ mode === 'finished' ? '暂无往期活动' : '暂无进行中的活动' }}</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">
|
||||||
|
<text class="activity-name">{{ item.title }}</text>
|
||||||
|
<view class="activity-type-row">
|
||||||
|
<text class="activity-type" :class="typeClass(item.type)">{{ typeLabel(item.type) }}</text>
|
||||||
|
<text class="activity-status" :class="mode === 'finished' ? 'status-finished' : 'status-active'">
|
||||||
|
{{ mode === 'finished' ? '已结束' : '进行中' }}
|
||||||
|
</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { request } from '@/utils/request.js'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
loading: false,
|
||||||
|
mode: 'active',
|
||||||
|
activities: []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onLoad(options) {
|
||||||
|
this.mode = options?.mode === 'finished' ? 'finished' : 'active'
|
||||||
|
this.loadData()
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
async loadData() {
|
||||||
|
this.loading = true
|
||||||
|
try {
|
||||||
|
const status = this.mode === 'finished' ? 'finished' : 'active'
|
||||||
|
const res = await request({ url: `/api/app/welfare-activities?status=${status}&page=1&page_size=50` })
|
||||||
|
this.activities = Array.isArray(res?.list) ? res.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/welfare/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'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.welfare-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 { display: flex; overflow: hidden; border-radius: 28rpx; background: #fff; box-shadow: 0 12rpx 30rpx rgba(0,0,0,.06); min-height: 220rpx; }
|
||||||
|
.activity-cover { width: 240rpx; height: 220rpx; display: block; background: #f3f4f6; flex-shrink: 0; }
|
||||||
|
.activity-cover.empty { display: flex; align-items: center; justify-content: center; color: #9ca3af; }
|
||||||
|
.activity-meta { flex: 1; padding: 24rpx 22rpx; display: flex; flex-direction: column; justify-content: space-between; min-width: 0; }
|
||||||
|
.activity-name { display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; font-size: 30rpx; font-weight: 800; color: #1f2937; line-height: 1.4; }
|
||||||
|
.activity-type-row { display: flex; align-items: center; justify-content: space-between; gap: 16rpx; margin-top: 12rpx; flex-wrap: wrap; }
|
||||||
|
.activity-type { 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-status { font-size: 22rpx; font-weight: 700; }
|
||||||
|
.status-active { color: #10b981; }
|
||||||
|
.status-finished { color: #94a3b8; }
|
||||||
|
</style>
|
||||||
76
pages.json
76
pages.json
@ -36,38 +36,50 @@
|
|||||||
{
|
{
|
||||||
"root": "pages-activity",
|
"root": "pages-activity",
|
||||||
"pages": [
|
"pages": [
|
||||||
{
|
{
|
||||||
"path": "activity/yifanshang/index",
|
"path": "activity/yifanshang/index",
|
||||||
"style": {
|
"style": {
|
||||||
"navigationBarTitleText": "一番赏"
|
"navigationBarTitleText": "一番赏"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "activity/wuxianshang/index",
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "无限赏"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "activity/duiduipeng/index",
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "对对碰"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "activity/list/index",
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "活动列表"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "activity/pata/index",
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "爬塔"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "activity/welfare/index",
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "福利活动"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "activity/welfare/detail",
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "活动详情"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
]
|
||||||
{
|
},
|
||||||
"path": "activity/wuxianshang/index",
|
|
||||||
"style": {
|
|
||||||
"navigationBarTitleText": "无限赏"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"path": "activity/duiduipeng/index",
|
|
||||||
"style": {
|
|
||||||
"navigationBarTitleText": "对对碰"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"path": "activity/list/index",
|
|
||||||
"style": {
|
|
||||||
"navigationBarTitleText": "活动列表"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"path": "activity/pata/index",
|
|
||||||
"style": {
|
|
||||||
"navigationBarTitleText": "爬塔"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"root": "pages-user",
|
"root": "pages-user",
|
||||||
"pages": [
|
"pages": [
|
||||||
@ -289,4 +301,4 @@
|
|||||||
"__usePrivacyCheck__": true
|
"__usePrivacyCheck__": true
|
||||||
},
|
},
|
||||||
"uniIdRouter": {}
|
"uniIdRouter": {}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -61,12 +61,11 @@
|
|||||||
<view class="gameplay-grid-v2">
|
<view class="gameplay-grid-v2">
|
||||||
<!-- 上排:两大核心 -->
|
<!-- 上排:两大核心 -->
|
||||||
<view class="grid-row-top">
|
<view class="grid-row-top">
|
||||||
<view class="game-card-large card-yifan" @tap="navigateTo('/pages-activity/activity/list/index?category=一番赏')">
|
<view class="game-card-large card-match" @tap="navigateTo('/pages-activity/activity/list/index?category=对对碰')">
|
||||||
<view class="card-bg-decoration"></view>
|
|
||||||
<view class="card-content-large">
|
<view class="card-content-large">
|
||||||
<text class="card-title-large">一番赏</text>
|
<text class="card-title-large">对对碰</text>
|
||||||
<view class="card-tag-large">欧皇擂台</view>
|
<view class="card-tag-large">碰一碰消除</view>
|
||||||
<image class="card-mascot-large" src="https://via.placeholder.com/150/90EE90/000000?text=YI" mode="aspectFit" />
|
<image class="card-mascot-large" src="https://via.placeholder.com/150/FFB6C1/000000?text=Match" mode="aspectFit" />
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
<view class="game-card-large card-wuxian" @tap="navigateTo('/pages-activity/activity/list/index?category=无限赏')">
|
<view class="game-card-large card-wuxian" @tap="navigateTo('/pages-activity/activity/list/index?category=无限赏')">
|
||||||
@ -80,10 +79,10 @@
|
|||||||
|
|
||||||
<!-- 下排:三小功能 -->
|
<!-- 下排:三小功能 -->
|
||||||
<view class="grid-row-bottom">
|
<view class="grid-row-bottom">
|
||||||
<view class="game-card-small card-match" @tap="navigateTo('/pages-activity/activity/list/index?category=对对碰')">
|
<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-title-small">一番赏</text>
|
||||||
<text class="card-subtitle-small">碰一碰消除</text>
|
<text class="card-subtitle-small">欧皇擂台</text>
|
||||||
<image class="card-icon-small" src="https://via.placeholder.com/80/FFB6C1/000000?text=Match" mode="aspectFit" />
|
<image class="card-icon-small" src="https://via.placeholder.com/80/90EE90/000000?text=YI" mode="aspectFit" />
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<view class="game-card-small card-tower" @tap="navigateTo('/pages-game/game/minesweeper/index')">
|
<view class="game-card-small card-tower" @tap="navigateTo('/pages-game/game/minesweeper/index')">
|
||||||
@ -92,6 +91,12 @@
|
|||||||
<image class="card-icon-small" src="https://via.placeholder.com/80/9370DB/000000?text=Mine" mode="aspectFit" />
|
<image class="card-icon-small" src="https://via.placeholder.com/80/9370DB/000000?text=Mine" mode="aspectFit" />
|
||||||
</view>
|
</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-more" @tap="openLeaderboard">
|
<view class="game-card-small card-more" @tap="openLeaderboard">
|
||||||
<text class="card-title-small">排行榜</text>
|
<text class="card-title-small">排行榜</text>
|
||||||
<text class="card-subtitle-small">扫雷战绩榜</text>
|
<text class="card-subtitle-small">扫雷战绩榜</text>
|
||||||
@ -774,16 +779,28 @@ export default {
|
|||||||
background: $gradient-gold; /* 质感金渐变 */
|
background: $gradient-gold; /* 质感金渐变 */
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-match {
|
.card-yifan-small {
|
||||||
background: linear-gradient(135deg, #FF9A9E 0%, #FECFEF 100%); /* 柔和粉 */
|
background: linear-gradient(135deg, $brand-primary 0%, $brand-secondary 100%);
|
||||||
}
|
}
|
||||||
.card-match .card-title-small { color: $accent-pink; }
|
.card-yifan-small .card-title-small { color: #fff; }
|
||||||
|
.card-yifan-small .card-subtitle-small { color: rgba(255,255,255,.85); }
|
||||||
|
|
||||||
|
.card-match {
|
||||||
|
background: linear-gradient(135deg, #FF9A9E 0%, #FECFEF 100%);
|
||||||
|
}
|
||||||
|
.card-match .card-title-large { color: #fff; }
|
||||||
|
.card-match .card-tag-large { color: $accent-pink; }
|
||||||
|
|
||||||
.card-tower {
|
.card-tower {
|
||||||
background: linear-gradient(135deg, #FFE0CC 0%, #FFCBA4 100%); /* 品牌橙暖色 */
|
background: linear-gradient(135deg, #FFE0CC 0%, #FFCBA4 100%); /* 品牌橙暖色 */
|
||||||
}
|
}
|
||||||
.card-tower .card-title-small { color: $brand-primary; }
|
.card-tower .card-title-small { color: $brand-primary; }
|
||||||
|
|
||||||
|
.card-welfare {
|
||||||
|
background: linear-gradient(135deg, #D1FAE5 0%, #A7F3D0 100%);
|
||||||
|
}
|
||||||
|
.card-welfare .card-title-small { color: #047857; }
|
||||||
|
|
||||||
.card-more {
|
.card-more {
|
||||||
background: linear-gradient(135deg, $bg-secondary 0%, #E5E7EB 100%); /* 金属灰 */
|
background: linear-gradient(135deg, $bg-secondary 0%, #E5E7EB 100%); /* 金属灰 */
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
// const BASE_URL = 'http://127.0.0.1:9991'
|
const BASE_URL = 'http://127.0.0.1:9991'
|
||||||
const BASE_URL = 'https://kdy.1024tool.vip'
|
// const BASE_URL = 'https://kdy.1024tool.vip'
|
||||||
let authModalShown = false
|
let authModalShown = false
|
||||||
|
|
||||||
function handleAuthExpired() {
|
function handleAuthExpired() {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user