558 lines
14 KiB
Vue
558 lines
14 KiB
Vue
<template>
|
||
<ActivityPageLayout :cover-url="coverUrl">
|
||
<template #header>
|
||
<ActivityHeader
|
||
:title="detail.name || detail.title || '一番赏活动'"
|
||
:price="detail.price_draw"
|
||
price-unit="/发"
|
||
:cover-url="coverUrl"
|
||
:tags="['公开透明', '拒绝套路']"
|
||
:scheduled-time="scheduledTimeText"
|
||
@show-rules="showRules"
|
||
@go-cabinet="goCabinet"
|
||
/>
|
||
</template>
|
||
|
||
<template #content>
|
||
<!-- 赏品概览 -->
|
||
<ActivityTabs v-model="tabActive" :stagger="1">
|
||
<template #pool>
|
||
<RewardsPreview
|
||
title="奖品配置"
|
||
:rewards="currentIssueRewards"
|
||
:grouped="false"
|
||
@view-all="rewardsVisible = true"
|
||
/>
|
||
</template>
|
||
<template #records>
|
||
<RecordsList :records="winRecords" />
|
||
</template>
|
||
</ActivityTabs>
|
||
|
||
<!-- 选号区域(一番赏专属) -->
|
||
<view class="section-container selector-container animate-enter stagger-2">
|
||
<!-- 期号切换 -->
|
||
<view class="issue-header">
|
||
<view class="issue-switch-btn" @click="prevIssue">
|
||
<text class="arrow">◀</text>
|
||
</view>
|
||
<view class="issue-info-center">
|
||
<text class="issue-current-text">{{ currentIssueTitle }}</text>
|
||
<text class="issue-status-badge">进行中</text>
|
||
</view>
|
||
<view class="issue-switch-btn" @click="nextIssue">
|
||
<text class="arrow">▶</text>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="issue-block-tip" v-if="!isOrderAllowed">
|
||
<text class="issue-block-text">{{ orderBlockedReason }}</text>
|
||
</view>
|
||
|
||
<!-- 选号组件 - 隐藏内置操作栏 -->
|
||
<view class="selector-body" v-if="activityId && currentIssueId">
|
||
<YifanSelector
|
||
ref="yifanSelectorRef"
|
||
:activity-id="activityId"
|
||
:issue-id="currentIssueId"
|
||
:price-per-draw="Number(detail.price_draw || 0) / 100"
|
||
:disabled="!isOrderAllowed"
|
||
:disabled-text="orderBlockedReason"
|
||
:hide-action-bar="true"
|
||
@payment-success="onPaymentSuccess"
|
||
@selection-change="onSelectionChange"
|
||
/>
|
||
</view>
|
||
</view>
|
||
</template>
|
||
|
||
<template #footer>
|
||
<!-- 固定底部操作栏 -->
|
||
<view class="float-bar">
|
||
<view class="float-bar-inner">
|
||
<view class="selection-info" v-if="selectedCount > 0">
|
||
已选 <text class="highlight">{{ selectedCount }}</text> 个位置
|
||
</view>
|
||
<view class="selection-info" v-else>
|
||
请选择位置
|
||
</view>
|
||
<view class="action-buttons">
|
||
<button v-if="selectedCount === 0" class="action-btn primary" @tap="handleRandomDraw" :disabled="!isOrderAllowed">随机一发</button>
|
||
<button v-else class="action-btn primary" @tap="handlePayment" :disabled="!isOrderAllowed">去支付</button>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</template>
|
||
|
||
<template #modals>
|
||
<!-- 翻牌弹窗 -->
|
||
<view v-if="showFlip" class="flip-overlay" @touchmove.stop.prevent>
|
||
<view class="flip-mask" @tap="closeFlip"></view>
|
||
<view class="flip-content" @tap.stop>
|
||
<FlipGrid ref="flipRef" :rewards="currentIssueRewards" :controls="false" />
|
||
<button class="overlay-close" @tap="closeFlip">关闭</button>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 奖品弹窗 -->
|
||
<RewardsPopup
|
||
v-model:visible="rewardsVisible"
|
||
:title="`${currentIssueTitle} · 奖品与概率`"
|
||
:reward-groups="rewardGroups"
|
||
/>
|
||
|
||
<!-- 规则弹窗 -->
|
||
<RulesPopup
|
||
v-model:visible="rulesVisible"
|
||
:content="detail.gameplay_intro"
|
||
/>
|
||
|
||
<!-- 盒柜预览弹窗 -->
|
||
<CabinetPreviewPopup
|
||
v-model:visible="cabinetVisible"
|
||
:activity-id="activityId"
|
||
/>
|
||
</template>
|
||
</ActivityPageLayout>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { ref, computed, watch } from 'vue'
|
||
import { onLoad, onUnload } from '@dcloudio/uni-app'
|
||
// 公共组件 - uni-app需要直接导入.vue文件
|
||
import ActivityPageLayout from '@/components/activity/ActivityPageLayout.vue'
|
||
import ActivityHeader from '@/components/activity/ActivityHeader.vue'
|
||
import ActivityTabs from '@/components/activity/ActivityTabs.vue'
|
||
import RewardsPreview from '@/components/activity/RewardsPreview.vue'
|
||
import RewardsPopup from '@/components/activity/RewardsPopup.vue'
|
||
import RecordsList from '@/components/activity/RecordsList.vue'
|
||
import RulesPopup from '@/components/activity/RulesPopup.vue'
|
||
import CabinetPreviewPopup from '@/components/activity/CabinetPreviewPopup.vue'
|
||
import FlipGrid from '@/components/FlipGrid.vue'
|
||
import YifanSelector from '@/components/YifanSelector.vue'
|
||
// Composables
|
||
import { useActivity, useIssues, useRewards, useRecords } from '../../composables'
|
||
// Utils
|
||
import { formatDateTime, parseTimeMs } from '@/utils/format'
|
||
|
||
// ============ 使用Composables ============
|
||
const activityId = ref('')
|
||
|
||
const {
|
||
detail,
|
||
coverUrl,
|
||
fetchDetail,
|
||
setNavigationTitle
|
||
} = useActivity(activityId)
|
||
|
||
const {
|
||
issues,
|
||
currentIssueId,
|
||
currentIssueTitle,
|
||
fetchIssues,
|
||
prevIssue,
|
||
nextIssue
|
||
} = useIssues(activityId)
|
||
|
||
const {
|
||
currentIssueRewards,
|
||
rewardGroups,
|
||
fetchRewardsForIssues
|
||
} = useRewards(activityId, currentIssueId)
|
||
|
||
const {
|
||
winRecords,
|
||
fetchWinRecords
|
||
} = useRecords()
|
||
|
||
// ============ 本地状态 ============
|
||
const tabActive = ref('pool')
|
||
const rewardsVisible = ref(false)
|
||
const rulesVisible = ref(false)
|
||
const cabinetVisible = ref(false)
|
||
const showFlip = ref(false)
|
||
const flipRef = ref(null)
|
||
const yifanSelectorRef = ref(null)
|
||
const selectedCount = ref(0) // 从外部追踪选中数量
|
||
|
||
// 接收选中变化事件
|
||
function onSelectionChange(items) {
|
||
selectedCount.value = Array.isArray(items) ? items.length : 0
|
||
}
|
||
|
||
// 触发随机选号
|
||
function handleRandomDraw() {
|
||
if (yifanSelectorRef.value && yifanSelectorRef.value.handleRandomOne) {
|
||
yifanSelectorRef.value.handleRandomOne()
|
||
}
|
||
}
|
||
|
||
// 触发支付
|
||
function handlePayment() {
|
||
if (yifanSelectorRef.value && yifanSelectorRef.value.handleBuy) {
|
||
yifanSelectorRef.value.handleBuy()
|
||
}
|
||
}
|
||
|
||
// ============ 倒计时相关(一番赏专属) ============
|
||
const nowMs = ref(Date.now())
|
||
let nowTimer = null
|
||
|
||
function startNowTicker() {
|
||
stopNowTicker()
|
||
nowMs.value = Date.now()
|
||
nowTimer = setInterval(() => { nowMs.value = Date.now() }, 1000)
|
||
}
|
||
|
||
function stopNowTicker() {
|
||
if (nowTimer) {
|
||
clearInterval(nowTimer)
|
||
nowTimer = null
|
||
}
|
||
}
|
||
|
||
const scheduledTime = computed(() => detail.value?.scheduled_time || detail.value?.scheduledTime || '')
|
||
const scheduledTimeMs = computed(() => parseTimeMs(scheduledTime.value))
|
||
const scheduledTimeText = computed(() => formatDateTime(scheduledTime.value))
|
||
|
||
const remainMs = computed(() => {
|
||
const end = scheduledTimeMs.value
|
||
if (!end) return null
|
||
return end - nowMs.value
|
||
})
|
||
|
||
const isOrderAllowed = computed(() => {
|
||
const ms = remainMs.value
|
||
if (ms === null) return true
|
||
return ms > 25000
|
||
})
|
||
|
||
const orderBlockedReason = computed(() => {
|
||
const ms = remainMs.value
|
||
if (ms === null) return ''
|
||
if (ms <= 0) return '本期已结束,暂不可下单'
|
||
if (ms <= 25000) return '距本期结束不足25秒,暂不可下单'
|
||
return ''
|
||
})
|
||
|
||
// ============ 业务方法 ============
|
||
function showRules() {
|
||
rulesVisible.value = true
|
||
}
|
||
|
||
function goCabinet() {
|
||
cabinetVisible.value = true
|
||
}
|
||
|
||
function closeFlip() {
|
||
showFlip.value = false
|
||
}
|
||
|
||
function onPaymentSuccess(payload) {
|
||
console.log('Payment Success:', payload)
|
||
|
||
const result = payload.result
|
||
const status = String(result?.status || result?.data?.status || result?.result?.status || '')
|
||
|
||
if (status === 'paid_waiting') {
|
||
const next = result?.next_draw_time || result?.nextDrawTime || result?.next_draw_at || result?.nextDrawAt
|
||
const nextText = next ? formatDateTime(next) : ''
|
||
const content = nextText
|
||
? `下单成功,等待系统自动开启本期赏品。\n预计开赏时间:${nextText}`
|
||
: '下单成功,等待系统自动开启本期赏品。'
|
||
uni.showModal({
|
||
title: '下单成功',
|
||
content,
|
||
showCancel: false
|
||
})
|
||
return
|
||
}
|
||
|
||
let wonItems = []
|
||
if (Array.isArray(result)) {
|
||
wonItems = result
|
||
} else if (result?.list) {
|
||
wonItems = result.list
|
||
} else if (result?.data) {
|
||
wonItems = result.data
|
||
} else if (result?.rewards) {
|
||
wonItems = result.rewards
|
||
} else {
|
||
wonItems = result ? [result] : []
|
||
}
|
||
|
||
const items = wonItems.map(data => ({
|
||
title: String(data?.title || data?.name || data?.product_name || data?.reward_name || '未知奖励'),
|
||
image: String(data?.image || data?.img || data?.pic || data?.product_image || data?.reward_image || '')
|
||
}))
|
||
|
||
showFlip.value = true
|
||
try { flipRef.value?.reset?.() } catch (_) {}
|
||
|
||
setTimeout(() => {
|
||
flipRef.value?.revealResults?.(items)
|
||
}, 100)
|
||
}
|
||
|
||
// ============ 生命周期 ============
|
||
onLoad(async (opts) => {
|
||
startNowTicker()
|
||
const id = opts?.id || ''
|
||
if (!id) return
|
||
activityId.value = id
|
||
// 并行获取活动详情和期数信息
|
||
await Promise.all([fetchDetail(), fetchIssues()])
|
||
setNavigationTitle('一番赏')
|
||
// 期数获取完成后获取奖励
|
||
await fetchRewardsForIssues(issues.value)
|
||
// 异步获取记录(不阻塞渲染)
|
||
if (currentIssueId.value) {
|
||
fetchWinRecords(id, currentIssueId.value)
|
||
}
|
||
})
|
||
|
||
onUnload(() => {
|
||
stopNowTicker()
|
||
})
|
||
|
||
// 监听期切换,刷新记录
|
||
watch(currentIssueId, (newId) => {
|
||
if (newId && activityId.value) {
|
||
fetchWinRecords(activityId.value, newId)
|
||
}
|
||
})
|
||
</script>
|
||
|
||
<style lang="scss" scoped>
|
||
/* 选号容器 - 与原始设计一致 */
|
||
.section-container {
|
||
margin: 0 $spacing-lg $spacing-lg;
|
||
background: rgba(255, 255, 255, 0.9);
|
||
border-radius: $radius-xl;
|
||
padding: $spacing-lg;
|
||
box-shadow: $shadow-sm;
|
||
backdrop-filter: blur(10rpx);
|
||
}
|
||
|
||
.selector-container {
|
||
margin-top: $spacing-md;
|
||
}
|
||
|
||
/* 期号切换 - 与原始设计一致 */
|
||
.issue-header {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
margin-bottom: 30rpx;
|
||
background: $bg-grey;
|
||
border-radius: $radius-round;
|
||
padding: 10rpx;
|
||
border: 1rpx solid $border-color-light;
|
||
}
|
||
|
||
.issue-switch-btn {
|
||
width: 72rpx;
|
||
height: 72rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
background: $bg-card;
|
||
border-radius: 50%;
|
||
box-shadow: $shadow-sm;
|
||
transition: all 0.2s;
|
||
color: $text-secondary;
|
||
|
||
&:active {
|
||
transform: scale(0.9);
|
||
background: $bg-secondary;
|
||
color: $brand-primary;
|
||
}
|
||
}
|
||
|
||
.arrow {
|
||
font-size: $font-sm;
|
||
font-weight: 800;
|
||
}
|
||
|
||
.issue-info-center {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
flex: 1;
|
||
}
|
||
|
||
.issue-current-text {
|
||
font-size: $font-lg;
|
||
font-weight: 700;
|
||
color: $text-main;
|
||
}
|
||
|
||
.issue-status-badge {
|
||
font-size: $font-xs;
|
||
color: $uni-color-success;
|
||
background: rgba($uni-color-success, 0.1);
|
||
padding: 2rpx $spacing-md;
|
||
border-radius: $radius-round;
|
||
margin-top: 4rpx;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.issue-block-tip {
|
||
background: rgba($color-warning, 0.1);
|
||
padding: $spacing-sm $spacing-md;
|
||
border-radius: $radius-md;
|
||
margin-bottom: $spacing-md;
|
||
}
|
||
|
||
.issue-block-text {
|
||
font-size: $font-sm;
|
||
color: $color-warning;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.selector-body {
|
||
margin-top: $spacing-sm;
|
||
}
|
||
|
||
/* 入场动画 */
|
||
.animate-enter {
|
||
animation: slideUp 0.5s ease-out both;
|
||
}
|
||
|
||
.stagger-2 {
|
||
animation-delay: 0.2s;
|
||
}
|
||
|
||
@keyframes slideUp {
|
||
from {
|
||
opacity: 0;
|
||
transform: translateY(20rpx);
|
||
}
|
||
to {
|
||
opacity: 1;
|
||
transform: translateY(0);
|
||
}
|
||
}
|
||
|
||
/* 翻牌弹窗 */
|
||
.flip-overlay {
|
||
position: fixed;
|
||
top: 0;
|
||
left: 0;
|
||
right: 0;
|
||
bottom: 0;
|
||
z-index: 1001;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
|
||
.flip-mask {
|
||
position: absolute;
|
||
top: 0;
|
||
left: 0;
|
||
right: 0;
|
||
bottom: 0;
|
||
background: rgba(0, 0, 0, 0.6);
|
||
}
|
||
|
||
.flip-content {
|
||
position: relative;
|
||
width: 90%;
|
||
max-height: 85vh;
|
||
background: $bg-card;
|
||
border-radius: $radius-xl;
|
||
padding: $spacing-lg;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.overlay-close {
|
||
margin-top: $spacing-lg;
|
||
width: 100%;
|
||
background: $gradient-brand;
|
||
color: #fff;
|
||
border: none;
|
||
border-radius: $radius-lg;
|
||
font-size: $font-md;
|
||
font-weight: 600;
|
||
padding: $spacing-md;
|
||
|
||
&::after {
|
||
border: none;
|
||
}
|
||
}
|
||
|
||
/* ============= 底部固定操作栏 ============= */
|
||
.float-bar {
|
||
position: fixed;
|
||
left: 32rpx;
|
||
right: 32rpx;
|
||
bottom: calc(40rpx + env(safe-area-inset-bottom));
|
||
z-index: 100;
|
||
animation: slideUp 0.4s cubic-bezier(0.23, 1, 0.32, 1) backwards;
|
||
}
|
||
|
||
.float-bar-inner {
|
||
background: rgba(255, 255, 255, 0.85);
|
||
backdrop-filter: blur(30rpx);
|
||
padding: 24rpx 40rpx;
|
||
border-radius: 999rpx;
|
||
display: flex;
|
||
flex-direction: row;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
box-shadow: 0 16rpx 48rpx rgba(0, 0, 0, 0.12);
|
||
border: 1rpx solid rgba(255, 255, 255, 0.6);
|
||
}
|
||
|
||
.selection-info {
|
||
font-size: 28rpx;
|
||
color: $text-main;
|
||
display: flex;
|
||
align-items: baseline;
|
||
font-weight: 800;
|
||
}
|
||
|
||
.highlight {
|
||
color: $brand-primary;
|
||
font-weight: 900;
|
||
font-size: 40rpx;
|
||
margin: 0 8rpx;
|
||
font-family: 'DIN Alternate', sans-serif;
|
||
}
|
||
|
||
.action-buttons {
|
||
display: flex;
|
||
gap: 20rpx;
|
||
}
|
||
|
||
.action-btn {
|
||
height: 88rpx;
|
||
line-height: 88rpx;
|
||
padding: 0 56rpx;
|
||
border-radius: 999rpx;
|
||
font-size: 30rpx;
|
||
font-weight: 900;
|
||
margin: 0;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
transition: all 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275);
|
||
border: none;
|
||
|
||
&::after {
|
||
border: none;
|
||
}
|
||
|
||
&:active {
|
||
transform: scale(0.92);
|
||
}
|
||
|
||
&.primary {
|
||
background: $gradient-brand !important;
|
||
color: #FFFFFF !important;
|
||
box-shadow: 0 12rpx 32rpx rgba($brand-primary, 0.35);
|
||
}
|
||
}
|
||
</style>
|