合并页面
This commit is contained in:
parent
0c4ed4360f
commit
8f044d68ca
1
App.vue
1
App.vue
@ -2,6 +2,7 @@
|
||||
export default {
|
||||
onLaunch: function() {
|
||||
console.log('App Launch')
|
||||
try { uni.setStorageSync('app_session_id', String(Date.now())) } catch (_) {}
|
||||
},
|
||||
onShow: function() {
|
||||
console.log('App Show')
|
||||
|
||||
@ -58,3 +58,11 @@ export function getActivityIssues(activity_id) {
|
||||
export function getActivityIssueRewards(activity_id, issue_id) {
|
||||
return authRequest({ url: `/api/app/activities/${activity_id}/issues/${issue_id}/rewards`, method: 'GET' })
|
||||
}
|
||||
|
||||
export function drawActivityIssue(activity_id, issue_id) {
|
||||
return authRequest({ url: `/api/app/activities/${activity_id}/issues/${issue_id}/draw`, method: 'POST' })
|
||||
}
|
||||
|
||||
export function getActivityWinRecords(activity_id, page = 1, page_size = 20) {
|
||||
return authRequest({ url: `/api/app/activities/${activity_id}/wins`, method: 'GET', data: { page, page_size } })
|
||||
}
|
||||
|
||||
30
components/ElCard.vue
Normal file
30
components/ElCard.vue
Normal file
@ -0,0 +1,30 @@
|
||||
<template>
|
||||
<view class="ep-card" :class="shadowClass">
|
||||
<view v-if="$slots.header" class="ep-card__header">
|
||||
<slot name="header" />
|
||||
</view>
|
||||
<view class="ep-card__body">
|
||||
<slot />
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
const props = defineProps({ shadow: { type: String, default: 'always' } })
|
||||
const shadowClass = computed(() => {
|
||||
const s = String(props.shadow || '').toLowerCase()
|
||||
if (s === 'hover') return 'is-hover-shadow'
|
||||
if (s === 'never') return 'is-no-shadow'
|
||||
return 'is-always-shadow'
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.ep-card { background: #fff; border-radius: 12rpx; overflow: hidden; border: 1rpx solid #eee }
|
||||
.is-always-shadow { box-shadow: 0 6rpx 16rpx rgba(0,0,0,0.06) }
|
||||
.is-hover-shadow { box-shadow: 0 6rpx 16rpx rgba(0,0,0,0.06) }
|
||||
.is-no-shadow { box-shadow: none }
|
||||
.ep-card__header { padding: 12rpx 16rpx; border-bottom: 1rpx solid #f0f0f0 }
|
||||
.ep-card__body { padding: 12rpx 16rpx }
|
||||
</style>
|
||||
83
components/FlipGrid.vue
Normal file
83
components/FlipGrid.vue
Normal file
@ -0,0 +1,83 @@
|
||||
<template>
|
||||
<view class="flip-root">
|
||||
<view v-if="controls" class="flip-actions">
|
||||
<button class="flip-btn" @tap="onDraw(1)">单次抽选</button>
|
||||
<button class="flip-btn" @tap="onDraw(10)">十次抽选</button>
|
||||
</view>
|
||||
<view class="flip-grid">
|
||||
<view v-for="(cell, i) in cells" :key="i" class="flip-card" :class="{ flipped: cell.flipped }">
|
||||
<view class="flip-inner">
|
||||
<view class="flip-front">
|
||||
<view class="front-placeholder"></view>
|
||||
</view>
|
||||
<view class="flip-back" @tap="onPreview(cell)">
|
||||
<image v-if="cell.image" class="flip-image" :src="cell.image" mode="widthFix" />
|
||||
<text class="flip-title">{{ cell.title || '' }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view v-if="controls" class="flip-toolbar">
|
||||
<button class="flip-reset" @tap="reset">重置</button>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, defineExpose } from 'vue'
|
||||
|
||||
const props = defineProps({ rewards: { type: Array, default: () => [] }, controls: { type: Boolean, default: true } })
|
||||
const emit = defineEmits(['draw'])
|
||||
|
||||
const total = 16
|
||||
const cells = ref(Array(total).fill(0).map(() => ({ flipped: false, title: '', image: '' })))
|
||||
|
||||
function onDraw(count) { emit('draw', count) }
|
||||
|
||||
function revealResults(list) {
|
||||
const arr = Array.isArray(list) ? list : list ? [list] : []
|
||||
const toFill = Math.min(arr.length, total)
|
||||
const indices = Array(total).fill(0).map((_, i) => i)
|
||||
for (let i = indices.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); const tmp = indices[i]; indices[i] = indices[j]; indices[j] = tmp }
|
||||
const chosen = indices.slice(0, toFill)
|
||||
const res = arr.slice(0, toFill)
|
||||
for (let i = res.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); const t = res[i]; res[i] = res[j]; res[j] = t }
|
||||
chosen.forEach((pos, i) => {
|
||||
const it = res[i] || {}
|
||||
const title = String(it.title || it.name || '')
|
||||
const image = String(it.image || it.img || it.pic || '')
|
||||
cells.value[pos] = { flipped: false, title, image }
|
||||
const delay = 100 * i + Math.floor(Math.random() * 120)
|
||||
setTimeout(() => { cells.value[pos].flipped = true }, delay)
|
||||
})
|
||||
}
|
||||
|
||||
function reset() {
|
||||
cells.value = Array(total).fill(0).map(() => ({ flipped: false, title: '', image: '' }))
|
||||
}
|
||||
|
||||
function onPreview(cell) {
|
||||
const img = String(cell && cell.image || '')
|
||||
if (img) uni.previewImage({ urls: [img], current: img })
|
||||
}
|
||||
|
||||
defineExpose({ revealResults, reset })
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.flip-root { display: flex; flex-direction: column; gap: 16rpx; padding: 16rpx }
|
||||
.flip-actions { display: flex; gap: 12rpx }
|
||||
.flip-btn { flex: 1; background: #007AFF; color: #fff; border-radius: 8rpx }
|
||||
.flip-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 12rpx }
|
||||
.flip-card { perspective: 1000px }
|
||||
.flip-inner { position: relative; width: 100%; height: 200rpx; transform-style: preserve-3d; transition: transform 0.5s }
|
||||
.flip-card.flipped .flip-inner { transform: rotateY(180deg) }
|
||||
.flip-front, .flip-back { position: absolute; width: 100%; height: 100%; backface-visibility: hidden; border-radius: 12rpx; overflow: hidden }
|
||||
.flip-front { background: #e2e8f0; display: flex; align-items: center; justify-content: center }
|
||||
.front-placeholder { width: 80%; height: 80%; border-radius: 12rpx; background: linear-gradient(135deg, #f8fafc, #e2e8f0) }
|
||||
.flip-back { background: #fff; transform: rotateY(180deg); display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 12rpx }
|
||||
.flip-image { width: 80%; border-radius: 8rpx; margin-bottom: 8rpx; background: #f5f5f5 }
|
||||
.flip-title { font-size: 26rpx; color: #222; text-align: center; max-width: 90%; word-break: break-all }
|
||||
.flip-toolbar { display: flex; justify-content: flex-end }
|
||||
.flip-reset { background: #ffd166; color: #6b4b1f; border-radius: 999rpx }
|
||||
</style>
|
||||
@ -5,46 +5,83 @@
|
||||
</view>
|
||||
<view class="header">
|
||||
<view class="title">{{ detail.name || detail.title || '-' }}</view>
|
||||
<view class="meta">分类:{{ detail.category_name || '对对碰' }}</view>
|
||||
<view class="meta" v-if="detail.price_draw !== undefined">参与价:¥{{ detail.price_draw }}</view>
|
||||
<view class="meta" v-if="detail.status !== undefined">状态:{{ statusText }}</view>
|
||||
</view>
|
||||
<view class="actions">
|
||||
<button class="btn" @click="onPreviewBanner">查看图片</button>
|
||||
<button class="btn primary" @click="onParticipate">立即参与</button>
|
||||
</view>
|
||||
<view class="issues">
|
||||
|
||||
<view class="issues" v-if="showIssues">
|
||||
<view class="issues-title">期数</view>
|
||||
<view v-if="issues.length" class="issues-list">
|
||||
<view class="issue-item" v-for="it in issues" :key="it.id">
|
||||
<text class="issue-title">{{ it.title || ('第' + (it.no || it.index || it.issue_no || '-') + '期') }}</text>
|
||||
<text class="issue-status" v-if="it.status_text">{{ it.status_text }}</text>
|
||||
<view class="rewards" v-if="rewardsMap[it.id] && rewardsMap[it.id].length">
|
||||
<view class="reward" v-for="rw in rewardsMap[it.id]" :key="rw.id">
|
||||
<image v-if="rw.image" class="reward-img" :src="rw.image" mode="aspectFill" />
|
||||
<view class="reward-texts">
|
||||
<text class="reward-title">{{ rw.title }}</text>
|
||||
<text class="reward-meta" v-if="rw.rarity || rw.odds">{{ [rw.rarity, rw.odds].filter(Boolean).join(' · ') }}</text>
|
||||
<picker-view class="issue-picker" :value="[selectedIssueIndex]" @change="onIssueChange">
|
||||
<picker-view-column>
|
||||
<view class="picker-item" v-for="it in issues" :key="it.id">{{ it.title || ('第' + (it.no || it.index || it.issue_no || '-') + '期') }}</view>
|
||||
</picker-view-column>
|
||||
</picker-view>
|
||||
|
||||
<view class="tabs">
|
||||
<view class="tab" :class="{ active: tabActive === 'pool' }" @click="tabActive = 'pool'">本机奖池</view>
|
||||
<view class="tab" :class="{ active: tabActive === 'records' }" @click="tabActive = 'records'">中奖记录</view>
|
||||
</view>
|
||||
|
||||
<view v-show="tabActive === 'pool'">
|
||||
<view class="rewards" v-if="currentIssueId && rewardsMap[currentIssueId] && rewardsMap[currentIssueId].length">
|
||||
<el-card v-for="rw in rewardsMap[currentIssueId]" :key="rw.id" class="el-reward-card" shadow="hover">
|
||||
<template #header>
|
||||
<view class="el-card-header">
|
||||
<text class="el-card-title">{{ rw.title }}</text>
|
||||
<text v-if="rw.boss" class="badge-boss">BOSS</text>
|
||||
</view>
|
||||
</template>
|
||||
<view class="card-image-wrap">
|
||||
<image v-if="rw.image" class="card-image" :src="rw.image" mode="widthFix" />
|
||||
<text class="prob-corner tl">概率 {{ rw.percent }}%</text>
|
||||
</view>
|
||||
</view>
|
||||
</el-card>
|
||||
</view>
|
||||
<view class="rewards-empty" v-else>暂无奖励配置</view>
|
||||
</view>
|
||||
|
||||
<view v-show="tabActive === 'records'">
|
||||
<view class="records" v-if="winRecords.length">
|
||||
<el-card v-for="it in winRecords" :key="it.id" class="el-reward-card" shadow="hover">
|
||||
<template #header>
|
||||
<view class="el-card-header">
|
||||
<text class="el-card-title">{{ it.title }}</text>
|
||||
<text v-if="it.count !== undefined" class="badge-count">x{{ it.count }}</text>
|
||||
</view>
|
||||
</template>
|
||||
<view class="card-image-wrap">
|
||||
<image v-if="it.image" class="card-image" :src="it.image" mode="widthFix" />
|
||||
<text v-if="it.percent !== undefined" class="prob-corner tl">占比 {{ it.percent }}%</text>
|
||||
</view>
|
||||
</el-card>
|
||||
</view>
|
||||
<view class="rewards-empty" v-else>暂无中奖记录</view>
|
||||
</view>
|
||||
</view>
|
||||
<view v-else class="issues-empty">暂无期数</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
<view class="float-actions">
|
||||
<button class="float-btn primary" @click="onParticipate">立即参与</button>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import { ref, computed, getCurrentInstance } from 'vue'
|
||||
import ElCard from '../../../components/ElCard.vue'
|
||||
import { onLoad } from '@dcloudio/uni-app'
|
||||
import { getActivityDetail, getActivityIssues, getActivityIssueRewards } from '../../../api/appUser'
|
||||
import { getActivityDetail, getActivityIssues, getActivityIssueRewards, drawActivityIssue, getActivityWinRecords } 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 tabActive = ref('pool')
|
||||
const winRecords = ref([])
|
||||
|
||||
function statusToText(s) {
|
||||
if (s === 1) return '进行中'
|
||||
@ -67,6 +104,23 @@ function unwrap(list) {
|
||||
return Array.isArray(arr) ? arr : []
|
||||
}
|
||||
|
||||
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 normalizeIssues(list) {
|
||||
const arr = unwrap(list)
|
||||
return arr.map((i, idx) => ({
|
||||
@ -78,32 +132,127 @@ function normalizeIssues(list) {
|
||||
}
|
||||
function normalizeRewards(list) {
|
||||
const arr = unwrap(list)
|
||||
return arr.map((i, idx) => ({
|
||||
id: i.id ?? String(idx),
|
||||
title: i.title ?? i.name ?? '',
|
||||
image: i.image ?? i.img ?? i.pic ?? i.banner ?? '',
|
||||
odds: i.odds ?? i.rate ?? i.probability ?? i.prob ?? '',
|
||||
rarity: i.rarity ?? i.rarity_name ?? ''
|
||||
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 normalizeWinRecords(list) {
|
||||
const arr = unwrap(list)
|
||||
return arr.map((i, idx) => ({
|
||||
id: i.id ?? i.record_id ?? i.product_id ?? String(idx),
|
||||
title: i.title ?? i.name ?? i.product_name ?? '',
|
||||
image: cleanUrl(i.image ?? i.img ?? i.pic ?? i.product_image ?? ''),
|
||||
count: Number(i.count ?? i.total ?? i.qty ?? 1) || 1,
|
||||
percent: i.percent !== undefined ? Math.round(Number(i.percent) * 10) / 10 : undefined
|
||||
}))
|
||||
}
|
||||
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 promises = list.map(it => getActivityIssueRewards(activityId, it.id))
|
||||
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 = list[i] && list[i].id
|
||||
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)
|
||||
}
|
||||
|
||||
async function fetchWinRecords(activityId) {
|
||||
try {
|
||||
const data = await getActivityWinRecords(activityId, 1, 50)
|
||||
winRecords.value = normalizeWinRecords(data)
|
||||
} catch (e) {
|
||||
winRecords.value = []
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
const v = e && e.detail && e.detail.value
|
||||
const idx = Array.isArray(v) ? (v[0] || 0) : 0
|
||||
const arr = issues.value || []
|
||||
const bounded = Math.min(Math.max(0, idx), arr.length - 1)
|
||||
selectedIssueIndex.value = bounded
|
||||
const cur = arr[bounded]
|
||||
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)
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@ -113,41 +262,70 @@ function onPreviewBanner() {
|
||||
}
|
||||
|
||||
function onParticipate() {
|
||||
uni.showToast({ title: '功能待接入', icon: 'none' })
|
||||
const aid = activityId.value || ''
|
||||
const iid = currentIssueId.value || ''
|
||||
if (!aid || !iid) { uni.showToast({ title: '期数未选择', icon: 'none' }); return }
|
||||
uni.showLoading({ title: '抽选中...' })
|
||||
drawActivityIssue(aid, iid).then(res => {
|
||||
try { uni.hideLoading() } catch (_) {}
|
||||
const obj = res || {}
|
||||
const data = obj.data || obj.result || obj.reward || obj.item || obj
|
||||
const name = String((data && (data.title || data.name || data.product_name)) || '未知奖励')
|
||||
const img = String((data && (data.image || data.img || data.pic || data.product_image)) || '')
|
||||
uni.showModal({ title: '抽选结果', content: '恭喜获得:' + name, showCancel: false, success: () => { if (img) uni.previewImage({ urls: [img], current: img }) } })
|
||||
}).catch(err => {
|
||||
try { uni.hideLoading() } catch (_) {}
|
||||
const msg = String((err && (err.message || err.msg)) || '抽选失败')
|
||||
uni.showToast({ title: msg, icon: 'none' })
|
||||
})
|
||||
}
|
||||
|
||||
onLoad((opts) => {
|
||||
const id = (opts && opts.id) || ''
|
||||
if (id) {
|
||||
activityId.value = id
|
||||
fetchDetail(id)
|
||||
fetchIssues(id)
|
||||
fetchWinRecords(id)
|
||||
}
|
||||
ensureElCard()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.page { height: 100vh }
|
||||
.page { height: 100vh; padding-bottom: 140rpx }
|
||||
.banner { padding: 24rpx }
|
||||
.banner-img { width: 100% }
|
||||
.header { padding: 0 24rpx }
|
||||
.title { font-size: 36rpx; font-weight: 700 }
|
||||
.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 }
|
||||
.float-actions { position: fixed; left: 0; right: 0; bottom: 0; padding: 16rpx 24rpx; padding-bottom: calc(16rpx + env(safe-area-inset-bottom)); background: rgba(255,255,255,0.9); box-shadow: 0 -6rpx 16rpx rgba(0,0,0,0.06); z-index: 9999 }
|
||||
.float-btn { width: 100%; border-radius: 999rpx }
|
||||
.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-item { display: flex; justify-content: space-between; padding: 12rpx 0; border-bottom: 1rpx solid #f0f0f0 }
|
||||
.issue-item:last-child { border-bottom: 0 }
|
||||
.issue-title { font-size: 26rpx }
|
||||
.issue-status { font-size: 24rpx; color: #666 }
|
||||
.rewards { width: 100%; margin-top: 12rpx }
|
||||
.reward { display: flex; align-items: center; margin-bottom: 8rpx }
|
||||
.reward-img { width: 80rpx; height: 80rpx; border-radius: 8rpx; margin-right: 12rpx; background: #f5f5f5 }
|
||||
.reward-texts { display: flex; flex-direction: column }
|
||||
.reward-title { font-size: 26rpx }
|
||||
.reward-meta { font-size: 22rpx; color: #888 }
|
||||
.issue-picker { height: 200rpx; background: #f8f8f8; border-radius: 12rpx; margin-bottom: 64rpx }
|
||||
.picker-item { height: 40rpx; line-height: 40rpx; text-align: center; font-size: 26rpx }
|
||||
.tabs { display: flex; padding: 0 12rpx; margin-bottom: 16rpx }
|
||||
.tab { flex: 1; text-align: center; font-size: 28rpx; padding: 16rpx 0; border: 2rpx solid #f0c58a; color: #8a5a2b; background: #fff3df; border-radius: 16rpx }
|
||||
.tab + .tab { margin-left: 12rpx }
|
||||
.tab.active { background: #ffdfaa; border-color: #ffb74d; color: #6b4b1f; font-weight: 600 }
|
||||
.rewards { width: 100%; margin-top: 24rpx }
|
||||
.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 }
|
||||
.badge-count { background: #ffd166; color: #6b4b1f; font-size: 22rpx; padding: 4rpx 10rpx; border-radius: 999rpx }
|
||||
.rewards-empty { font-size: 24rpx; color: #999 }
|
||||
.issues-empty { font-size: 24rpx; color: #999 }
|
||||
</style>
|
||||
@ -5,46 +5,57 @@
|
||||
</view>
|
||||
<view class="header">
|
||||
<view class="title">{{ detail.name || detail.title || '-' }}</view>
|
||||
<view class="meta">分类:{{ detail.category_name || '无限赏' }}</view>
|
||||
<view class="meta" v-if="detail.price_draw !== undefined">单次抽选:¥{{ detail.price_draw }}</view>
|
||||
<view class="meta" v-if="detail.status !== undefined">状态:{{ statusText }}</view>
|
||||
</view>
|
||||
<view class="actions">
|
||||
<button class="btn" @click="onPreviewBanner">查看图片</button>
|
||||
<button class="btn primary" @click="onParticipate">立即参与</button>
|
||||
<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">
|
||||
<view class="issues-title">期数</view>
|
||||
<view v-if="issues.length" class="issues-list">
|
||||
<view class="issue-item" v-for="it in issues" :key="it.id">
|
||||
<text class="issue-title">{{ it.title || ('第' + (it.no || it.index || it.issue_no || '-') + '期') }}</text>
|
||||
<text class="issue-status" v-if="it.status_text">{{ it.status_text }}</text>
|
||||
<view class="rewards" v-if="rewardsMap[it.id] && rewardsMap[it.id].length">
|
||||
<view class="reward" v-for="rw in rewardsMap[it.id]" :key="rw.id">
|
||||
<image v-if="rw.image" class="reward-img" :src="rw.image" mode="aspectFill" />
|
||||
<view class="reward-texts">
|
||||
<text class="reward-title">{{ rw.title }}</text>
|
||||
<text class="reward-meta" v-if="rw.rarity || rw.odds">{{ [rw.rarity, rw.odds].filter(Boolean).join(' · ') }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="rewards-empty" v-else>暂无奖励配置</view>
|
||||
</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 v-else class="issues-empty">暂无期数</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 } from 'vue'
|
||||
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 } from '../../../api/appUser'
|
||||
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 '进行中'
|
||||
@ -76,76 +87,220 @@ function normalizeIssues(list) {
|
||||
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)
|
||||
return arr.map((i, idx) => ({
|
||||
id: i.id ?? String(idx),
|
||||
title: i.title ?? i.name ?? '',
|
||||
image: i.image ?? i.img ?? i.pic ?? i.banner ?? '',
|
||||
odds: i.odds ?? i.rate ?? i.probability ?? i.prob ?? '',
|
||||
rarity: i.rarity ?? i.rarity_name ?? ''
|
||||
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 promises = list.map(it => getActivityIssueRewards(activityId, it.id))
|
||||
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 = list[i] && list[i].id
|
||||
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 onParticipate() {
|
||||
uni.showToast({ title: '功能待接入', icon: 'none' })
|
||||
|
||||
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 }
|
||||
.page { height: 100vh; padding-bottom: 140rpx }
|
||||
.banner { padding: 24rpx }
|
||||
.banner-img { width: 100% }
|
||||
.header { padding: 0 24rpx }
|
||||
.title { font-size: 36rpx; font-weight: 700 }
|
||||
.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-item { display: flex; justify-content: space-between; padding: 12rpx 0; border-bottom: 1rpx solid #f0f0f0 }
|
||||
.issue-item:last-child { border-bottom: 0 }
|
||||
.issue-title { font-size: 26rpx }
|
||||
.issue-status { font-size: 24rpx; color: #666 }
|
||||
.rewards { width: 100%; margin-top: 12rpx }
|
||||
.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-texts { display: flex; flex-direction: column }
|
||||
.reward-title { font-size: 26rpx }
|
||||
.reward-meta { font-size: 22rpx; color: #888 }
|
||||
.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>
|
||||
|
||||
@ -5,45 +5,58 @@
|
||||
</view>
|
||||
<view class="header">
|
||||
<view class="title">{{ detail.name || detail.title || '-' }}</view>
|
||||
<view class="meta">分类:{{ detail.category_name || '一番赏' }}</view>
|
||||
<view class="meta" v-if="detail.price_draw !== undefined">抽选价:¥{{ detail.price_draw }}</view>
|
||||
<view class="meta" v-if="detail.status !== undefined">状态:{{ statusText }}</view>
|
||||
</view>
|
||||
<view class="actions">
|
||||
<button class="btn" @click="onPreviewBanner">查看图片</button>
|
||||
<button class="btn primary" @click="onParticipate">立即参与</button>
|
||||
<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">
|
||||
<view class="issues-title">期数</view>
|
||||
<view v-if="issues.length" class="issues-list">
|
||||
<view class="issue-item" v-for="it in issues" :key="it.id">
|
||||
<text class="issue-title">{{ it.title || ('第' + (it.no || it.index || it.issue_no || '-') + '期') }}</text>
|
||||
<text class="issue-status" v-if="it.status_text">{{ it.status_text }}</text>
|
||||
<view class="rewards" v-if="rewardsMap[it.id] && rewardsMap[it.id].length">
|
||||
<view class="reward" v-for="rw in rewardsMap[it.id]" :key="rw.id">
|
||||
<image v-if="rw.image" class="reward-img" :src="rw.image" mode="aspectFill" />
|
||||
<view class="reward-texts">
|
||||
<text class="reward-title">{{ rw.title }}</text>
|
||||
<text class="reward-meta" v-if="rw.rarity || rw.odds">{{ [rw.rarity, rw.odds].filter(Boolean).join(' · ') }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="rewards-empty" v-else>暂无奖励配置</view>
|
||||
</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 v-else class="issues-empty">暂无期数</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 } from 'vue'
|
||||
import { ref, computed, getCurrentInstance } from 'vue'
|
||||
import ElCard from '../../../components/ElCard.vue'
|
||||
import { onLoad } from '@dcloudio/uni-app'
|
||||
import { getActivityDetail, getActivityIssues, getActivityIssueRewards } from '../../../api/appUser'
|
||||
import FlipGrid from '../../../components/FlipGrid.vue'
|
||||
import { getActivityDetail, getActivityIssues, getActivityIssueRewards, drawActivityIssue, getActivityWinRecords } from '../../../api/appUser'
|
||||
|
||||
const detail = 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 tabActive = ref('pool')
|
||||
const winRecords = ref([])
|
||||
const drawLoading = ref(false)
|
||||
const points = ref(0)
|
||||
const flipRef = ref(null)
|
||||
const showFlip = 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
|
||||
})
|
||||
|
||||
function statusToText(s) {
|
||||
if (s === 1) return '进行中'
|
||||
@ -77,75 +90,239 @@ function normalizeIssues(list) {
|
||||
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)
|
||||
return arr.map((i, idx) => ({
|
||||
id: i.id ?? String(idx),
|
||||
title: i.title ?? i.name ?? '',
|
||||
image: i.image ?? i.img ?? i.pic ?? i.banner ?? '',
|
||||
odds: i.odds ?? i.rate ?? i.probability ?? i.prob ?? '',
|
||||
rarity: i.rarity ?? i.rarity_name ?? ''
|
||||
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 normalizeWinRecords(list) {
|
||||
const arr = unwrap(list)
|
||||
return arr.map((i, idx) => ({
|
||||
id: i.id ?? i.record_id ?? i.product_id ?? String(idx),
|
||||
title: i.title ?? i.name ?? i.product_name ?? '',
|
||||
image: cleanUrl(i.image ?? i.img ?? i.pic ?? i.product_image ?? ''),
|
||||
count: Number(i.count ?? i.total ?? i.qty ?? 1) || 1,
|
||||
percent: i.percent !== undefined ? Math.round(Number(i.percent) * 10) / 10 : undefined
|
||||
}))
|
||||
}
|
||||
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 promises = list.map(it => getActivityIssueRewards(activityId, it.id))
|
||||
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 = list[i] && list[i].id
|
||||
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)
|
||||
}
|
||||
|
||||
async function fetchWinRecords(activityId) {
|
||||
try {
|
||||
const data = await getActivityWinRecords(activityId, 1, 50)
|
||||
winRecords.value = normalizeWinRecords(data)
|
||||
} catch (e) {
|
||||
winRecords.value = []
|
||||
}
|
||||
}
|
||||
|
||||
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 onParticipate() {
|
||||
uni.showToast({ title: '功能待接入', icon: 'none' })
|
||||
|
||||
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)
|
||||
fetchWinRecords(id)
|
||||
}
|
||||
ensureElCard()
|
||||
})
|
||||
|
||||
function closeFlip() { showFlip.value = false }
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.page { height: 100vh }
|
||||
.page { height: 100vh; padding-bottom: 140rpx }
|
||||
.banner { padding: 24rpx }
|
||||
.banner-img { width: 100% }
|
||||
.header { padding: 0 24rpx }
|
||||
.title { font-size: 36rpx; font-weight: 700 }
|
||||
.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-item { display: flex; justify-content: space-between; padding: 12rpx 0; border-bottom: 1rpx solid #f0f0f0 }
|
||||
.issue-item:last-child { border-bottom: 0 }
|
||||
.issue-title { font-size: 26rpx }
|
||||
.issue-status { font-size: 24rpx; color: #666 }
|
||||
.rewards { width: 100%; margin-top: 12rpx }
|
||||
.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 }
|
||||
.reward { display: flex; align-items: center; margin-bottom: 8rpx }
|
||||
.reward-img { width: 80rpx; height: 80rpx; border-radius: 8rpx; margin-right: 12rpx; background: #f5f5f5 }
|
||||
.reward-texts { display: flex; flex-direction: column }
|
||||
.reward-title { font-size: 26rpx }
|
||||
.reward-meta { font-size: 22rpx; color: #888 }
|
||||
.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 }
|
||||
.badge-count { background: #ffd166; color: #6b4b1f; font-size: 22rpx; padding: 4rpx 10rpx; border-radius: 999rpx }
|
||||
.rewards-empty { font-size: 24rpx; color: #999 }
|
||||
.issues-empty { font-size: 24rpx; color: #999 }
|
||||
</style>
|
||||
@ -2,8 +2,8 @@
|
||||
<scroll-view class="page" scroll-y>
|
||||
<view class="h1">购买协议</view>
|
||||
<view class="meta">生效日期:2025年11月18日</view>
|
||||
<view class="meta">运营方:【公司全称】</view>
|
||||
<view class="p">本《购买协议》适用于您在【您的小程序名称】(以下简称“本平台”)购买盲盒商品的行为。当您点击“立即购买”并完成支付时,即视为您已阅读、理解并同意本协议全部内容。</view>
|
||||
<view class="meta">运营方:柯大鸭潮玩</view>
|
||||
<view class="p">本《购买协议》适用于您在【柯大鸭潮玩】(以下简称“本平台”)购买盲盒商品的行为。当您点击“立即购买”并完成支付时,即视为您已阅读、理解并同意本协议全部内容。</view>
|
||||
<view class="h2">一、商品说明</view>
|
||||
<view class="ol">
|
||||
<view class="li">盲盒特性:本平台所售盲盒为系列化商品,包装外观一致,内部款式随机,具体款式无法提前指定或预知。</view>
|
||||
@ -22,7 +22,7 @@
|
||||
</view>
|
||||
<view class="h2">四、售后服务</view>
|
||||
<view class="ol">
|
||||
<view class="li">质量问题(如商品破损、漏发、错发、非盲盒系列商品):请在签收后2到4小时内联系客服并提供凭证(如开箱视频、照片);经核实后,平台将为您补发、换货或退款。</view>
|
||||
<view class="li">质量问题(如商品破损、漏发、错发、非盲盒系列商品):请在签收后2小时内联系客服并提供凭证(如开箱视频、照片);经核实后,平台将为您补发、换货或退款。</view>
|
||||
<view class="li">非质量问题(如抽中重复款式、不喜欢款式、未抽中隐藏款等):不支持无理由退换货。</view>
|
||||
<view class="li">拆封后商品:出于卫生与二次销售考虑,已拆封盲盒恕不退换(质量问题除外)。</view>
|
||||
</view>
|
||||
@ -45,10 +45,9 @@
|
||||
<view class="h2">八、协议效力</view>
|
||||
<view class="p">本购买协议为《用户协议》的补充,两者冲突时,以本协议中关于交易的条款为准。未尽事宜,依照《消费者权益保护法》《电子商务法》等法律法规执行。</view>
|
||||
<view class="h2">九、联系我们</view>
|
||||
<view class="p">售后专线:service@yourdomain.com</view>
|
||||
<view class="p">工作时间:工作日 9:00–18:00</view>
|
||||
<view class="p">运营主体:【公司全称】</view>
|
||||
<view class="p">统一社会信用代码:【XXXXXXXXXXXXXX】</view>
|
||||
<view class="p">售后专线:请联系企业客服,客服将在工作时间内为您服务。</view>
|
||||
<view class="p">工作时间:工作日 13:00–04:00</view>
|
||||
<view class="p">运营主体:【柯大鸭潮玩】</view>
|
||||
<view class="tip">理性消费提醒:盲盒是一种娱乐消费形式,请根据自身经济能力合理购买,切勿沉迷或过度投入。</view>
|
||||
</scroll-view>
|
||||
</template>
|
||||
|
||||
@ -2,8 +2,8 @@
|
||||
<scroll-view class="page" scroll-y>
|
||||
<view class="h1">用户协议</view>
|
||||
<view class="meta">生效日期:2025年11月18日</view>
|
||||
<view class="meta">运营方:【公司全称】</view>
|
||||
<view class="p">欢迎您使用【您的小程序名称】(以下简称“本平台”)提供的服务。请您在注册、登录或使用本平台前,认真阅读并充分理解本《用户协议》(以下简称“本协议”)。一旦您完成注册、登录或以任何方式使用本平台服务,即视为您已完全接受本协议全部条款。如您不同意,请勿使用本平台。</view>
|
||||
<view class="meta">运营方:【柯大鸭潮玩】</view>
|
||||
<view class="p">欢迎您使用【柯大鸭潮玩】(以下简称“本平台”)提供的服务。请您在注册、登录或使用本平台前,认真阅读并充分理解本《用户协议》(以下简称“本协议”)。一旦您完成注册、登录或以任何方式使用本平台服务,即视为您已完全接受本协议全部条款。如您不同意,请勿使用本平台。</view>
|
||||
<view class="h2">一、协议范围</view>
|
||||
<view class="p">本协议规范您作为用户在本平台注册、浏览、互动、参与活动等行为,是您与本平台之间的基本权利义务约定。</view>
|
||||
<view class="h2">二、用户资格</view>
|
||||
@ -44,10 +44,9 @@
|
||||
<view class="h2">九、法律适用与争议解决</view>
|
||||
<view class="p">本协议适用中华人民共和国法律。因本协议引起的争议,双方应协商解决;协商不成的,提交本平台运营方所在地有管辖权的人民法院诉讼解决。</view>
|
||||
<view class="h2">十、联系我们</view>
|
||||
<view class="p">客服邮箱:service@yourdomain.com</view>
|
||||
<view class="p">客服电话:400-XXX-XXXX(工作日 9:00–18:00)</view>
|
||||
<view class="p">运营主体:【公司全称】</view>
|
||||
<view class="p">地址:【公司注册地址】</view>
|
||||
<view class="p">客服邮箱:请通过企业客服联系我们,客服将在工作时间内为您服务。</view>
|
||||
<view class="p">工作时间:(工作日 13:00–04:00)</view>
|
||||
<view class="p">运营主体:【柯大鸭潮玩】</view>
|
||||
<view class="tip">温馨提示:盲盒具有随机性和娱乐性,请理性参与,避免沉迷。未成年人禁止参与购买。</view>
|
||||
</scroll-view>
|
||||
</template>
|
||||
|
||||
@ -21,15 +21,21 @@
|
||||
|
||||
<view class="activity-section">
|
||||
<view class="section-title">活动</view>
|
||||
<view v-if="activities.length" class="activity-grid">
|
||||
<view class="activity-item" v-for="a in activities" :key="a.id" @tap="onActivityTap(a)">
|
||||
<image v-if="a.image" class="activity-thumb" :src="a.image" mode="aspectFill" />
|
||||
<view v-else class="banner-fallback">
|
||||
<text class="banner-fallback-text">{{ a.title || '活动敬请期待' }}</text>
|
||||
<view v-if="activityGroups.length">
|
||||
<scroll-view class="tabs" scroll-x>
|
||||
<view class="tab" v-for="g in activityGroups" :key="g.name" :class="{ active: g.name === selectedGroupName }" @tap="onSelectGroup(g.name)">{{ g.name }}</view>
|
||||
</scroll-view>
|
||||
<view v-if="activeGroupItems.length" class="activity-grid">
|
||||
<view class="activity-item" v-for="a in activeGroupItems" :key="a.id" @tap="onActivityTap(a)">
|
||||
<image v-if="a.image" class="activity-thumb" :src="a.image" mode="aspectFill" />
|
||||
<view v-else class="banner-fallback">
|
||||
<text class="banner-fallback-text">{{ a.title || '活动敬请期待' }}</text>
|
||||
</view>
|
||||
<text class="activity-name">{{ a.title }}</text>
|
||||
<text class="activity-desc" v-if="a.subtitle">{{ a.subtitle }}</text>
|
||||
</view>
|
||||
<text class="activity-name">{{ a.title }}</text>
|
||||
<text class="activity-desc" v-if="a.subtitle">{{ a.subtitle }}</text>
|
||||
</view>
|
||||
<view v-else class="activity-empty">该分组暂无活动</view>
|
||||
</view>
|
||||
<view v-else class="activity-empty">暂无活动</view>
|
||||
</view>
|
||||
@ -43,7 +49,8 @@ export default {
|
||||
return {
|
||||
notices: [],
|
||||
banners: [],
|
||||
activities: []
|
||||
activities: [],
|
||||
selectedGroupName: ''
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
@ -61,6 +68,22 @@ export default {
|
||||
{ id: 'ph-2', title: '敬请期待', image: '' },
|
||||
{ id: 'ph-3', title: '更多活动请关注', image: '' }
|
||||
]
|
||||
},
|
||||
activityGroups() {
|
||||
const list = Array.isArray(this.activities) ? this.activities : []
|
||||
const map = new Map()
|
||||
list.forEach(a => {
|
||||
const key = (a.category_name || '').trim() || '其他'
|
||||
if (!map.has(key)) map.set(key, [])
|
||||
map.get(key).push(a)
|
||||
})
|
||||
return Array.from(map.entries()).map(([name, items]) => ({ name, items }))
|
||||
},
|
||||
activeGroupItems() {
|
||||
const groups = this.activityGroups
|
||||
const name = this.selectedGroupName || (groups[0] && groups[0].name) || ''
|
||||
const g = groups.find(x => x.name === name)
|
||||
return g ? g.items : []
|
||||
}
|
||||
},
|
||||
onShow() {
|
||||
@ -82,6 +105,16 @@ export default {
|
||||
this.loadHomeData()
|
||||
},
|
||||
methods: {
|
||||
onSelectGroup(name) {
|
||||
this.selectedGroupName = String(name || '')
|
||||
},
|
||||
updateSelectedGroup() {
|
||||
const groups = this.activityGroups
|
||||
if (!groups.length) { this.selectedGroupName = ''; return }
|
||||
if (!groups.find(g => g.name === this.selectedGroupName)) {
|
||||
this.selectedGroupName = groups[0].name
|
||||
}
|
||||
},
|
||||
toArray(x) { return Array.isArray(x) ? x : [] },
|
||||
unwrap(list) {
|
||||
if (Array.isArray(list)) return list
|
||||
@ -126,7 +159,7 @@ export default {
|
||||
console.log('normalizeActivities input', list, 'unwrapped', arr)
|
||||
const mapped = arr.map((i, idx) => ({
|
||||
id: i.id ?? String(idx),
|
||||
image: this.cleanUrl(i.banner ?? i.coverUrl ?? i.cover_url ?? i.image ?? i.img ?? i.pic ?? ''),
|
||||
image: this.cleanUrl(i.image ?? i.banner ?? i.coverUrl ?? i.cover_url ?? i.img ?? i.pic ?? ''),
|
||||
title: i.title ?? i.name ?? '',
|
||||
subtitle: this.buildActivitySubtitle(i),
|
||||
link: this.cleanUrl(i.linkUrl ?? i.link_url ?? i.link ?? i.url ?? ''),
|
||||
@ -168,9 +201,11 @@ export default {
|
||||
if (acRes.status === 'fulfilled') {
|
||||
console.log('activities ok', acRes.value)
|
||||
this.activities = this.normalizeActivities(acRes.value)
|
||||
this.updateSelectedGroup()
|
||||
} else {
|
||||
console.error('activities error', acRes.reason)
|
||||
this.activities = []
|
||||
this.updateSelectedGroup()
|
||||
}
|
||||
console.log('home normalized', { notices: this.notices, banners: this.banners, activities: this.activities })
|
||||
},
|
||||
@ -216,6 +251,9 @@ export default {
|
||||
.banner-fallback-text { color: #666; font-size: 28rpx }
|
||||
.activity-section { background: #ffffff; border-radius: 12rpx; padding: 24rpx }
|
||||
.section-title { font-size: 30rpx; font-weight: 600; margin-bottom: 16rpx }
|
||||
.tabs { white-space: nowrap; display: flex; gap: 12rpx; margin-bottom: 16rpx }
|
||||
.tab { display: inline-flex; align-items: center; height: 56rpx; padding: 0 20rpx; border-radius: 999rpx; background: #f5f7fa; color: #555; font-size: 26rpx }
|
||||
.tab.active { background: #007AFF; color: #fff }
|
||||
.activity-grid { display: flex; flex-wrap: wrap; margin: -12rpx }
|
||||
.activity-item { width: 50%; padding: 12rpx }
|
||||
.activity-thumb { width: 100%; height: 200rpx; border-radius: 8rpx }
|
||||
|
||||
@ -1,5 +1,18 @@
|
||||
<template>
|
||||
<view class="page">
|
||||
<view v-if="showNotice" class="notice-mask">
|
||||
<view class="notice-dialog">
|
||||
<view class="notice-title">提示</view>
|
||||
<view class="notice-content">由于价格浮动,当前暂不支持自行兑换商品,兑换请联系客服核对价格。</view>
|
||||
<view class="notice-actions">
|
||||
<view class="notice-check" @tap="toggleHideForever">
|
||||
<view class="check-box" :class="{ on: hideForever }"></view>
|
||||
<text class="check-text">不再显示</text>
|
||||
</view>
|
||||
<button class="notice-btn" type="primary" @tap="onDismissNotice">我知道了</button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view v-if="loading" class="loading-wrap"><view class="spinner"></view></view>
|
||||
<view class="products-section" v-else>
|
||||
<view class="section-title">商品</view>
|
||||
@ -53,6 +66,9 @@ const minPrice = ref('')
|
||||
const maxPrice = ref('')
|
||||
const displayCount = computed(() => (columns.value[0].length + columns.value[1].length))
|
||||
const loadedMap = ref({})
|
||||
const showNotice = ref(false)
|
||||
const hideForever = ref(false)
|
||||
const skipReloadOnce = ref(false)
|
||||
function getKey(p) { return String((p && p.id) ?? '') + '|' + String((p && p.image) ?? '') }
|
||||
function unwrap(list) {
|
||||
if (Array.isArray(list)) return list
|
||||
@ -146,6 +162,8 @@ function onProductTap(p) {
|
||||
const imgs = (Array.isArray(products.value) ? products.value : []).map(x => x.image).filter(Boolean)
|
||||
const current = p && p.image
|
||||
if (current) {
|
||||
skipReloadOnce.value = true
|
||||
try { uni.setStorageSync('shop_skip_reload_once', '1') } catch (_) {}
|
||||
uni.previewImage({ urls: imgs.length ? imgs : [current], current })
|
||||
return
|
||||
}
|
||||
@ -249,14 +267,50 @@ onShow(async () => {
|
||||
})
|
||||
return
|
||||
}
|
||||
try {
|
||||
const sess = String(uni.getStorageSync('app_session_id') || '')
|
||||
const hiddenSess = String(uni.getStorageSync('shop_notice_hidden_session_id') || '')
|
||||
const hiddenThisSession = !!(sess && hiddenSess && hiddenSess === sess)
|
||||
showNotice.value = !hiddenThisSession
|
||||
hideForever.value = hiddenThisSession
|
||||
} catch (_) { showNotice.value = true; hideForever.value = false }
|
||||
try {
|
||||
const skip = !!uni.getStorageSync('shop_skip_reload_once')
|
||||
if (skipReloadOnce.value || skip) {
|
||||
skipReloadOnce.value = false
|
||||
uni.setStorageSync('shop_skip_reload_once', '')
|
||||
return
|
||||
}
|
||||
} catch (_) {}
|
||||
loading.value = true
|
||||
await loadProducts()
|
||||
loading.value = false
|
||||
})
|
||||
|
||||
function toggleHideForever() { hideForever.value = !hideForever.value }
|
||||
function onDismissNotice() {
|
||||
if (hideForever.value) {
|
||||
try {
|
||||
const sess = String(uni.getStorageSync('app_session_id') || '')
|
||||
if (sess) uni.setStorageSync('shop_notice_hidden_session_id', sess)
|
||||
} catch (_) {}
|
||||
}
|
||||
showNotice.value = false
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.page { padding: 24rpx }
|
||||
.notice-mask { position: fixed; left: 0; top: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.45); z-index: 9999; display: flex; align-items: center; justify-content: center }
|
||||
.notice-dialog { width: 86%; max-width: 640rpx; background: #fff; border-radius: 16rpx; overflow: hidden; box-shadow: 0 12rpx 24rpx rgba(0,0,0,0.18) }
|
||||
.notice-title { font-size: 32rpx; font-weight: 600; padding: 24rpx 24rpx 0 }
|
||||
.notice-content { padding: 16rpx 24rpx; font-size: 26rpx; color: #333; line-height: 1.6 }
|
||||
.notice-actions { display: flex; align-items: center; justify-content: space-between; padding: 16rpx 24rpx 24rpx }
|
||||
.notice-check { display: flex; align-items: center; gap: 8rpx }
|
||||
.check-box { width: 28rpx; height: 28rpx; border-radius: 6rpx; border: 2rpx solid #007AFF; background: #fff }
|
||||
.check-box.on { background: #007AFF }
|
||||
.check-text { font-size: 26rpx; color: #555 }
|
||||
.notice-btn { background: #007AFF; color: #fff; border-radius: 999rpx; padding: 0 28rpx }
|
||||
.section-title { font-size: 30rpx; font-weight: 600; margin-bottom: 16rpx }
|
||||
.products-section { background: #ffffff; border-radius: 12rpx; padding: 24rpx; margin-top: 24rpx }
|
||||
.loading-wrap { min-height: 60vh; display: flex; align-items: center; justify-content: center }
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user