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 }} + + + + + + + + {{ 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 @@