合并页面
This commit is contained in:
parent
0c4ed4360f
commit
8f044d68ca
1
App.vue
1
App.vue
@ -2,6 +2,7 @@
|
|||||||
export default {
|
export default {
|
||||||
onLaunch: function() {
|
onLaunch: function() {
|
||||||
console.log('App Launch')
|
console.log('App Launch')
|
||||||
|
try { uni.setStorageSync('app_session_id', String(Date.now())) } catch (_) {}
|
||||||
},
|
},
|
||||||
onShow: function() {
|
onShow: function() {
|
||||||
console.log('App Show')
|
console.log('App Show')
|
||||||
|
|||||||
@ -57,4 +57,12 @@ export function getActivityIssues(activity_id) {
|
|||||||
|
|
||||||
export function getActivityIssueRewards(activity_id, issue_id) {
|
export function getActivityIssueRewards(activity_id, issue_id) {
|
||||||
return authRequest({ url: `/api/app/activities/${activity_id}/issues/${issue_id}/rewards`, method: 'GET' })
|
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>
|
||||||
<view class="header">
|
<view class="header">
|
||||||
<view class="title">{{ detail.name || detail.title || '-' }}</view>
|
<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.price_draw !== undefined">参与价:¥{{ detail.price_draw }}</view>
|
||||||
<view class="meta" v-if="detail.status !== undefined">状态:{{ statusText }}</view>
|
|
||||||
</view>
|
</view>
|
||||||
<view class="actions">
|
|
||||||
<button class="btn" @click="onPreviewBanner">查看图片</button>
|
<view class="issues" v-if="showIssues">
|
||||||
<button class="btn primary" @click="onParticipate">立即参与</button>
|
|
||||||
</view>
|
|
||||||
<view class="issues">
|
|
||||||
<view class="issues-title">期数</view>
|
<view class="issues-title">期数</view>
|
||||||
<view v-if="issues.length" class="issues-list">
|
<view v-if="issues.length" class="issues-list">
|
||||||
<view class="issue-item" v-for="it in issues" :key="it.id">
|
<picker-view class="issue-picker" :value="[selectedIssueIndex]" @change="onIssueChange">
|
||||||
<text class="issue-title">{{ it.title || ('第' + (it.no || it.index || it.issue_no || '-') + '期') }}</text>
|
<picker-view-column>
|
||||||
<text class="issue-status" v-if="it.status_text">{{ it.status_text }}</text>
|
<view class="picker-item" v-for="it in issues" :key="it.id">{{ it.title || ('第' + (it.no || it.index || it.issue_no || '-') + '期') }}</view>
|
||||||
<view class="rewards" v-if="rewardsMap[it.id] && rewardsMap[it.id].length">
|
</picker-view-column>
|
||||||
<view class="reward" v-for="rw in rewardsMap[it.id]" :key="rw.id">
|
</picker-view>
|
||||||
<image v-if="rw.image" class="reward-img" :src="rw.image" mode="aspectFill" />
|
|
||||||
<view class="reward-texts">
|
<view class="tabs">
|
||||||
<text class="reward-title">{{ rw.title }}</text>
|
<view class="tab" :class="{ active: tabActive === 'pool' }" @click="tabActive = 'pool'">本机奖池</view>
|
||||||
<text class="reward-meta" v-if="rw.rarity || rw.odds">{{ [rw.rarity, rw.odds].filter(Boolean).join(' · ') }}</text>
|
<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>
|
||||||
</view>
|
</el-card>
|
||||||
</view>
|
</view>
|
||||||
<view class="rewards-empty" v-else>暂无奖励配置</view>
|
<view class="rewards-empty" v-else>暂无奖励配置</view>
|
||||||
</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>
|
||||||
<view v-else class="issues-empty">暂无期数</view>
|
<view v-else class="issues-empty">暂无期数</view>
|
||||||
</view>
|
</view>
|
||||||
</scroll-view>
|
</scroll-view>
|
||||||
|
<view class="float-actions">
|
||||||
|
<button class="float-btn primary" @click="onParticipate">立即参与</button>
|
||||||
|
</view>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<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 { 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 detail = ref({})
|
||||||
const statusText = ref('')
|
const statusText = ref('')
|
||||||
const issues = ref([])
|
const issues = ref([])
|
||||||
const rewardsMap = 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) {
|
function statusToText(s) {
|
||||||
if (s === 1) return '进行中'
|
if (s === 1) return '进行中'
|
||||||
@ -67,6 +104,23 @@ function unwrap(list) {
|
|||||||
return Array.isArray(arr) ? arr : []
|
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) {
|
function normalizeIssues(list) {
|
||||||
const arr = unwrap(list)
|
const arr = unwrap(list)
|
||||||
return arr.map((i, idx) => ({
|
return arr.map((i, idx) => ({
|
||||||
@ -78,32 +132,127 @@ function normalizeIssues(list) {
|
|||||||
}
|
}
|
||||||
function normalizeRewards(list) {
|
function normalizeRewards(list) {
|
||||||
const arr = unwrap(list)
|
const arr = unwrap(list)
|
||||||
return arr.map((i, idx) => ({
|
const items = arr.map((i, idx) => ({
|
||||||
id: i.id ?? String(idx),
|
id: i.product_id ?? i.id ?? String(idx),
|
||||||
title: i.title ?? i.name ?? '',
|
title: i.name ?? i.title ?? '',
|
||||||
image: i.image ?? i.img ?? i.pic ?? i.banner ?? '',
|
image: cleanUrl(i.product_image ?? i.image ?? i.img ?? i.pic ?? i.banner ?? ''),
|
||||||
odds: i.odds ?? i.rate ?? i.probability ?? i.prob ?? '',
|
weight: Number(i.weight) || 0,
|
||||||
rarity: i.rarity ?? i.rarity_name ?? ''
|
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) {
|
async function fetchRewardsForIssues(activityId) {
|
||||||
const list = issues.value || []
|
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 results = await Promise.allSettled(promises)
|
||||||
|
const nextAct = { ...act }
|
||||||
results.forEach((res, i) => {
|
results.forEach((res, i) => {
|
||||||
const issueId = list[i] && list[i].id
|
const issueId = toFetch[i] && toFetch[i].id
|
||||||
if (!issueId) return
|
if (!issueId) return
|
||||||
const value = res.status === 'fulfilled' ? normalizeRewards(res.value) : []
|
const value = res.status === 'fulfilled' ? normalizeRewards(res.value) : []
|
||||||
rewardsMap.value = { ...(rewardsMap.value || {}), [issueId]: 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) {
|
async function fetchIssues(id) {
|
||||||
const data = await getActivityIssues(id)
|
const data = await getActivityIssues(id)
|
||||||
issues.value = normalizeIssues(data)
|
issues.value = normalizeIssues(data)
|
||||||
|
const latestId = pickLatestIssueId(issues.value)
|
||||||
|
setSelectedById(latestId)
|
||||||
await fetchRewardsForIssues(id)
|
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() {
|
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) => {
|
onLoad((opts) => {
|
||||||
const id = (opts && opts.id) || ''
|
const id = (opts && opts.id) || ''
|
||||||
if (id) {
|
if (id) {
|
||||||
|
activityId.value = id
|
||||||
fetchDetail(id)
|
fetchDetail(id)
|
||||||
fetchIssues(id)
|
fetchIssues(id)
|
||||||
|
fetchWinRecords(id)
|
||||||
}
|
}
|
||||||
|
ensureElCard()
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.page { height: 100vh }
|
.page { height: 100vh; padding-bottom: 140rpx }
|
||||||
.banner { padding: 24rpx }
|
.banner { padding: 24rpx }
|
||||||
.banner-img { width: 100% }
|
.banner-img { width: 100% }
|
||||||
.header { padding: 0 24rpx }
|
.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 }
|
.meta { margin-top: 8rpx; font-size: 26rpx; color: #666 }
|
||||||
.actions { display: flex; padding: 24rpx; gap: 16rpx }
|
.actions { display: flex; padding: 24rpx; gap: 16rpx }
|
||||||
.btn { flex: 1 }
|
.btn { flex: 1 }
|
||||||
.primary { background-color: #007AFF; color: #fff }
|
.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 { background: #fff; border-radius: 12rpx; margin: 0 24rpx 24rpx; padding: 16rpx }
|
||||||
.issues-title { font-size: 30rpx; font-weight: 600; margin-bottom: 12rpx }
|
.issues-title { font-size: 30rpx; font-weight: 600; margin-bottom: 12rpx }
|
||||||
.issues-list { }
|
.issues-list { }
|
||||||
.issue-item { display: flex; justify-content: space-between; padding: 12rpx 0; border-bottom: 1rpx solid #f0f0f0 }
|
.issue-picker { height: 200rpx; background: #f8f8f8; border-radius: 12rpx; margin-bottom: 64rpx }
|
||||||
.issue-item:last-child { border-bottom: 0 }
|
.picker-item { height: 40rpx; line-height: 40rpx; text-align: center; font-size: 26rpx }
|
||||||
.issue-title { font-size: 26rpx }
|
.tabs { display: flex; padding: 0 12rpx; margin-bottom: 16rpx }
|
||||||
.issue-status { font-size: 24rpx; color: #666 }
|
.tab { flex: 1; text-align: center; font-size: 28rpx; padding: 16rpx 0; border: 2rpx solid #f0c58a; color: #8a5a2b; background: #fff3df; border-radius: 16rpx }
|
||||||
.rewards { width: 100%; margin-top: 12rpx }
|
.tab + .tab { margin-left: 12rpx }
|
||||||
.reward { display: flex; align-items: center; margin-bottom: 8rpx }
|
.tab.active { background: #ffdfaa; border-color: #ffb74d; color: #6b4b1f; font-weight: 600 }
|
||||||
.reward-img { width: 80rpx; height: 80rpx; border-radius: 8rpx; margin-right: 12rpx; background: #f5f5f5 }
|
.rewards { width: 100%; margin-top: 24rpx }
|
||||||
.reward-texts { display: flex; flex-direction: column }
|
.reward-card { background: #fff; border-radius: 12rpx; overflow: hidden; box-shadow: 0 6rpx 16rpx rgba(0,0,0,0.06); margin-bottom: 12rpx }
|
||||||
.reward-title { font-size: 26rpx }
|
.el-reward-card { margin-bottom: 12rpx }
|
||||||
.reward-meta { font-size: 22rpx; color: #888 }
|
.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 }
|
.rewards-empty { font-size: 24rpx; color: #999 }
|
||||||
.issues-empty { font-size: 24rpx; color: #999 }
|
.issues-empty { font-size: 24rpx; color: #999 }
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -5,46 +5,57 @@
|
|||||||
</view>
|
</view>
|
||||||
<view class="header">
|
<view class="header">
|
||||||
<view class="title">{{ detail.name || detail.title || '-' }}</view>
|
<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.price_draw !== undefined">单次抽选:¥{{ detail.price_draw }}</view>
|
||||||
<view class="meta" v-if="detail.status !== undefined">状态:{{ statusText }}</view>
|
|
||||||
</view>
|
</view>
|
||||||
<view class="actions">
|
<view class="draw-actions">
|
||||||
<button class="btn" @click="onPreviewBanner">查看图片</button>
|
<button class="draw-btn" @click="() => onMachineDraw(1)">单次抽选</button>
|
||||||
<button class="btn primary" @click="onParticipate">立即参与</button>
|
<button class="draw-btn" @click="() => onMachineDraw(10)">十次抽选</button>
|
||||||
|
<button class="draw-btn secondary" @click="onMachineTry">试一试</button>
|
||||||
</view>
|
</view>
|
||||||
<view class="issues">
|
<view class="issues" v-if="showIssues && issues.length">
|
||||||
<view class="issues-title">期数</view>
|
<view class="issue-switch">
|
||||||
<view v-if="issues.length" class="issues-list">
|
<button class="switch-btn" @click="prevIssue">〈</button>
|
||||||
<view class="issue-item" v-for="it in issues" :key="it.id">
|
<text class="issue-title">{{ currentIssueTitle }}</text>
|
||||||
<text class="issue-title">{{ it.title || ('第' + (it.no || it.index || it.issue_no || '-') + '期') }}</text>
|
<button class="switch-btn" @click="nextIssue">〉</button>
|
||||||
<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>
|
</view>
|
||||||
<view v-else class="issues-empty">暂无期数</view>
|
|
||||||
</view>
|
</view>
|
||||||
</scroll-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>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<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 { onLoad } from '@dcloudio/uni-app'
|
||||||
import { getActivityDetail, getActivityIssues, getActivityIssueRewards } from '../../../api/appUser'
|
import { getActivityDetail, getActivityIssues, getActivityIssueRewards, drawActivityIssue } from '../../../api/appUser'
|
||||||
|
|
||||||
const detail = ref({})
|
const detail = ref({})
|
||||||
const statusText = ref('')
|
const statusText = ref('')
|
||||||
const issues = ref([])
|
const issues = ref([])
|
||||||
const rewardsMap = 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) {
|
function statusToText(s) {
|
||||||
if (s === 1) return '进行中'
|
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 ? '已结束' : '')
|
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) {
|
function normalizeRewards(list) {
|
||||||
const arr = unwrap(list)
|
const arr = unwrap(list)
|
||||||
return arr.map((i, idx) => ({
|
const items = arr.map((i, idx) => ({
|
||||||
id: i.id ?? String(idx),
|
id: i.product_id ?? i.id ?? String(idx),
|
||||||
title: i.title ?? i.name ?? '',
|
title: i.name ?? i.title ?? '',
|
||||||
image: i.image ?? i.img ?? i.pic ?? i.banner ?? '',
|
image: cleanUrl(i.product_image ?? i.image ?? i.img ?? i.pic ?? i.banner ?? ''),
|
||||||
odds: i.odds ?? i.rate ?? i.probability ?? i.prob ?? '',
|
weight: Number(i.weight) || 0,
|
||||||
rarity: i.rarity ?? i.rarity_name ?? ''
|
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) {
|
async function fetchRewardsForIssues(activityId) {
|
||||||
const list = issues.value || []
|
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 results = await Promise.allSettled(promises)
|
||||||
|
const nextAct = { ...act }
|
||||||
results.forEach((res, i) => {
|
results.forEach((res, i) => {
|
||||||
const issueId = list[i] && list[i].id
|
const issueId = toFetch[i] && toFetch[i].id
|
||||||
if (!issueId) return
|
if (!issueId) return
|
||||||
const value = res.status === 'fulfilled' ? normalizeRewards(res.value) : []
|
const value = res.status === 'fulfilled' ? normalizeRewards(res.value) : []
|
||||||
rewardsMap.value = { ...(rewardsMap.value || {}), [issueId]: 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) {
|
async function fetchIssues(id) {
|
||||||
const data = await getActivityIssues(id)
|
const data = await getActivityIssues(id)
|
||||||
issues.value = normalizeIssues(data)
|
issues.value = normalizeIssues(data)
|
||||||
|
const latestId = pickLatestIssueId(issues.value)
|
||||||
|
setSelectedById(latestId)
|
||||||
await fetchRewardsForIssues(id)
|
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() {
|
function onPreviewBanner() {
|
||||||
const url = detail.value.banner || ''
|
const url = detail.value.banner || ''
|
||||||
if (url) uni.previewImage({ urls: [url], current: url })
|
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) => {
|
onLoad((opts) => {
|
||||||
const id = (opts && opts.id) || ''
|
const id = (opts && opts.id) || ''
|
||||||
if (id) {
|
if (id) {
|
||||||
|
activityId.value = id
|
||||||
fetchDetail(id)
|
fetchDetail(id)
|
||||||
fetchIssues(id)
|
fetchIssues(id)
|
||||||
}
|
}
|
||||||
|
ensureElCard()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
function closeFlip() { showFlip.value = false }
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.page { height: 100vh }
|
.page { height: 100vh; padding-bottom: 140rpx }
|
||||||
.banner { padding: 24rpx }
|
.banner { padding: 24rpx }
|
||||||
.banner-img { width: 100% }
|
.banner-img { width: 100% }
|
||||||
.header { padding: 0 24rpx }
|
.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 }
|
.meta { margin-top: 8rpx; font-size: 26rpx; color: #666 }
|
||||||
.actions { display: flex; padding: 24rpx; gap: 16rpx }
|
.actions { display: flex; padding: 24rpx; gap: 16rpx }
|
||||||
.btn { flex: 1 }
|
.btn { flex: 1 }
|
||||||
.primary { background-color: #007AFF; color: #fff }
|
.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 { background: #fff; border-radius: 12rpx; margin: 0 24rpx 24rpx; padding: 16rpx }
|
||||||
.issues-title { font-size: 30rpx; font-weight: 600; margin-bottom: 12rpx }
|
.issues-title { font-size: 30rpx; font-weight: 600; margin-bottom: 12rpx }
|
||||||
.issues-list { }
|
.issues-list { }
|
||||||
.issue-item { display: flex; justify-content: space-between; padding: 12rpx 0; border-bottom: 1rpx solid #f0f0f0 }
|
.issue-switch { display: flex; align-items: center; justify-content: center; gap: 12rpx; margin: 0 24rpx 24rpx }
|
||||||
.issue-item:last-child { border-bottom: 0 }
|
.switch-btn { width: 72rpx; height: 72rpx; border-radius: 999rpx; background: #fff3df; border: 2rpx solid #f0c58a; color: #8a5a2b }
|
||||||
.issue-title { font-size: 26rpx }
|
.issue-title { font-size: 28rpx; color: #6b4b1f; background: #ffdfaa; border-radius: 12rpx; padding: 8rpx 16rpx }
|
||||||
.issue-status { font-size: 24rpx; color: #666 }
|
.rewards { width: 100%; margin-top: 24rpx }
|
||||||
.rewards { width: 100%; margin-top: 12rpx }
|
|
||||||
.reward { display: flex; align-items: center; margin-bottom: 8rpx }
|
.reward { display: flex; align-items: center; margin-bottom: 8rpx }
|
||||||
.reward-img { width: 80rpx; height: 80rpx; border-radius: 8rpx; margin-right: 12rpx; background: #f5f5f5 }
|
.reward-img { width: 80rpx; height: 80rpx; border-radius: 8rpx; margin-right: 12rpx; background: #f5f5f5 }
|
||||||
.reward-texts { display: flex; flex-direction: column }
|
.reward-card { background: #fff; border-radius: 12rpx; overflow: hidden; box-shadow: 0 6rpx 16rpx rgba(0,0,0,0.06); margin-bottom: 12rpx }
|
||||||
.reward-title { font-size: 26rpx }
|
.el-reward-card { margin-bottom: 12rpx }
|
||||||
.reward-meta { font-size: 22rpx; color: #888 }
|
.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 }
|
.rewards-empty { font-size: 24rpx; color: #999 }
|
||||||
.issues-empty { font-size: 24rpx; color: #999 }
|
.issues-empty { font-size: 24rpx; color: #999 }
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -5,45 +5,58 @@
|
|||||||
</view>
|
</view>
|
||||||
<view class="header">
|
<view class="header">
|
||||||
<view class="title">{{ detail.name || detail.title || '-' }}</view>
|
<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.price_draw !== undefined">抽选价:¥{{ detail.price_draw }}</view>
|
||||||
<view class="meta" v-if="detail.status !== undefined">状态:{{ statusText }}</view>
|
|
||||||
</view>
|
</view>
|
||||||
<view class="actions">
|
<view class="draw-actions">
|
||||||
<button class="btn" @click="onPreviewBanner">查看图片</button>
|
<button class="draw-btn" @click="() => onMachineDraw(1)">单次抽选</button>
|
||||||
<button class="btn primary" @click="onParticipate">立即参与</button>
|
<button class="draw-btn" @click="() => onMachineDraw(10)">十次抽选</button>
|
||||||
|
<button class="draw-btn secondary" @click="onMachineTry">试一试</button>
|
||||||
</view>
|
</view>
|
||||||
<view class="issues">
|
<view class="issues" v-if="showIssues && issues.length">
|
||||||
<view class="issues-title">期数</view>
|
<view class="issue-switch">
|
||||||
<view v-if="issues.length" class="issues-list">
|
<button class="switch-btn" @click="prevIssue">〈</button>
|
||||||
<view class="issue-item" v-for="it in issues" :key="it.id">
|
<text class="issue-title">{{ currentIssueTitle }}</text>
|
||||||
<text class="issue-title">{{ it.title || ('第' + (it.no || it.index || it.issue_no || '-') + '期') }}</text>
|
<button class="switch-btn" @click="nextIssue">〉</button>
|
||||||
<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>
|
</view>
|
||||||
<view v-else class="issues-empty">暂无期数</view>
|
|
||||||
</view>
|
</view>
|
||||||
</scroll-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>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<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 { 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 detail = ref({})
|
||||||
const issues = ref([])
|
const issues = ref([])
|
||||||
const rewardsMap = 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) {
|
function statusToText(s) {
|
||||||
if (s === 1) return '进行中'
|
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 ? '已结束' : '')
|
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) {
|
function normalizeRewards(list) {
|
||||||
const arr = unwrap(list)
|
const arr = unwrap(list)
|
||||||
return arr.map((i, idx) => ({
|
const items = arr.map((i, idx) => ({
|
||||||
id: i.id ?? String(idx),
|
id: i.product_id ?? i.id ?? String(idx),
|
||||||
title: i.title ?? i.name ?? '',
|
title: i.name ?? i.title ?? '',
|
||||||
image: i.image ?? i.img ?? i.pic ?? i.banner ?? '',
|
image: cleanUrl(i.product_image ?? i.image ?? i.img ?? i.pic ?? i.banner ?? ''),
|
||||||
odds: i.odds ?? i.rate ?? i.probability ?? i.prob ?? '',
|
weight: Number(i.weight) || 0,
|
||||||
rarity: i.rarity ?? i.rarity_name ?? ''
|
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) {
|
async function fetchRewardsForIssues(activityId) {
|
||||||
const list = issues.value || []
|
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 results = await Promise.allSettled(promises)
|
||||||
|
const nextAct = { ...act }
|
||||||
results.forEach((res, i) => {
|
results.forEach((res, i) => {
|
||||||
const issueId = list[i] && list[i].id
|
const issueId = toFetch[i] && toFetch[i].id
|
||||||
if (!issueId) return
|
if (!issueId) return
|
||||||
const value = res.status === 'fulfilled' ? normalizeRewards(res.value) : []
|
const value = res.status === 'fulfilled' ? normalizeRewards(res.value) : []
|
||||||
rewardsMap.value = { ...(rewardsMap.value || {}), [issueId]: 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) {
|
async function fetchIssues(id) {
|
||||||
const data = await getActivityIssues(id)
|
const data = await getActivityIssues(id)
|
||||||
issues.value = normalizeIssues(data)
|
issues.value = normalizeIssues(data)
|
||||||
|
const latestId = pickLatestIssueId(issues.value)
|
||||||
|
setSelectedById(latestId)
|
||||||
await fetchRewardsForIssues(id)
|
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() {
|
function onPreviewBanner() {
|
||||||
const url = detail.value.banner || ''
|
const url = detail.value.banner || ''
|
||||||
if (url) uni.previewImage({ urls: [url], current: url })
|
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) => {
|
onLoad((opts) => {
|
||||||
const id = (opts && opts.id) || ''
|
const id = (opts && opts.id) || ''
|
||||||
if (id) {
|
if (id) {
|
||||||
|
activityId.value = id
|
||||||
fetchDetail(id)
|
fetchDetail(id)
|
||||||
fetchIssues(id)
|
fetchIssues(id)
|
||||||
|
fetchWinRecords(id)
|
||||||
}
|
}
|
||||||
|
ensureElCard()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
function closeFlip() { showFlip.value = false }
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.page { height: 100vh }
|
.page { height: 100vh; padding-bottom: 140rpx }
|
||||||
.banner { padding: 24rpx }
|
.banner { padding: 24rpx }
|
||||||
.banner-img { width: 100% }
|
.banner-img { width: 100% }
|
||||||
.header { padding: 0 24rpx }
|
.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 }
|
.meta { margin-top: 8rpx; font-size: 26rpx; color: #666 }
|
||||||
.actions { display: flex; padding: 24rpx; gap: 16rpx }
|
.actions { display: flex; padding: 24rpx; gap: 16rpx }
|
||||||
.btn { flex: 1 }
|
.btn { flex: 1 }
|
||||||
.primary { background-color: #007AFF; color: #fff }
|
.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 { background: #fff; border-radius: 12rpx; margin: 0 24rpx 24rpx; padding: 16rpx }
|
||||||
.issues-title { font-size: 30rpx; font-weight: 600; margin-bottom: 12rpx }
|
.issues-title { font-size: 30rpx; font-weight: 600; margin-bottom: 12rpx }
|
||||||
.issues-list { }
|
.issues-list { }
|
||||||
.issue-item { display: flex; justify-content: space-between; padding: 12rpx 0; border-bottom: 1rpx solid #f0f0f0 }
|
.issue-switch { display: flex; align-items: center; justify-content: center; gap: 12rpx; margin: 0 24rpx 24rpx }
|
||||||
.issue-item:last-child { border-bottom: 0 }
|
.switch-btn { width: 72rpx; height: 72rpx; border-radius: 999rpx; background: #fff3df; border: 2rpx solid #f0c58a; color: #8a5a2b }
|
||||||
.issue-title { font-size: 26rpx }
|
.issue-title { font-size: 28rpx; color: #6b4b1f; background: #ffdfaa; border-radius: 12rpx; padding: 8rpx 16rpx }
|
||||||
.issue-status { font-size: 24rpx; color: #666 }
|
|
||||||
.rewards { width: 100%; margin-top: 12rpx }
|
|
||||||
.reward { display: flex; align-items: center; margin-bottom: 8rpx }
|
.reward { display: flex; align-items: center; margin-bottom: 8rpx }
|
||||||
.reward-img { width: 80rpx; height: 80rpx; border-radius: 8rpx; margin-right: 12rpx; background: #f5f5f5 }
|
.reward-img { width: 80rpx; height: 80rpx; border-radius: 8rpx; margin-right: 12rpx; background: #f5f5f5 }
|
||||||
.reward-texts { display: flex; flex-direction: column }
|
.reward-card { background: #fff; border-radius: 12rpx; overflow: hidden; box-shadow: 0 6rpx 16rpx rgba(0,0,0,0.06); margin-bottom: 12rpx }
|
||||||
.reward-title { font-size: 26rpx }
|
.el-reward-card { margin-bottom: 12rpx }
|
||||||
.reward-meta { font-size: 22rpx; color: #888 }
|
.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 }
|
.rewards-empty { font-size: 24rpx; color: #999 }
|
||||||
.issues-empty { font-size: 24rpx; color: #999 }
|
.issues-empty { font-size: 24rpx; color: #999 }
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -2,8 +2,8 @@
|
|||||||
<scroll-view class="page" scroll-y>
|
<scroll-view class="page" scroll-y>
|
||||||
<view class="h1">购买协议</view>
|
<view class="h1">购买协议</view>
|
||||||
<view class="meta">生效日期:2025年11月18日</view>
|
<view class="meta">生效日期:2025年11月18日</view>
|
||||||
<view class="meta">运营方:【公司全称】</view>
|
<view class="meta">运营方:柯大鸭潮玩</view>
|
||||||
<view class="p">本《购买协议》适用于您在【您的小程序名称】(以下简称“本平台”)购买盲盒商品的行为。当您点击“立即购买”并完成支付时,即视为您已阅读、理解并同意本协议全部内容。</view>
|
<view class="p">本《购买协议》适用于您在【柯大鸭潮玩】(以下简称“本平台”)购买盲盒商品的行为。当您点击“立即购买”并完成支付时,即视为您已阅读、理解并同意本协议全部内容。</view>
|
||||||
<view class="h2">一、商品说明</view>
|
<view class="h2">一、商品说明</view>
|
||||||
<view class="ol">
|
<view class="ol">
|
||||||
<view class="li">盲盒特性:本平台所售盲盒为系列化商品,包装外观一致,内部款式随机,具体款式无法提前指定或预知。</view>
|
<view class="li">盲盒特性:本平台所售盲盒为系列化商品,包装外观一致,内部款式随机,具体款式无法提前指定或预知。</view>
|
||||||
@ -22,7 +22,7 @@
|
|||||||
</view>
|
</view>
|
||||||
<view class="h2">四、售后服务</view>
|
<view class="h2">四、售后服务</view>
|
||||||
<view class="ol">
|
<view class="ol">
|
||||||
<view class="li">质量问题(如商品破损、漏发、错发、非盲盒系列商品):请在签收后2到4小时内联系客服并提供凭证(如开箱视频、照片);经核实后,平台将为您补发、换货或退款。</view>
|
<view class="li">质量问题(如商品破损、漏发、错发、非盲盒系列商品):请在签收后2小时内联系客服并提供凭证(如开箱视频、照片);经核实后,平台将为您补发、换货或退款。</view>
|
||||||
<view class="li">非质量问题(如抽中重复款式、不喜欢款式、未抽中隐藏款等):不支持无理由退换货。</view>
|
<view class="li">非质量问题(如抽中重复款式、不喜欢款式、未抽中隐藏款等):不支持无理由退换货。</view>
|
||||||
<view class="li">拆封后商品:出于卫生与二次销售考虑,已拆封盲盒恕不退换(质量问题除外)。</view>
|
<view class="li">拆封后商品:出于卫生与二次销售考虑,已拆封盲盒恕不退换(质量问题除外)。</view>
|
||||||
</view>
|
</view>
|
||||||
@ -45,10 +45,9 @@
|
|||||||
<view class="h2">八、协议效力</view>
|
<view class="h2">八、协议效力</view>
|
||||||
<view class="p">本购买协议为《用户协议》的补充,两者冲突时,以本协议中关于交易的条款为准。未尽事宜,依照《消费者权益保护法》《电子商务法》等法律法规执行。</view>
|
<view class="p">本购买协议为《用户协议》的补充,两者冲突时,以本协议中关于交易的条款为准。未尽事宜,依照《消费者权益保护法》《电子商务法》等法律法规执行。</view>
|
||||||
<view class="h2">九、联系我们</view>
|
<view class="h2">九、联系我们</view>
|
||||||
<view class="p">售后专线:service@yourdomain.com</view>
|
<view class="p">售后专线:请联系企业客服,客服将在工作时间内为您服务。</view>
|
||||||
<view class="p">工作时间:工作日 9:00–18:00</view>
|
<view class="p">工作时间:工作日 13:00–04:00</view>
|
||||||
<view class="p">运营主体:【公司全称】</view>
|
<view class="p">运营主体:【柯大鸭潮玩】</view>
|
||||||
<view class="p">统一社会信用代码:【XXXXXXXXXXXXXX】</view>
|
|
||||||
<view class="tip">理性消费提醒:盲盒是一种娱乐消费形式,请根据自身经济能力合理购买,切勿沉迷或过度投入。</view>
|
<view class="tip">理性消费提醒:盲盒是一种娱乐消费形式,请根据自身经济能力合理购买,切勿沉迷或过度投入。</view>
|
||||||
</scroll-view>
|
</scroll-view>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@ -2,8 +2,8 @@
|
|||||||
<scroll-view class="page" scroll-y>
|
<scroll-view class="page" scroll-y>
|
||||||
<view class="h1">用户协议</view>
|
<view class="h1">用户协议</view>
|
||||||
<view class="meta">生效日期:2025年11月18日</view>
|
<view class="meta">生效日期:2025年11月18日</view>
|
||||||
<view class="meta">运营方:【公司全称】</view>
|
<view class="meta">运营方:【柯大鸭潮玩】</view>
|
||||||
<view class="p">欢迎您使用【您的小程序名称】(以下简称“本平台”)提供的服务。请您在注册、登录或使用本平台前,认真阅读并充分理解本《用户协议》(以下简称“本协议”)。一旦您完成注册、登录或以任何方式使用本平台服务,即视为您已完全接受本协议全部条款。如您不同意,请勿使用本平台。</view>
|
<view class="p">欢迎您使用【柯大鸭潮玩】(以下简称“本平台”)提供的服务。请您在注册、登录或使用本平台前,认真阅读并充分理解本《用户协议》(以下简称“本协议”)。一旦您完成注册、登录或以任何方式使用本平台服务,即视为您已完全接受本协议全部条款。如您不同意,请勿使用本平台。</view>
|
||||||
<view class="h2">一、协议范围</view>
|
<view class="h2">一、协议范围</view>
|
||||||
<view class="p">本协议规范您作为用户在本平台注册、浏览、互动、参与活动等行为,是您与本平台之间的基本权利义务约定。</view>
|
<view class="p">本协议规范您作为用户在本平台注册、浏览、互动、参与活动等行为,是您与本平台之间的基本权利义务约定。</view>
|
||||||
<view class="h2">二、用户资格</view>
|
<view class="h2">二、用户资格</view>
|
||||||
@ -44,10 +44,9 @@
|
|||||||
<view class="h2">九、法律适用与争议解决</view>
|
<view class="h2">九、法律适用与争议解决</view>
|
||||||
<view class="p">本协议适用中华人民共和国法律。因本协议引起的争议,双方应协商解决;协商不成的,提交本平台运营方所在地有管辖权的人民法院诉讼解决。</view>
|
<view class="p">本协议适用中华人民共和国法律。因本协议引起的争议,双方应协商解决;协商不成的,提交本平台运营方所在地有管辖权的人民法院诉讼解决。</view>
|
||||||
<view class="h2">十、联系我们</view>
|
<view class="h2">十、联系我们</view>
|
||||||
<view class="p">客服邮箱:service@yourdomain.com</view>
|
<view class="p">客服邮箱:请通过企业客服联系我们,客服将在工作时间内为您服务。</view>
|
||||||
<view class="p">客服电话:400-XXX-XXXX(工作日 9:00–18:00)</view>
|
<view class="p">工作时间:(工作日 13:00–04:00)</view>
|
||||||
<view class="p">运营主体:【公司全称】</view>
|
<view class="p">运营主体:【柯大鸭潮玩】</view>
|
||||||
<view class="p">地址:【公司注册地址】</view>
|
|
||||||
<view class="tip">温馨提示:盲盒具有随机性和娱乐性,请理性参与,避免沉迷。未成年人禁止参与购买。</view>
|
<view class="tip">温馨提示:盲盒具有随机性和娱乐性,请理性参与,避免沉迷。未成年人禁止参与购买。</view>
|
||||||
</scroll-view>
|
</scroll-view>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@ -21,15 +21,21 @@
|
|||||||
|
|
||||||
<view class="activity-section">
|
<view class="activity-section">
|
||||||
<view class="section-title">活动</view>
|
<view class="section-title">活动</view>
|
||||||
<view v-if="activities.length" class="activity-grid">
|
<view v-if="activityGroups.length">
|
||||||
<view class="activity-item" v-for="a in activities" :key="a.id" @tap="onActivityTap(a)">
|
<scroll-view class="tabs" scroll-x>
|
||||||
<image v-if="a.image" class="activity-thumb" :src="a.image" mode="aspectFill" />
|
<view class="tab" v-for="g in activityGroups" :key="g.name" :class="{ active: g.name === selectedGroupName }" @tap="onSelectGroup(g.name)">{{ g.name }}</view>
|
||||||
<view v-else class="banner-fallback">
|
</scroll-view>
|
||||||
<text class="banner-fallback-text">{{ a.title || '活动敬请期待' }}</text>
|
<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>
|
</view>
|
||||||
<text class="activity-name">{{ a.title }}</text>
|
|
||||||
<text class="activity-desc" v-if="a.subtitle">{{ a.subtitle }}</text>
|
|
||||||
</view>
|
</view>
|
||||||
|
<view v-else class="activity-empty">该分组暂无活动</view>
|
||||||
</view>
|
</view>
|
||||||
<view v-else class="activity-empty">暂无活动</view>
|
<view v-else class="activity-empty">暂无活动</view>
|
||||||
</view>
|
</view>
|
||||||
@ -43,7 +49,8 @@ export default {
|
|||||||
return {
|
return {
|
||||||
notices: [],
|
notices: [],
|
||||||
banners: [],
|
banners: [],
|
||||||
activities: []
|
activities: [],
|
||||||
|
selectedGroupName: ''
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
@ -61,6 +68,22 @@ export default {
|
|||||||
{ id: 'ph-2', title: '敬请期待', image: '' },
|
{ id: 'ph-2', title: '敬请期待', image: '' },
|
||||||
{ id: 'ph-3', 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() {
|
onShow() {
|
||||||
@ -82,6 +105,16 @@ export default {
|
|||||||
this.loadHomeData()
|
this.loadHomeData()
|
||||||
},
|
},
|
||||||
methods: {
|
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 : [] },
|
toArray(x) { return Array.isArray(x) ? x : [] },
|
||||||
unwrap(list) {
|
unwrap(list) {
|
||||||
if (Array.isArray(list)) return list
|
if (Array.isArray(list)) return list
|
||||||
@ -126,7 +159,7 @@ export default {
|
|||||||
console.log('normalizeActivities input', list, 'unwrapped', arr)
|
console.log('normalizeActivities input', list, 'unwrapped', arr)
|
||||||
const mapped = arr.map((i, idx) => ({
|
const mapped = arr.map((i, idx) => ({
|
||||||
id: i.id ?? String(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 ?? '',
|
title: i.title ?? i.name ?? '',
|
||||||
subtitle: this.buildActivitySubtitle(i),
|
subtitle: this.buildActivitySubtitle(i),
|
||||||
link: this.cleanUrl(i.linkUrl ?? i.link_url ?? i.link ?? i.url ?? ''),
|
link: this.cleanUrl(i.linkUrl ?? i.link_url ?? i.link ?? i.url ?? ''),
|
||||||
@ -168,9 +201,11 @@ export default {
|
|||||||
if (acRes.status === 'fulfilled') {
|
if (acRes.status === 'fulfilled') {
|
||||||
console.log('activities ok', acRes.value)
|
console.log('activities ok', acRes.value)
|
||||||
this.activities = this.normalizeActivities(acRes.value)
|
this.activities = this.normalizeActivities(acRes.value)
|
||||||
|
this.updateSelectedGroup()
|
||||||
} else {
|
} else {
|
||||||
console.error('activities error', acRes.reason)
|
console.error('activities error', acRes.reason)
|
||||||
this.activities = []
|
this.activities = []
|
||||||
|
this.updateSelectedGroup()
|
||||||
}
|
}
|
||||||
console.log('home normalized', { notices: this.notices, banners: this.banners, activities: this.activities })
|
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 }
|
.banner-fallback-text { color: #666; font-size: 28rpx }
|
||||||
.activity-section { background: #ffffff; border-radius: 12rpx; padding: 24rpx }
|
.activity-section { background: #ffffff; border-radius: 12rpx; padding: 24rpx }
|
||||||
.section-title { font-size: 30rpx; font-weight: 600; margin-bottom: 16rpx }
|
.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-grid { display: flex; flex-wrap: wrap; margin: -12rpx }
|
||||||
.activity-item { width: 50%; padding: 12rpx }
|
.activity-item { width: 50%; padding: 12rpx }
|
||||||
.activity-thumb { width: 100%; height: 200rpx; border-radius: 8rpx }
|
.activity-thumb { width: 100%; height: 200rpx; border-radius: 8rpx }
|
||||||
|
|||||||
@ -1,5 +1,18 @@
|
|||||||
<template>
|
<template>
|
||||||
<view class="page">
|
<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 v-if="loading" class="loading-wrap"><view class="spinner"></view></view>
|
||||||
<view class="products-section" v-else>
|
<view class="products-section" v-else>
|
||||||
<view class="section-title">商品</view>
|
<view class="section-title">商品</view>
|
||||||
@ -53,6 +66,9 @@ const minPrice = ref('')
|
|||||||
const maxPrice = ref('')
|
const maxPrice = ref('')
|
||||||
const displayCount = computed(() => (columns.value[0].length + columns.value[1].length))
|
const displayCount = computed(() => (columns.value[0].length + columns.value[1].length))
|
||||||
const loadedMap = ref({})
|
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 getKey(p) { return String((p && p.id) ?? '') + '|' + String((p && p.image) ?? '') }
|
||||||
function unwrap(list) {
|
function unwrap(list) {
|
||||||
if (Array.isArray(list)) return 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 imgs = (Array.isArray(products.value) ? products.value : []).map(x => x.image).filter(Boolean)
|
||||||
const current = p && p.image
|
const current = p && p.image
|
||||||
if (current) {
|
if (current) {
|
||||||
|
skipReloadOnce.value = true
|
||||||
|
try { uni.setStorageSync('shop_skip_reload_once', '1') } catch (_) {}
|
||||||
uni.previewImage({ urls: imgs.length ? imgs : [current], current })
|
uni.previewImage({ urls: imgs.length ? imgs : [current], current })
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -249,14 +267,50 @@ onShow(async () => {
|
|||||||
})
|
})
|
||||||
return
|
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
|
loading.value = true
|
||||||
await loadProducts()
|
await loadProducts()
|
||||||
loading.value = false
|
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>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.page { padding: 24rpx }
|
.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 }
|
.section-title { font-size: 30rpx; font-weight: 600; margin-bottom: 16rpx }
|
||||||
.products-section { background: #ffffff; border-radius: 12rpx; padding: 24rpx; margin-top: 24rpx }
|
.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-wrap { min-height: 60vh; display: flex; align-items: center; justify-content: center }
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user