1248 lines
34 KiB
Vue
1248 lines
34 KiB
Vue
<template>
|
||
<view class="page-wrapper">
|
||
<view class="bg-decoration">
|
||
<view class="orb orb-1"></view>
|
||
<view class="orb orb-2"></view>
|
||
</view>
|
||
|
||
<view class="page-bg">
|
||
<image class="bg-image" :src="coverUrl" mode="aspectFill" />
|
||
<view class="bg-mask"></view>
|
||
</view>
|
||
|
||
<scroll-view class="main-scroll" scroll-y>
|
||
<view class="header-card animate-enter">
|
||
<image class="header-cover" :src="coverUrl" mode="aspectFill" />
|
||
<view class="header-info">
|
||
<view class="header-title">{{ detail.name || detail.title || '无限赏活动' }}</view>
|
||
<view class="header-price-row">
|
||
<text class="price-symbol">¥</text>
|
||
<text class="price-num">{{ (Number(detail.price_draw || 0) / 100).toFixed(2) }}</text>
|
||
<text class="price-unit">/发</text>
|
||
</view>
|
||
<view class="header-tags">
|
||
<view class="tag-item">公开透明</view>
|
||
<view class="tag-item">随机掉落</view>
|
||
</view>
|
||
</view>
|
||
<view class="header-actions">
|
||
<view class="action-btn" @tap="showRules">
|
||
<text class="icon">📋</text>
|
||
<text>规则</text>
|
||
</view>
|
||
<view class="action-btn" @tap="goCabinet">
|
||
<text class="icon">📦</text>
|
||
<text>盒柜</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="section-container animate-enter stagger-1" v-if="currentIssueRewards.length > 0">
|
||
<view class="section-header">
|
||
<text class="section-title">奖池一览</text>
|
||
<text class="section-more" @tap="openRewardsPopup">查看全部</text>
|
||
</view>
|
||
<scroll-view class="preview-scroll" scroll-x>
|
||
<view class="preview-item" v-for="(item, idx) in currentIssueRewards" :key="item.id || idx">
|
||
<view class="prize-tag" :class="{ 'tag-boss': item.boss }">{{ item.boss ? 'BOSS' : '赏' }}</view>
|
||
<image class="preview-img" :src="item.image" mode="aspectFill" />
|
||
<view class="preview-name">{{ item.title }}</view>
|
||
</view>
|
||
</scroll-view>
|
||
</view>
|
||
|
||
<view class="section-container selector-container animate-enter stagger-2">
|
||
<view class="issue-header" v-if="showIssues && issues.length">
|
||
<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">{{ statusText || '进行中' }}</text>
|
||
</view>
|
||
<view class="issue-switch-btn" @click="nextIssue">
|
||
<text class="arrow">▶</text>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="gameplay-tags">
|
||
<view class="tag tag-pool">聚宝盆</view>
|
||
<view class="tag tag-drop">随机掉落 10%</view>
|
||
<view class="tag tag-free">随机免单 10%</view>
|
||
</view>
|
||
</view>
|
||
|
||
<view style="height: 220rpx;"></view>
|
||
</scroll-view>
|
||
</view>
|
||
|
||
<!-- 底部多档位抽赏按钮 -->
|
||
<view class="bottom-actions">
|
||
<button class="tier-btn" @click="() => openPayment(1)">
|
||
<text class="tier-price">¥{{ (pricePerDrawYuan * 1).toFixed(2) }}</text>
|
||
<text class="tier-label">抽1发</text>
|
||
</button>
|
||
<button class="tier-btn" @click="() => openPayment(3)">
|
||
<text class="tier-price">¥{{ (pricePerDrawYuan * 3).toFixed(2) }}</text>
|
||
<text class="tier-label">抽3发</text>
|
||
</button>
|
||
<button class="tier-btn" @click="() => openPayment(5)">
|
||
<text class="tier-price">¥{{ (pricePerDrawYuan * 5).toFixed(2) }}</text>
|
||
<text class="tier-label">抽5发</text>
|
||
</button>
|
||
<button class="tier-btn tier-hot" @click="() => openPayment(10)">
|
||
<text class="tier-price">¥{{ (pricePerDrawYuan * 10).toFixed(2) }}</text>
|
||
<text class="tier-label">抽10发</text>
|
||
</button>
|
||
</view>
|
||
|
||
<view v-if="rewardsVisible" class="rewards-overlay" @touchmove.stop.prevent>
|
||
<view class="rewards-mask" @tap="closeRewardsPopup"></view>
|
||
<view class="rewards-panel" @tap.stop>
|
||
<view class="rewards-header">
|
||
<text class="rewards-title">{{ currentIssueTitle }} · 奖池与概率</text>
|
||
<text class="rewards-close" @tap="closeRewardsPopup">×</text>
|
||
</view>
|
||
<scroll-view scroll-y class="rewards-list">
|
||
<view v-for="(item, idx) in rewardsForPopup" :key="item.id || idx" class="rewards-item">
|
||
<image class="rewards-thumb" :src="item.image" mode="aspectFill" />
|
||
<view class="rewards-info">
|
||
<view class="rewards-name-row">
|
||
<text class="rewards-name">{{ item.title || '-' }}</text>
|
||
<view class="rewards-tag" v-if="item.boss">BOSS</view>
|
||
</view>
|
||
<text class="rewards-percent">概率 {{ formatPercent(item.percent) }}</text>
|
||
</view>
|
||
</view>
|
||
<view v-if="!rewardsForPopup.length" class="rewards-empty">暂无奖池数据</view>
|
||
</scroll-view>
|
||
</view>
|
||
</view>
|
||
|
||
<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>
|
||
|
||
<script setup>
|
||
import { ref, computed, nextTick } from 'vue'
|
||
import FlipGrid from '../../../components/FlipGrid.vue'
|
||
import { onLoad } from '@dcloudio/uni-app'
|
||
import PaymentPopup from '../../../components/PaymentPopup.vue'
|
||
import { getActivityDetail, getActivityIssues, getActivityIssueRewards, joinLottery, createWechatOrder, getLotteryResult, getItemCards, getUserCoupons } from '../../../api/appUser'
|
||
|
||
const detail = ref({})
|
||
const statusText = ref('')
|
||
const rewardsVisible = ref(false)
|
||
const issues = ref([])
|
||
const rewardsMap = ref({})
|
||
const currentIssueId = ref('')
|
||
const selectedIssueIndex = ref(0)
|
||
const showIssues = computed(() => (detail.value && detail.value.status !== 2))
|
||
const activityId = ref('')
|
||
const drawLoading = ref(false)
|
||
const currentIssueRewards = computed(() => (currentIssueId.value && rewardsMap.value[currentIssueId.value]) ? rewardsMap.value[currentIssueId.value] : [])
|
||
const coverUrl = computed(() => cleanUrl(detail.value && (detail.value.image || detail.value.banner || '')))
|
||
const currentIssueTitle = computed(() => {
|
||
const arr = issues.value || []
|
||
const cur = arr[selectedIssueIndex.value]
|
||
const t = (cur && (cur.title || ('第' + (cur.no || '-') + '期'))) || '-'
|
||
return t
|
||
})
|
||
const points = ref(0)
|
||
const flipRef = ref(null)
|
||
const showFlip = ref(false)
|
||
const rewardsForPopup = computed(() => {
|
||
const arr = currentIssueRewards.value || []
|
||
return Array.isArray(arr) ? arr : []
|
||
})
|
||
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)
|
||
const pricePerDrawYuan = computed(() => ((Number(detail.value.price_draw || 0) / 100) || 0))
|
||
|
||
function statusToText(s) {
|
||
if (s === 1) return '进行中'
|
||
if (s === 0) return '未开始'
|
||
if (s === 2) return '已结束'
|
||
return String(s || '')
|
||
}
|
||
|
||
function formatPercent(v) {
|
||
const n = Number(v)
|
||
if (!Number.isFinite(n)) return '0%'
|
||
return `${n}%`
|
||
}
|
||
|
||
function openRewardsPopup() {
|
||
rewardsVisible.value = true
|
||
}
|
||
function closeRewardsPopup() {
|
||
rewardsVisible.value = false
|
||
}
|
||
|
||
function showRules() {
|
||
uni.showModal({
|
||
title: '活动规则',
|
||
content: detail.value.rules || '1. 选择档位进行抽赏\n2. 每次抽赏随机获得奖品\n3. 奖池与概率以页面展示为准',
|
||
showCancel: false
|
||
})
|
||
}
|
||
|
||
function goCabinet() {
|
||
uni.navigateTo({ url: '/pages/cabinet/index' })
|
||
}
|
||
|
||
async function fetchDetail(id) {
|
||
const data = await getActivityDetail(id)
|
||
detail.value = data || {}
|
||
statusText.value = statusToText(detail.value.status)
|
||
const title = String(detail.value.name || detail.value.title || '无限赏')
|
||
try { uni.setNavigationBarTitle({ title }) } catch (_) {}
|
||
}
|
||
|
||
function unwrap(list) {
|
||
if (Array.isArray(list)) return list
|
||
const obj = list || {}
|
||
const data = obj.data || {}
|
||
const arr = obj.list || obj.items || data.list || data.items || data
|
||
return Array.isArray(arr) ? arr : []
|
||
}
|
||
|
||
function normalizeIssues(list) {
|
||
const arr = unwrap(list)
|
||
return arr.map((i, idx) => ({
|
||
id: i.id ?? String(idx),
|
||
title: i.title ?? i.name ?? '',
|
||
no: i.no ?? i.index ?? i.issue_no ?? i.issue_number ?? null,
|
||
status_text: i.status_text ?? (i.status === 1 ? '进行中' : i.status === 0 ? '未开始' : i.status === 2 ? '已结束' : '')
|
||
}))
|
||
}
|
||
function cleanUrl(u) {
|
||
const s = String(u || '').trim()
|
||
const m = s.match(/https?:\/\/[^\s'"`]+/)
|
||
if (m && m[0]) return m[0]
|
||
return s.replace(/[`'\"]/g, '').trim()
|
||
}
|
||
function truthy(v) {
|
||
if (typeof v === 'boolean') return v
|
||
const s = String(v || '').trim().toLowerCase()
|
||
if (!s) return false
|
||
return s === '1' || s === 'true' || s === 'yes' || s === 'y' || s === '是' || s === 'boss是真的' || s === 'boss' || s === '大boss'
|
||
}
|
||
function detectBoss(i) {
|
||
return truthy(i.is_boss) || truthy(i.boss) || truthy(i.isBoss) || truthy(i.boss_true) || truthy(i.boss_is_true) || truthy(i.bossText) || truthy(i.tag)
|
||
}
|
||
function normalizeRewards(list) {
|
||
const arr = unwrap(list)
|
||
const items = arr.map((i, idx) => ({
|
||
id: i.product_id ?? i.id ?? String(idx),
|
||
title: i.name ?? i.title ?? '',
|
||
image: cleanUrl(i.product_image ?? i.image ?? i.img ?? i.pic ?? i.banner ?? ''),
|
||
weight: Number(i.weight) || 0,
|
||
boss: detectBoss(i)
|
||
}))
|
||
const total = items.reduce((acc, it) => acc + (it.weight > 0 ? it.weight : 0), 0)
|
||
const enriched = items.map(it => ({
|
||
...it,
|
||
percent: total > 0 ? Math.round((it.weight / total) * 1000) / 10 : 0
|
||
}))
|
||
enriched.sort((a, b) => (b.percent - a.percent))
|
||
return enriched
|
||
}
|
||
function isFresh(ts) {
|
||
const now = Date.now()
|
||
const v = Number(ts || 0)
|
||
return now - v < 24 * 60 * 60 * 1000
|
||
}
|
||
function getRewardCache() {
|
||
const obj = uni.getStorageSync('reward_cache_v1') || {}
|
||
return typeof obj === 'object' && obj ? obj : {}
|
||
}
|
||
async function fetchRewardsForIssues(activityId) {
|
||
const list = issues.value || []
|
||
const cache = getRewardCache()
|
||
const act = cache[activityId] || {}
|
||
const toFetch = []
|
||
list.forEach(it => {
|
||
const c = act[it.id]
|
||
if (c && isFresh(c.ts) && Array.isArray(c.value)) {
|
||
rewardsMap.value = { ...(rewardsMap.value || {}), [it.id]: c.value }
|
||
} else {
|
||
toFetch.push(it)
|
||
}
|
||
})
|
||
if (!toFetch.length) return
|
||
const promises = toFetch.map(it => getActivityIssueRewards(activityId, it.id))
|
||
const results = await Promise.allSettled(promises)
|
||
const nextAct = { ...act }
|
||
results.forEach((res, i) => {
|
||
const issueId = toFetch[i] && toFetch[i].id
|
||
if (!issueId) return
|
||
const value = res.status === 'fulfilled' ? normalizeRewards(res.value) : []
|
||
rewardsMap.value = { ...(rewardsMap.value || {}), [issueId]: value }
|
||
nextAct[issueId] = { value, ts: Date.now() }
|
||
})
|
||
cache[activityId] = nextAct
|
||
uni.setStorageSync('reward_cache_v1', cache)
|
||
}
|
||
|
||
async function fetchIssues(id) {
|
||
const data = await getActivityIssues(id)
|
||
issues.value = normalizeIssues(data)
|
||
const latestId = pickLatestIssueId(issues.value)
|
||
setSelectedById(latestId)
|
||
await fetchRewardsForIssues(id)
|
||
}
|
||
|
||
function pickLatestIssueId(list) {
|
||
const arr = Array.isArray(list) ? list : []
|
||
let latest = arr[arr.length - 1] && arr[arr.length - 1].id
|
||
let maxNo = -Infinity
|
||
arr.forEach(i => {
|
||
const n = Number(i.no)
|
||
if (!Number.isNaN(n) && Number.isFinite(n) && n > maxNo) {
|
||
maxNo = n
|
||
latest = i.id
|
||
}
|
||
})
|
||
return latest || (arr[0] && arr[0].id) || ''
|
||
}
|
||
function setSelectedById(id) {
|
||
const arr = issues.value || []
|
||
const idx = Math.max(0, arr.findIndex(x => x && x.id === id))
|
||
selectedIssueIndex.value = idx
|
||
const cur = arr[idx]
|
||
currentIssueId.value = (cur && cur.id) || ''
|
||
}
|
||
function onIssueChange(e) {
|
||
// deprecated picker
|
||
}
|
||
function prevIssue() {
|
||
const arr = issues.value || []
|
||
const idx = Math.max(0, Math.min(arr.length - 1, selectedIssueIndex.value - 1))
|
||
selectedIssueIndex.value = idx
|
||
const cur = arr[idx]
|
||
currentIssueId.value = (cur && cur.id) || ''
|
||
}
|
||
function nextIssue() {
|
||
const arr = issues.value || []
|
||
const idx = Math.max(0, Math.min(arr.length - 1, selectedIssueIndex.value + 1))
|
||
selectedIssueIndex.value = idx
|
||
const cur = arr[idx]
|
||
currentIssueId.value = (cur && cur.id) || ''
|
||
}
|
||
|
||
function onPreviewBanner() {
|
||
const url = detail.value.banner || ''
|
||
if (url) uni.previewImage({ urls: [url], current: url })
|
||
}
|
||
|
||
function openPayment(count) {
|
||
const times = Math.max(1, Number(count || 1))
|
||
pendingCount.value = times
|
||
paymentAmount.value = (pricePerDrawYuan.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 && data.coupon ? data.coupon : null
|
||
selectedCard.value = data && data.card ? 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 = []
|
||
if (Array.isArray(res)) list = res
|
||
else if (res && Array.isArray(res.list)) list = res.list
|
||
else if (res && Array.isArray(res.data)) 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 = []
|
||
if (Array.isArray(res)) list = res
|
||
else if (res && Array.isArray(res.list)) list = res.list
|
||
else if (res && Array.isArray(res.data)) list = res.data
|
||
coupons.value = list.map((i, idx) => {
|
||
const amountCents = (i.remaining !== undefined && i.remaining !== null) ? Number(i.remaining) : Number(i.amount ?? i.value ?? 0)
|
||
const amt = isNaN(amountCents) ? 0 : (amountCents / 100)
|
||
return {
|
||
id: i.id ?? i.coupon_id ?? String(idx),
|
||
name: i.name ?? i.title ?? '优惠券',
|
||
amount: Number(amt).toFixed(2)
|
||
}
|
||
})
|
||
} catch (e) {
|
||
coupons.value = []
|
||
}
|
||
}
|
||
|
||
function buildRewardLookup(list) {
|
||
const map = new Map()
|
||
const arr = Array.isArray(list) ? list : []
|
||
arr.forEach(it => {
|
||
const id = it && (it.id ?? it.reward_id ?? it.product_id)
|
||
const key = Number(id)
|
||
if (Number.isFinite(key)) map.set(key, it)
|
||
})
|
||
return map
|
||
}
|
||
|
||
function extractResultList(resultRes) {
|
||
const root = resultRes && (resultRes.data ?? resultRes.result ?? resultRes)
|
||
if (!root) return []
|
||
if (Array.isArray(root.results)) return root.results
|
||
if (Array.isArray(root.list)) return root.list
|
||
if (Array.isArray(root.items)) return root.items
|
||
if (Array.isArray(root.data)) return root.data
|
||
if (root.results && root.results.data && Array.isArray(root.results.data)) return root.results.data
|
||
return []
|
||
}
|
||
|
||
function mapResultsToFlipItems(resultRes, poolRewards) {
|
||
const list = extractResultList(resultRes)
|
||
const lookup = buildRewardLookup(poolRewards)
|
||
const poolArr = Array.isArray(poolRewards) ? poolRewards : []
|
||
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 key = Number(rewardId)
|
||
const fromId = Number.isFinite(key) ? lookup.get(key) : null
|
||
const fromName = !fromId && rewardName ? (poolArr.find(x => String(x && x.title || '') === rewardName) || poolArr.find(x => String(x && x.title || '').includes(rewardName))) : null
|
||
const it = fromId || fromName || null
|
||
const title = rewardName || String((it && it.title) || '奖励')
|
||
const image = String((it && it.image) || d.image || d.img || d.pic || d.product_image || '')
|
||
return { title, 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 && selectedCoupon.value.id ? Number(selectedCoupon.value.id) : 0,
|
||
item_card_id: selectedCard.value && selectedCard.value.id ? Number(selectedCard.value.id) : 0
|
||
})
|
||
const orderNo = joinRes && (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 { if (flipRef.value && flipRef.value.reset) flipRef.value.reset() } catch (_) {}
|
||
setTimeout(() => {
|
||
if (flipRef.value && flipRef.value.revealResults) flipRef.value.revealResults(items)
|
||
}, 100)
|
||
} catch (e) {
|
||
uni.showToast({ title: e.message || '操作失败', icon: 'none' })
|
||
} finally {
|
||
drawLoading.value = false
|
||
}
|
||
}
|
||
|
||
function onMachineTry() {
|
||
const list = rewardsMap.value[currentIssueId.value] || []
|
||
if (!list.length) { uni.showToast({ title: '暂无奖池', icon: 'none' }); return }
|
||
const idx = Math.floor(Math.random() * list.length)
|
||
const it = list[idx]
|
||
uni.showModal({ title: '试一试', content: it.title || '随机预览', showCancel: false, success: () => { if (it.image) uni.previewImage({ urls: [it.image], current: it.image }) } })
|
||
}
|
||
|
||
onLoad((opts) => {
|
||
const id = (opts && opts.id) || ''
|
||
if (id) {
|
||
activityId.value = id
|
||
fetchDetail(id)
|
||
fetchIssues(id)
|
||
}
|
||
})
|
||
|
||
function closeFlip() { showFlip.value = false }
|
||
</script>
|
||
|
||
<style lang="scss" scoped>
|
||
/* 柯大鸭潮玩 - 无限赏活动页面 */
|
||
.page-wrapper {
|
||
min-height: 100vh;
|
||
background: $bg-page;
|
||
position: relative;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.bg-decoration {
|
||
position: absolute;
|
||
top: 0;
|
||
left: 0;
|
||
width: 100%;
|
||
height: 100%;
|
||
z-index: 0;
|
||
overflow: hidden;
|
||
pointer-events: none;
|
||
}
|
||
|
||
.orb {
|
||
position: absolute;
|
||
border-radius: 50%;
|
||
filter: blur(80px);
|
||
opacity: 0.6;
|
||
}
|
||
.orb-1 {
|
||
width: 600rpx;
|
||
height: 600rpx;
|
||
background: radial-gradient(circle, rgba($brand-primary, 0.2), transparent 70%);
|
||
top: -200rpx;
|
||
left: -200rpx;
|
||
animation: float 10s ease-in-out infinite;
|
||
}
|
||
.orb-2 {
|
||
width: 500rpx;
|
||
height: 500rpx;
|
||
background: radial-gradient(circle, rgba($accent-gold, 0.2), transparent 70%);
|
||
bottom: 20%;
|
||
right: -100rpx;
|
||
animation: float 12s ease-in-out infinite reverse;
|
||
}
|
||
|
||
@keyframes float {
|
||
0%, 100% { transform: translate(0, 0); }
|
||
50% { transform: translate(30rpx, 50rpx); }
|
||
}
|
||
|
||
.page-bg {
|
||
position: absolute;
|
||
top: 0;
|
||
left: 0;
|
||
width: 100%;
|
||
height: 700rpx;
|
||
z-index: 1;
|
||
}
|
||
.bg-image {
|
||
width: 100%;
|
||
height: 100%;
|
||
filter: blur(30rpx) brightness(0.9);
|
||
transform: scale(1.1);
|
||
}
|
||
.bg-mask {
|
||
position: absolute;
|
||
top: 0;
|
||
left: 0;
|
||
width: 100%;
|
||
height: 100%;
|
||
background: linear-gradient(180deg, rgba($bg-page, 0.2) 0%, $bg-page 90%, $bg-page 100%);
|
||
}
|
||
.main-scroll {
|
||
position: relative;
|
||
z-index: 2;
|
||
height: 100vh;
|
||
}
|
||
|
||
.header-card {
|
||
margin: $spacing-xl $spacing-lg;
|
||
background: rgba($bg-card, 0.85);
|
||
backdrop-filter: blur(24rpx);
|
||
border-radius: $radius-xl;
|
||
padding: $spacing-lg;
|
||
display: flex;
|
||
align-items: center;
|
||
box-shadow: $shadow-card;
|
||
border: 1rpx solid rgba(255, 255, 255, 0.6);
|
||
position: relative;
|
||
overflow: hidden;
|
||
}
|
||
.header-cover {
|
||
width: 180rpx;
|
||
height: 180rpx;
|
||
border-radius: $radius-md;
|
||
margin-right: $spacing-lg;
|
||
background: $bg-secondary;
|
||
box-shadow: $shadow-md;
|
||
flex-shrink: 0;
|
||
}
|
||
.header-info {
|
||
flex: 1;
|
||
display: flex;
|
||
flex-direction: column;
|
||
justify-content: flex-start;
|
||
min-height: 180rpx;
|
||
padding: 6rpx 0;
|
||
}
|
||
.header-title {
|
||
font-size: $font-xl;
|
||
font-weight: 800;
|
||
color: $text-main;
|
||
margin-bottom: $spacing-xs;
|
||
line-height: 1.3;
|
||
@include text-ellipsis(2);
|
||
}
|
||
.header-price-row {
|
||
display: flex;
|
||
align-items: baseline;
|
||
color: $brand-primary;
|
||
margin-bottom: $spacing-sm;
|
||
text-shadow: 0 2rpx 4rpx rgba($brand-primary, 0.1);
|
||
}
|
||
.price-symbol { font-size: $font-md; font-weight: 700; }
|
||
.price-num { font-size: $font-xxl; font-weight: 900; margin: 0 4rpx; font-family: 'DIN Alternate', sans-serif; }
|
||
.price-unit { font-size: $font-sm; color: $text-sub; margin-left: 4rpx; }
|
||
|
||
.header-tags {
|
||
display: flex;
|
||
gap: $spacing-xs;
|
||
flex-wrap: wrap;
|
||
}
|
||
.tag-item {
|
||
font-size: $font-xs;
|
||
color: $brand-primary-dark;
|
||
background: rgba($brand-primary, 0.08);
|
||
padding: 4rpx $spacing-sm;
|
||
border-radius: $radius-sm;
|
||
font-weight: 600;
|
||
border: 1rpx solid rgba($brand-primary, 0.1);
|
||
}
|
||
.header-actions {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: $spacing-lg;
|
||
margin-left: 20rpx;
|
||
padding-left: $spacing-lg;
|
||
border-left: 1rpx solid rgba(0,0,0,0.06);
|
||
justify-content: center;
|
||
height: 140rpx;
|
||
}
|
||
.action-btn {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
font-size: $font-xs;
|
||
color: $text-sub;
|
||
transition: all 0.2s;
|
||
&:active {
|
||
transform: scale(0.9);
|
||
color: $text-main;
|
||
}
|
||
}
|
||
.action-btn .icon {
|
||
font-size: $font-xl;
|
||
margin-bottom: 6rpx;
|
||
filter: grayscale(0.2);
|
||
}
|
||
|
||
.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);
|
||
}
|
||
.section-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 24rpx;
|
||
padding: 0 4rpx;
|
||
}
|
||
.section-title {
|
||
font-size: $font-lg;
|
||
font-weight: 800;
|
||
color: $text-main;
|
||
position: relative;
|
||
padding-left: $spacing-lg;
|
||
&::before {
|
||
content: '';
|
||
position: absolute;
|
||
left: 0;
|
||
top: 50%;
|
||
transform: translateY(-50%);
|
||
width: 8rpx;
|
||
height: 28rpx;
|
||
background: $gradient-brand;
|
||
border-radius: 4rpx;
|
||
}
|
||
}
|
||
.section-more {
|
||
font-size: $font-sm;
|
||
color: $text-tertiary;
|
||
display: flex;
|
||
align-items: center;
|
||
&::after {
|
||
content: '>';
|
||
font-family: monospace;
|
||
margin-left: 6rpx;
|
||
font-weight: 700;
|
||
}
|
||
}
|
||
|
||
.preview-scroll {
|
||
white-space: nowrap;
|
||
margin: 0 -$spacing-lg;
|
||
padding: 0 $spacing-lg;
|
||
width: calc(100% + 40rpx);
|
||
}
|
||
.preview-item {
|
||
display: inline-block;
|
||
width: 200rpx;
|
||
margin-right: $spacing-lg;
|
||
vertical-align: top;
|
||
position: relative;
|
||
transition: transform 0.2s;
|
||
&:active { transform: scale(0.96); }
|
||
&:last-child { margin-right: 40rpx; }
|
||
}
|
||
.preview-img {
|
||
width: 200rpx;
|
||
height: 200rpx;
|
||
border-radius: $radius-lg;
|
||
background: $bg-secondary;
|
||
margin-bottom: $spacing-md;
|
||
box-shadow: $shadow-sm;
|
||
border: 1rpx solid rgba(0,0,0,0.03);
|
||
}
|
||
.preview-name {
|
||
font-size: $font-sm;
|
||
color: $text-secondary;
|
||
width: 100%;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
text-align: center;
|
||
font-weight: 500;
|
||
}
|
||
.prize-tag {
|
||
position: absolute;
|
||
top: 10rpx;
|
||
left: 10rpx;
|
||
background: rgba(0,0,0,0.6);
|
||
color: #fff;
|
||
font-size: $font-xs;
|
||
padding: 4rpx $spacing-sm;
|
||
border-radius: $radius-sm;
|
||
z-index: 10;
|
||
font-weight: 700;
|
||
backdrop-filter: blur(4rpx);
|
||
transform: scale(0.9);
|
||
transform-origin: top left;
|
||
}
|
||
.prize-tag.tag-boss {
|
||
background: $gradient-brand;
|
||
box-shadow: 0 4rpx 12rpx rgba($brand-primary, 0.4);
|
||
}
|
||
|
||
.selector-container {
|
||
display: flex;
|
||
flex-direction: column;
|
||
background: rgba($bg-card, 0.95);
|
||
backdrop-filter: blur(20rpx);
|
||
}
|
||
.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;
|
||
font-weight: 700;
|
||
}
|
||
|
||
.animate-enter {
|
||
animation: fadeInUp 0.6s cubic-bezier(0.2, 0.8, 0.2, 1) backwards;
|
||
}
|
||
.stagger-1 { animation-delay: 0.1s; }
|
||
.stagger-2 { animation-delay: 0.2s; }
|
||
|
||
@keyframes fadeInUp {
|
||
from { opacity: 0; transform: translateY(40rpx); }
|
||
to { opacity: 1; transform: translateY(0); }
|
||
}
|
||
|
||
.banner {
|
||
padding: $spacing-lg $spacing-lg 0;
|
||
animation: fadeInDown 0.6s $ease-out;
|
||
}
|
||
.banner-img {
|
||
width: 100%;
|
||
border-radius: $radius-lg;
|
||
box-shadow: $shadow-lg;
|
||
}
|
||
|
||
/* 商品信息卡片 */
|
||
.product-card {
|
||
margin: $spacing-lg;
|
||
background: $bg-glass;
|
||
backdrop-filter: blur(20rpx);
|
||
border-radius: $radius-lg;
|
||
padding: $spacing-lg;
|
||
box-shadow: $shadow-card;
|
||
animation: fadeInUp 0.6s $ease-out 0.1s backwards;
|
||
border: 1rpx solid rgba(255, 255, 255, 0.6);
|
||
}
|
||
.product-info {
|
||
display: flex;
|
||
align-items: flex-start;
|
||
gap: $spacing-lg;
|
||
}
|
||
.product-thumb {
|
||
width: 140rpx;
|
||
height: 140rpx;
|
||
border-radius: $radius-md;
|
||
flex-shrink: 0;
|
||
background: $bg-page;
|
||
box-shadow: $shadow-inner;
|
||
}
|
||
.product-detail {
|
||
flex: 1;
|
||
min-width: 0;
|
||
}
|
||
.product-name {
|
||
font-size: $font-lg;
|
||
font-weight: 700;
|
||
color: $text-main;
|
||
margin-bottom: $spacing-sm;
|
||
display: -webkit-box;
|
||
-webkit-line-clamp: 2;
|
||
-webkit-box-orient: vertical;
|
||
overflow: hidden;
|
||
line-height: 1.4;
|
||
}
|
||
.product-price {
|
||
font-size: $font-xl;
|
||
font-weight: 800;
|
||
color: $brand-primary;
|
||
font-family: 'DIN Alternate', sans-serif;
|
||
}
|
||
.product-actions {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: $spacing-sm;
|
||
}
|
||
|
||
/* 期号切换条 */
|
||
.issue-bar {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: $spacing-lg;
|
||
margin: 0 $spacing-lg $spacing-lg;
|
||
padding: $spacing-md $spacing-lg;
|
||
background: $bg-glass;
|
||
backdrop-filter: blur(20rpx);
|
||
border-radius: $radius-round;
|
||
box-shadow: $shadow-sm;
|
||
animation: fadeInUp 0.6s $ease-out 0.2s backwards;
|
||
border: 1rpx solid rgba(255, 255, 255, 0.6);
|
||
}
|
||
.nav-btn {
|
||
width: 64rpx;
|
||
height: 64rpx;
|
||
border-radius: 50%;
|
||
background: $bg-page;
|
||
color: $text-sub;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-size: $font-sm;
|
||
padding: 0;
|
||
margin: 0;
|
||
line-height: 1;
|
||
transition: all $transition-fast;
|
||
border: none;
|
||
|
||
&:active {
|
||
background: darken($bg-page, 5%);
|
||
transform: scale(0.9);
|
||
}
|
||
}
|
||
.issue-info {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
gap: 4rpx;
|
||
min-width: 200rpx;
|
||
}
|
||
.issue-label {
|
||
font-size: $font-lg;
|
||
font-weight: 700;
|
||
color: $text-main;
|
||
}
|
||
|
||
/* 玩法福利标签 */
|
||
.gameplay-tags {
|
||
display: flex;
|
||
gap: $spacing-md;
|
||
padding: 0 $spacing-lg;
|
||
margin-bottom: $spacing-lg;
|
||
flex-wrap: wrap;
|
||
animation: fadeInUp 0.6s $ease-out 0.3s backwards;
|
||
}
|
||
.tag {
|
||
padding: $spacing-sm $spacing-lg;
|
||
border-radius: $radius-round;
|
||
font-size: $font-sm;
|
||
font-weight: 600;
|
||
display: flex;
|
||
align-items: center;
|
||
box-shadow: $shadow-sm;
|
||
backdrop-filter: blur(4px);
|
||
}
|
||
.tag-pool {
|
||
background: $color-success;
|
||
color: #FFFFFF;
|
||
box-shadow: 0 4rpx 12rpx rgba($color-success, 0.3);
|
||
border: 1rpx solid rgba(255, 255, 255, 0.2);
|
||
}
|
||
.tag-drop {
|
||
background: $gradient-brand;
|
||
color: #FFFFFF;
|
||
box-shadow: 0 4rpx 12rpx rgba($brand-primary, 0.3);
|
||
border: 1rpx solid rgba(255, 255, 255, 0.2);
|
||
}
|
||
.tag-free {
|
||
background: $gradient-gold;
|
||
color: #FFFFFF;
|
||
box-shadow: 0 4rpx 12rpx rgba($accent-gold, 0.3);
|
||
text-shadow: 0 1rpx 2rpx rgba(0,0,0,0.1);
|
||
border: 1rpx solid rgba(255, 255, 255, 0.2);
|
||
}
|
||
|
||
/* 底部多档位抽赏按钮 */
|
||
.bottom-actions {
|
||
position: fixed;
|
||
left: 0;
|
||
right: 0;
|
||
bottom: 0;
|
||
display: flex;
|
||
gap: $spacing-md;
|
||
padding: $spacing-lg $spacing-lg;
|
||
padding-bottom: calc($spacing-lg + env(safe-area-inset-bottom));
|
||
background: rgba(255, 255, 255, 0.9);
|
||
backdrop-filter: blur(20rpx);
|
||
box-shadow: 0 -8rpx 24rpx rgba(0, 0, 0, 0.08);
|
||
z-index: 999;
|
||
animation: slideUp $transition-slow $ease-out backwards;
|
||
border-top: 1rpx solid rgba(0,0,0,0.05);
|
||
}
|
||
.tier-btn {
|
||
flex: 1;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
padding: $spacing-md $spacing-xs;
|
||
background: $bg-card;
|
||
border: 1rpx solid $border-color-light;
|
||
border-radius: $radius-lg;
|
||
box-shadow: $shadow-sm;
|
||
transition: all $transition-fast;
|
||
|
||
&:active {
|
||
transform: scale(0.95);
|
||
background: $bg-page;
|
||
}
|
||
}
|
||
.tier-price {
|
||
font-size: $font-lg;
|
||
font-weight: 800;
|
||
color: $text-main;
|
||
font-family: 'DIN Alternate', sans-serif;
|
||
}
|
||
.tier-label {
|
||
font-size: $font-xs;
|
||
color: $text-sub;
|
||
margin-top: 4rpx;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.tier-hot {
|
||
background: $gradient-brand;
|
||
border: none;
|
||
box-shadow: $shadow-warm;
|
||
position: relative;
|
||
overflow: hidden;
|
||
|
||
.tier-price, .tier-label {
|
||
color: #fff;
|
||
}
|
||
|
||
&::after {
|
||
content: 'HOT';
|
||
position: absolute;
|
||
top: 0;
|
||
right: 0;
|
||
background: linear-gradient(135deg, $accent-red, #D32F2F);
|
||
color: #fff;
|
||
font-size: 18rpx;
|
||
font-weight: 800;
|
||
padding: 4rpx 10rpx;
|
||
border-bottom-left-radius: $radius-md;
|
||
box-shadow: -2rpx 2rpx 4rpx rgba(0,0,0,0.1);
|
||
}
|
||
|
||
&:active {
|
||
opacity: 0.9;
|
||
transform: scale(0.96);
|
||
}
|
||
}
|
||
.tier-hot .tier-price, .tier-hot .tier-label {
|
||
color: #FFFFFF;
|
||
}
|
||
|
||
.rewards-overlay { position: fixed; left: 0; right: 0; top: 0; bottom: 0; z-index: 9000; }
|
||
.rewards-mask {
|
||
position: absolute; left: 0; right: 0; top: 0; bottom: 0;
|
||
background: rgba(0,0,0,0.6);
|
||
backdrop-filter: blur(10rpx);
|
||
}
|
||
.rewards-panel {
|
||
position: absolute;
|
||
left: $spacing-lg;
|
||
right: $spacing-lg;
|
||
bottom: calc(env(safe-area-inset-bottom) + 24rpx);
|
||
max-height: 70vh;
|
||
background: rgba($bg-card, 0.95);
|
||
border-radius: $radius-xl;
|
||
box-shadow: $shadow-card;
|
||
border: 1rpx solid rgba(255,255,255,0.5);
|
||
overflow: hidden;
|
||
animation: fadeInUp 0.25s ease-out;
|
||
}
|
||
.rewards-header {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
padding: $spacing-lg;
|
||
border-bottom: 1rpx solid rgba(0,0,0,0.06);
|
||
}
|
||
.rewards-title {
|
||
font-size: $font-md;
|
||
font-weight: 800;
|
||
color: $text-main;
|
||
}
|
||
.rewards-close {
|
||
font-size: 48rpx;
|
||
line-height: 1;
|
||
color: $text-tertiary;
|
||
padding: 0 10rpx;
|
||
}
|
||
.rewards-list {
|
||
max-height: 60vh;
|
||
padding: $spacing-lg;
|
||
}
|
||
.rewards-item {
|
||
display: flex;
|
||
align-items: center;
|
||
padding: $spacing-md;
|
||
border-radius: $radius-lg;
|
||
background: rgba(255,255,255,0.75);
|
||
border: 1rpx solid rgba(0,0,0,0.03);
|
||
box-shadow: $shadow-xs;
|
||
margin-bottom: $spacing-md;
|
||
}
|
||
.rewards-thumb {
|
||
width: 96rpx;
|
||
height: 96rpx;
|
||
border-radius: $radius-md;
|
||
background: $bg-secondary;
|
||
flex-shrink: 0;
|
||
margin-right: $spacing-md;
|
||
}
|
||
.rewards-info { flex: 1; min-width: 0; }
|
||
.rewards-name-row {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
gap: $spacing-sm;
|
||
margin-bottom: 8rpx;
|
||
}
|
||
.rewards-name {
|
||
font-size: $font-md;
|
||
font-weight: 700;
|
||
color: $text-main;
|
||
@include text-ellipsis(1);
|
||
}
|
||
.rewards-tag {
|
||
font-size: $font-xs;
|
||
font-weight: 800;
|
||
color: #fff;
|
||
background: $gradient-brand;
|
||
border-radius: $radius-sm;
|
||
padding: 4rpx 10rpx;
|
||
flex-shrink: 0;
|
||
}
|
||
.rewards-percent {
|
||
font-size: $font-sm;
|
||
color: $text-sub;
|
||
font-weight: 600;
|
||
}
|
||
.rewards-empty {
|
||
padding: 60rpx 0 20rpx;
|
||
text-align: center;
|
||
color: $text-tertiary;
|
||
font-size: $font-sm;
|
||
}
|
||
|
||
/* 翻牌弹窗 */
|
||
.flip-overlay {
|
||
position: fixed;
|
||
left: 0;
|
||
right: 0;
|
||
top: 0;
|
||
bottom: 0;
|
||
z-index: 10000;
|
||
animation: fadeIn 0.3s ease-out;
|
||
}
|
||
.flip-mask {
|
||
position: absolute;
|
||
left: 0;
|
||
right: 0;
|
||
top: 0;
|
||
bottom: 0;
|
||
background: rgba(0,0,0,0.8);
|
||
backdrop-filter: blur(10px);
|
||
z-index: 1;
|
||
}
|
||
.flip-content {
|
||
position: relative;
|
||
display: flex;
|
||
flex-direction: column;
|
||
height: 100%;
|
||
padding: 24rpx;
|
||
z-index: 2;
|
||
justify-content: center;
|
||
align-items: center;
|
||
animation: zoomIn 0.3s $ease-bounce;
|
||
}
|
||
.overlay-close {
|
||
margin-top: 60rpx;
|
||
width: 240rpx;
|
||
height: 88rpx;
|
||
line-height: 88rpx;
|
||
background: rgba(255,255,255,0.15) !important;
|
||
border: 1rpx solid rgba(255,255,255,0.3);
|
||
color: #FFFFFF !important;
|
||
border-radius: $radius-round;
|
||
font-weight: 600;
|
||
font-size: 30rpx;
|
||
backdrop-filter: blur(10px);
|
||
transition: all $transition-fast;
|
||
|
||
&:active {
|
||
background: rgba(255,255,255,0.25) !important;
|
||
transform: scale(0.95);
|
||
}
|
||
}
|
||
</style>
|