diff --git a/api/appUser.js b/api/appUser.js
index 1e6f17a..e0dd8ba 100644
--- a/api/appUser.js
+++ b/api/appUser.js
@@ -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' })
}
-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) {
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 } })
}
-/**
- * 获取游戏状态 (用于重连)
- * @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 }
})
}
+
+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 }
+ })
+}
diff --git a/pages/activity/duiduipeng/index.vue b/pages/activity/duiduipeng/index.vue
index ed4d5bf..e890df5 100644
--- a/pages/activity/duiduipeng/index.vue
+++ b/pages/activity/duiduipeng/index.vue
@@ -116,14 +116,17 @@
-
+
¥
{{ (Number(detail.price_draw || 0) / 100).toFixed(2) }}
/次
-
+
+ 继续游戏
+
+
立即参与
@@ -131,6 +134,49 @@
+
+
+
+
+
+ 加载中...
+ {{ gameError }}
+
+
+
+ 总对数:{{ totalPairs }} 摸牌机会:{{ chance }}
+
+ 牌组剩余 {{ deckRemaining }}
+ 位置 {{ selectedPositionText }}
+ ID {{ gameIdText }}
+
+
+
+
+ onCellTap(cell)"
+ >
+
+
+ {{ cell.type }}
+
+
+
+
+
+
+
+
+
+
+
@@ -168,7 +214,7 @@
import { ref, computed } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
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 statusText = ref('')
@@ -190,6 +236,91 @@ const cardTypesLoading = ref(false)
const cardTypes = ref([])
const selectedCardTypeCode = ref('')
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 currentIssueRewards = computed(() => {
@@ -252,6 +383,99 @@ function cleanUrl(u) {
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) {
if (typeof v === 'boolean') return v
const s = String(v || '').trim().toLowerCase()
@@ -288,16 +512,6 @@ function normalizeRewards(list) {
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)
@@ -343,15 +557,6 @@ async function fetchIssues(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
@@ -371,6 +576,7 @@ function setSelectedById(id) {
selectedIssueIndex.value = idx
const cur = arr[idx]
currentIssueId.value = (cur && cur.id) || ''
+ syncResumeGame(activityId.value)
}
function prevIssue() {
@@ -379,6 +585,7 @@ function prevIssue() {
const next = Math.max(0, Number(selectedIssueIndex.value || 0) - 1)
selectedIssueIndex.value = next
currentIssueId.value = (arr[next] && arr[next].id) || ''
+ syncResumeGame(activityId.value)
}
function nextIssue() {
const arr = issues.value || []
@@ -386,6 +593,7 @@ function nextIssue() {
const next = Math.min(arr.length - 1, Number(selectedIssueIndex.value || 0) + 1)
selectedIssueIndex.value = next
currentIssueId.value = (arr[next] && arr[next].id) || ''
+ syncResumeGame(activityId.value)
}
function formatPercent(v) {
@@ -406,6 +614,345 @@ function onPreviewBanner() {
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() {
const aid = activityId.value || ''
const iid = currentIssueId.value || ''
@@ -424,6 +971,12 @@ async function onParticipate() {
const openid = uni.getStorageSync('openid')
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()
if (!selectedCardType.value) {
uni.showToast({ title: '请选择卡牌类型', icon: 'none' })
@@ -438,6 +991,20 @@ async function onParticipate() {
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) {
selectedCardTypeCode.value = it && it.code ? String(it.code) : ''
}
@@ -470,7 +1037,7 @@ async function doDraw() {
const openid = uni.getStorageSync('openid')
if (!openid) { uni.showToast({ title: '缺少OpenID,请重新登录', icon: 'none' }); return }
- uni.showLoading({ title: '处理中...' })
+ uni.showLoading({ title: '拉起支付...' })
try {
if (!selectedCardType.value) {
uni.hideLoading()
@@ -478,14 +1045,15 @@ async function doDraw() {
return
}
- const preRes = await createMatchingPreorder({
+ const joinRes = await joinLottery({
+ activity_id: Number(aid),
issue_id: Number(iid),
- position: String(selectedCardType.value.code || ''),
- coupon_id: selectedCoupon.value?.id ? Number(selectedCoupon.value.id) : 0,
- item_card_id: selectedCard.value?.id ? Number(selectedCard.value.id) : 0
+ channel: 'miniapp',
+ count: 1,
+ coupon_id: selectedCoupon.value?.id ? Number(selectedCoupon.value.id) : 0
})
- if (!preRes) throw new Error('预下单失败')
- const orderNo = preRes.order_no || preRes.data?.order_no || preRes.result?.order_no || preRes.orderNo
+ if (!joinRes) throw new Error('下单失败')
+ const orderNo = joinRes.order_no || joinRes.data?.order_no || joinRes.result?.order_no || joinRes.orderNo
if (!orderNo) throw new Error('未获取到订单号')
const payRes = await createWechatOrder({ openid, order_no: orderNo })
@@ -502,20 +1070,31 @@ async function doDraw() {
})
})
- const resultRes = await getLotteryResult(orderNo)
- const raw = resultRes?.list || resultRes?.items || resultRes?.data || resultRes?.result || (Array.isArray(resultRes) ? resultRes : [resultRes])
- const first = Array.isArray(raw) ? raw[0] : raw
- const name = String((first && (first.title || first.name || first.product_name)) || '未知奖励')
- const img = String((first && (first.image || first.img || first.pic || first.product_image)) || '')
+ uni.showLoading({ title: '创建游戏...' })
+ const preRes = await createMatchingPreorder({
+ issue_id: Number(iid),
+ position: String(selectedCardType.value.code || ''),
+ 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.showModal({
- title: '抽选结果',
- content: '恭喜获得:' + name,
- showCancel: false,
- success: () => { if (img) uni.previewImage({ urls: [img], current: img }) }
+ title: '支付成功',
+ content: '已创建对对碰游戏,可点击“继续游戏”继续。',
+ showCancel: false
})
- fetchWinRecords(aid)
} catch (e) {
uni.hideLoading()
if (e?.errMsg && String(e.errMsg).includes('cancel')) {
@@ -571,9 +1150,9 @@ onLoad((opts) => {
const id = (opts && opts.id) || ''
if (id) {
activityId.value = id
+ syncResumeGame(id)
fetchDetail(id)
fetchIssues(id)
- fetchWinRecords(id)
}
fetchCardTypes()
})
@@ -1185,6 +1764,53 @@ onLoad((opts) => {
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 {
display: flex;
@@ -1360,6 +1986,14 @@ onLoad((opts) => {
color: #fff;
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); }
}
@@ -1376,6 +2010,39 @@ onLoad((opts) => {
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 */
.animate-stagger {
animation: fadeInUp 0.5s ease-out backwards;
diff --git a/pages/activity/wuxianshang/index.vue b/pages/activity/wuxianshang/index.vue
index 228c8b2..efcd5eb 100644
--- a/pages/activity/wuxianshang/index.vue
+++ b/pages/activity/wuxianshang/index.vue
@@ -1,42 +1,80 @@
-
-
-
-
-
+
+
+
+
-
-
-
-
-
-
- {{ detail.name || detail.title || '无限赏活动' }}
- ¥{{ (Number(detail.price_draw || 0) / 100).toFixed(2) }}
+
+
+
+
+
+
+
+
-
-
-
-
-
- {{ currentIssueTitle }}
+
+
+
+
+
+ {{ item.boss ? 'BOSS' : '赏' }}
+
+ {{ item.title }}
+
+
-
-
-
-
-
- 聚宝盆
- 随机掉落 10%
- 随机免单 10%
-
-
-
+
+
+
+
+
+ 聚宝盆
+ 随机掉落 10%
+ 随机免单 10%
+
+
+
+
+
+
@@ -57,7 +95,30 @@
抽10发
-
+
+
+
+
+
+
+
+
+
+
+ {{ item.title || '-' }}
+ BOSS
+
+ 概率 {{ formatPercent(item.percent) }}
+
+
+ 暂无奖池数据
+
+
+
+
@@ -84,6 +145,7 @@ import { getActivityDetail, getActivityIssues, getActivityIssueRewards, joinLott
const detail = ref({})
const statusText = ref('')
+const rewardsVisible = ref(false)
const issues = ref([])
const rewardsMap = ref({})
const currentIssueId = ref('')
@@ -92,6 +154,7 @@ const showIssues = computed(() => (detail.value && detail.value.status !== 2))
const activityId = ref('')
const drawLoading = ref(false)
const currentIssueRewards = computed(() => (currentIssueId.value && rewardsMap.value[currentIssueId.value]) ? rewardsMap.value[currentIssueId.value] : [])
+const coverUrl = computed(() => cleanUrl(detail.value && (detail.value.image || detail.value.banner || '')))
const currentIssueTitle = computed(() => {
const arr = issues.value || []
const cur = arr[selectedIssueIndex.value]
@@ -101,6 +164,10 @@ const currentIssueTitle = computed(() => {
const points = ref(0)
const flipRef = ref(null)
const showFlip = ref(false)
+const rewardsForPopup = computed(() => {
+ const arr = currentIssueRewards.value || []
+ return Array.isArray(arr) ? arr : []
+})
const paymentVisible = ref(false)
const paymentAmount = ref('0.00')
const coupons = ref([])
@@ -117,10 +184,37 @@ function statusToText(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) {
const data = await getActivityDetail(id)
detail.value = data || {}
statusText.value = statusToText(detail.value.status)
+ const title = String(detail.value.name || detail.value.title || '无限赏')
+ try { uni.setNavigationBarTitle({ title }) } catch (_) {}
}
function unwrap(list) {
@@ -412,50 +506,45 @@ function closeFlip() { showFlip.value = false }