501 lines
13 KiB
Vue
501 lines
13 KiB
Vue
<template>
|
||
<ActivityPageLayout :cover-url="coverUrl" bottom-padding="220rpx">
|
||
<template #header>
|
||
<ActivityHeader
|
||
:title="detail.name || detail.title || '无限赏'"
|
||
:price="detail.price_draw"
|
||
price-unit="/发"
|
||
:cover-url="coverUrl"
|
||
:tags="['公开透明', '随机掉落']"
|
||
@show-rules="showRules"
|
||
@go-cabinet="goCabinet"
|
||
/>
|
||
</template>
|
||
|
||
<template #content>
|
||
<ActivityTabs v-model="tabActive" :stagger="1">
|
||
<template #pool>
|
||
<RewardsPreview
|
||
title="奖池配置"
|
||
:rewards="currentIssueRewards"
|
||
:grouped="true"
|
||
@view-all="rewardsVisible = true"
|
||
/>
|
||
</template>
|
||
<template #records>
|
||
<RecordsList :records="winRecords" />
|
||
</template>
|
||
</ActivityTabs>
|
||
</template>
|
||
|
||
<template #footer>
|
||
<!-- 底部多档位抽赏按钮 -->
|
||
<view class="bottom-actions">
|
||
<view class="tier-btn" @tap="openPayment(1)">
|
||
<text class="tier-price">¥{{ (pricePerDraw * 1).toFixed(2) }}</text>
|
||
<text class="tier-label">抽1发</text>
|
||
</view>
|
||
<view class="tier-btn" @tap="openPayment(3)">
|
||
<text class="tier-price">¥{{ (pricePerDraw * 3).toFixed(2) }}</text>
|
||
<text class="tier-label">抽3发</text>
|
||
</view>
|
||
<view class="tier-btn" @tap="openPayment(5)">
|
||
<text class="tier-price">¥{{ (pricePerDraw * 5).toFixed(2) }}</text>
|
||
<text class="tier-label">抽5发</text>
|
||
</view>
|
||
<view class="tier-btn tier-hot" @tap="openPayment(10)">
|
||
<text class="tier-price">¥{{ (pricePerDraw * 10).toFixed(2) }}</text>
|
||
<text class="tier-label">抽10发</text>
|
||
</view>
|
||
</view>
|
||
</template>
|
||
|
||
<template #modals>
|
||
<RewardsPopup
|
||
v-model:visible="rewardsVisible"
|
||
:title="`${currentIssueTitle} · 奖池与概率`"
|
||
:reward-groups="rewardGroups"
|
||
/>
|
||
|
||
<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>
|
||
|
||
<PaymentPopup
|
||
v-model:visible="paymentVisible"
|
||
:amount="paymentAmount"
|
||
:coupons="coupons"
|
||
:propCards="propCards"
|
||
@confirm="onPaymentConfirm"
|
||
/>
|
||
</template>
|
||
</ActivityPageLayout>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { ref, computed, nextTick, watch } from 'vue'
|
||
import { onLoad } 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 FlipGrid from '@/components/FlipGrid.vue'
|
||
import PaymentPopup from '@/components/PaymentPopup.vue'
|
||
// Composables
|
||
import { useActivity, useIssues, useRewards, useRecords } from '@/composables'
|
||
// API
|
||
import { joinLottery, createWechatOrder, getLotteryResult, getItemCards, getUserCoupons } from '@/api/appUser'
|
||
|
||
// ============ 使用Composables ============
|
||
const activityId = ref('')
|
||
|
||
const {
|
||
detail,
|
||
coverUrl,
|
||
fetchDetail,
|
||
setNavigationTitle
|
||
} = useActivity(activityId)
|
||
|
||
const pricePerDraw = computed(() => Number(detail.value?.price_draw || 0) / 100)
|
||
|
||
const {
|
||
issues,
|
||
currentIssueId,
|
||
currentIssueTitle,
|
||
fetchIssues
|
||
} = useIssues(activityId)
|
||
|
||
const {
|
||
currentIssueRewards,
|
||
rewardGroups,
|
||
fetchRewardsForIssues
|
||
} = useRewards(activityId, currentIssueId)
|
||
|
||
const {
|
||
winRecords,
|
||
fetchWinRecords
|
||
} = useRecords()
|
||
|
||
// ============ 本地状态 ============
|
||
const tabActive = ref('pool')
|
||
const rewardsVisible = ref(false)
|
||
const showFlip = ref(false)
|
||
const flipRef = ref(null)
|
||
const drawLoading = ref(false)
|
||
|
||
// 支付相关
|
||
const paymentVisible = ref(false)
|
||
const paymentAmount = ref('0.00')
|
||
const coupons = ref([])
|
||
const propCards = ref([])
|
||
const pendingCount = ref(1)
|
||
const selectedCoupon = ref(null)
|
||
const selectedCard = ref(null)
|
||
|
||
// ============ 业务方法 ============
|
||
function showRules() {
|
||
uni.showModal({
|
||
title: '活动规则',
|
||
content: detail.value.rules || '1. 选择档位进行抽赏\n2. 每次抽赏随机获得奖品\n3. 奖池与概率以页面展示为准',
|
||
showCancel: false
|
||
})
|
||
}
|
||
|
||
function goCabinet() {
|
||
uni.switchTab({ url: '/pages/cabinet/index' })
|
||
}
|
||
|
||
function closeFlip() {
|
||
showFlip.value = false
|
||
}
|
||
|
||
function openPayment(count) {
|
||
const times = Math.max(1, Number(count || 1))
|
||
pendingCount.value = times
|
||
paymentAmount.value = (pricePerDraw.value * times).toFixed(2)
|
||
const token = uni.getStorageSync('token')
|
||
const phoneBound = !!uni.getStorageSync('phone_bound')
|
||
if (!token || !phoneBound) {
|
||
uni.showModal({
|
||
title: '提示',
|
||
content: '请先登录并绑定手机号',
|
||
confirmText: '去登录',
|
||
success: (res) => { if (res.confirm) uni.navigateTo({ url: '/pages/login/index' }) }
|
||
})
|
||
return
|
||
}
|
||
paymentVisible.value = true
|
||
fetchPropCards()
|
||
fetchCoupons()
|
||
}
|
||
|
||
async function onPaymentConfirm(data) {
|
||
selectedCoupon.value = data?.coupon || null
|
||
selectedCard.value = data?.card || null
|
||
paymentVisible.value = false
|
||
await onMachineDraw(pendingCount.value)
|
||
}
|
||
|
||
async function fetchPropCards() {
|
||
const user_id = uni.getStorageSync('user_id')
|
||
if (!user_id) return
|
||
try {
|
||
const res = await getItemCards(user_id)
|
||
let list = Array.isArray(res) ? res : (res?.list || res?.data || [])
|
||
propCards.value = list.map((i, idx) => ({
|
||
id: i.id ?? i.card_id ?? String(idx),
|
||
name: i.name ?? i.title ?? '道具卡'
|
||
}))
|
||
} catch (e) {
|
||
propCards.value = []
|
||
}
|
||
}
|
||
|
||
async function fetchCoupons() {
|
||
const user_id = uni.getStorageSync('user_id')
|
||
if (!user_id) return
|
||
try {
|
||
const res = await getUserCoupons(user_id, 0, 1, 100)
|
||
let list = Array.isArray(res) ? res : (res?.list || res?.data || [])
|
||
coupons.value = list.map((i, idx) => {
|
||
const amountCents = i.remaining ?? i.amount ?? i.value ?? 0
|
||
const amt = isNaN(amountCents) ? 0 : (Number(amountCents) / 100)
|
||
return {
|
||
id: i.id ?? i.coupon_id ?? String(idx),
|
||
name: i.name ?? i.title ?? '优惠券',
|
||
amount: amt.toFixed(2)
|
||
}
|
||
})
|
||
} catch (e) {
|
||
coupons.value = []
|
||
}
|
||
}
|
||
|
||
function extractResultList(resultRes) {
|
||
const root = resultRes?.data ?? resultRes?.result ?? resultRes
|
||
if (!root) return []
|
||
return root.results || root.list || root.items || root.data || []
|
||
}
|
||
|
||
function mapResultsToFlipItems(resultRes, poolRewards) {
|
||
const list = extractResultList(resultRes)
|
||
const poolArr = Array.isArray(poolRewards) ? poolRewards : []
|
||
const lookup = new Map()
|
||
poolArr.forEach(it => {
|
||
const id = it?.id ?? it?.reward_id ?? it?.product_id
|
||
if (id !== undefined) lookup.set(Number(id), it)
|
||
})
|
||
|
||
return list.filter(Boolean).map(d => {
|
||
const rewardId = d.reward_id ?? d.rewardId ?? d.product_id ?? d.productId ?? d.id
|
||
const rewardName = String(d.reward_name ?? d.rewardName ?? d.product_name ?? d.productName ?? d.title ?? d.name ?? '')
|
||
const fromId = Number.isFinite(Number(rewardId)) ? lookup.get(Number(rewardId)) : null
|
||
const fromName = !fromId && rewardName ? poolArr.find(x => x?.title === rewardName) : null
|
||
const it = fromId || fromName || null
|
||
return {
|
||
title: rewardName || it?.title || '奖励',
|
||
image: it?.image || d.image || d.img || d.pic || d.product_image || ''
|
||
}
|
||
})
|
||
}
|
||
|
||
async function onMachineDraw(count) {
|
||
const aid = activityId.value
|
||
const iid = currentIssueId.value
|
||
if (!aid || !iid) {
|
||
uni.showToast({ title: '期数未选择', icon: 'none' })
|
||
return
|
||
}
|
||
|
||
const token = uni.getStorageSync('token')
|
||
const phoneBound = !!uni.getStorageSync('phone_bound')
|
||
if (!token || !phoneBound) {
|
||
uni.showModal({
|
||
title: '提示',
|
||
content: '请先登录并绑定手机号',
|
||
confirmText: '去登录',
|
||
success: (res) => { if (res.confirm) uni.navigateTo({ url: '/pages/login/index' }) }
|
||
})
|
||
return
|
||
}
|
||
|
||
const openid = uni.getStorageSync('openid')
|
||
if (!openid) {
|
||
uni.showToast({ title: '缺少OpenID,请重新登录', icon: 'none' })
|
||
return
|
||
}
|
||
|
||
drawLoading.value = true
|
||
try {
|
||
const times = Math.max(1, Number(count || 1))
|
||
const joinRes = await joinLottery({
|
||
activity_id: Number(aid),
|
||
issue_id: Number(iid),
|
||
channel: 'miniapp',
|
||
count: times,
|
||
coupon_id: selectedCoupon.value?.id ? Number(selectedCoupon.value.id) : 0,
|
||
item_card_id: selectedCard.value?.id ? Number(selectedCard.value.id) : 0
|
||
})
|
||
|
||
const orderNo = joinRes?.order_no || joinRes?.data?.order_no || joinRes?.result?.order_no
|
||
if (!orderNo) throw new Error('未获取到订单号')
|
||
|
||
const payRes = await createWechatOrder({ openid, order_no: orderNo })
|
||
await new Promise((resolve, reject) => {
|
||
uni.requestPayment({
|
||
provider: 'wxpay',
|
||
timeStamp: payRes.timeStamp || payRes.timestamp,
|
||
nonceStr: payRes.nonceStr || payRes.noncestr,
|
||
package: payRes.package,
|
||
signType: payRes.signType || 'MD5',
|
||
paySign: payRes.paySign,
|
||
success: resolve,
|
||
fail: reject
|
||
})
|
||
})
|
||
|
||
const resultRes = await getLotteryResult(orderNo)
|
||
const items = mapResultsToFlipItems(resultRes, currentIssueRewards.value)
|
||
|
||
showFlip.value = true
|
||
await nextTick()
|
||
try { flipRef.value?.reset?.() } catch (_) {}
|
||
setTimeout(() => {
|
||
flipRef.value?.revealResults?.(items)
|
||
}, 100)
|
||
} catch (e) {
|
||
uni.showToast({ title: e.message || '操作失败', icon: 'none' })
|
||
} finally {
|
||
drawLoading.value = false
|
||
}
|
||
}
|
||
|
||
// ============ 生命周期 ============
|
||
onLoad(async (opts) => {
|
||
const id = opts?.id || ''
|
||
if (id) {
|
||
activityId.value = id
|
||
await fetchDetail()
|
||
setNavigationTitle('无限赏')
|
||
await fetchIssues()
|
||
await fetchRewardsForIssues(issues.value)
|
||
if (currentIssueId.value) {
|
||
fetchWinRecords(id, currentIssueId.value)
|
||
}
|
||
}
|
||
})
|
||
|
||
// 监听期切换,刷新记录
|
||
watch(currentIssueId, (newId) => {
|
||
if (newId && activityId.value) {
|
||
fetchWinRecords(activityId.value, newId)
|
||
}
|
||
})
|
||
</script>
|
||
|
||
<style lang="scss" scoped>
|
||
/* 底部多档位操作按钮 - 原始设计 */
|
||
.bottom-actions {
|
||
position: fixed;
|
||
left: 0;
|
||
right: 0;
|
||
bottom: 0;
|
||
display: flex;
|
||
gap: 20rpx;
|
||
padding: 32rpx 32rpx;
|
||
padding-bottom: calc(32rpx + env(safe-area-inset-bottom));
|
||
background: rgba(255, 255, 255, 0.85);
|
||
backdrop-filter: blur(30rpx);
|
||
box-shadow: 0 -12rpx 40rpx rgba(0, 0, 0, 0.08);
|
||
z-index: 999;
|
||
border-top: 1rpx solid rgba(255, 255, 255, 0.8);
|
||
}
|
||
|
||
.tier-btn {
|
||
flex: 1;
|
||
min-width: 0;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
padding: 24rpx 10rpx;
|
||
background: #FFF;
|
||
border: 2rpx solid rgba($brand-primary, 0.1);
|
||
border-radius: 28rpx;
|
||
box-shadow: 0 4rpx 12rpx rgba(0,0,0,0.03);
|
||
transition: all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1);
|
||
margin: 0;
|
||
line-height: normal;
|
||
|
||
&::after {
|
||
border: none;
|
||
}
|
||
|
||
&:active {
|
||
transform: scale(0.92);
|
||
background: #F9F9F9;
|
||
box-shadow: none;
|
||
}
|
||
}
|
||
|
||
.tier-price {
|
||
font-size: 34rpx;
|
||
font-weight: 900;
|
||
color: $text-main;
|
||
font-family: 'DIN Alternate', sans-serif;
|
||
letter-spacing: -1rpx;
|
||
}
|
||
|
||
.tier-label {
|
||
font-size: 22rpx;
|
||
color: $brand-primary;
|
||
margin-top: 6rpx;
|
||
font-weight: 800;
|
||
font-style: italic;
|
||
}
|
||
|
||
/* 热门/最高档位 - 高级动效 */
|
||
.tier-hot {
|
||
background: $gradient-brand !important;
|
||
border: none !important;
|
||
box-shadow: 0 12rpx 32rpx rgba($brand-primary, 0.35) !important;
|
||
position: relative;
|
||
overflow: hidden;
|
||
|
||
.tier-price {
|
||
color: #FFF !important;
|
||
text-shadow: 0 2rpx 4rpx rgba(0, 0, 0, 0.1);
|
||
}
|
||
|
||
.tier-label {
|
||
color: rgba(255, 255, 255, 0.9) !important;
|
||
text-shadow: 0 1rpx 2rpx rgba(0, 0, 0, 0.1);
|
||
}
|
||
|
||
/* 流光效果 */
|
||
&::before {
|
||
content: '';
|
||
position: absolute;
|
||
top: -50%;
|
||
left: -150%;
|
||
width: 200%;
|
||
height: 200%;
|
||
background: linear-gradient(
|
||
to right,
|
||
transparent,
|
||
rgba(255, 255, 255, 0.25),
|
||
transparent
|
||
);
|
||
transform: rotate(30deg);
|
||
animation: shine 3s ease-in-out infinite;
|
||
}
|
||
|
||
&:active {
|
||
transform: scale(0.92);
|
||
box-shadow: 0 6rpx 16rpx rgba($brand-primary, 0.25) !important;
|
||
}
|
||
}
|
||
|
||
@keyframes shine {
|
||
0% { left: -150%; }
|
||
50%, 100% { left: 150%; }
|
||
}
|
||
|
||
/* 翻牌弹窗 */
|
||
.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);
|
||
backdrop-filter: blur(10rpx);
|
||
}
|
||
|
||
.flip-content {
|
||
position: relative;
|
||
width: 90%;
|
||
max-height: 85vh;
|
||
background: rgba($bg-card, 0.95);
|
||
border-radius: $radius-xl;
|
||
padding: $spacing-lg;
|
||
overflow: hidden;
|
||
box-shadow: $shadow-card;
|
||
}
|
||
|
||
.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;
|
||
}
|
||
}
|
||
</style>
|