1161 lines
31 KiB
Vue
1161 lines
31 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="shouldShowSelector">
|
||
<!-- 直接调用choice接口加载数据 -->
|
||
<view class="choice-container">
|
||
<view v-if="choicesLoading" class="loading-state">正在加载位置...</view>
|
||
<view v-else-if="choicesList.length === 0" class="empty-state">暂无可选位置</view>
|
||
<view v-else class="choices-grid">
|
||
<view
|
||
v-for="(item, index) in choicesList"
|
||
:key="item.id || index"
|
||
class="choice-item"
|
||
:class="{
|
||
'is-sold': item.status === 'sold' || item.is_sold,
|
||
'is-selected': isChoiceSelected(item),
|
||
'is-available': !item.status || item.status === 'available'
|
||
}"
|
||
@tap="handleSelectChoice(item)"
|
||
>
|
||
<text class="choice-number">{{ item.number || item.position || index + 1 }}</text>
|
||
<view class="choice-status">
|
||
<text v-if="item.status === 'sold' || item.is_sold">已售</text>
|
||
<text v-else-if="isChoiceSelected(item)">已选</text>
|
||
<text v-else>可选</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</template>
|
||
|
||
<template #footer>
|
||
<!-- 固定底部操作栏 -->
|
||
<view class="float-bar" v-show="!isPaymentVisible">
|
||
<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 v-if="gamePassRemaining > 0" class="game-pass-badge" @tap="() => {}">
|
||
<text class="badge-icon">🎮</text>
|
||
<text class="badge-text" style="font-size: 24rpx; font-weight: bold; color: #10B981;">{{ gamePassRemaining }}</text>
|
||
</view>
|
||
<!-- 充值入口 -->
|
||
<view class="game-pass-buy-btn" @tap="openPurchasePopup">
|
||
<text>购买次数</text>
|
||
</view>
|
||
请选择位置
|
||
</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"
|
||
/>
|
||
|
||
<!-- 支付弹窗(从 YifanSelector 提升到这里,确保祝福动画位置正确) -->
|
||
<PaymentPopup
|
||
v-model:visible="paymentVisible"
|
||
:amount="paymentAmount"
|
||
:coupons="paymentCoupons"
|
||
:gamePasses="gamePasses"
|
||
:showCards="false"
|
||
@confirm="onPaymentConfirm"
|
||
@cancel="onPaymentCancel"
|
||
/>
|
||
|
||
<GamePassPurchasePopup
|
||
v-model:visible="purchasePopupVisible"
|
||
:activity-id="activityId"
|
||
@success="onPurchaseSuccess"
|
||
/>
|
||
</template>
|
||
</ActivityPageLayout>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { ref, computed, watch } from 'vue'
|
||
import { onLoad, onUnload } from '@dcloudio/uni-app'
|
||
import { getIssueChoices, joinLottery, createWechatOrder, getLotteryResult, getUserCoupons } from '@/api/appUser'
|
||
import { requestLotterySubscription } from '@/utils/subscribe'
|
||
// 公共组件 - 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 PaymentPopup from '@/components/PaymentPopup.vue'
|
||
import GamePassPurchasePopup from '@/components/GamePassPurchasePopup.vue'
|
||
import { getGamePasses } from '@/api/appUser'
|
||
// 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) // 从外部追踪选中数量
|
||
const isPaymentVisible = ref(false) // 支付弹窗是否显示
|
||
const paymentVisible = ref(false) // 控制支付弹窗显示
|
||
const paymentAmount = ref('0') // 支付金额
|
||
const paymentCoupons = ref([]) // 可用优惠券
|
||
|
||
// Choices 相关状态
|
||
const choicesLoading = ref(false)
|
||
const choicesList = ref([])
|
||
const selectedChoices = ref([])
|
||
|
||
// 计算属性:判断是否应该显示选号器
|
||
const shouldShowSelector = computed(() => {
|
||
const result = !!(
|
||
activityId.value &&
|
||
currentIssueId.value &&
|
||
detail.value?.play_type === 'ichiban'
|
||
)
|
||
console.log('[Yifanshang] shouldShowSelector computed:', {
|
||
activityId: activityId.value,
|
||
currentIssueId: currentIssueId.value,
|
||
play_type: detail.value?.play_type,
|
||
result
|
||
})
|
||
return result
|
||
})
|
||
|
||
// 监控 shouldShowSelector 的变化
|
||
watch(shouldShowSelector, (newVal, oldVal) => {
|
||
console.log('[Yifanshang] shouldShowSelector changed:', {
|
||
from: oldVal,
|
||
to: newVal,
|
||
yifanSelectorRef: yifanSelectorRef.value
|
||
})
|
||
if (newVal && !yifanSelectorRef.value) {
|
||
console.warn('[Yifanshang] shouldShowSelector is true but yifanSelectorRef is null!')
|
||
}
|
||
})
|
||
|
||
// 加载choices数据
|
||
async function loadChoices() {
|
||
console.log('[Yifanshang] loadChoices called with:', {
|
||
activityId: activityId.value,
|
||
issueId: currentIssueId.value
|
||
})
|
||
|
||
if (!activityId.value || !currentIssueId.value) {
|
||
console.warn('[Yifanshang] Missing activityId or issueId')
|
||
return
|
||
}
|
||
|
||
choicesLoading.value = true
|
||
try {
|
||
console.log('[Yifanshang] Calling getIssueChoices API...')
|
||
const res = await getIssueChoices(activityId.value, currentIssueId.value)
|
||
console.log('[Yifanshang] getIssueChoices response:', res)
|
||
|
||
// 处理 { total_slots: 1, available: [1], claimed: [] } 这种格式
|
||
if (res && typeof res.total_slots === 'number' && Array.isArray(res.available)) {
|
||
const total = res.total_slots
|
||
const list = []
|
||
const availableSet = new Set(res.available.map(v => Number(v)))
|
||
|
||
for (let i = 1; i <= total; i++) {
|
||
const isAvailable = availableSet.has(i)
|
||
list.push({
|
||
id: i,
|
||
number: i,
|
||
position: i,
|
||
status: isAvailable ? 'available' : 'sold',
|
||
is_sold: !isAvailable
|
||
})
|
||
}
|
||
choicesList.value = list
|
||
} else if (Array.isArray(res)) {
|
||
choicesList.value = res
|
||
} else if (res && Array.isArray(res.data)) {
|
||
choicesList.value = res.data
|
||
} else if (res && Array.isArray(res.choices)) {
|
||
choicesList.value = res.choices
|
||
} else {
|
||
choicesList.value = []
|
||
}
|
||
|
||
console.log('[Yifanshang] Choices loaded, total:', choicesList.value.length)
|
||
} catch (error) {
|
||
console.error('[Yifanshang] Failed to load choices:', error)
|
||
uni.showToast({ title: '加载位置失败', icon: 'none' })
|
||
choicesList.value = []
|
||
} finally {
|
||
choicesLoading.value = false
|
||
}
|
||
}
|
||
|
||
// 判断某个choice是否被选中
|
||
function isChoiceSelected(item) {
|
||
return selectedChoices.value.some(i => i.id === item.id || (i.position && i.position === item.position))
|
||
}
|
||
|
||
// 处理选择choice
|
||
function handleSelectChoice(item) {
|
||
if (!isOrderAllowed.value) {
|
||
uni.showToast({ title: orderBlockedReason.value, icon: 'none' })
|
||
return
|
||
}
|
||
if (item.status === 'sold' || item.is_sold) {
|
||
return
|
||
}
|
||
|
||
const index = selectedChoices.value.findIndex(i => i.id === item.id || (i.position && i.position === item.position))
|
||
if (index > -1) {
|
||
selectedChoices.value.splice(index, 1)
|
||
} else {
|
||
selectedChoices.value.push(item)
|
||
}
|
||
|
||
console.log('[Yifanshang] Selected choices:', selectedChoices.value.length)
|
||
selectedCount.value = selectedChoices.value.length
|
||
}
|
||
|
||
// 监听 shouldShowSelector 变化,加载choices
|
||
watch(shouldShowSelector, (newVal) => {
|
||
if (newVal && (choicesList.value.length === 0 || !choicesList.value)) {
|
||
loadChoices()
|
||
}
|
||
})
|
||
|
||
// 接收选中变化事件
|
||
function onSelectionChange(items) {
|
||
selectedCount.value = Array.isArray(items) ? items.length : 0
|
||
}
|
||
|
||
// 接收支付弹窗显示状态变化(从 YifanSelector)
|
||
function onPaymentVisibleChange(visible) {
|
||
isPaymentVisible.value = visible
|
||
paymentVisible.value = visible
|
||
}
|
||
|
||
// 接收支付金额变化
|
||
function onPaymentAmountChange(amount) {
|
||
paymentAmount.value = amount
|
||
}
|
||
|
||
// 接收优惠券变化
|
||
function onPaymentCouponsChange(coupons) {
|
||
paymentCoupons.value = coupons
|
||
}
|
||
|
||
// 支付确认处理
|
||
async function onPaymentConfirm(paymentData) {
|
||
console.log('[Yifanshang] onPaymentConfirm called with:', paymentData)
|
||
|
||
if (selectedChoices.value.length === 0) {
|
||
uni.showToast({ title: '请先选择位置', icon: 'none' })
|
||
return
|
||
}
|
||
|
||
// 请求订阅消息 (尽可能早调用以确保在用户交互上下文中)
|
||
await requestLotterySubscription()
|
||
|
||
try {
|
||
uni.showLoading({ title: '创建订单中...' })
|
||
|
||
// 第一步:调用 join 接口创建订单
|
||
const joinData = {
|
||
activity_id: Number(activityId.value),
|
||
issue_id: Number(currentIssueId.value),
|
||
channel: 'miniapp',
|
||
count: selectedChoices.value.length,
|
||
slot_index: selectedChoices.value.map(c => Number(c.id || c.number)),
|
||
coupon_id: paymentData.coupon?.id ? Number(paymentData.coupon.id) : 0,
|
||
item_card_id: paymentData.card?.id ? Number(paymentData.card.id) : 0,
|
||
use_game_pass: paymentData.useGamePass || false
|
||
}
|
||
|
||
console.log('[Yifanshang] Calling join with:', joinData)
|
||
|
||
const joinResult = await joinLottery(joinData)
|
||
|
||
console.log('[Yifanshang] Join result:', joinResult)
|
||
|
||
if (!joinResult.order_no) {
|
||
throw new Error('创建订单失败:未返回订单号')
|
||
}
|
||
|
||
// 第二步:判断是否需要支付
|
||
if (joinResult.actual_amount > 0 && joinResult.status !== 2) {
|
||
// 使用订单号调用微信支付预下单接口
|
||
uni.showLoading({ title: '拉起支付...' })
|
||
|
||
// 获取 openid
|
||
const openid = uni.getStorageSync('openid')
|
||
if (!openid) {
|
||
throw new Error('缺少OpenID,请重新登录')
|
||
}
|
||
|
||
const preorderData = {
|
||
openid: openid,
|
||
order_no: joinResult.order_no
|
||
}
|
||
|
||
console.log('[Yifanshang] Calling wechat preorder with:', preorderData)
|
||
|
||
const paymentParams = await createWechatOrder(preorderData)
|
||
console.log('[Yifanshang] Wechat payment params:', paymentParams)
|
||
|
||
uni.hideLoading()
|
||
|
||
// 第三步:拉起支付
|
||
// #ifdef MP-WEIXIN
|
||
const payResult = await uni.requestPayment({
|
||
provider: 'wxpay',
|
||
...paymentParams
|
||
})
|
||
console.log('[Yifanshang] Payment result:', payResult)
|
||
// #endif
|
||
|
||
// #ifdef MP-TOUTIAO
|
||
const payResult = await tt.pay({
|
||
...paymentParams
|
||
})
|
||
console.log('[Yifanshang] Payment result:', payResult)
|
||
// #endif
|
||
} else {
|
||
console.log('[Yifanshang] Order is free or paid, skipping payment flow. Amount:', joinResult.actual_amount, 'Status:', joinResult.status)
|
||
}
|
||
|
||
// 第四步:支付成功后,查询抽奖结果
|
||
uni.showLoading({ title: '查询结果中...' })
|
||
|
||
console.log('[Yifanshang] Getting lottery result with order_no:', joinResult.order_no)
|
||
|
||
const lotteryResult = await getLotteryResult(joinResult.order_no)
|
||
|
||
console.log('[Yifanshang] Lottery result:', lotteryResult)
|
||
|
||
uni.hideLoading()
|
||
|
||
// 检查状态:如果是 paid_waiting,提示用户等待系统开启
|
||
const status = lotteryResult?.status || (lotteryResult?.result?.status) || ''
|
||
if (status === 'paid_waiting') {
|
||
const nextDrawTime = lotteryResult?.next_draw_time || lotteryResult?.result?.next_draw_time
|
||
const nextTimeText = nextDrawTime ? formatDateTime(nextDrawTime) : ''
|
||
const content = nextTimeText
|
||
? `下单成功,等待系统自动开启本期赏品。\n预计开赏时间:${nextTimeText}`
|
||
: '下单成功,等待系统自动开启本期赏品。'
|
||
|
||
uni.showModal({
|
||
title: '下单成功',
|
||
content,
|
||
showCancel: false,
|
||
success: () => {
|
||
// 清空选择并关闭弹窗
|
||
selectedChoices.value = []
|
||
selectedCount.value = 0
|
||
paymentVisible.value = false
|
||
loadChoices()
|
||
}
|
||
})
|
||
return
|
||
}
|
||
|
||
// 抽奖成功,清空选择
|
||
selectedChoices.value = []
|
||
selectedCount.value = 0
|
||
|
||
uni.showToast({
|
||
title: '购买成功!',
|
||
icon: 'success'
|
||
})
|
||
|
||
// 关闭支付弹窗
|
||
paymentVisible.value = false
|
||
|
||
// 重新加载choices
|
||
await loadChoices()
|
||
|
||
// 显示翻牌结果
|
||
if (lotteryResult) {
|
||
showFlip.value = true
|
||
try { flipRef.value?.reset?.() } catch (_) {}
|
||
|
||
setTimeout(() => {
|
||
// 处理不同的结果格式
|
||
let items = []
|
||
|
||
if (Array.isArray(lotteryResult)) {
|
||
items = lotteryResult.map(data => ({
|
||
title: String(data?.title || data?.name || '未知奖励'),
|
||
image: String(data?.image || data?.img || '')
|
||
}))
|
||
} else if (lotteryResult.list && Array.isArray(lotteryResult.list)) {
|
||
items = lotteryResult.list.map(data => ({
|
||
title: String(data?.title || data?.name || '未知奖励'),
|
||
image: String(data?.image || data?.img || '')
|
||
}))
|
||
} else if (lotteryResult.rewards && Array.isArray(lotteryResult.rewards)) {
|
||
items = lotteryResult.rewards.map(data => ({
|
||
title: String(data?.title || data?.name || '未知奖励'),
|
||
image: String(data?.image || data?.img || '')
|
||
}))
|
||
} else if (lotteryResult.data && Array.isArray(lotteryResult.data)) {
|
||
items = lotteryResult.data.map(data => ({
|
||
title: String(data?.title || data?.name || '未知奖励'),
|
||
image: String(data?.image || data?.img || '')
|
||
}))
|
||
} else {
|
||
// 单个结果
|
||
items = [{
|
||
title: String(lotteryResult?.title || lotteryResult?.name || '未知奖励'),
|
||
image: String(lotteryResult?.image || lotteryResult?.img || '')
|
||
}]
|
||
}
|
||
|
||
console.log('[Yifanshang] Processed reward items:', items)
|
||
flipRef.value?.revealResults?.(items)
|
||
}, 100)
|
||
}
|
||
|
||
} catch (error) {
|
||
console.error('[Yifanshang] Payment/Join failed:', error)
|
||
uni.hideLoading()
|
||
|
||
// 处理支付取消
|
||
if (error.errMsg && error.errMsg.includes('cancel')) {
|
||
uni.showToast({
|
||
title: '已取消支付',
|
||
icon: 'none'
|
||
})
|
||
} else {
|
||
uni.showToast({
|
||
title: error.message || error.errMsg || '支付失败',
|
||
icon: 'none'
|
||
})
|
||
}
|
||
}
|
||
}
|
||
|
||
// 支付取消处理
|
||
function onPaymentCancel() {
|
||
console.log('[Yifanshang] Payment cancelled')
|
||
// 支付弹窗会自动关闭
|
||
}
|
||
|
||
// 触发支付
|
||
function handlePayment() {
|
||
console.log('[Yifanshang] handlePayment called, selectedChoices:', selectedChoices.value.length)
|
||
|
||
if (selectedChoices.value.length === 0) {
|
||
uni.showToast({ title: '请先选择位置', icon: 'none' })
|
||
return
|
||
}
|
||
|
||
if (!isOrderAllowed.value) {
|
||
uni.showToast({ title: orderBlockedReason.value, icon: 'none' })
|
||
return
|
||
}
|
||
|
||
// 计算支付金额
|
||
const amount = (selectedChoices.value.length * Number(detail.value.price_draw || 0) / 100).toFixed(2)
|
||
paymentAmount.value = amount
|
||
|
||
// 获取优惠券
|
||
fetchCoupons()
|
||
|
||
console.log('[Yifanshang] Payment amount:', amount, 'Opening payment popup...')
|
||
|
||
// 显示支付弹窗
|
||
paymentVisible.value = true
|
||
}
|
||
|
||
// 触发随机选号
|
||
function handleRandomDraw() {
|
||
console.log('[Yifanshang] handleRandomDraw called')
|
||
|
||
if (!isOrderAllowed.value) {
|
||
uni.showToast({ title: orderBlockedReason.value, icon: 'none' })
|
||
return
|
||
}
|
||
|
||
// 随机选择一个可用位置
|
||
const availableChoices = choicesList.value.filter(c => c.status === 'available' || !c.is_sold)
|
||
if (availableChoices.length === 0) {
|
||
uni.showToast({ title: '暂无可用位置', icon: 'none' })
|
||
return
|
||
}
|
||
|
||
const randomIndex = Math.floor(Math.random() * availableChoices.length)
|
||
const randomChoice = availableChoices[randomIndex]
|
||
selectedChoices.value = [randomChoice]
|
||
selectedCount.value = 1
|
||
|
||
console.log('[Yifanshang] Random choice selected:', randomChoice)
|
||
|
||
// 自动触发支付
|
||
handlePayment()
|
||
}
|
||
|
||
// ============ 优惠券逻辑 (Fix BUG 5) ============
|
||
async function fetchCoupons() {
|
||
const userId = uni.getStorageSync('user_id')
|
||
if (!userId) return
|
||
|
||
try {
|
||
// 获取未使用(status=1)的优惠券
|
||
const res = await getUserCoupons(userId, 1, 1, 100)
|
||
if (res && Array.isArray(res.list)) {
|
||
// 简单过滤:只显示未过期的(虽然接口可能已过滤)
|
||
// TODO: 如果需要根据活动ID过滤适用券,需后端支持或在此处根据规则过滤
|
||
paymentCoupons.value = res.list
|
||
} else {
|
||
paymentCoupons.value = []
|
||
}
|
||
console.log('[Yifanshang] Fetched coupons:', paymentCoupons.value.length)
|
||
} catch (e) {
|
||
console.error('[Yifanshang] Failed to fetch coupons:', e)
|
||
paymentCoupons.value = []
|
||
}
|
||
}
|
||
|
||
// ============ 次数卡逻辑 ============
|
||
const gamePasses = ref(null)
|
||
const gamePassRemaining = computed(() => gamePasses.value?.total_remaining || 0)
|
||
const purchasePopupVisible = ref(false)
|
||
|
||
async function fetchPasses() {
|
||
if (!activityId.value) return
|
||
try {
|
||
const res = await getGamePasses(activityId.value)
|
||
gamePasses.value = res || null
|
||
} catch (e) {
|
||
gamePasses.value = null
|
||
}
|
||
}
|
||
|
||
function openPurchasePopup() {
|
||
purchasePopupVisible.value = true
|
||
}
|
||
|
||
function onPurchaseSuccess() {
|
||
fetchPasses()
|
||
}
|
||
|
||
// ============ 倒计时相关(一番赏专属) ============
|
||
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)
|
||
}
|
||
// 获取次数卡
|
||
fetchPasses()
|
||
})
|
||
|
||
onUnload(() => {
|
||
stopNowTicker()
|
||
})
|
||
|
||
// 监听期切换,刷新记录
|
||
watch(currentIssueId, (newId) => {
|
||
console.log('[Yifanshang] currentIssueId changed:', newId, 'activityId:', activityId.value)
|
||
if (newId && activityId.value) {
|
||
fetchWinRecords(activityId.value, newId)
|
||
}
|
||
})
|
||
|
||
// 监听 activityId 和 currentIssueId,用于调试
|
||
watch([activityId, currentIssueId], ([newActId, newIssueId]) => {
|
||
console.log('[Yifanshang] Props state:', {
|
||
activityId: newActId,
|
||
currentIssueId: newIssueId,
|
||
play_type: detail.value?.play_type,
|
||
bothExist: !!(newActId && newIssueId),
|
||
shouldShowSelector: !!(newActId && newIssueId && detail.value?.play_type === 'ichiban')
|
||
})
|
||
}, { immediate: true })
|
||
</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;
|
||
}
|
||
|
||
/* Choices 网格 */
|
||
.choice-container {
|
||
padding: $spacing-md;
|
||
}
|
||
|
||
.loading-state,
|
||
.empty-state {
|
||
text-align: center;
|
||
padding: 40rpx;
|
||
color: $text-tertiary;
|
||
font-size: $font-md;
|
||
}
|
||
|
||
.choices-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(5, 1fr);
|
||
gap: 16rpx;
|
||
padding: 20rpx 0;
|
||
}
|
||
|
||
.choice-item {
|
||
aspect-ratio: 1;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
background: $bg-card;
|
||
border-radius: $radius-md;
|
||
border: 2rpx solid $border-color;
|
||
transition: all 0.2s;
|
||
position: relative;
|
||
|
||
&.is-available {
|
||
background: linear-gradient(135deg, #e3f2fd, #bbdefb);
|
||
border-color: #2196f3;
|
||
cursor: pointer;
|
||
|
||
&:active {
|
||
transform: scale(0.95);
|
||
}
|
||
}
|
||
|
||
&.is-sold {
|
||
background: #f5f5f5;
|
||
border-color: #ddd;
|
||
opacity: 0.6;
|
||
}
|
||
|
||
&.is-selected {
|
||
background: linear-gradient(135deg, #fff3e0, #ffe0b2);
|
||
border-color: #ff9800;
|
||
box-shadow: 0 0 0 4rpx rgba(255, 152, 0, 0.2);
|
||
}
|
||
}
|
||
|
||
.choice-number {
|
||
font-size: 32rpx;
|
||
font-weight: 700;
|
||
color: $text-main;
|
||
margin-bottom: 8rpx;
|
||
}
|
||
|
||
.choice-status {
|
||
font-size: 20rpx;
|
||
color: $text-tertiary;
|
||
|
||
text {
|
||
padding: 4rpx 12rpx;
|
||
border-radius: 20rpx;
|
||
background: rgba(255, 255, 255, 0.8);
|
||
}
|
||
}
|
||
|
||
/* 入场动画 */
|
||
.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>
|
||
|
||
<style lang="scss" scoped>
|
||
/* 浮动操作栏扩展 - 充值按钮 & Badge */
|
||
.game-pass-badge {
|
||
display: flex;
|
||
align-items: center;
|
||
background: rgba(16, 185, 129, 0.15);
|
||
padding: 6rpx 16rpx;
|
||
border-radius: 30rpx;
|
||
border: 1rpx solid rgba(16, 185, 129, 0.3);
|
||
margin: 0 12rpx;
|
||
animation: pulse 2s infinite;
|
||
|
||
.badge-icon {
|
||
font-size: 28rpx;
|
||
margin-right: 6rpx;
|
||
}
|
||
|
||
.badge-text {
|
||
font-size: 24rpx;
|
||
color: #10B981;
|
||
font-weight: 600;
|
||
}
|
||
|
||
&:active {
|
||
opacity: 0.8;
|
||
}
|
||
}
|
||
|
||
.game-pass-buy-btn {
|
||
background: linear-gradient(90deg, #FF9F43, #FF6B00);
|
||
color: #fff;
|
||
font-size: 22rpx;
|
||
padding: 6rpx 16rpx;
|
||
border-radius: 24rpx;
|
||
margin-right: 12rpx;
|
||
font-weight: 600;
|
||
box-shadow: 0 4rpx 8rpx rgba(255, 107, 0, 0.2);
|
||
|
||
&:active {
|
||
transform: scale(0.95);
|
||
}
|
||
}
|
||
|
||
@keyframes pulse {
|
||
0% { transform: scale(1); }
|
||
50% { transform: scale(1.05); }
|
||
100% { transform: scale(1); }
|
||
}
|
||
</style>
|