558 lines
14 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

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

<template>
<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>