无限赏更新UI
This commit is contained in:
parent
9f7c98ddad
commit
2571d4a698
@ -78,10 +78,6 @@ export function drawActivityIssue(activity_id, issue_id) {
|
|||||||
return authRequest({ url: `/api/app/activities/${activity_id}/issues/${issue_id}/draw`, method: 'POST' })
|
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 } })
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getIssueChoices(activity_id, issue_id) {
|
export function getIssueChoices(activity_id, issue_id) {
|
||||||
return authRequest({ url: `/api/app/activities/${activity_id}/issues/${issue_id}/choices`, method: 'GET' })
|
return authRequest({ url: `/api/app/activities/${activity_id}/issues/${issue_id}/choices`, method: 'GET' })
|
||||||
}
|
}
|
||||||
@ -195,13 +191,6 @@ export function playMatchingGame(game_id) {
|
|||||||
return authRequest({ url: '/api/app/matching/play', method: 'POST', data: { game_id } })
|
return authRequest({ url: '/api/app/matching/play', method: 'POST', data: { game_id } })
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取游戏状态 (用于重连)
|
|
||||||
* @param {string} game_id - 游戏ID
|
|
||||||
*/
|
|
||||||
export function getMatchingGameState(game_id) {
|
|
||||||
return authRequest({ url: '/api/app/matching/state', method: 'GET', data: { game_id } })
|
|
||||||
}
|
|
||||||
/**
|
/**
|
||||||
* 获取所有启用的卡牌配置
|
* 获取所有启用的卡牌配置
|
||||||
*/
|
*/
|
||||||
@ -216,3 +205,15 @@ export function createMatchingPreorder({ issue_id, position, coupon_id = 0, item
|
|||||||
data: { issue_id, position, coupon_id, item_card_id }
|
data: { issue_id, position, coupon_id, item_card_id }
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function checkMatchingGame(game_id, total_pairs) {
|
||||||
|
if (game_id && typeof game_id === 'object') {
|
||||||
|
total_pairs = game_id.total_pairs
|
||||||
|
game_id = game_id.game_id
|
||||||
|
}
|
||||||
|
return authRequest({
|
||||||
|
url: '/api/app/matching/check',
|
||||||
|
method: 'POST',
|
||||||
|
data: { game_id, total_pairs }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@ -116,14 +116,17 @@
|
|||||||
<view style="height: 180rpx;"></view>
|
<view style="height: 180rpx;"></view>
|
||||||
</scroll-view>
|
</scroll-view>
|
||||||
|
|
||||||
<view class="float-bar">
|
<view class="float-bar">
|
||||||
<view class="float-bar-inner">
|
<view class="float-bar-inner">
|
||||||
<view class="float-price">
|
<view class="float-price">
|
||||||
<text class="currency">¥</text>
|
<text class="currency">¥</text>
|
||||||
<text class="amount">{{ (Number(detail.price_draw || 0) / 100).toFixed(2) }}</text>
|
<text class="amount">{{ (Number(detail.price_draw || 0) / 100).toFixed(2) }}</text>
|
||||||
<text class="unit">/次</text>
|
<text class="unit">/次</text>
|
||||||
</view>
|
</view>
|
||||||
<view class="action-btn primary" @tap="onParticipate">
|
<view v-if="hasResumeGame" class="action-btn secondary" @tap="onResumeGame">
|
||||||
|
继续游戏
|
||||||
|
</view>
|
||||||
|
<view v-if="!hasResumeGame" class="action-btn primary" @tap="onParticipate">
|
||||||
立即参与
|
立即参与
|
||||||
<view class="btn-shine"></view>
|
<view class="btn-shine"></view>
|
||||||
</view>
|
</view>
|
||||||
@ -131,6 +134,49 @@
|
|||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
|
<view v-if="gameVisible" class="rewards-overlay" @touchmove.stop.prevent>
|
||||||
|
<view class="rewards-mask" @tap="closeGame"></view>
|
||||||
|
<view class="rewards-panel" @tap.stop>
|
||||||
|
<view class="rewards-header">
|
||||||
|
<text class="rewards-title">对对碰游戏</text>
|
||||||
|
<text class="rewards-close" @tap="closeGame">×</text>
|
||||||
|
</view>
|
||||||
|
<scroll-view scroll-y class="rewards-list">
|
||||||
|
<view v-if="gameLoading" class="rewards-empty">加载中...</view>
|
||||||
|
<view v-else-if="gameError" class="rewards-empty">{{ gameError }}</view>
|
||||||
|
<view v-else>
|
||||||
|
<view class="record-item">
|
||||||
|
<view class="record-info">
|
||||||
|
<view class="record-title">总对数:{{ totalPairs }} 摸牌机会:{{ chance }}</view>
|
||||||
|
<view class="record-meta">
|
||||||
|
<text>牌组剩余 {{ deckRemaining }}</text>
|
||||||
|
<text v-if="selectedPositionText">位置 {{ selectedPositionText }}</text>
|
||||||
|
<text v-if="gameIdText">ID {{ gameIdText }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="match-grid">
|
||||||
|
<view
|
||||||
|
v-for="(cell, idx) in handGridCells"
|
||||||
|
:key="idx"
|
||||||
|
class="match-cell"
|
||||||
|
:class="{ empty: cell.empty, chosen: cell.isChosen, picked: cell.isPicked }"
|
||||||
|
@tap="() => onCellTap(cell)"
|
||||||
|
>
|
||||||
|
<image v-if="cell.image" class="match-cell-img" :src="cell.image" mode="aspectFill" />
|
||||||
|
<view v-else class="match-cell-img"></view>
|
||||||
|
<text v-if="!cell.empty && cell.type" class="match-cell-type">{{ cell.type }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</scroll-view>
|
||||||
|
<view class="flip-actions" style="padding: 20rpx 24rpx;">
|
||||||
|
<button class="close-btn" style="flex: 1;" @tap="manualDraw" :disabled="gameLoading || !canManualDraw">摸牌</button>
|
||||||
|
<button class="close-btn" style="flex: 1; background: linear-gradient(135deg, #ff7a18, #ffb347); color: #fff;" @tap="advanceOne" :disabled="gameLoading">下一步</button>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
<view v-if="rewardsVisible" class="rewards-overlay" @touchmove.stop.prevent>
|
<view v-if="rewardsVisible" class="rewards-overlay" @touchmove.stop.prevent>
|
||||||
<view class="rewards-mask" @tap="closeRewardsPopup"></view>
|
<view class="rewards-mask" @tap="closeRewardsPopup"></view>
|
||||||
<view class="rewards-panel" @tap.stop>
|
<view class="rewards-panel" @tap.stop>
|
||||||
@ -168,7 +214,7 @@
|
|||||||
import { ref, computed } from 'vue'
|
import { ref, computed } from 'vue'
|
||||||
import { onLoad } from '@dcloudio/uni-app'
|
import { onLoad } from '@dcloudio/uni-app'
|
||||||
import PaymentPopup from '../../../components/PaymentPopup.vue'
|
import PaymentPopup from '../../../components/PaymentPopup.vue'
|
||||||
import { getActivityDetail, getActivityIssues, getActivityIssueRewards, getActivityWinRecords, getUserCoupons, getItemCards, createWechatOrder, getLotteryResult, getMatchingCardTypes, createMatchingPreorder } from '../../../api/appUser'
|
import { getActivityDetail, getActivityIssues, getActivityIssueRewards, getUserCoupons, getItemCards, joinLottery, createWechatOrder, getMatchingCardTypes, createMatchingPreorder, checkMatchingGame } from '../../../api/appUser'
|
||||||
|
|
||||||
const detail = ref({})
|
const detail = ref({})
|
||||||
const statusText = ref('')
|
const statusText = ref('')
|
||||||
@ -190,6 +236,91 @@ const cardTypesLoading = ref(false)
|
|||||||
const cardTypes = ref([])
|
const cardTypes = ref([])
|
||||||
const selectedCardTypeCode = ref('')
|
const selectedCardTypeCode = ref('')
|
||||||
const rewardsVisible = ref(false)
|
const rewardsVisible = ref(false)
|
||||||
|
const resumeGame = ref(null)
|
||||||
|
const resumeIssueId = ref('')
|
||||||
|
const hasResumeGame = computed(() => {
|
||||||
|
const v = resumeGame.value || null
|
||||||
|
return !!(v && v.game_id)
|
||||||
|
})
|
||||||
|
const gameVisible = ref(false)
|
||||||
|
const gameLoading = ref(false)
|
||||||
|
const gameError = ref('')
|
||||||
|
const gameEntry = ref(null)
|
||||||
|
const gameIssueId = ref('')
|
||||||
|
const hand = ref([])
|
||||||
|
const deckIndex = ref(0)
|
||||||
|
const chance = ref(0)
|
||||||
|
const totalPairs = ref(0)
|
||||||
|
const gameFinished = ref(false)
|
||||||
|
const pickedHandIndex = ref(-1)
|
||||||
|
const gameIdText = computed(() => String((gameEntry.value && gameEntry.value.game_id) || ''))
|
||||||
|
const deckRemaining = computed(() => {
|
||||||
|
const entry = gameEntry.value || null
|
||||||
|
const deck = entry && Array.isArray(entry.all_cards) ? entry.all_cards : []
|
||||||
|
return Math.max(0, deck.length - Number(deckIndex.value || 0))
|
||||||
|
})
|
||||||
|
const cardTypeImageMap = computed(() => {
|
||||||
|
const map = {}
|
||||||
|
;(cardTypes.value || []).forEach(it => {
|
||||||
|
if (!it) return
|
||||||
|
const code = String(it.code || '')
|
||||||
|
if (!code) return
|
||||||
|
map[code] = cleanUrl(it.image_url || it.image || it.img || it.pic || '')
|
||||||
|
})
|
||||||
|
return map
|
||||||
|
})
|
||||||
|
const cardTypeNameMap = computed(() => {
|
||||||
|
const map = {}
|
||||||
|
;(cardTypes.value || []).forEach(it => {
|
||||||
|
if (!it) return
|
||||||
|
const code = String(it.code || '')
|
||||||
|
if (!code) return
|
||||||
|
map[code] = String(it.name || it.title || it.label || code)
|
||||||
|
})
|
||||||
|
return map
|
||||||
|
})
|
||||||
|
const selectedPositionCode = computed(() => String((gameEntry.value && gameEntry.value.position) || selectedCardTypeCode.value || ''))
|
||||||
|
const selectedPositionText = computed(() => {
|
||||||
|
const code = String(selectedPositionCode.value || '')
|
||||||
|
if (!code) return ''
|
||||||
|
const name = (cardTypeNameMap.value || {})[code]
|
||||||
|
return name ? `${name}(${code})` : code
|
||||||
|
})
|
||||||
|
const canManualDraw = computed(() => {
|
||||||
|
const entry = gameEntry.value || null
|
||||||
|
if (!entry || !entry.game_id) return false
|
||||||
|
if (Number(chance.value || 0) <= 0) return false
|
||||||
|
return canDrawOne()
|
||||||
|
})
|
||||||
|
|
||||||
|
function getCardTypeCode(card) {
|
||||||
|
const c = card || {}
|
||||||
|
const v = c.type ?? c.type_code ?? c.card_type_code ?? c.position ?? c.pos ?? c.code
|
||||||
|
return String(v || '')
|
||||||
|
}
|
||||||
|
|
||||||
|
const handGridCells = computed(() => {
|
||||||
|
const cards = Array.isArray(hand.value) ? hand.value : []
|
||||||
|
const imgMap = cardTypeImageMap.value || {}
|
||||||
|
const chosen = String(selectedPositionCode.value || '')
|
||||||
|
const picked = Number(pickedHandIndex.value || -1)
|
||||||
|
const cells = []
|
||||||
|
for (let i = 0; i < 18; i++) {
|
||||||
|
const raw = cards[i]
|
||||||
|
const code = raw ? getCardTypeCode(raw) : ''
|
||||||
|
const image = code ? (imgMap[code] || cleanUrl(raw.image || raw.image_url || raw.img || raw.pic || '')) : ''
|
||||||
|
cells.push({
|
||||||
|
id: raw && (raw.id ?? raw.card_id) ? (raw.id ?? raw.card_id) : String(i),
|
||||||
|
handIndex: raw ? i : -1,
|
||||||
|
type: code,
|
||||||
|
image,
|
||||||
|
empty: !raw,
|
||||||
|
isChosen: !!(code && chosen && code === chosen),
|
||||||
|
isPicked: !!(raw && i === picked)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return cells
|
||||||
|
})
|
||||||
|
|
||||||
const coverUrl = computed(() => cleanUrl(detail.value.banner || detail.value.cover || detail.value.image || ''))
|
const coverUrl = computed(() => cleanUrl(detail.value.banner || detail.value.cover || detail.value.image || ''))
|
||||||
const currentIssueRewards = computed(() => {
|
const currentIssueRewards = computed(() => {
|
||||||
@ -252,6 +383,99 @@ function cleanUrl(u) {
|
|||||||
return s.replace(/[`'\"]/g, '').trim()
|
return s.replace(/[`'\"]/g, '').trim()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const MATCHING_GAME_CACHE_KEY = 'matching_game_cache_v1'
|
||||||
|
|
||||||
|
function getMatchingGameCache() {
|
||||||
|
const obj = uni.getStorageSync(MATCHING_GAME_CACHE_KEY) || {}
|
||||||
|
return typeof obj === 'object' && obj ? obj : {}
|
||||||
|
}
|
||||||
|
|
||||||
|
function findLatestMatchingGameCacheEntry(aid) {
|
||||||
|
const activityKey = String(aid || '')
|
||||||
|
if (!activityKey) return null
|
||||||
|
const cache = getMatchingGameCache()
|
||||||
|
const act = cache[activityKey]
|
||||||
|
if (!act || typeof act !== 'object') return null
|
||||||
|
let bestIssueId = ''
|
||||||
|
let bestEntry = null
|
||||||
|
let bestTs = -Infinity
|
||||||
|
Object.keys(act).forEach(issueId => {
|
||||||
|
const entry = act[issueId]
|
||||||
|
if (!entry || typeof entry !== 'object' || !entry.game_id) return
|
||||||
|
const ts = Number(entry.ts || 0)
|
||||||
|
if (!bestEntry || ts > bestTs) {
|
||||||
|
bestTs = ts
|
||||||
|
bestIssueId = issueId
|
||||||
|
bestEntry = entry
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if (!bestEntry) return null
|
||||||
|
return { issue_id: bestIssueId, entry: bestEntry }
|
||||||
|
}
|
||||||
|
|
||||||
|
function syncResumeGame(aid) {
|
||||||
|
const latest = findLatestMatchingGameCacheEntry(aid)
|
||||||
|
if (!latest || !latest.entry || !latest.entry.game_id) {
|
||||||
|
resumeIssueId.value = ''
|
||||||
|
resumeGame.value = null
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
resumeIssueId.value = String(latest.issue_id || '')
|
||||||
|
resumeGame.value = latest.entry
|
||||||
|
return latest
|
||||||
|
}
|
||||||
|
|
||||||
|
function writeMatchingGameCacheEntry(aid, iid, entry) {
|
||||||
|
const activityKey = String(aid || '')
|
||||||
|
const issueKey = String(iid || '')
|
||||||
|
if (!activityKey || !issueKey) return
|
||||||
|
const cache = getMatchingGameCache()
|
||||||
|
const act = (cache[activityKey] && typeof cache[activityKey] === 'object') ? cache[activityKey] : {}
|
||||||
|
act[issueKey] = entry
|
||||||
|
cache[activityKey] = act
|
||||||
|
uni.setStorageSync(MATCHING_GAME_CACHE_KEY, cache)
|
||||||
|
syncResumeGame(activityKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
function readMatchingGameCacheEntry(aid, iid) {
|
||||||
|
const activityKey = String(aid || '')
|
||||||
|
const issueKey = String(iid || '')
|
||||||
|
if (!activityKey || !issueKey) return null
|
||||||
|
const cache = getMatchingGameCache()
|
||||||
|
const act = cache[activityKey] || {}
|
||||||
|
const entry = act && act[issueKey]
|
||||||
|
const ok = entry && typeof entry === 'object' && entry.game_id
|
||||||
|
return ok ? entry : null
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearMatchingGameCacheEntry(aid, iid) {
|
||||||
|
const activityKey = String(aid || '')
|
||||||
|
const issueKey = String(iid || '')
|
||||||
|
const cache = getMatchingGameCache()
|
||||||
|
const act = cache[activityKey]
|
||||||
|
if (!act || typeof act !== 'object') {
|
||||||
|
syncResumeGame(activityKey)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (act[issueKey] !== undefined) delete act[issueKey]
|
||||||
|
if (Object.keys(act).length === 0) delete cache[activityKey]
|
||||||
|
else cache[activityKey] = act
|
||||||
|
uni.setStorageSync(MATCHING_GAME_CACHE_KEY, cache)
|
||||||
|
syncResumeGame(activityKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeAllCards(v) {
|
||||||
|
const arr = Array.isArray(v) ? v : (v ? [v] : [])
|
||||||
|
return arr.map((it, idx) => {
|
||||||
|
const obj = (it && typeof it === 'object') ? { ...it } : { value: it }
|
||||||
|
if (obj.image_url !== undefined) obj.image_url = cleanUrl(obj.image_url)
|
||||||
|
if (obj.image !== undefined) obj.image = cleanUrl(obj.image)
|
||||||
|
if (!obj.id && obj.card_id) obj.id = obj.card_id
|
||||||
|
if (!obj.id) obj.id = String(idx)
|
||||||
|
return obj
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
function truthy(v) {
|
function truthy(v) {
|
||||||
if (typeof v === 'boolean') return v
|
if (typeof v === 'boolean') return v
|
||||||
const s = String(v || '').trim().toLowerCase()
|
const s = String(v || '').trim().toLowerCase()
|
||||||
@ -288,16 +512,6 @@ function normalizeRewards(list) {
|
|||||||
enriched.sort((a, b) => (b.percent - a.percent))
|
enriched.sort((a, b) => (b.percent - a.percent))
|
||||||
return enriched
|
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) {
|
function isFresh(ts) {
|
||||||
const now = Date.now()
|
const now = Date.now()
|
||||||
const v = Number(ts || 0)
|
const v = Number(ts || 0)
|
||||||
@ -343,15 +557,6 @@ async function fetchIssues(id) {
|
|||||||
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) {
|
function pickLatestIssueId(list) {
|
||||||
const arr = Array.isArray(list) ? list : []
|
const arr = Array.isArray(list) ? list : []
|
||||||
let latest = arr[arr.length - 1] && arr[arr.length - 1].id
|
let latest = arr[arr.length - 1] && arr[arr.length - 1].id
|
||||||
@ -371,6 +576,7 @@ function setSelectedById(id) {
|
|||||||
selectedIssueIndex.value = idx
|
selectedIssueIndex.value = idx
|
||||||
const cur = arr[idx]
|
const cur = arr[idx]
|
||||||
currentIssueId.value = (cur && cur.id) || ''
|
currentIssueId.value = (cur && cur.id) || ''
|
||||||
|
syncResumeGame(activityId.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
function prevIssue() {
|
function prevIssue() {
|
||||||
@ -379,6 +585,7 @@ function prevIssue() {
|
|||||||
const next = Math.max(0, Number(selectedIssueIndex.value || 0) - 1)
|
const next = Math.max(0, Number(selectedIssueIndex.value || 0) - 1)
|
||||||
selectedIssueIndex.value = next
|
selectedIssueIndex.value = next
|
||||||
currentIssueId.value = (arr[next] && arr[next].id) || ''
|
currentIssueId.value = (arr[next] && arr[next].id) || ''
|
||||||
|
syncResumeGame(activityId.value)
|
||||||
}
|
}
|
||||||
function nextIssue() {
|
function nextIssue() {
|
||||||
const arr = issues.value || []
|
const arr = issues.value || []
|
||||||
@ -386,6 +593,7 @@ function nextIssue() {
|
|||||||
const next = Math.min(arr.length - 1, Number(selectedIssueIndex.value || 0) + 1)
|
const next = Math.min(arr.length - 1, Number(selectedIssueIndex.value || 0) + 1)
|
||||||
selectedIssueIndex.value = next
|
selectedIssueIndex.value = next
|
||||||
currentIssueId.value = (arr[next] && arr[next].id) || ''
|
currentIssueId.value = (arr[next] && arr[next].id) || ''
|
||||||
|
syncResumeGame(activityId.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatPercent(v) {
|
function formatPercent(v) {
|
||||||
@ -406,6 +614,345 @@ function onPreviewBanner() {
|
|||||||
if (url) uni.previewImage({ urls: [url], current: url })
|
if (url) uni.previewImage({ urls: [url], current: url })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function previewCard(c) {
|
||||||
|
const img = String(c && c.image || '')
|
||||||
|
if (img) uni.previewImage({ urls: [img], current: img })
|
||||||
|
}
|
||||||
|
|
||||||
|
async function openGame(latest) {
|
||||||
|
const aid = activityId.value || ''
|
||||||
|
const targetIssueId = String(latest && latest.issue_id || '')
|
||||||
|
if (targetIssueId && targetIssueId !== String(currentIssueId.value || '')) {
|
||||||
|
const inList = (issues.value || []).some(x => x && String(x.id) === targetIssueId)
|
||||||
|
if (inList) setSelectedById(targetIssueId)
|
||||||
|
}
|
||||||
|
|
||||||
|
gameIssueId.value = targetIssueId || String(currentIssueId.value || '')
|
||||||
|
gameEntry.value = latest && latest.entry ? latest.entry : null
|
||||||
|
gameVisible.value = true
|
||||||
|
gameError.value = ''
|
||||||
|
await applyResumeEntry(gameEntry.value)
|
||||||
|
restoreOrInitLocalGame()
|
||||||
|
await autoDrawIfStuck()
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeGame() {
|
||||||
|
gameVisible.value = false
|
||||||
|
gameLoading.value = false
|
||||||
|
gameError.value = ''
|
||||||
|
gameEntry.value = null
|
||||||
|
gameIssueId.value = ''
|
||||||
|
pickedHandIndex.value = -1
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSelectedCodeFromEntry(entry) {
|
||||||
|
const e = entry || {}
|
||||||
|
return String(e.position || selectedCardTypeCode.value || '')
|
||||||
|
}
|
||||||
|
|
||||||
|
function isSelectedCard(card, selectedCode) {
|
||||||
|
const code = String(selectedCode || '')
|
||||||
|
if (!code) return false
|
||||||
|
const c = card || {}
|
||||||
|
const v = c.position ?? c.pos ?? c.type_code ?? c.card_type_code ?? c.type ?? c.code
|
||||||
|
return String(v || '') === code
|
||||||
|
}
|
||||||
|
|
||||||
|
function getMatchKey(card) {
|
||||||
|
const c = card || {}
|
||||||
|
const v = c.match_key ?? c.key ?? c.card_key ?? c.type ?? c.type_code ?? c.card_type_code ?? c.position ?? c.code ?? c.product_id ?? c.reward_id ?? c.prize_id ?? c.card_id ?? c.name ?? c.title
|
||||||
|
return String(v || '')
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDeckFromEntry(entry) {
|
||||||
|
const e = entry || {}
|
||||||
|
return Array.isArray(e.all_cards) ? e.all_cards : []
|
||||||
|
}
|
||||||
|
|
||||||
|
function restoreOrInitLocalGame() {
|
||||||
|
const entry = gameEntry.value || null
|
||||||
|
const aid = activityId.value || ''
|
||||||
|
const issueId = String(gameIssueId.value || currentIssueId.value || '')
|
||||||
|
if (!entry || !entry.game_id) return
|
||||||
|
|
||||||
|
pickedHandIndex.value = -1
|
||||||
|
const deck = normalizeAllCards(getDeckFromEntry(entry))
|
||||||
|
const cachedHand = Array.isArray(entry.hand) ? entry.hand : null
|
||||||
|
const cachedDeckIndexRaw = entry.deck_index ?? entry.deckIndex
|
||||||
|
const cachedChanceRaw = entry.chance
|
||||||
|
const cachedPairsRaw = entry.total_pairs ?? entry.totalPairs
|
||||||
|
|
||||||
|
if (cachedHand) {
|
||||||
|
hand.value = cachedHand
|
||||||
|
const idx = Number(cachedDeckIndexRaw)
|
||||||
|
const minIdx = cachedHand.length
|
||||||
|
const nextIdx = Number.isFinite(idx) ? Math.max(minIdx, idx) : minIdx
|
||||||
|
deckIndex.value = Math.min(deck.length, Math.max(0, nextIdx))
|
||||||
|
chance.value = Number.isFinite(Number(cachedChanceRaw)) ? Number(cachedChanceRaw) : 0
|
||||||
|
totalPairs.value = Number.isFinite(Number(cachedPairsRaw)) ? Number(cachedPairsRaw) : 0
|
||||||
|
gameFinished.value = false
|
||||||
|
if (deck !== entry.all_cards) gameEntry.value = { ...entry, all_cards: deck }
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const initialHand = deck.slice(0, 9)
|
||||||
|
const selectedCode = getSelectedCodeFromEntry(entry)
|
||||||
|
const initialChance = initialHand.reduce((acc, it) => acc + (isSelectedCard(it, selectedCode) ? 1 : 0), 0)
|
||||||
|
hand.value = initialHand
|
||||||
|
deckIndex.value = initialHand.length
|
||||||
|
chance.value = initialChance
|
||||||
|
totalPairs.value = 0
|
||||||
|
gameFinished.value = false
|
||||||
|
|
||||||
|
writeMatchingGameCacheEntry(aid, issueId, {
|
||||||
|
...entry,
|
||||||
|
all_cards: deck,
|
||||||
|
hand: initialHand,
|
||||||
|
deck_index: initialHand.length,
|
||||||
|
chance: initialChance,
|
||||||
|
total_pairs: 0,
|
||||||
|
ts: Date.now()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function persistLocalGame() {
|
||||||
|
const aid = activityId.value || ''
|
||||||
|
const issueId = String(gameIssueId.value || currentIssueId.value || '')
|
||||||
|
const entry = gameEntry.value || null
|
||||||
|
if (!entry || !entry.game_id) return
|
||||||
|
|
||||||
|
const deck = normalizeAllCards(getDeckFromEntry(entry))
|
||||||
|
const next = {
|
||||||
|
...entry,
|
||||||
|
all_cards: deck,
|
||||||
|
hand: Array.isArray(hand.value) ? hand.value : [],
|
||||||
|
deck_index: Number(deckIndex.value || 0),
|
||||||
|
chance: Number(chance.value || 0),
|
||||||
|
total_pairs: Number(totalPairs.value || 0),
|
||||||
|
ts: Date.now()
|
||||||
|
}
|
||||||
|
gameEntry.value = next
|
||||||
|
writeMatchingGameCacheEntry(aid, issueId, next)
|
||||||
|
}
|
||||||
|
|
||||||
|
function eliminateAllPairs() {
|
||||||
|
const cards = Array.isArray(hand.value) ? [...hand.value] : []
|
||||||
|
let removed = 0
|
||||||
|
for (;;) {
|
||||||
|
const counts = new Map()
|
||||||
|
for (const c of cards) {
|
||||||
|
const k = getMatchKey(c)
|
||||||
|
if (!k) continue
|
||||||
|
counts.set(k, (counts.get(k) || 0) + 1)
|
||||||
|
}
|
||||||
|
let targetKey = ''
|
||||||
|
for (const [k, n] of counts.entries()) {
|
||||||
|
if (n >= 2) { targetKey = k; break }
|
||||||
|
}
|
||||||
|
if (!targetKey) break
|
||||||
|
|
||||||
|
let first = -1
|
||||||
|
let second = -1
|
||||||
|
for (let i = 0; i < cards.length; i++) {
|
||||||
|
if (getMatchKey(cards[i]) !== targetKey) continue
|
||||||
|
if (first === -1) first = i
|
||||||
|
else { second = i; break }
|
||||||
|
}
|
||||||
|
if (first === -1 || second === -1) break
|
||||||
|
const a = Math.max(first, second)
|
||||||
|
const b = Math.min(first, second)
|
||||||
|
cards.splice(a, 1)
|
||||||
|
cards.splice(b, 1)
|
||||||
|
removed += 1
|
||||||
|
}
|
||||||
|
if (removed > 0) {
|
||||||
|
hand.value = cards
|
||||||
|
totalPairs.value = Number(totalPairs.value || 0) + removed
|
||||||
|
chance.value = Number(chance.value || 0) + removed
|
||||||
|
}
|
||||||
|
return removed
|
||||||
|
}
|
||||||
|
|
||||||
|
function canEliminateNow() {
|
||||||
|
const cards = Array.isArray(hand.value) ? hand.value : []
|
||||||
|
const counts = new Map()
|
||||||
|
for (const c of cards) {
|
||||||
|
const k = getMatchKey(c)
|
||||||
|
if (!k) continue
|
||||||
|
const n = (counts.get(k) || 0) + 1
|
||||||
|
if (n >= 2) return true
|
||||||
|
counts.set(k, n)
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
function canDrawOne() {
|
||||||
|
const entry = gameEntry.value || null
|
||||||
|
if (!entry) return false
|
||||||
|
const deck = getDeckFromEntry(entry)
|
||||||
|
return Number(deckIndex.value || 0) < deck.length
|
||||||
|
}
|
||||||
|
|
||||||
|
function drawOne() {
|
||||||
|
const entry = gameEntry.value || null
|
||||||
|
if (!entry) return null
|
||||||
|
const deck = getDeckFromEntry(entry)
|
||||||
|
const idx = Number(deckIndex.value || 0)
|
||||||
|
if (idx >= deck.length) return null
|
||||||
|
const next = deck[idx]
|
||||||
|
hand.value = [...(Array.isArray(hand.value) ? hand.value : []), next]
|
||||||
|
deckIndex.value = idx + 1
|
||||||
|
return next
|
||||||
|
}
|
||||||
|
|
||||||
|
function manualDraw() {
|
||||||
|
if (gameLoading.value) return
|
||||||
|
if (!canManualDraw.value) return
|
||||||
|
drawOne()
|
||||||
|
chance.value = Math.max(0, Number(chance.value || 0) - 1)
|
||||||
|
pickedHandIndex.value = -1
|
||||||
|
persistLocalGame()
|
||||||
|
}
|
||||||
|
|
||||||
|
async function autoDrawIfStuck() {
|
||||||
|
const entry = gameEntry.value || null
|
||||||
|
const gameId = entry && entry.game_id ? String(entry.game_id) : ''
|
||||||
|
if (!gameId) return
|
||||||
|
if (gameFinished.value) return
|
||||||
|
if (canEliminateNow()) return
|
||||||
|
|
||||||
|
let guard = 0
|
||||||
|
while (!canEliminateNow() && Number(chance.value || 0) > 0 && canDrawOne()) {
|
||||||
|
guard += 1
|
||||||
|
if (guard > 1000) throw new Error('自动摸牌次数过多')
|
||||||
|
drawOne()
|
||||||
|
chance.value = Math.max(0, Number(chance.value || 0) - 1)
|
||||||
|
pickedHandIndex.value = -1
|
||||||
|
persistLocalGame()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!canEliminateNow() && (Number(chance.value || 0) <= 0 || !canDrawOne())) {
|
||||||
|
await finishAndReport()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onCellTap(cell) {
|
||||||
|
if (gameLoading.value) return
|
||||||
|
if (!cell || cell.empty) return
|
||||||
|
const hi = Number(cell.handIndex)
|
||||||
|
if (!Number.isFinite(hi) || hi < 0) return
|
||||||
|
|
||||||
|
const cards = Array.isArray(hand.value) ? hand.value : []
|
||||||
|
if (!cards[hi]) return
|
||||||
|
|
||||||
|
const picked = Number(pickedHandIndex.value || -1)
|
||||||
|
if (picked < 0) {
|
||||||
|
pickedHandIndex.value = hi
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (picked === hi) {
|
||||||
|
pickedHandIndex.value = -1
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const a = cards[picked]
|
||||||
|
const b = cards[hi]
|
||||||
|
const ka = getMatchKey(a)
|
||||||
|
const kb = getMatchKey(b)
|
||||||
|
if (ka && kb && ka === kb) {
|
||||||
|
const next = [...cards]
|
||||||
|
const max = Math.max(picked, hi)
|
||||||
|
const min = Math.min(picked, hi)
|
||||||
|
next.splice(max, 1)
|
||||||
|
next.splice(min, 1)
|
||||||
|
hand.value = next
|
||||||
|
totalPairs.value = Number(totalPairs.value || 0) + 1
|
||||||
|
chance.value = Number(chance.value || 0) + 1
|
||||||
|
pickedHandIndex.value = -1
|
||||||
|
persistLocalGame()
|
||||||
|
await autoDrawIfStuck()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
pickedHandIndex.value = hi
|
||||||
|
uni.showToast({ title: '不相同', icon: 'none' })
|
||||||
|
}
|
||||||
|
|
||||||
|
async function finishAndReport() {
|
||||||
|
const aid = activityId.value || ''
|
||||||
|
const issueId = String(gameIssueId.value || currentIssueId.value || '')
|
||||||
|
const entry = gameEntry.value || null
|
||||||
|
const gameId = entry && entry.game_id ? String(entry.game_id) : ''
|
||||||
|
if (!gameId) return
|
||||||
|
await checkMatchingGame(gameId, Number(totalPairs.value || 0))
|
||||||
|
clearMatchingGameCacheEntry(aid, issueId)
|
||||||
|
gameFinished.value = true
|
||||||
|
closeGame()
|
||||||
|
uni.showModal({
|
||||||
|
title: '游戏结束',
|
||||||
|
content: `总对数:${Number(totalPairs.value || 0)}`,
|
||||||
|
showCancel: false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async function advanceOne() {
|
||||||
|
if (gameLoading.value) return
|
||||||
|
const entry = gameEntry.value || null
|
||||||
|
const gameId = entry && entry.game_id ? String(entry.game_id) : ''
|
||||||
|
if (!gameId) return
|
||||||
|
|
||||||
|
gameLoading.value = true
|
||||||
|
gameError.value = ''
|
||||||
|
try {
|
||||||
|
const removed = eliminateAllPairs()
|
||||||
|
if (removed > 0) {
|
||||||
|
persistLocalGame()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!canEliminateNow()) {
|
||||||
|
await autoDrawIfStuck()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
persistLocalGame()
|
||||||
|
} catch (e) {
|
||||||
|
gameError.value = e?.message || '操作失败'
|
||||||
|
} finally {
|
||||||
|
gameLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function autoRun() {
|
||||||
|
if (gameLoading.value) return
|
||||||
|
const entry = gameEntry.value || null
|
||||||
|
const gameId = entry && entry.game_id ? String(entry.game_id) : ''
|
||||||
|
if (!gameId) return
|
||||||
|
|
||||||
|
gameLoading.value = true
|
||||||
|
gameError.value = ''
|
||||||
|
try {
|
||||||
|
let guard = 0
|
||||||
|
for (;;) {
|
||||||
|
guard += 1
|
||||||
|
if (guard > 1000) throw new Error('自动进行次数过多')
|
||||||
|
|
||||||
|
const removed = eliminateAllPairs()
|
||||||
|
if (removed > 0) {
|
||||||
|
persistLocalGame()
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
await autoDrawIfStuck()
|
||||||
|
break
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
gameError.value = e?.message || '操作失败'
|
||||||
|
} finally {
|
||||||
|
gameLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function onParticipate() {
|
async function onParticipate() {
|
||||||
const aid = activityId.value || ''
|
const aid = activityId.value || ''
|
||||||
const iid = currentIssueId.value || ''
|
const iid = currentIssueId.value || ''
|
||||||
@ -424,6 +971,12 @@ async function onParticipate() {
|
|||||||
const openid = uni.getStorageSync('openid')
|
const openid = uni.getStorageSync('openid')
|
||||||
if (!openid) { uni.showToast({ title: '缺少OpenID,请重新登录', icon: 'none' }); return }
|
if (!openid) { uni.showToast({ title: '缺少OpenID,请重新登录', icon: 'none' }); return }
|
||||||
|
|
||||||
|
const latest = syncResumeGame(aid)
|
||||||
|
if (latest && latest.entry && latest.entry.game_id) {
|
||||||
|
await openGame(latest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
await fetchCardTypes()
|
await fetchCardTypes()
|
||||||
if (!selectedCardType.value) {
|
if (!selectedCardType.value) {
|
||||||
uni.showToast({ title: '请选择卡牌类型', icon: 'none' })
|
uni.showToast({ title: '请选择卡牌类型', icon: 'none' })
|
||||||
@ -438,6 +991,20 @@ async function onParticipate() {
|
|||||||
fetchPropCards()
|
fetchPropCards()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function applyResumeEntry(entry) {
|
||||||
|
if (!entry) return
|
||||||
|
await fetchCardTypes()
|
||||||
|
const pos = String(entry.position || '')
|
||||||
|
if (pos) selectedCardTypeCode.value = pos
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onResumeGame() {
|
||||||
|
const aid = activityId.value || ''
|
||||||
|
const latest = syncResumeGame(aid)
|
||||||
|
if (!latest || !latest.entry || !latest.entry.game_id) return
|
||||||
|
await openGame(latest)
|
||||||
|
}
|
||||||
|
|
||||||
function selectCardType(it) {
|
function selectCardType(it) {
|
||||||
selectedCardTypeCode.value = it && it.code ? String(it.code) : ''
|
selectedCardTypeCode.value = it && it.code ? String(it.code) : ''
|
||||||
}
|
}
|
||||||
@ -470,7 +1037,7 @@ async function doDraw() {
|
|||||||
const openid = uni.getStorageSync('openid')
|
const openid = uni.getStorageSync('openid')
|
||||||
if (!openid) { uni.showToast({ title: '缺少OpenID,请重新登录', icon: 'none' }); return }
|
if (!openid) { uni.showToast({ title: '缺少OpenID,请重新登录', icon: 'none' }); return }
|
||||||
|
|
||||||
uni.showLoading({ title: '处理中...' })
|
uni.showLoading({ title: '拉起支付...' })
|
||||||
try {
|
try {
|
||||||
if (!selectedCardType.value) {
|
if (!selectedCardType.value) {
|
||||||
uni.hideLoading()
|
uni.hideLoading()
|
||||||
@ -478,14 +1045,15 @@ async function doDraw() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const preRes = await createMatchingPreorder({
|
const joinRes = await joinLottery({
|
||||||
|
activity_id: Number(aid),
|
||||||
issue_id: Number(iid),
|
issue_id: Number(iid),
|
||||||
position: String(selectedCardType.value.code || ''),
|
channel: 'miniapp',
|
||||||
coupon_id: selectedCoupon.value?.id ? Number(selectedCoupon.value.id) : 0,
|
count: 1,
|
||||||
item_card_id: selectedCard.value?.id ? Number(selectedCard.value.id) : 0
|
coupon_id: selectedCoupon.value?.id ? Number(selectedCoupon.value.id) : 0
|
||||||
})
|
})
|
||||||
if (!preRes) throw new Error('预下单失败')
|
if (!joinRes) throw new Error('下单失败')
|
||||||
const orderNo = preRes.order_no || preRes.data?.order_no || preRes.result?.order_no || preRes.orderNo
|
const orderNo = joinRes.order_no || joinRes.data?.order_no || joinRes.result?.order_no || joinRes.orderNo
|
||||||
if (!orderNo) throw new Error('未获取到订单号')
|
if (!orderNo) throw new Error('未获取到订单号')
|
||||||
|
|
||||||
const payRes = await createWechatOrder({ openid, order_no: orderNo })
|
const payRes = await createWechatOrder({ openid, order_no: orderNo })
|
||||||
@ -502,20 +1070,31 @@ async function doDraw() {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
const resultRes = await getLotteryResult(orderNo)
|
uni.showLoading({ title: '创建游戏...' })
|
||||||
const raw = resultRes?.list || resultRes?.items || resultRes?.data || resultRes?.result || (Array.isArray(resultRes) ? resultRes : [resultRes])
|
const preRes = await createMatchingPreorder({
|
||||||
const first = Array.isArray(raw) ? raw[0] : raw
|
issue_id: Number(iid),
|
||||||
const name = String((first && (first.title || first.name || first.product_name)) || '未知奖励')
|
position: String(selectedCardType.value.code || ''),
|
||||||
const img = String((first && (first.image || first.img || first.pic || first.product_image)) || '')
|
coupon_id: selectedCoupon.value?.id ? Number(selectedCoupon.value.id) : 0,
|
||||||
|
item_card_id: selectedCard.value?.id ? Number(selectedCard.value.id) : 0
|
||||||
|
})
|
||||||
|
if (!preRes) throw new Error('创建游戏失败')
|
||||||
|
const gameId = preRes.game_id || preRes.data?.game_id || preRes.result?.game_id || preRes.gameId
|
||||||
|
const allCards = normalizeAllCards(preRes.all_cards || preRes.data?.all_cards || preRes.result?.all_cards || [])
|
||||||
|
if (gameId) {
|
||||||
|
writeMatchingGameCacheEntry(aid, iid, {
|
||||||
|
game_id: String(gameId),
|
||||||
|
position: String(selectedCardType.value.code || ''),
|
||||||
|
all_cards: allCards,
|
||||||
|
ts: Date.now()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
uni.hideLoading()
|
uni.hideLoading()
|
||||||
uni.showModal({
|
uni.showModal({
|
||||||
title: '抽选结果',
|
title: '支付成功',
|
||||||
content: '恭喜获得:' + name,
|
content: '已创建对对碰游戏,可点击“继续游戏”继续。',
|
||||||
showCancel: false,
|
showCancel: false
|
||||||
success: () => { if (img) uni.previewImage({ urls: [img], current: img }) }
|
|
||||||
})
|
})
|
||||||
fetchWinRecords(aid)
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
uni.hideLoading()
|
uni.hideLoading()
|
||||||
if (e?.errMsg && String(e.errMsg).includes('cancel')) {
|
if (e?.errMsg && String(e.errMsg).includes('cancel')) {
|
||||||
@ -571,9 +1150,9 @@ onLoad((opts) => {
|
|||||||
const id = (opts && opts.id) || ''
|
const id = (opts && opts.id) || ''
|
||||||
if (id) {
|
if (id) {
|
||||||
activityId.value = id
|
activityId.value = id
|
||||||
|
syncResumeGame(id)
|
||||||
fetchDetail(id)
|
fetchDetail(id)
|
||||||
fetchIssues(id)
|
fetchIssues(id)
|
||||||
fetchWinRecords(id)
|
|
||||||
}
|
}
|
||||||
fetchCardTypes()
|
fetchCardTypes()
|
||||||
})
|
})
|
||||||
@ -1185,6 +1764,53 @@ onLoad((opts) => {
|
|||||||
border-radius: $radius-sm;
|
border-radius: $radius-sm;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.match-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(6, 1fr);
|
||||||
|
gap: $spacing-sm;
|
||||||
|
}
|
||||||
|
.match-cell {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
aspect-ratio: 1 / 1;
|
||||||
|
border-radius: $radius-md;
|
||||||
|
overflow: hidden;
|
||||||
|
background: rgba(255,255,255,0.75);
|
||||||
|
border: 2rpx solid rgba(0,0,0,0.06);
|
||||||
|
box-shadow: $shadow-xs;
|
||||||
|
}
|
||||||
|
.match-cell.chosen {
|
||||||
|
border-color: rgba($brand-primary, 0.8);
|
||||||
|
box-shadow: 0 10rpx 22rpx rgba($brand-primary, 0.18);
|
||||||
|
}
|
||||||
|
.match-cell.picked {
|
||||||
|
border-color: rgba($accent-gold, 0.9);
|
||||||
|
box-shadow: 0 10rpx 22rpx rgba($accent-gold, 0.22);
|
||||||
|
}
|
||||||
|
.match-cell.empty {
|
||||||
|
background: rgba(255,255,255,0.35);
|
||||||
|
border-style: dashed;
|
||||||
|
}
|
||||||
|
.match-cell-img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: $bg-secondary;
|
||||||
|
}
|
||||||
|
.match-cell-type {
|
||||||
|
position: absolute;
|
||||||
|
left: 8rpx;
|
||||||
|
bottom: 8rpx;
|
||||||
|
max-width: 90%;
|
||||||
|
font-size: 20rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #fff;
|
||||||
|
background: rgba(0, 0, 0, 0.5);
|
||||||
|
padding: 2rpx 8rpx;
|
||||||
|
border-radius: 10rpx;
|
||||||
|
pointer-events: none;
|
||||||
|
@include text-ellipsis(1);
|
||||||
|
}
|
||||||
|
|
||||||
/* Empty State */
|
/* Empty State */
|
||||||
.empty-state {
|
.empty-state {
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -1360,6 +1986,14 @@ onLoad((opts) => {
|
|||||||
color: #fff;
|
color: #fff;
|
||||||
box-shadow: $shadow-warm;
|
box-shadow: $shadow-warm;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.secondary {
|
||||||
|
background: rgba($bg-card, 0.9);
|
||||||
|
color: $text-main;
|
||||||
|
border: 2rpx solid rgba($brand-primary, 0.25);
|
||||||
|
box-shadow: $shadow-sm;
|
||||||
|
padding: 0 40rpx;
|
||||||
|
}
|
||||||
|
|
||||||
&:active { transform: scale(0.98); }
|
&:active { transform: scale(0.98); }
|
||||||
}
|
}
|
||||||
@ -1376,6 +2010,39 @@ onLoad((opts) => {
|
|||||||
50%, 100% { left: 200%; }
|
50%, 100% { left: 200%; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.flip-overlay {
|
||||||
|
position: fixed; top: 0; left: 0; right: 0; bottom: 0; z-index: 9999;
|
||||||
|
}
|
||||||
|
.flip-mask {
|
||||||
|
position: absolute; top: 0; bottom: 0; width: 100%; background: rgba(0,0,0,0.85);
|
||||||
|
backdrop-filter: blur(10rpx);
|
||||||
|
animation: fadeIn 0.3s ease-out;
|
||||||
|
}
|
||||||
|
.flip-content {
|
||||||
|
position: relative;
|
||||||
|
z-index: 2;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
padding: 40rpx;
|
||||||
|
justify-content: center;
|
||||||
|
animation: scaleIn 0.3s ease-out;
|
||||||
|
}
|
||||||
|
.close-btn {
|
||||||
|
margin-top: 60rpx;
|
||||||
|
background: #fff;
|
||||||
|
color: #333;
|
||||||
|
border-radius: 100rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
width: 50%;
|
||||||
|
height: 80rpx;
|
||||||
|
line-height: 80rpx;
|
||||||
|
align-self: center;
|
||||||
|
box-shadow: 0 10rpx 30rpx rgba(255,255,255,0.15);
|
||||||
|
transition: all 0.2s;
|
||||||
|
&:active { transform: scale(0.95); }
|
||||||
|
}
|
||||||
|
|
||||||
/* Animation Utilities */
|
/* Animation Utilities */
|
||||||
.animate-stagger {
|
.animate-stagger {
|
||||||
animation: fadeInUp 0.5s ease-out backwards;
|
animation: fadeInUp 0.5s ease-out backwards;
|
||||||
|
|||||||
@ -1,42 +1,80 @@
|
|||||||
<template>
|
<template>
|
||||||
<view class="bg-decoration"></view>
|
<view class="page-wrapper">
|
||||||
<scroll-view class="page" scroll-y>
|
<view class="bg-decoration">
|
||||||
<!-- 顶部 Banner -->
|
<view class="orb orb-1"></view>
|
||||||
<view class="banner" v-if="detail.banner">
|
<view class="orb orb-2"></view>
|
||||||
<image class="banner-img" :src="detail.banner" mode="widthFix" />
|
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<!-- 商品信息卡片 -->
|
<view class="page-bg">
|
||||||
<view class="product-card">
|
<image class="bg-image" :src="coverUrl" mode="aspectFill" />
|
||||||
<view class="product-info">
|
<view class="bg-mask"></view>
|
||||||
<image v-if="detail.banner" class="product-thumb" :src="detail.banner" mode="aspectFill" />
|
</view>
|
||||||
<view class="product-detail">
|
|
||||||
<view class="product-name">{{ detail.name || detail.title || '无限赏活动' }}</view>
|
<scroll-view class="main-scroll" scroll-y>
|
||||||
<view class="product-price">¥{{ (Number(detail.price_draw || 0) / 100).toFixed(2) }}</view>
|
<view class="header-card animate-enter">
|
||||||
|
<image class="header-cover" :src="coverUrl" mode="aspectFill" />
|
||||||
|
<view class="header-info">
|
||||||
|
<view class="header-title">{{ detail.name || detail.title || '无限赏活动' }}</view>
|
||||||
|
<view class="header-price-row">
|
||||||
|
<text class="price-symbol">¥</text>
|
||||||
|
<text class="price-num">{{ (Number(detail.price_draw || 0) / 100).toFixed(2) }}</text>
|
||||||
|
<text class="price-unit">/发</text>
|
||||||
|
</view>
|
||||||
|
<view class="header-tags">
|
||||||
|
<view class="tag-item">公开透明</view>
|
||||||
|
<view class="tag-item">随机掉落</view>
|
||||||
|
</view>
|
||||||
</view>
|
</view>
|
||||||
<view class="product-actions">
|
<view class="header-actions">
|
||||||
<view class="action-btn">📦 盒柜</view>
|
<view class="action-btn" @tap="showRules">
|
||||||
|
<text class="icon">📋</text>
|
||||||
|
<text>规则</text>
|
||||||
|
</view>
|
||||||
|
<view class="action-btn" @tap="goCabinet">
|
||||||
|
<text class="icon">📦</text>
|
||||||
|
<text>盒柜</text>
|
||||||
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
|
||||||
|
<view class="section-container animate-enter stagger-1" v-if="currentIssueRewards.length > 0">
|
||||||
<!-- 期号切换条 -->
|
<view class="section-header">
|
||||||
<view class="issue-bar" v-if="showIssues && issues.length">
|
<text class="section-title">奖池一览</text>
|
||||||
<button class="nav-btn" @click="prevIssue">◀</button>
|
<text class="section-more" @tap="openRewardsPopup">查看全部</text>
|
||||||
<view class="issue-info">
|
</view>
|
||||||
<text class="issue-label">{{ currentIssueTitle }}</text>
|
<scroll-view class="preview-scroll" scroll-x>
|
||||||
|
<view class="preview-item" v-for="(item, idx) in currentIssueRewards" :key="item.id || idx">
|
||||||
|
<view class="prize-tag" :class="{ 'tag-boss': item.boss }">{{ item.boss ? 'BOSS' : '赏' }}</view>
|
||||||
|
<image class="preview-img" :src="item.image" mode="aspectFill" />
|
||||||
|
<view class="preview-name">{{ item.title }}</view>
|
||||||
|
</view>
|
||||||
|
</scroll-view>
|
||||||
</view>
|
</view>
|
||||||
<button class="nav-btn" @click="nextIssue">▶</button>
|
|
||||||
</view>
|
<view class="section-container selector-container animate-enter stagger-2">
|
||||||
|
<view class="issue-header" v-if="showIssues && issues.length">
|
||||||
<!-- 玩法福利标签 -->
|
<view class="issue-switch-btn" @click="prevIssue">
|
||||||
<view class="gameplay-tags">
|
<text class="arrow">◀</text>
|
||||||
<view class="tag tag-pool">聚宝盆</view>
|
</view>
|
||||||
<view class="tag tag-drop">随机掉落 10%</view>
|
<view class="issue-info-center">
|
||||||
<view class="tag tag-free">随机免单 10%</view>
|
<text class="issue-current-text">{{ currentIssueTitle }}</text>
|
||||||
</view>
|
<text class="issue-status-badge">{{ statusText || '进行中' }}</text>
|
||||||
|
</view>
|
||||||
</scroll-view>
|
<view class="issue-switch-btn" @click="nextIssue">
|
||||||
|
<text class="arrow">▶</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="gameplay-tags">
|
||||||
|
<view class="tag tag-pool">聚宝盆</view>
|
||||||
|
<view class="tag tag-drop">随机掉落 10%</view>
|
||||||
|
<view class="tag tag-free">随机免单 10%</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view style="height: 220rpx;"></view>
|
||||||
|
</scroll-view>
|
||||||
|
</view>
|
||||||
|
|
||||||
<!-- 底部多档位抽赏按钮 -->
|
<!-- 底部多档位抽赏按钮 -->
|
||||||
<view class="bottom-actions">
|
<view class="bottom-actions">
|
||||||
@ -57,7 +95,30 @@
|
|||||||
<text class="tier-label">抽10发</text>
|
<text class="tier-label">抽10发</text>
|
||||||
</button>
|
</button>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
|
<view v-if="rewardsVisible" class="rewards-overlay" @touchmove.stop.prevent>
|
||||||
|
<view class="rewards-mask" @tap="closeRewardsPopup"></view>
|
||||||
|
<view class="rewards-panel" @tap.stop>
|
||||||
|
<view class="rewards-header">
|
||||||
|
<text class="rewards-title">{{ currentIssueTitle }} · 奖池与概率</text>
|
||||||
|
<text class="rewards-close" @tap="closeRewardsPopup">×</text>
|
||||||
|
</view>
|
||||||
|
<scroll-view scroll-y class="rewards-list">
|
||||||
|
<view v-for="(item, idx) in rewardsForPopup" :key="item.id || idx" class="rewards-item">
|
||||||
|
<image class="rewards-thumb" :src="item.image" mode="aspectFill" />
|
||||||
|
<view class="rewards-info">
|
||||||
|
<view class="rewards-name-row">
|
||||||
|
<text class="rewards-name">{{ item.title || '-' }}</text>
|
||||||
|
<view class="rewards-tag" v-if="item.boss">BOSS</view>
|
||||||
|
</view>
|
||||||
|
<text class="rewards-percent">概率 {{ formatPercent(item.percent) }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view v-if="!rewardsForPopup.length" class="rewards-empty">暂无奖池数据</view>
|
||||||
|
</scroll-view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
<view v-if="showFlip" class="flip-overlay" @touchmove.stop.prevent>
|
<view v-if="showFlip" class="flip-overlay" @touchmove.stop.prevent>
|
||||||
<view class="flip-mask" @tap="closeFlip"></view>
|
<view class="flip-mask" @tap="closeFlip"></view>
|
||||||
<view class="flip-content" @tap.stop>
|
<view class="flip-content" @tap.stop>
|
||||||
@ -84,6 +145,7 @@ import { getActivityDetail, getActivityIssues, getActivityIssueRewards, joinLott
|
|||||||
|
|
||||||
const detail = ref({})
|
const detail = ref({})
|
||||||
const statusText = ref('')
|
const statusText = ref('')
|
||||||
|
const rewardsVisible = ref(false)
|
||||||
const issues = ref([])
|
const issues = ref([])
|
||||||
const rewardsMap = ref({})
|
const rewardsMap = ref({})
|
||||||
const currentIssueId = ref('')
|
const currentIssueId = ref('')
|
||||||
@ -92,6 +154,7 @@ const showIssues = computed(() => (detail.value && detail.value.status !== 2))
|
|||||||
const activityId = ref('')
|
const activityId = ref('')
|
||||||
const drawLoading = ref(false)
|
const drawLoading = ref(false)
|
||||||
const currentIssueRewards = computed(() => (currentIssueId.value && rewardsMap.value[currentIssueId.value]) ? rewardsMap.value[currentIssueId.value] : [])
|
const currentIssueRewards = computed(() => (currentIssueId.value && rewardsMap.value[currentIssueId.value]) ? rewardsMap.value[currentIssueId.value] : [])
|
||||||
|
const coverUrl = computed(() => cleanUrl(detail.value && (detail.value.image || detail.value.banner || '')))
|
||||||
const currentIssueTitle = computed(() => {
|
const currentIssueTitle = computed(() => {
|
||||||
const arr = issues.value || []
|
const arr = issues.value || []
|
||||||
const cur = arr[selectedIssueIndex.value]
|
const cur = arr[selectedIssueIndex.value]
|
||||||
@ -101,6 +164,10 @@ const currentIssueTitle = computed(() => {
|
|||||||
const points = ref(0)
|
const points = ref(0)
|
||||||
const flipRef = ref(null)
|
const flipRef = ref(null)
|
||||||
const showFlip = ref(false)
|
const showFlip = ref(false)
|
||||||
|
const rewardsForPopup = computed(() => {
|
||||||
|
const arr = currentIssueRewards.value || []
|
||||||
|
return Array.isArray(arr) ? arr : []
|
||||||
|
})
|
||||||
const paymentVisible = ref(false)
|
const paymentVisible = ref(false)
|
||||||
const paymentAmount = ref('0.00')
|
const paymentAmount = ref('0.00')
|
||||||
const coupons = ref([])
|
const coupons = ref([])
|
||||||
@ -117,10 +184,37 @@ function statusToText(s) {
|
|||||||
return String(s || '')
|
return String(s || '')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function formatPercent(v) {
|
||||||
|
const n = Number(v)
|
||||||
|
if (!Number.isFinite(n)) return '0%'
|
||||||
|
return `${n}%`
|
||||||
|
}
|
||||||
|
|
||||||
|
function openRewardsPopup() {
|
||||||
|
rewardsVisible.value = true
|
||||||
|
}
|
||||||
|
function closeRewardsPopup() {
|
||||||
|
rewardsVisible.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
function showRules() {
|
||||||
|
uni.showModal({
|
||||||
|
title: '活动规则',
|
||||||
|
content: detail.value.rules || '1. 选择档位进行抽赏\n2. 每次抽赏随机获得奖品\n3. 奖池与概率以页面展示为准',
|
||||||
|
showCancel: false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function goCabinet() {
|
||||||
|
uni.navigateTo({ url: '/pages/cabinet/index' })
|
||||||
|
}
|
||||||
|
|
||||||
async function fetchDetail(id) {
|
async function fetchDetail(id) {
|
||||||
const data = await getActivityDetail(id)
|
const data = await getActivityDetail(id)
|
||||||
detail.value = data || {}
|
detail.value = data || {}
|
||||||
statusText.value = statusToText(detail.value.status)
|
statusText.value = statusToText(detail.value.status)
|
||||||
|
const title = String(detail.value.name || detail.value.title || '无限赏')
|
||||||
|
try { uni.setNavigationBarTitle({ title }) } catch (_) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
function unwrap(list) {
|
function unwrap(list) {
|
||||||
@ -412,50 +506,45 @@ function closeFlip() { showFlip.value = false }
|
|||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
/* 柯大鸭潮玩 - 无限赏活动页面 */
|
/* 柯大鸭潮玩 - 无限赏活动页面 */
|
||||||
.page {
|
.page-wrapper {
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
padding-bottom: calc(200rpx + env(safe-area-inset-bottom));
|
background: $bg-page;
|
||||||
background: transparent;
|
|
||||||
position: relative;
|
position: relative;
|
||||||
z-index: 1;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.bg-decoration {
|
.bg-decoration {
|
||||||
position: fixed;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100vh;
|
height: 100%;
|
||||||
background-color: $bg-page;
|
|
||||||
z-index: 0;
|
z-index: 0;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
|
}
|
||||||
&::before, &::after {
|
|
||||||
content: '';
|
.orb {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
filter: blur(80px);
|
filter: blur(80px);
|
||||||
opacity: 0.5;
|
opacity: 0.6;
|
||||||
}
|
}
|
||||||
|
.orb-1 {
|
||||||
&::before {
|
width: 600rpx;
|
||||||
width: 600rpx;
|
height: 600rpx;
|
||||||
height: 600rpx;
|
background: radial-gradient(circle, rgba($brand-primary, 0.2), transparent 70%);
|
||||||
background: radial-gradient(circle, rgba($brand-primary, 0.12), transparent 70%);
|
top: -200rpx;
|
||||||
top: -200rpx;
|
left: -200rpx;
|
||||||
left: -200rpx;
|
animation: float 10s ease-in-out infinite;
|
||||||
animation: float 10s ease-in-out infinite;
|
}
|
||||||
}
|
.orb-2 {
|
||||||
|
width: 500rpx;
|
||||||
&::after {
|
height: 500rpx;
|
||||||
width: 500rpx;
|
background: radial-gradient(circle, rgba($accent-gold, 0.2), transparent 70%);
|
||||||
height: 500rpx;
|
bottom: 20%;
|
||||||
background: radial-gradient(circle, rgba($accent-gold, 0.15), transparent 70%);
|
right: -100rpx;
|
||||||
bottom: 10%;
|
animation: float 12s ease-in-out infinite reverse;
|
||||||
right: -100rpx;
|
|
||||||
animation: float 12s ease-in-out infinite reverse;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes float {
|
@keyframes float {
|
||||||
@ -463,6 +552,288 @@ function closeFlip() { showFlip.value = false }
|
|||||||
50% { transform: translate(30rpx, 50rpx); }
|
50% { transform: translate(30rpx, 50rpx); }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.page-bg {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 700rpx;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
.bg-image {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
filter: blur(30rpx) brightness(0.9);
|
||||||
|
transform: scale(1.1);
|
||||||
|
}
|
||||||
|
.bg-mask {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: linear-gradient(180deg, rgba($bg-page, 0.2) 0%, $bg-page 90%, $bg-page 100%);
|
||||||
|
}
|
||||||
|
.main-scroll {
|
||||||
|
position: relative;
|
||||||
|
z-index: 2;
|
||||||
|
height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-card {
|
||||||
|
margin: $spacing-xl $spacing-lg;
|
||||||
|
background: rgba($bg-card, 0.85);
|
||||||
|
backdrop-filter: blur(24rpx);
|
||||||
|
border-radius: $radius-xl;
|
||||||
|
padding: $spacing-lg;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
box-shadow: $shadow-card;
|
||||||
|
border: 1rpx solid rgba(255, 255, 255, 0.6);
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.header-cover {
|
||||||
|
width: 180rpx;
|
||||||
|
height: 180rpx;
|
||||||
|
border-radius: $radius-md;
|
||||||
|
margin-right: $spacing-lg;
|
||||||
|
background: $bg-secondary;
|
||||||
|
box-shadow: $shadow-md;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.header-info {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: flex-start;
|
||||||
|
min-height: 180rpx;
|
||||||
|
padding: 6rpx 0;
|
||||||
|
}
|
||||||
|
.header-title {
|
||||||
|
font-size: $font-xl;
|
||||||
|
font-weight: 800;
|
||||||
|
color: $text-main;
|
||||||
|
margin-bottom: $spacing-xs;
|
||||||
|
line-height: 1.3;
|
||||||
|
@include text-ellipsis(2);
|
||||||
|
}
|
||||||
|
.header-price-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
color: $brand-primary;
|
||||||
|
margin-bottom: $spacing-sm;
|
||||||
|
text-shadow: 0 2rpx 4rpx rgba($brand-primary, 0.1);
|
||||||
|
}
|
||||||
|
.price-symbol { font-size: $font-md; font-weight: 700; }
|
||||||
|
.price-num { font-size: $font-xxl; font-weight: 900; margin: 0 4rpx; font-family: 'DIN Alternate', sans-serif; }
|
||||||
|
.price-unit { font-size: $font-sm; color: $text-sub; margin-left: 4rpx; }
|
||||||
|
|
||||||
|
.header-tags {
|
||||||
|
display: flex;
|
||||||
|
gap: $spacing-xs;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
.tag-item {
|
||||||
|
font-size: $font-xs;
|
||||||
|
color: $brand-primary-dark;
|
||||||
|
background: rgba($brand-primary, 0.08);
|
||||||
|
padding: 4rpx $spacing-sm;
|
||||||
|
border-radius: $radius-sm;
|
||||||
|
font-weight: 600;
|
||||||
|
border: 1rpx solid rgba($brand-primary, 0.1);
|
||||||
|
}
|
||||||
|
.header-actions {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: $spacing-lg;
|
||||||
|
margin-left: 20rpx;
|
||||||
|
padding-left: $spacing-lg;
|
||||||
|
border-left: 1rpx solid rgba(0,0,0,0.06);
|
||||||
|
justify-content: center;
|
||||||
|
height: 140rpx;
|
||||||
|
}
|
||||||
|
.action-btn {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
font-size: $font-xs;
|
||||||
|
color: $text-sub;
|
||||||
|
transition: all 0.2s;
|
||||||
|
&:active {
|
||||||
|
transform: scale(0.9);
|
||||||
|
color: $text-main;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.action-btn .icon {
|
||||||
|
font-size: $font-xl;
|
||||||
|
margin-bottom: 6rpx;
|
||||||
|
filter: grayscale(0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-container {
|
||||||
|
margin: 0 $spacing-lg $spacing-lg;
|
||||||
|
background: rgba(255, 255, 255, 0.9);
|
||||||
|
border-radius: $radius-xl;
|
||||||
|
padding: $spacing-lg;
|
||||||
|
box-shadow: $shadow-sm;
|
||||||
|
backdrop-filter: blur(10rpx);
|
||||||
|
}
|
||||||
|
.section-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 24rpx;
|
||||||
|
padding: 0 4rpx;
|
||||||
|
}
|
||||||
|
.section-title {
|
||||||
|
font-size: $font-lg;
|
||||||
|
font-weight: 800;
|
||||||
|
color: $text-main;
|
||||||
|
position: relative;
|
||||||
|
padding-left: $spacing-lg;
|
||||||
|
&::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
width: 8rpx;
|
||||||
|
height: 28rpx;
|
||||||
|
background: $gradient-brand;
|
||||||
|
border-radius: 4rpx;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.section-more {
|
||||||
|
font-size: $font-sm;
|
||||||
|
color: $text-tertiary;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
&::after {
|
||||||
|
content: '>';
|
||||||
|
font-family: monospace;
|
||||||
|
margin-left: 6rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-scroll {
|
||||||
|
white-space: nowrap;
|
||||||
|
margin: 0 -$spacing-lg;
|
||||||
|
padding: 0 $spacing-lg;
|
||||||
|
width: calc(100% + 40rpx);
|
||||||
|
}
|
||||||
|
.preview-item {
|
||||||
|
display: inline-block;
|
||||||
|
width: 200rpx;
|
||||||
|
margin-right: $spacing-lg;
|
||||||
|
vertical-align: top;
|
||||||
|
position: relative;
|
||||||
|
transition: transform 0.2s;
|
||||||
|
&:active { transform: scale(0.96); }
|
||||||
|
&:last-child { margin-right: 40rpx; }
|
||||||
|
}
|
||||||
|
.preview-img {
|
||||||
|
width: 200rpx;
|
||||||
|
height: 200rpx;
|
||||||
|
border-radius: $radius-lg;
|
||||||
|
background: $bg-secondary;
|
||||||
|
margin-bottom: $spacing-md;
|
||||||
|
box-shadow: $shadow-sm;
|
||||||
|
border: 1rpx solid rgba(0,0,0,0.03);
|
||||||
|
}
|
||||||
|
.preview-name {
|
||||||
|
font-size: $font-sm;
|
||||||
|
color: $text-secondary;
|
||||||
|
width: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-align: center;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
.prize-tag {
|
||||||
|
position: absolute;
|
||||||
|
top: 10rpx;
|
||||||
|
left: 10rpx;
|
||||||
|
background: rgba(0,0,0,0.6);
|
||||||
|
color: #fff;
|
||||||
|
font-size: $font-xs;
|
||||||
|
padding: 4rpx $spacing-sm;
|
||||||
|
border-radius: $radius-sm;
|
||||||
|
z-index: 10;
|
||||||
|
font-weight: 700;
|
||||||
|
backdrop-filter: blur(4rpx);
|
||||||
|
transform: scale(0.9);
|
||||||
|
transform-origin: top left;
|
||||||
|
}
|
||||||
|
.prize-tag.tag-boss {
|
||||||
|
background: $gradient-brand;
|
||||||
|
box-shadow: 0 4rpx 12rpx rgba($brand-primary, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.selector-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
background: rgba($bg-card, 0.95);
|
||||||
|
backdrop-filter: blur(20rpx);
|
||||||
|
}
|
||||||
|
.issue-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 30rpx;
|
||||||
|
background: $bg-grey;
|
||||||
|
border-radius: $radius-round;
|
||||||
|
padding: 10rpx;
|
||||||
|
border: 1rpx solid $border-color-light;
|
||||||
|
}
|
||||||
|
.issue-switch-btn {
|
||||||
|
width: 72rpx;
|
||||||
|
height: 72rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: $bg-card;
|
||||||
|
border-radius: 50%;
|
||||||
|
box-shadow: $shadow-sm;
|
||||||
|
transition: all 0.2s;
|
||||||
|
color: $text-secondary;
|
||||||
|
&:active {
|
||||||
|
transform: scale(0.9);
|
||||||
|
background: $bg-secondary;
|
||||||
|
color: $brand-primary;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.arrow { font-size: $font-sm; font-weight: 800; }
|
||||||
|
.issue-info-center {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
.issue-current-text {
|
||||||
|
font-size: $font-lg;
|
||||||
|
font-weight: 700;
|
||||||
|
color: $text-main;
|
||||||
|
}
|
||||||
|
.issue-status-badge {
|
||||||
|
font-size: $font-xs;
|
||||||
|
color: $uni-color-success;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.animate-enter {
|
||||||
|
animation: fadeInUp 0.6s cubic-bezier(0.2, 0.8, 0.2, 1) backwards;
|
||||||
|
}
|
||||||
|
.stagger-1 { animation-delay: 0.1s; }
|
||||||
|
.stagger-2 { animation-delay: 0.2s; }
|
||||||
|
|
||||||
|
@keyframes fadeInUp {
|
||||||
|
from { opacity: 0; transform: translateY(40rpx); }
|
||||||
|
to { opacity: 1; transform: translateY(0); }
|
||||||
|
}
|
||||||
|
|
||||||
.banner {
|
.banner {
|
||||||
padding: $spacing-lg $spacing-lg 0;
|
padding: $spacing-lg $spacing-lg 0;
|
||||||
animation: fadeInDown 0.6s $ease-out;
|
animation: fadeInDown 0.6s $ease-out;
|
||||||
@ -523,21 +894,6 @@ function closeFlip() { showFlip.value = false }
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: $spacing-sm;
|
gap: $spacing-sm;
|
||||||
}
|
}
|
||||||
.action-btn {
|
|
||||||
background: rgba($brand-primary, 0.05);
|
|
||||||
border: 1rpx solid rgba($brand-primary, 0.2);
|
|
||||||
border-radius: $radius-sm;
|
|
||||||
padding: $spacing-sm $spacing-lg;
|
|
||||||
font-size: $font-sm;
|
|
||||||
color: $brand-primary-dark;
|
|
||||||
text-align: center;
|
|
||||||
font-weight: 600;
|
|
||||||
transition: all $transition-fast;
|
|
||||||
}
|
|
||||||
.action-btn:active {
|
|
||||||
background: rgba($brand-primary, 0.1);
|
|
||||||
transform: scale(0.95);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 期号切换条 */
|
/* 期号切换条 */
|
||||||
.issue-bar {
|
.issue-bar {
|
||||||
@ -709,6 +1065,100 @@ function closeFlip() { showFlip.value = false }
|
|||||||
color: #FFFFFF;
|
color: #FFFFFF;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.rewards-overlay { position: fixed; left: 0; right: 0; top: 0; bottom: 0; z-index: 9000; }
|
||||||
|
.rewards-mask {
|
||||||
|
position: absolute; left: 0; right: 0; top: 0; bottom: 0;
|
||||||
|
background: rgba(0,0,0,0.6);
|
||||||
|
backdrop-filter: blur(10rpx);
|
||||||
|
}
|
||||||
|
.rewards-panel {
|
||||||
|
position: absolute;
|
||||||
|
left: $spacing-lg;
|
||||||
|
right: $spacing-lg;
|
||||||
|
bottom: calc(env(safe-area-inset-bottom) + 24rpx);
|
||||||
|
max-height: 70vh;
|
||||||
|
background: rgba($bg-card, 0.95);
|
||||||
|
border-radius: $radius-xl;
|
||||||
|
box-shadow: $shadow-card;
|
||||||
|
border: 1rpx solid rgba(255,255,255,0.5);
|
||||||
|
overflow: hidden;
|
||||||
|
animation: fadeInUp 0.25s ease-out;
|
||||||
|
}
|
||||||
|
.rewards-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: $spacing-lg;
|
||||||
|
border-bottom: 1rpx solid rgba(0,0,0,0.06);
|
||||||
|
}
|
||||||
|
.rewards-title {
|
||||||
|
font-size: $font-md;
|
||||||
|
font-weight: 800;
|
||||||
|
color: $text-main;
|
||||||
|
}
|
||||||
|
.rewards-close {
|
||||||
|
font-size: 48rpx;
|
||||||
|
line-height: 1;
|
||||||
|
color: $text-tertiary;
|
||||||
|
padding: 0 10rpx;
|
||||||
|
}
|
||||||
|
.rewards-list {
|
||||||
|
max-height: 60vh;
|
||||||
|
padding: $spacing-lg;
|
||||||
|
}
|
||||||
|
.rewards-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: $spacing-md;
|
||||||
|
border-radius: $radius-lg;
|
||||||
|
background: rgba(255,255,255,0.75);
|
||||||
|
border: 1rpx solid rgba(0,0,0,0.03);
|
||||||
|
box-shadow: $shadow-xs;
|
||||||
|
margin-bottom: $spacing-md;
|
||||||
|
}
|
||||||
|
.rewards-thumb {
|
||||||
|
width: 96rpx;
|
||||||
|
height: 96rpx;
|
||||||
|
border-radius: $radius-md;
|
||||||
|
background: $bg-secondary;
|
||||||
|
flex-shrink: 0;
|
||||||
|
margin-right: $spacing-md;
|
||||||
|
}
|
||||||
|
.rewards-info { flex: 1; min-width: 0; }
|
||||||
|
.rewards-name-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: $spacing-sm;
|
||||||
|
margin-bottom: 8rpx;
|
||||||
|
}
|
||||||
|
.rewards-name {
|
||||||
|
font-size: $font-md;
|
||||||
|
font-weight: 700;
|
||||||
|
color: $text-main;
|
||||||
|
@include text-ellipsis(1);
|
||||||
|
}
|
||||||
|
.rewards-tag {
|
||||||
|
font-size: $font-xs;
|
||||||
|
font-weight: 800;
|
||||||
|
color: #fff;
|
||||||
|
background: $gradient-brand;
|
||||||
|
border-radius: $radius-sm;
|
||||||
|
padding: 4rpx 10rpx;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.rewards-percent {
|
||||||
|
font-size: $font-sm;
|
||||||
|
color: $text-sub;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
.rewards-empty {
|
||||||
|
padding: 60rpx 0 20rpx;
|
||||||
|
text-align: center;
|
||||||
|
color: $text-tertiary;
|
||||||
|
font-size: $font-sm;
|
||||||
|
}
|
||||||
|
|
||||||
/* 翻牌弹窗 */
|
/* 翻牌弹窗 */
|
||||||
.flip-overlay {
|
.flip-overlay {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
|
|||||||
@ -142,7 +142,7 @@ import { ref, computed } from 'vue'
|
|||||||
import { onLoad, onUnload } from '@dcloudio/uni-app'
|
import { onLoad, onUnload } from '@dcloudio/uni-app'
|
||||||
import FlipGrid from '../../../components/FlipGrid.vue'
|
import FlipGrid from '../../../components/FlipGrid.vue'
|
||||||
import YifanSelector from '@/components/YifanSelector.vue'
|
import YifanSelector from '@/components/YifanSelector.vue'
|
||||||
import { getActivityDetail, getActivityIssues, getActivityIssueRewards, getActivityWinRecords } from '../../../api/appUser'
|
import { getActivityDetail, getActivityIssues, getActivityIssueRewards } from '../../../api/appUser'
|
||||||
|
|
||||||
const detail = ref({})
|
const detail = ref({})
|
||||||
const issues = ref([])
|
const issues = ref([])
|
||||||
@ -336,16 +336,6 @@ function normalizeRewards(list) {
|
|||||||
enriched.sort((a, b) => (b.percent - a.percent))
|
enriched.sort((a, b) => (b.percent - a.percent))
|
||||||
return enriched
|
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) {
|
function isFresh(ts) {
|
||||||
const now = Date.now()
|
const now = Date.now()
|
||||||
const v = Number(ts || 0)
|
const v = Number(ts || 0)
|
||||||
@ -391,15 +381,6 @@ async function fetchIssues(id) {
|
|||||||
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) {
|
function pickLatestIssueId(list) {
|
||||||
const arr = Array.isArray(list) ? list : []
|
const arr = Array.isArray(list) ? list : []
|
||||||
let latest = arr[arr.length - 1] && arr[arr.length - 1].id
|
let latest = arr[arr.length - 1] && arr[arr.length - 1].id
|
||||||
@ -501,7 +482,6 @@ onLoad((opts) => {
|
|||||||
activityId.value = id
|
activityId.value = id
|
||||||
fetchDetail(id)
|
fetchDetail(id)
|
||||||
fetchIssues(id)
|
fetchIssues(id)
|
||||||
fetchWinRecords(id)
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@ -131,7 +131,7 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
import { onLoad } from '@dcloudio/uni-app'
|
import { onLoad } from '@dcloudio/uni-app'
|
||||||
import { getOrderDetail, cancelOrder } from '../../api/appUser'
|
import { getOrderDetail, cancelOrder, createWechatOrder } from '../../api/appUser'
|
||||||
|
|
||||||
const orderId = ref('')
|
const orderId = ref('')
|
||||||
const order = ref(null)
|
const order = ref(null)
|
||||||
@ -186,7 +186,40 @@ function handleCancel() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function handlePay() {
|
function handlePay() {
|
||||||
uni.showToast({ title: '支付功能开发中', icon: 'none' })
|
const openid = uni.getStorageSync('openid')
|
||||||
|
if (!openid) {
|
||||||
|
uni.showToast({ title: '缺少OpenID,请重新登录', icon: 'none' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const ord = order.value
|
||||||
|
if (!ord || !ord.order_no) return
|
||||||
|
uni.showLoading({ title: '拉起支付...' })
|
||||||
|
createWechatOrder({ openid, order_no: ord.order_no })
|
||||||
|
.then((payRes) => new Promise((resolve, reject) => {
|
||||||
|
uni.requestPayment({
|
||||||
|
provider: 'wxpay',
|
||||||
|
timeStamp: payRes.timeStamp || payRes.timestamp,
|
||||||
|
nonceStr: payRes.nonceStr || payRes.noncestr,
|
||||||
|
package: payRes.package,
|
||||||
|
signType: payRes.signType || 'MD5',
|
||||||
|
paySign: payRes.paySign,
|
||||||
|
success: resolve,
|
||||||
|
fail: reject
|
||||||
|
})
|
||||||
|
}))
|
||||||
|
.then(async () => {
|
||||||
|
uni.hideLoading()
|
||||||
|
uni.showToast({ title: '支付成功', icon: 'success' })
|
||||||
|
await loadOrder()
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
uni.hideLoading()
|
||||||
|
if (e?.errMsg && String(e.errMsg).includes('cancel')) {
|
||||||
|
uni.showToast({ title: '支付已取消', icon: 'none' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
uni.showToast({ title: e?.message || '支付失败', icon: 'none' })
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function copyText(text) {
|
function copyText(text) {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user