307 lines
12 KiB
Vue
307 lines
12 KiB
Vue
<template>
|
||
<scroll-view class="page" scroll-y>
|
||
<view class="banner" v-if="detail.banner">
|
||
<image class="banner-img" :src="detail.banner" mode="widthFix" />
|
||
</view>
|
||
<view class="header">
|
||
<view class="title">{{ detail.name || detail.title || '-' }}</view>
|
||
<view class="meta" v-if="detail.price_draw !== undefined">单次抽选:¥{{ detail.price_draw }}</view>
|
||
</view>
|
||
<view class="draw-actions">
|
||
<button class="draw-btn" @click="() => onMachineDraw(1)">单次抽选</button>
|
||
<button class="draw-btn" @click="() => onMachineDraw(10)">十次抽选</button>
|
||
<button class="draw-btn secondary" @click="onMachineTry">试一试</button>
|
||
</view>
|
||
<view class="issues" v-if="showIssues && issues.length">
|
||
<view class="issue-switch">
|
||
<button class="switch-btn" @click="prevIssue">〈</button>
|
||
<text class="issue-title">{{ currentIssueTitle }}</text>
|
||
<button class="switch-btn" @click="nextIssue">〉</button>
|
||
</view>
|
||
</view>
|
||
</scroll-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>
|
||
|
||
</template>
|
||
|
||
<script setup>
|
||
import { ref, computed, getCurrentInstance } from 'vue'
|
||
import ElCard from '../../../components/ElCard.vue'
|
||
import FlipGrid from '../../../components/FlipGrid.vue'
|
||
import { onLoad } from '@dcloudio/uni-app'
|
||
import { getActivityDetail, getActivityIssues, getActivityIssueRewards, drawActivityIssue } from '../../../api/appUser'
|
||
|
||
const detail = ref({})
|
||
const statusText = ref('')
|
||
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 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)
|
||
|
||
function statusToText(s) {
|
||
if (s === 1) return '进行中'
|
||
if (s === 0) return '未开始'
|
||
if (s === 2) return '已结束'
|
||
return String(s || '')
|
||
}
|
||
|
||
async function fetchDetail(id) {
|
||
const data = await getActivityDetail(id)
|
||
detail.value = data || {}
|
||
statusText.value = statusToText(detail.value.status)
|
||
}
|
||
|
||
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) || ''
|
||
}
|
||
|
||
async function ensureElCard() {
|
||
const inst = getCurrentInstance()
|
||
const app = inst && inst.appContext && inst.appContext.app
|
||
let comp = null
|
||
if (typeof window !== 'undefined' && window.ElementPlus && window.ElementPlus.ElCard) {
|
||
comp = window.ElementPlus.ElCard
|
||
} else {
|
||
comp = ElCard
|
||
}
|
||
if (app && !app.component('el-card')) app.component('el-card', comp)
|
||
}
|
||
|
||
|
||
function onPreviewBanner() {
|
||
const url = detail.value.banner || ''
|
||
if (url) uni.previewImage({ urls: [url], current: url })
|
||
}
|
||
|
||
|
||
function onMachineDraw(count) {
|
||
showFlip.value = true
|
||
try { if (flipRef.value && flipRef.value.reset) flipRef.value.reset() } catch (_) {}
|
||
const aid = activityId.value || ''
|
||
const iid = currentIssueId.value || ''
|
||
if (!aid || !iid) { uni.showToast({ title: '期数未选择', icon: 'none' }); return }
|
||
drawLoading.value = true
|
||
const times = Math.max(1, Number(count || 1))
|
||
const calls = Array(times).fill(0).map(() => drawActivityIssue(aid, iid))
|
||
Promise.allSettled(calls).then(list => {
|
||
drawLoading.value = false
|
||
const items = list.map(r => {
|
||
const obj = r.status === 'fulfilled' ? r.value : {}
|
||
const data = obj && (obj.data || obj.result || obj.reward || obj.item || obj)
|
||
const title = String((data && (data.title || data.name || data.product_name)) || '未知奖励')
|
||
const image = String((data && (data.image || data.img || data.pic || data.product_image)) || '')
|
||
return { title, image }
|
||
})
|
||
if (flipRef.value && flipRef.value.revealResults) flipRef.value.revealResults(items)
|
||
}).catch(() => { drawLoading.value = false; if (flipRef.value && flipRef.value.revealResults) flipRef.value.revealResults([{ title: '抽选失败', image: '' }]) })
|
||
}
|
||
|
||
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)
|
||
}
|
||
ensureElCard()
|
||
})
|
||
|
||
function closeFlip() { showFlip.value = false }
|
||
</script>
|
||
|
||
<style scoped>
|
||
.page { height: 100vh; padding-bottom: 140rpx }
|
||
.banner { padding: 24rpx }
|
||
.banner-img { width: 100% }
|
||
.header { padding: 0 24rpx }
|
||
.title { font-size: 36rpx; font-weight: 700; color: #DD2C00; text-align: center }
|
||
.meta { margin-top: 8rpx; font-size: 26rpx; color: #666 }
|
||
.actions { display: flex; padding: 24rpx; gap: 16rpx }
|
||
.btn { flex: 1 }
|
||
.primary { background-color: #007AFF; color: #fff }
|
||
.draw-actions { display: flex; gap: 12rpx; padding: 24rpx }
|
||
.draw-btn { flex: 1; background: #007AFF; color: #fff; border-radius: 8rpx }
|
||
.draw-btn.secondary { background: #ffd166; color: #6b4b1f }
|
||
.flip-overlay { position: fixed; left: 0; right: 0; top: 0; bottom: 0; z-index: 10000 }
|
||
.flip-mask { position: absolute; left: 0; right: 0; top: 0; bottom: 0; background: rgba(0,0,0,0.5); z-index: 1 }
|
||
.flip-content { position: relative; display: flex; flex-direction: column; height: 100%; padding: 24rpx; z-index: 2 }
|
||
.overlay-close { background: #ffd166; color: #6b4b1f; border-radius: 999rpx; align-self: flex-end }
|
||
.issues { background: #fff; border-radius: 12rpx; margin: 0 24rpx 24rpx; padding: 16rpx }
|
||
.issues-title { font-size: 30rpx; font-weight: 600; margin-bottom: 12rpx }
|
||
.issues-list { }
|
||
.issue-switch { display: flex; align-items: center; justify-content: center; gap: 12rpx; margin: 0 24rpx 24rpx }
|
||
.switch-btn { width: 72rpx; height: 72rpx; border-radius: 999rpx; background: #fff3df; border: 2rpx solid #f0c58a; color: #8a5a2b }
|
||
.issue-title { font-size: 28rpx; color: #6b4b1f; background: #ffdfaa; border-radius: 12rpx; padding: 8rpx 16rpx }
|
||
.rewards { width: 100%; margin-top: 24rpx }
|
||
.reward { display: flex; align-items: center; margin-bottom: 8rpx }
|
||
.reward-img { width: 80rpx; height: 80rpx; border-radius: 8rpx; margin-right: 12rpx; background: #f5f5f5 }
|
||
.reward-card { background: #fff; border-radius: 12rpx; overflow: hidden; box-shadow: 0 6rpx 16rpx rgba(0,0,0,0.06); margin-bottom: 12rpx }
|
||
.el-reward-card { margin-bottom: 12rpx }
|
||
.el-card-header { display: flex; align-items: center; justify-content: space-between }
|
||
.el-card-title { font-size: 28rpx; color: #222; flex: 1; margin-right: 8rpx; word-break: break-all }
|
||
.card-image-wrap { position: relative; padding-bottom: 48rpx }
|
||
.card-image { width: 100%; height: auto; display: block; background: #f0f4ff; position: relative; z-index: 1 }
|
||
.prob-corner { position: absolute; background: rgba(221,82,77,0.9); color: #fff; font-size: 22rpx; padding: 6rpx 12rpx; border-radius: 999rpx; z-index: 2 }
|
||
.prob-corner.tl { top: 12rpx; left: 12rpx }
|
||
.card-body { display: flex; align-items: center; justify-content: space-between; padding: 12rpx }
|
||
.card-title { font-size: 28rpx; color: #222; flex: 1; margin-right: 8rpx; word-break: break-all }
|
||
.badge-boss { background: #ff9f0a; color: #222; font-size: 22rpx; padding: 4rpx 10rpx; border-radius: 999rpx }
|
||
.rewards-empty { font-size: 24rpx; color: #999 }
|
||
.issues-empty { font-size: 24rpx; color: #999 }
|
||
</style>
|