587 lines
30 KiB
Vue
587 lines
30 KiB
Vue
<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>
|