2295 lines
63 KiB
Vue
2295 lines
63 KiB
Vue
<template>
|
||
<ActivityPageLayout :cover-url="coverUrl">
|
||
<view class="bg-decoration"></view>
|
||
<template #header>
|
||
<ActivityHeader
|
||
:title="detail.name || detail.title || '对对碰'"
|
||
:price="detail.price_draw"
|
||
price-unit="/次"
|
||
:cover-url="coverUrl"
|
||
:tags="['随机玩法', '理性消费']"
|
||
@show-rules="showRules"
|
||
@go-cabinet="goCabinet"
|
||
/>
|
||
|
||
</template>
|
||
|
||
<view class="section-container animate-enter stagger-1">
|
||
<view class="section-header">
|
||
<text class="section-title">卡牌类型</text>
|
||
</view>
|
||
<view v-if="cardTypesLoading" class="card-types-loading">加载中...</view>
|
||
<view v-else-if="cardTypes.length === 0" class="card-types-empty">暂无可选卡牌类型</view>
|
||
<scroll-view v-else class="card-types-scroll" scroll-x="true">
|
||
<view class="card-types-row">
|
||
<view
|
||
v-for="it in cardTypes"
|
||
:key="it.code"
|
||
class="card-type-item"
|
||
:class="{ active: selectedCardTypeCode === it.code }"
|
||
@tap="() => selectCardType(it)"
|
||
>
|
||
<image v-if="it.image_url" class="card-type-img" :src="it.image_url" mode="aspectFill" />
|
||
<view class="card-type-name">{{ it.name }}</view>
|
||
<view v-if="it.quantity !== undefined && it.quantity !== null" class="card-type-qty">×{{ it.quantity }}</view>
|
||
</view>
|
||
</view>
|
||
</scroll-view>
|
||
</view>
|
||
|
||
<!-- 奖池/记录切换 -->
|
||
<ActivityTabs
|
||
v-model="tabActive"
|
||
:tabs="[{key: 'pool', label: '本机奖池'}, {key: 'records', label: '购买记录'}]"
|
||
>
|
||
<!-- 奖池预览 -->
|
||
<RewardsPreview
|
||
v-if="tabActive === 'pool'"
|
||
:rewards="previewRewards"
|
||
:grouped="!['match', 'matching'].includes(detail?.play_type)"
|
||
:play-type="detail?.play_type || 'normal'"
|
||
@view-all="openRewardsPopup"
|
||
/>
|
||
|
||
<!-- 购买记录 -->
|
||
<RecordsList
|
||
v-if="tabActive === 'records'"
|
||
:records="winRecords"
|
||
/>
|
||
</ActivityTabs>
|
||
|
||
<view style="height: 180rpx;"></view>
|
||
|
||
<template #footer>
|
||
<view class="float-bar">
|
||
<view class="float-bar-inner">
|
||
<view class="float-price">
|
||
<text class="currency">¥</text>
|
||
<text class="amount">{{ (Number(detail.price_draw || 0) / 100).toFixed(2) }}</text>
|
||
<text class="unit">/次</text>
|
||
</view>
|
||
<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>
|
||
</view>
|
||
</view>
|
||
</template>
|
||
|
||
<template #modals>
|
||
<!-- 游戏弹窗 - 全屏沉浸式 -->
|
||
<view v-if="gameVisible" class="game-overlay" @touchmove.stop.prevent>
|
||
<view class="game-mask"></view>
|
||
<view class="game-fullscreen" @tap.stop>
|
||
<!-- 顶部标题栏 -->
|
||
<view class="game-topbar">
|
||
<text class="game-topbar-title">对对碰游戏</text>
|
||
<view class="game-close-btn" @tap="closeGame">
|
||
<text>×</text>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 游戏信息 -->
|
||
<view class="game-stats glass-card">
|
||
<view class="stat-item">
|
||
<text class="stat-label">总对数</text>
|
||
<text class="stat-value">{{ totalPairs }}</text>
|
||
</view>
|
||
<view class="stat-item">
|
||
<text class="stat-label">摸牌机会</text>
|
||
<text class="stat-value">{{ chance }}</text>
|
||
</view>
|
||
<view class="stat-item">
|
||
<text class="stat-label">牌组剩余</text>
|
||
<text class="stat-value">{{ deckRemaining }}</text>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 游戏区域 -->
|
||
<view class="game-content">
|
||
<view v-if="gameLoading" class="game-loading">
|
||
<text class="loading-icon">⏳</text>
|
||
<text class="loading-text">加载中...</text>
|
||
</view>
|
||
<view v-else-if="gameError" class="game-error">
|
||
<text class="error-icon">⚠️</text>
|
||
<text class="error-text">{{ gameError }}</text>
|
||
</view>
|
||
<view v-else class="game-board glass-card">
|
||
<view class="match-grid-fullscreen">
|
||
<view
|
||
v-for="(cell, idx) in handGridCells"
|
||
:key="idx"
|
||
class="match-cell-large"
|
||
:class="{ empty: cell.empty, chosen: cell.isChosen, picked: cell.isPicked }"
|
||
@tap="() => onCellTap(cell)"
|
||
>
|
||
<image v-if="cell.image" class="match-cell-img-large" :src="cell.image" mode="aspectFill" />
|
||
<view v-else class="match-cell-img-large"></view>
|
||
<text v-if="!cell.empty && cell.type" class="match-cell-type-large">{{ cell.type }}</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 底部操作栏 -->
|
||
<view class="game-actions">
|
||
<button class="game-btn btn-secondary" @tap="manualDraw" :disabled="gameLoading || !canManualDraw">
|
||
<text>摸牌</text>
|
||
</button>
|
||
<button class="game-btn btn-primary" @tap="advanceOne" :disabled="gameLoading">
|
||
<text>下一步</text>
|
||
</button>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 奖品详情弹窗 -->
|
||
<RewardsPopup
|
||
v-model:visible="rewardsVisible"
|
||
:title="`${detail.name || '奖池'} · 奖品与概率`"
|
||
:reward-groups="rewardGroups"
|
||
/>
|
||
|
||
<PaymentPopup
|
||
v-model:visible="paymentVisible"
|
||
:amount="paymentAmount"
|
||
:coupons="coupons"
|
||
:propCards="propCards"
|
||
:showCards="true"
|
||
@confirm="onPaymentConfirm"
|
||
/>
|
||
<RulesPopup
|
||
v-model:visible="rulesVisible"
|
||
:content="detail.gameplay_intro"
|
||
/>
|
||
|
||
<CabinetPreviewPopup
|
||
v-model:visible="cabinetVisible"
|
||
:activity-id="activityId"
|
||
/>
|
||
|
||
<!-- 开奖结果弹窗 -->
|
||
<LotteryResultPopup
|
||
v-model:visible="resultVisible"
|
||
:results="resultItems"
|
||
@close="onResultClose"
|
||
/>
|
||
</template>
|
||
</ActivityPageLayout>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { ref, computed } from 'vue'
|
||
import { onLoad } from '@dcloudio/uni-app'
|
||
import PaymentPopup from '../../../components/PaymentPopup.vue'
|
||
import ActivityPageLayout from '@/components/activity/ActivityPageLayout.vue'
|
||
import ActivityHeader from '@/components/activity/ActivityHeader.vue'
|
||
import ActivityTabs from '@/components/activity/ActivityTabs.vue'
|
||
import RewardsPreview from '@/components/activity/RewardsPreview.vue'
|
||
import RewardsPopup from '@/components/activity/RewardsPopup.vue'
|
||
import RecordsList from '@/components/activity/RecordsList.vue'
|
||
import RulesPopup from '@/components/activity/RulesPopup.vue'
|
||
import CabinetPreviewPopup from '@/components/activity/CabinetPreviewPopup.vue'
|
||
import LotteryResultPopup from '@/components/activity/LotteryResultPopup.vue'
|
||
import { getActivityDetail, getActivityIssues, getActivityIssueRewards, getUserCoupons, getItemCards, createWechatOrder, getMatchingCardTypes, createMatchingPreorder, checkMatchingGame, getIssueDrawLogs, getMatchingGameCards } from '../../../api/appUser'
|
||
import { levelToAlpha } from '@/utils/activity'
|
||
import { vibrateShort } from '@/utils/vibrate.js'
|
||
|
||
const detail = ref({})
|
||
const statusText = ref('')
|
||
const issues = ref([])
|
||
const rewardsMap = ref({})
|
||
const currentIssueId = ref('')
|
||
const selectedIssueIndex = ref(0)
|
||
const showIssues = computed(() => (detail.value && detail.value.status !== 2))
|
||
const activityId = ref('')
|
||
const tabActive = ref('pool')
|
||
const winRecords = ref([])
|
||
const paymentVisible = ref(false)
|
||
const coupons = ref([])
|
||
const propCards = ref([])
|
||
const selectedCoupon = ref(null)
|
||
const selectedCard = ref(null)
|
||
const paymentAmount = computed(() => (((Number(detail.value.price_draw || 0) / 100) || 0)).toFixed(2))
|
||
const cardTypesLoading = ref(false)
|
||
const cardTypes = ref([])
|
||
const selectedCardTypeCode = ref('')
|
||
const rewardsVisible = ref(false)
|
||
const rulesVisible = ref(false)
|
||
const cabinetVisible = ref(false)
|
||
const resultVisible = ref(false)
|
||
const resultItems = ref([])
|
||
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(() => {
|
||
const iid = currentIssueId.value || ''
|
||
const m = rewardsMap.value || {}
|
||
return (iid && Array.isArray(m[iid])) ? m[iid] : []
|
||
})
|
||
|
||
// 用于奖池预览的 rewards(已排序)
|
||
const previewRewards = computed(() => {
|
||
const isMatchType = ['match', 'matching'].includes(detail.value?.play_type)
|
||
|
||
if (isMatchType) {
|
||
// 对对碰模式:按 min_score 升序
|
||
const sorted = [...currentIssueRewards.value].sort((a, b) => (a.min_score - b.min_score))
|
||
console.log('[对对碰] previewRewards 排序后:', sorted.map(r => ({
|
||
name: r.title,
|
||
min_score: r.min_score,
|
||
weight: r.weight,
|
||
percent: r.percent
|
||
})))
|
||
return sorted
|
||
} else {
|
||
// 普通模式:返回原数组
|
||
return currentIssueRewards.value
|
||
}
|
||
})
|
||
|
||
const rewardGroups = computed(() => {
|
||
const isMatchType = ['match', 'matching'].includes(detail.value?.play_type)
|
||
|
||
// 对对碰模式:不分组,直接按 min_score 平铺所有奖品
|
||
if (isMatchType) {
|
||
// 先按 min_score 升序排序
|
||
const sortedRewards = [...currentIssueRewards.value].sort((a, b) => (a.min_score - b.min_score))
|
||
|
||
// 将每个奖品作为一个单独的分组
|
||
const groups = sortedRewards.map(item => ({
|
||
level: `${item.min_score}对子`,
|
||
rewards: [item],
|
||
totalPercent: item.percent.toFixed(1)
|
||
}))
|
||
|
||
console.log('[对对碰] rewardGroups 分组后:', groups.map(g => ({
|
||
level: g.level,
|
||
count: g.rewards.length,
|
||
totalPercent: g.totalPercent,
|
||
firstItem: g.rewards[0]?.title
|
||
})))
|
||
|
||
return groups
|
||
}
|
||
|
||
// 普通模式:按原来的分组逻辑
|
||
const groups = {}
|
||
currentIssueRewards.value.forEach(item => {
|
||
let level = item.level || '赏'
|
||
|
||
// 普通模式:只显示 min_score > 0 的奖品
|
||
if (item.min_score > 0 && level !== 'BOSS') {
|
||
level = `${item.min_score}对子`
|
||
}
|
||
|
||
if (!groups[level]) groups[level] = []
|
||
groups[level].push(item)
|
||
})
|
||
|
||
return Object.keys(groups).sort((a, b) => {
|
||
// 普通模式:Last 和 BOSS 优先
|
||
if (a === 'Last' || a === 'BOSS') return -1
|
||
if (b === 'Last' || b === 'BOSS') return 1
|
||
|
||
// 普通模式:分组之间按该组最小 weight 排序(升序)
|
||
const minWeightA = Math.min(...groups[a].map(item => item.weight || 0))
|
||
const minWeightB = Math.min(...groups[b].map(item => item.weight || 0))
|
||
return minWeightA - minWeightB
|
||
}).map(key => {
|
||
const rewards = groups[key]
|
||
|
||
// 普通模式:分组内按 weight 升序排列
|
||
rewards.sort((a, b) => (a.weight - b.weight))
|
||
|
||
const total = rewards.reduce((sum, item) => sum + (Number(item.percent) || 0), 0)
|
||
return {
|
||
level: key,
|
||
rewards: rewards,
|
||
totalPercent: total.toFixed(1)
|
||
}
|
||
})
|
||
})
|
||
const currentIssueTitle = computed(() => {
|
||
const arr = issues.value || []
|
||
const cur = arr[selectedIssueIndex.value]
|
||
// 对对碰不显示“期数”,优先显示标题,兜底显示“奖池”
|
||
return (cur && (cur.title || '奖池')) || '-'
|
||
})
|
||
const currentIssueStatusText = computed(() => {
|
||
const arr = issues.value || []
|
||
const cur = arr[selectedIssueIndex.value]
|
||
return (cur && (cur.status_text || '')) || ''
|
||
})
|
||
|
||
const selectedCardType = computed(() => {
|
||
const code = String(selectedCardTypeCode.value || '')
|
||
if (!code) return null
|
||
return (cardTypes.value || []).find(i => i && i.code === code) || null
|
||
})
|
||
|
||
function statusToText(s) {
|
||
if (s === 1) return '进行中'
|
||
if (s === 0) return '未开始'
|
||
if (s === 2) return '已结束'
|
||
return String(s || '')
|
||
}
|
||
|
||
async function fetchDetail(id) {
|
||
const data = await getActivityDetail(id)
|
||
detail.value = data || {}
|
||
statusText.value = statusToText(detail.value.status)
|
||
}
|
||
|
||
function unwrap(list) {
|
||
if (Array.isArray(list)) return list
|
||
const obj = list || {}
|
||
const data = obj.data || {}
|
||
const arr = obj.list || obj.items || data.list || data.items || data
|
||
return Array.isArray(arr) ? arr : []
|
||
}
|
||
|
||
function normalizeCardTypes(list) {
|
||
const arr = unwrap(list)
|
||
return arr.map((i, idx) => ({
|
||
code: String(i.code ?? i.type_code ?? i.card_type_code ?? i.id ?? String(idx)),
|
||
name: i.name ?? i.title ?? i.label ?? '卡牌类型',
|
||
image_url: cleanUrl(i.image_url ?? i.image ?? i.img ?? i.pic ?? ''),
|
||
quantity: i.quantity !== undefined ? Number(i.quantity) : undefined
|
||
}))
|
||
}
|
||
|
||
function cleanUrl(u) {
|
||
const s = String(u || '').trim()
|
||
const m = s.match(/https?:\/\/[^\s'"`]+/)
|
||
if (m && m[0]) return m[0]
|
||
return s.replace(/[`'\"]/g, '').trim()
|
||
}
|
||
|
||
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()
|
||
if (!s) return false
|
||
return s === '1' || s === 'true' || s === 'yes' || s === 'y' || s === '是' || s === 'boss是真的' || s === 'boss' || s === '大boss'
|
||
}
|
||
function detectBoss(i) {
|
||
return truthy(i.is_boss) || truthy(i.boss) || truthy(i.isBoss) || truthy(i.boss_true) || truthy(i.boss_is_true) || truthy(i.bossText) || truthy(i.tag)
|
||
}
|
||
|
||
function normalizeIssues(list) {
|
||
const arr = unwrap(list)
|
||
return arr.map((i, idx) => ({
|
||
id: i.id ?? String(idx),
|
||
title: i.title ?? i.name ?? '',
|
||
no: i.no ?? i.index ?? i.issue_no ?? i.issue_number ?? null,
|
||
status_text: i.status_text ?? (i.status === 1 ? '进行中' : i.status === 0 ? '未开始' : i.status === 2 ? '已结束' : '')
|
||
}))
|
||
}
|
||
function normalizeRewards(list, playType = 'normal') {
|
||
const arr = unwrap(list)
|
||
const items = arr.map((i, idx) => ({
|
||
...i, // Spread original properties first
|
||
id: i.product_id ?? i.id ?? String(idx),
|
||
title: i.name ?? i.title ?? '',
|
||
image: cleanUrl(i.product_image ?? i.image ?? i.img ?? i.pic ?? i.banner ?? ''),
|
||
weight: Number(i.weight) || 0,
|
||
boss: detectBoss(i),
|
||
min_score: Number(i.min_score) || 0, // Extract min_score
|
||
level: levelToAlpha(i.prize_level ?? i.level ?? (detectBoss(i) ? 'BOSS' : '赏'))
|
||
}))
|
||
const total = items.reduce((acc, it) => acc + (it.weight > 0 ? it.weight : 0), 0)
|
||
const enriched = items.map(it => ({
|
||
...it,
|
||
percent: total > 0 ? Math.round((it.weight / total) * 1000) / 10 : 0
|
||
}))
|
||
|
||
// 根据 play_type 决定排序方式
|
||
if (['match', 'matching'].includes(playType)) {
|
||
// 对对碰:按 min_score 升序排列,不过滤 min_score=0 的奖品
|
||
enriched.sort((a, b) => (a.min_score - b.min_score))
|
||
} else {
|
||
// 普通活动:按 weight 升序排列(从小到大)
|
||
enriched.sort((a, b) => (a.weight - b.weight))
|
||
}
|
||
|
||
return enriched
|
||
}
|
||
async function fetchRewardsForIssues(activityId) {
|
||
const list = issues.value || []
|
||
const promises = list.map(it => getActivityIssueRewards(activityId, it.id))
|
||
const results = await Promise.allSettled(promises)
|
||
|
||
// 获取 play_type
|
||
const playType = detail.value?.play_type || 'normal'
|
||
|
||
results.forEach((res, i) => {
|
||
const issueId = list[i] && list[i].id
|
||
if (!issueId) return
|
||
const value = res.status === 'fulfilled' ? normalizeRewards(res.value, playType) : []
|
||
rewardsMap.value = { ...(rewardsMap.value || {}), [issueId]: value }
|
||
})
|
||
}
|
||
|
||
async function fetchIssues(id) {
|
||
const data = await getActivityIssues(id)
|
||
issues.value = normalizeIssues(data)
|
||
const latestId = pickLatestIssueId(issues.value)
|
||
setSelectedById(latestId)
|
||
await fetchRewardsForIssues(id)
|
||
// 获取购买记录
|
||
if (currentIssueId.value) {
|
||
fetchWinRecords(id, currentIssueId.value)
|
||
}
|
||
}
|
||
|
||
function pickLatestIssueId(list) {
|
||
const arr = Array.isArray(list) ? list : []
|
||
let latest = arr[arr.length - 1] && arr[arr.length - 1].id
|
||
let maxNo = -Infinity
|
||
arr.forEach(i => {
|
||
const n = Number(i.no)
|
||
if (!Number.isNaN(n) && Number.isFinite(n) && n > maxNo) {
|
||
maxNo = n
|
||
latest = i.id
|
||
}
|
||
})
|
||
return latest || (arr[0] && arr[0].id) || ''
|
||
}
|
||
function setSelectedById(id) {
|
||
const arr = issues.value || []
|
||
const idx = Math.max(0, arr.findIndex(x => x && x.id === id))
|
||
selectedIssueIndex.value = idx
|
||
const cur = arr[idx]
|
||
currentIssueId.value = (cur && cur.id) || ''
|
||
syncResumeGame(activityId.value)
|
||
}
|
||
|
||
function prevIssue() {
|
||
const arr = issues.value || []
|
||
if (!arr.length) return
|
||
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 || []
|
||
if (!arr.length) return
|
||
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)
|
||
}
|
||
|
||
async function fetchWinRecords(actId, issId) {
|
||
if (!actId || !issId) return
|
||
try {
|
||
const res = await getIssueDrawLogs(actId, issId)
|
||
const list = (res && res.list) || (Array.isArray(res) ? res : [])
|
||
// 聚合同一奖品的记录
|
||
const aggregate = {}
|
||
list.forEach(it => {
|
||
const key = it.reward_id || it.id
|
||
if (!aggregate[key]) {
|
||
aggregate[key] = {
|
||
id: key,
|
||
title: it.reward_name || it.title || it.name || '-',
|
||
image: it.reward_image || it.image || '',
|
||
count: 0
|
||
}
|
||
}
|
||
aggregate[key].count += 1
|
||
})
|
||
const total = list.length || 1
|
||
winRecords.value = Object.values(aggregate).map(it => ({
|
||
...it,
|
||
percent: ((it.count / total) * 100).toFixed(1)
|
||
}))
|
||
} catch (e) {
|
||
console.error('fetchWinRecords error', e)
|
||
winRecords.value = []
|
||
}
|
||
}
|
||
|
||
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() {
|
||
rulesVisible.value = true
|
||
}
|
||
|
||
function goCabinet() {
|
||
cabinetVisible.value = true
|
||
}
|
||
|
||
function onPreviewBanner() {
|
||
const url = detail.value.banner || ''
|
||
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
|
||
vibrateShort()
|
||
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
|
||
vibrateShort()
|
||
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
|
||
|
||
try {
|
||
const checkRes = await checkMatchingGame(gameId, Number(totalPairs.value || 0))
|
||
clearMatchingGameCacheEntry(aid, issueId)
|
||
gameFinished.value = true
|
||
closeGame()
|
||
|
||
console.log('[对对碰] checkRes:', JSON.stringify(checkRes))
|
||
console.log('[对对碰] currentIssueRewards:', currentIssueRewards.value?.length, 'items')
|
||
console.log('[对对碰] rewardsMap keys:', Object.keys(rewardsMap.value || {}))
|
||
|
||
// 解析中奖结果 - 后端返回格式是 { reward: { reward_id, name, level } }
|
||
let wonItems = []
|
||
|
||
// 后端返回单个 reward 对象的情况
|
||
if (checkRes?.reward && checkRes.reward.reward_id) {
|
||
const reward = checkRes.reward
|
||
console.log('[对对碰] 检测到reward对象:', reward)
|
||
// 从本地 rewardsMap 查找图片
|
||
const allRewards = rewardsMap.value[issueId] || currentIssueRewards.value || []
|
||
console.log('[对对碰] 本地奖励数据:', allRewards.length, 'items')
|
||
const foundReward = allRewards.find(r =>
|
||
String(r.id) === String(reward.reward_id) ||
|
||
String(r.reward_id) === String(reward.reward_id)
|
||
)
|
||
console.log('[对对碰] 匹配到的奖励:', foundReward)
|
||
wonItems = [{
|
||
title: reward.name || foundReward?.name || foundReward?.title || '神秘奖励',
|
||
image: foundReward?.image || foundReward?.pic || foundReward?.img || foundReward?.product_image || '',
|
||
reward_id: reward.reward_id
|
||
}]
|
||
}
|
||
// 处理数组格式(兼容)
|
||
else if (Array.isArray(checkRes) && checkRes.length > 0) {
|
||
wonItems = checkRes
|
||
} else if (checkRes?.list && checkRes.list.length > 0) {
|
||
wonItems = checkRes.list
|
||
} else if (checkRes?.data && Array.isArray(checkRes.data) && checkRes.data.length > 0) {
|
||
wonItems = checkRes.data
|
||
} else if (checkRes?.rewards && checkRes.rewards.length > 0) {
|
||
wonItems = checkRes.rewards
|
||
} else if (checkRes?.items && checkRes.items.length > 0) {
|
||
wonItems = checkRes.items
|
||
}
|
||
|
||
console.log('[对对碰] wonItems:', wonItems)
|
||
|
||
// 转换为 LotteryResultPopup 需要的格式
|
||
if (wonItems.length > 0) {
|
||
const allRewards = rewardsMap.value[issueId] || currentIssueRewards.value || []
|
||
resultItems.value = wonItems.map(item => {
|
||
// 如果已经有 title,直接使用
|
||
if (item.title) return item
|
||
// 否则尝试从本地数据匹配
|
||
const found = allRewards.find(r =>
|
||
String(r.id) === String(item.reward_id) ||
|
||
String(r.reward_id) === String(item.reward_id)
|
||
)
|
||
return {
|
||
title: item.title || item.name || found?.name || found?.title || '神秘奖励',
|
||
image: cleanUrl(item.image || item.img || found?.image || found?.pic || found?.product_image || ''),
|
||
reward_id: item.reward_id || item.id
|
||
}
|
||
})
|
||
} else {
|
||
resultItems.value = []
|
||
}
|
||
|
||
console.log('[对对碰] resultItems:', resultItems.value)
|
||
|
||
// 显示结果弹窗
|
||
if (resultItems.value.length > 0) {
|
||
resultVisible.value = true
|
||
} else {
|
||
// 没有中奖物品时显示简单提示
|
||
uni.showModal({
|
||
title: '游戏结束',
|
||
content: `总对数:${Number(totalPairs.value || 0)}`,
|
||
showCancel: false
|
||
})
|
||
}
|
||
} catch (e) {
|
||
console.error('finishAndReport error', e)
|
||
uni.showToast({ title: e?.message || '结算失败', icon: 'none' })
|
||
}
|
||
}
|
||
|
||
function onResultClose() {
|
||
resultVisible.value = false
|
||
resultItems.value = []
|
||
}
|
||
|
||
async function advanceOne() {
|
||
if (gameLoading.value) return
|
||
vibrateShort()
|
||
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() {
|
||
vibrateShort()
|
||
const aid = activityId.value || ''
|
||
const iid = currentIssueId.value || ''
|
||
if (!aid || !iid) { uni.showToast({ title: '期数未选择', icon: 'none' }); return }
|
||
const token = uni.getStorageSync('token')
|
||
const phoneBound = !!uni.getStorageSync('phone_bound')
|
||
if (!token || !phoneBound) {
|
||
uni.showModal({
|
||
title: '提示',
|
||
content: '请先登录并绑定手机号',
|
||
confirmText: '去登录',
|
||
success: (res) => { if (res.confirm) uni.navigateTo({ url: '/pages/login/index' }) }
|
||
})
|
||
return
|
||
}
|
||
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' })
|
||
return
|
||
}
|
||
coupons.value = []
|
||
propCards.value = []
|
||
selectedCoupon.value = null
|
||
selectedCard.value = null
|
||
paymentVisible.value = true
|
||
fetchCoupons()
|
||
fetchPropCards()
|
||
}
|
||
|
||
async function applyResumeEntry(entry) {
|
||
if (!entry) return
|
||
await fetchCardTypes()
|
||
const pos = String(entry.position || '')
|
||
if (pos) selectedCardTypeCode.value = pos
|
||
}
|
||
|
||
async function onResumeGame() {
|
||
vibrateShort()
|
||
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) : ''
|
||
}
|
||
|
||
async function fetchCardTypes() {
|
||
if (cardTypesLoading.value) return
|
||
cardTypesLoading.value = true
|
||
try {
|
||
const res = await getMatchingCardTypes()
|
||
cardTypes.value = normalizeCardTypes(res)
|
||
} catch (e) {
|
||
cardTypes.value = []
|
||
selectedCardTypeCode.value = ''
|
||
} finally {
|
||
cardTypesLoading.value = false
|
||
}
|
||
}
|
||
|
||
async function onPaymentConfirm(data) {
|
||
selectedCoupon.value = data?.coupon || null
|
||
selectedCard.value = data?.card || null
|
||
paymentVisible.value = false
|
||
await doDraw()
|
||
}
|
||
|
||
async function doDraw() {
|
||
const aid = activityId.value || ''
|
||
const iid = currentIssueId.value || ''
|
||
if (!aid || !iid) return
|
||
const openid = uni.getStorageSync('openid')
|
||
if (!openid) { uni.showToast({ title: '缺少OpenID,请重新登录', icon: 'none' }); return }
|
||
|
||
uni.showLoading({ title: '创建订单...' })
|
||
try {
|
||
if (!selectedCardType.value) {
|
||
uni.hideLoading()
|
||
uni.showToast({ title: '请选择卡牌类型', icon: 'none' })
|
||
return
|
||
}
|
||
|
||
// 1. 调用 createMatchingPreorder 创建对对碰订单(不再返回游戏数据)
|
||
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('创建订单失败')
|
||
|
||
// 2. 提取订单号和游戏ID(注意:all_cards 不再在这里返回)
|
||
const orderNo = preRes.order_no || preRes.data?.order_no || preRes.result?.order_no || preRes.orderNo
|
||
if (!orderNo) throw new Error('未获取到订单号')
|
||
|
||
const gameId = preRes.game_id || preRes.data?.game_id || preRes.result?.game_id || preRes.gameId
|
||
if (!gameId) throw new Error('未获取到游戏ID')
|
||
|
||
// 3. 用对对碰订单号调用微信支付
|
||
uni.showLoading({ title: '拉起支付...' })
|
||
const payRes = await createWechatOrder({ openid, order_no: orderNo })
|
||
await new Promise((resolve, reject) => {
|
||
uni.requestPayment({
|
||
provider: 'wxpay',
|
||
timeStamp: payRes.timeStamp || payRes.timestamp,
|
||
nonceStr: payRes.nonceStr || payRes.noncestr,
|
||
package: payRes.package,
|
||
signType: payRes.signType || 'RSA',
|
||
paySign: payRes.paySign,
|
||
success: resolve,
|
||
fail: reject
|
||
})
|
||
})
|
||
|
||
// 4. 【关键】支付成功后,调用新接口获取游戏数据
|
||
uni.showLoading({ title: '加载游戏...' })
|
||
const cardsRes = await getMatchingGameCards(gameId)
|
||
if (!cardsRes) throw new Error('获取游戏数据失败')
|
||
|
||
const allCards = normalizeAllCards(cardsRes.all_cards || cardsRes.data?.all_cards || [])
|
||
if (!allCards.length) throw new Error('游戏数据为空')
|
||
|
||
// 5. 保存游戏数据到本地缓存
|
||
writeMatchingGameCacheEntry(aid, iid, {
|
||
game_id: String(gameId),
|
||
position: String(selectedCardType.value.code || ''),
|
||
all_cards: allCards,
|
||
ts: Date.now()
|
||
})
|
||
|
||
uni.hideLoading()
|
||
uni.showToast({ title: '支付成功', icon: 'success' })
|
||
|
||
// 6. 自动打开游戏
|
||
syncResumeGame(aid)
|
||
const latest = findLatestMatchingGameCacheEntry(aid)
|
||
if (latest && latest.entry && latest.entry.game_id) {
|
||
await openGame(latest)
|
||
}
|
||
} 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' })
|
||
}
|
||
}
|
||
|
||
async function fetchCoupons() {
|
||
const user_id = uni.getStorageSync('user_id')
|
||
if (!user_id) return
|
||
try {
|
||
const res = await getUserCoupons(user_id, 0, 1, 100)
|
||
let list = []
|
||
if (Array.isArray(res)) list = res
|
||
else if (res && Array.isArray(res.list)) list = res.list
|
||
else if (res && Array.isArray(res.data)) list = res.data
|
||
coupons.value = list.map((i, idx) => {
|
||
const cents = (i.remaining !== undefined && i.remaining !== null) ? Number(i.remaining) : Number(i.amount ?? i.value ?? 0)
|
||
const yuan = isNaN(cents) ? 0 : (cents / 100)
|
||
return {
|
||
id: i.id ?? i.coupon_id ?? String(idx),
|
||
name: i.name ?? i.title ?? '优惠券',
|
||
amount: Number(yuan).toFixed(2)
|
||
}
|
||
})
|
||
} catch (e) {
|
||
coupons.value = []
|
||
}
|
||
}
|
||
|
||
async function fetchPropCards() {
|
||
const user_id = uni.getStorageSync('user_id')
|
||
if (!user_id) return
|
||
try {
|
||
// Status 1 = Unused
|
||
const res = await getItemCards(user_id, 1)
|
||
let list = []
|
||
if (Array.isArray(res)) list = res
|
||
else if (res && Array.isArray(res.list)) list = res.list
|
||
else if (res && Array.isArray(res.data)) list = res.data
|
||
// Group identical cards by name
|
||
const groupedMap = new Map()
|
||
list.forEach((i, idx) => {
|
||
const name = i.name ?? i.title ?? i.card_name ?? '道具卡'
|
||
if (!groupedMap.has(name)) {
|
||
groupedMap.set(name, {
|
||
id: i.id ?? i.card_id ?? i.item_card_id ?? String(idx),
|
||
name: name,
|
||
count: 0
|
||
})
|
||
}
|
||
// If the API returns a 'count' or 'remaining', use it. Otherwise assume 1.
|
||
const inc = (i.count !== undefined && i.count !== null) ? Number(i.count) : ((i.remaining !== undefined && i.remaining !== null) ? Number(i.remaining) : 1)
|
||
groupedMap.get(name).count += inc
|
||
})
|
||
|
||
propCards.value = Array.from(groupedMap.values()).map(item => ({
|
||
id: item.id,
|
||
name: item.name, // PaymentPopup will handle the " (xN)" display if we pass it correctly.
|
||
// Wait, PaymentPopup.vue expects 'name' to be the display string?
|
||
// Let's check PaymentPopup.vue again.
|
||
// It shows {{ selectedCoupon.name }} (-¥...).
|
||
// For cards: {{ selectedCard.name }}.
|
||
// So I should format the name here OR update PaymentPopup to show count.
|
||
// The plan said "Update PaymentPopup text if needed (e.g. show count)".
|
||
// I will format it here for consistency if PaymentPopup is generic,
|
||
// BUT updating PaymentPopup to show "Name (xCount)" is cleaner.
|
||
// For now, I'll pass 'count' property.
|
||
count: item.count
|
||
}))
|
||
} catch (e) {
|
||
propCards.value = []
|
||
}
|
||
}
|
||
|
||
onLoad((opts) => {
|
||
const id = (opts && opts.id) || ''
|
||
if (id) {
|
||
activityId.value = id
|
||
syncResumeGame(id)
|
||
fetchDetail(id)
|
||
fetchIssues(id)
|
||
}
|
||
fetchCardTypes()
|
||
})
|
||
</script>
|
||
|
||
<style lang="scss" scoped>
|
||
/* ============================================
|
||
对对碰活动页面 - 高级设计重构 (SCSS Integration)
|
||
============================================ */
|
||
|
||
.page-wrapper {
|
||
min-height: 100vh;
|
||
background: $bg-page;
|
||
position: relative;
|
||
overflow: hidden;
|
||
}
|
||
|
||
/* 背景装饰 */
|
||
.bg-decoration {
|
||
position: absolute;
|
||
top: 0; left: 0; width: 100%; height: 100%;
|
||
z-index: 0;
|
||
pointer-events: none;
|
||
overflow: hidden;
|
||
}
|
||
.orb {
|
||
position: absolute;
|
||
border-radius: 50%;
|
||
filter: blur(80rpx);
|
||
opacity: 0.6;
|
||
}
|
||
.orb-1 {
|
||
width: 500rpx; height: 500rpx;
|
||
background: radial-gradient(circle, rgba($brand-primary, 0.2) 0%, transparent 70%);
|
||
top: -100rpx; left: -100rpx;
|
||
}
|
||
.orb-2 {
|
||
width: 600rpx; height: 600rpx;
|
||
background: radial-gradient(circle, rgba($accent-gold, 0.15) 0%, transparent 70%);
|
||
bottom: -100rpx; right: -100rpx;
|
||
}
|
||
|
||
.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;
|
||
|
||
&::before {
|
||
content: '';
|
||
position: absolute;
|
||
top: 0;
|
||
left: 0;
|
||
right: 0;
|
||
height: 2rpx;
|
||
background: linear-gradient(90deg, transparent, rgba(255,255,255,0.8), transparent);
|
||
}
|
||
}
|
||
.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;
|
||
min-width: 0;
|
||
display: flex;
|
||
flex-direction: column;
|
||
justify-content: center;
|
||
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: 28rpx;
|
||
margin-left: 16rpx;
|
||
padding-left: 24rpx;
|
||
border-left: 2rpx solid #E8E8E8;
|
||
justify-content: center;
|
||
align-self: stretch;
|
||
}
|
||
.action-btn {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
transition: all 0.2s;
|
||
&:active {
|
||
transform: scale(0.92);
|
||
opacity: 0.8;
|
||
}
|
||
}
|
||
.action-icon {
|
||
width: 44rpx;
|
||
height: 44rpx;
|
||
margin-bottom: 8rpx;
|
||
background-size: contain;
|
||
background-repeat: no-repeat;
|
||
}
|
||
/* Banner */
|
||
.banner-wrapper {
|
||
margin: $spacing-md $spacing-lg;
|
||
border-radius: $radius-lg;
|
||
overflow: hidden;
|
||
box-shadow: $shadow-lg;
|
||
position: relative;
|
||
animation: fadeInDown 0.6s ease-out;
|
||
}
|
||
.banner-img {
|
||
width: 100%;
|
||
display: block;
|
||
}
|
||
.banner-shadow {
|
||
position: absolute;
|
||
bottom: 0; left: 0; width: 100%; height: 40%;
|
||
background: linear-gradient(to top, rgba(0,0,0,0.3), transparent);
|
||
}
|
||
|
||
.card-types {
|
||
padding: 0 $spacing-lg;
|
||
margin-bottom: $spacing-lg;
|
||
}
|
||
.card-types-title {
|
||
font-size: $font-md;
|
||
font-weight: 700;
|
||
color: $text-main;
|
||
margin-bottom: $spacing-sm;
|
||
}
|
||
.card-types-loading,
|
||
.card-types-empty {
|
||
font-size: $font-sm;
|
||
color: $text-sub;
|
||
padding: $spacing-md 0;
|
||
}
|
||
.card-types-scroll {
|
||
width: 100%;
|
||
}
|
||
.card-types-row {
|
||
display: flex;
|
||
gap: $spacing-md;
|
||
padding-bottom: $spacing-xs;
|
||
}
|
||
.card-type-item {
|
||
width: 160rpx;
|
||
flex: 0 0 auto;
|
||
background: rgba($bg-card, 0.9);
|
||
border-radius: $radius-lg;
|
||
padding: $spacing-sm;
|
||
border: 2rpx solid rgba(0,0,0,0.05);
|
||
box-shadow: $shadow-sm;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: flex-start;
|
||
position: relative;
|
||
}
|
||
.card-type-item.active {
|
||
border-color: rgba($brand-primary, 0.6);
|
||
box-shadow: 0 8rpx 20rpx rgba($brand-primary, 0.18);
|
||
}
|
||
.card-type-img {
|
||
width: 120rpx;
|
||
height: 120rpx;
|
||
border-radius: $radius-md;
|
||
background: $bg-secondary;
|
||
}
|
||
.card-type-name {
|
||
margin-top: $spacing-sm;
|
||
font-size: $font-sm;
|
||
font-weight: 700;
|
||
color: $text-main;
|
||
text-align: center;
|
||
width: 100%;
|
||
white-space: nowrap;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
}
|
||
.card-type-qty {
|
||
margin-top: 6rpx;
|
||
font-size: 22rpx;
|
||
color: $text-sub;
|
||
}
|
||
|
||
/* Game Modal Styles */
|
||
.game-scroll-list {
|
||
max-height: 60vh;
|
||
padding: $spacing-lg;
|
||
}
|
||
.game-empty-state {
|
||
padding: 60rpx 0 20rpx;
|
||
text-align: center;
|
||
color: $text-tertiary;
|
||
font-size: $font-sm;
|
||
}
|
||
.game-info-card {
|
||
display: flex;
|
||
background: #FFFFFF;
|
||
padding: $spacing-lg;
|
||
border-radius: $radius-lg;
|
||
box-shadow: $shadow-sm;
|
||
align-items: center;
|
||
margin-bottom: $spacing-lg;
|
||
}
|
||
.game-info-content {
|
||
flex: 1;
|
||
}
|
||
.game-info-title {
|
||
font-size: $font-md;
|
||
font-weight: 600;
|
||
color: $text-main;
|
||
margin-bottom: $spacing-xs;
|
||
}
|
||
.game-info-meta {
|
||
display: flex;
|
||
gap: $spacing-md;
|
||
font-size: $font-sm;
|
||
color: $text-sub;
|
||
}
|
||
|
||
.section-container {
|
||
padding: 0 $spacing-lg;
|
||
margin-bottom: $spacing-xl;
|
||
}
|
||
|
||
.section-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: $spacing-md;
|
||
padding: 0 4rpx;
|
||
position: relative;
|
||
z-index: 20;
|
||
}
|
||
.section-title {
|
||
font-size: $font-lg;
|
||
font-weight: 800;
|
||
color: $text-main;
|
||
position: relative;
|
||
padding-left: 20rpx;
|
||
|
||
&::before {
|
||
content: '';
|
||
position: absolute;
|
||
left: 0; top: 50%; transform: translateY(-50%);
|
||
width: 8rpx; height: 32rpx;
|
||
background: $gradient-brand;
|
||
border-radius: 4rpx;
|
||
}
|
||
}
|
||
.issue-indicator {
|
||
font-size: $font-sm;
|
||
color: $brand-primary;
|
||
background: rgba($brand-primary, 0.1);
|
||
padding: 4rpx $spacing-md;
|
||
border-radius: $radius-round;
|
||
font-weight: 600;
|
||
}
|
||
|
||
/* Custom Picker */
|
||
.custom-picker {
|
||
height: 280rpx;
|
||
background: rgba($bg-secondary, 0.5);
|
||
border-radius: $radius-lg;
|
||
margin-bottom: $spacing-lg;
|
||
overflow: hidden;
|
||
}
|
||
.picker-item {
|
||
height: 80rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: $spacing-md;
|
||
}
|
||
.picker-text { font-size: $font-lg; color: $text-main; font-weight: 600; }
|
||
.picker-status {
|
||
font-size: $font-xs; color: $text-sub; background: rgba(0,0,0,0.05); padding: 2rpx $spacing-sm; border-radius: $radius-sm;
|
||
|
||
&.status-active { background: #D1FAE5; color: #059669; }
|
||
}
|
||
|
||
.record-count {
|
||
background: rgba($brand-primary, 0.1);
|
||
color: $brand-primary;
|
||
padding: 2rpx $spacing-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 {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
padding: 80rpx 0;
|
||
color: $text-placeholder;
|
||
}
|
||
.empty-icon { font-size: 80rpx; margin-bottom: $spacing-lg; opacity: 0.5; }
|
||
.empty-text { font-size: $font-md; }
|
||
|
||
/* 底部悬浮操作栏 - 高级重置 */
|
||
.float-bar {
|
||
position: fixed;
|
||
left: 32rpx;
|
||
right: 32rpx;
|
||
bottom: calc(40rpx + env(safe-area-inset-bottom));
|
||
z-index: 100;
|
||
animation: slideUp 0.6s cubic-bezier(0.23, 1, 0.32, 1) backwards;
|
||
}
|
||
|
||
.float-bar-inner {
|
||
background: rgba(255, 255, 255, 0.85);
|
||
backdrop-filter: blur(30rpx);
|
||
padding: 24rpx 40rpx;
|
||
border-radius: 999rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
box-shadow: 0 16rpx 48rpx rgba(0, 0, 0, 0.12);
|
||
border: 1rpx solid rgba(255, 255, 255, 0.6);
|
||
}
|
||
|
||
.float-price {
|
||
display: flex;
|
||
align-items: baseline;
|
||
color: $text-main;
|
||
font-weight: 800;
|
||
min-width: 0;
|
||
}
|
||
.float-price .currency { font-size: 26rpx; margin-right: 4rpx; color: $brand-primary; }
|
||
.float-price .amount { font-size: 44rpx; font-weight: 900; font-family: 'DIN Alternate', sans-serif; color: $brand-primary; }
|
||
.float-price .unit { font-size: 24rpx; color: $text-sub; margin-left: 4rpx; font-weight: 600; }
|
||
|
||
.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: slideUp 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-group-v2 {
|
||
margin-bottom: $spacing-xl;
|
||
&:last-child { margin-bottom: 0; }
|
||
}
|
||
|
||
.group-header-row {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: $spacing-md;
|
||
padding: 0 4rpx;
|
||
}
|
||
|
||
.group-badge {
|
||
font-size: $font-xs;
|
||
font-weight: 900;
|
||
color: $text-main;
|
||
background: #F0F0F0;
|
||
padding: 4rpx 16rpx;
|
||
border-radius: 8rpx;
|
||
font-style: italic;
|
||
border: 1rpx solid rgba(0,0,0,0.05);
|
||
box-shadow: $shadow-xs;
|
||
|
||
&.badge-boss {
|
||
background: $gradient-gold;
|
||
color: #78350F;
|
||
border-color: rgba(217, 119, 6, 0.3);
|
||
}
|
||
}
|
||
|
||
.group-total-prob {
|
||
font-size: 24rpx;
|
||
color: $brand-primary;
|
||
font-weight: 800;
|
||
}
|
||
.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;
|
||
}
|
||
|
||
.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; }
|
||
.stagger-3 { animation-delay: 0.3s; }
|
||
|
||
@keyframes fadeInUp {
|
||
from { opacity: 0; transform: translateY(40rpx); }
|
||
to { opacity: 1; transform: translateY(0); }
|
||
}
|
||
@keyframes fadeIn {
|
||
from { opacity: 0; }
|
||
to { opacity: 1; }
|
||
}
|
||
@keyframes scaleIn {
|
||
from { opacity: 0; transform: scale(0.9); }
|
||
to { opacity: 1; transform: scale(1); }
|
||
}
|
||
@keyframes slideUp {
|
||
from { transform: translateY(20rpx); opacity: 0; }
|
||
to { transform: translateY(0); opacity: 1; }
|
||
}
|
||
.action-btn {
|
||
height: 88rpx;
|
||
line-height: 88rpx;
|
||
padding: 0 56rpx;
|
||
border-radius: 999rpx;
|
||
font-size: 30rpx;
|
||
font-weight: 900;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
transition: all 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275);
|
||
position: relative;
|
||
overflow: hidden;
|
||
|
||
&.primary {
|
||
background: $gradient-brand !important;
|
||
color: #fff !important;
|
||
box-shadow: 0 12rpx 32rpx rgba($brand-primary, 0.35);
|
||
|
||
&::before {
|
||
content: '';
|
||
position: absolute;
|
||
top: -50%;
|
||
left: -150%;
|
||
width: 200%;
|
||
height: 200%;
|
||
background: linear-gradient(
|
||
120deg,
|
||
rgba(255, 255, 255, 0) 30%,
|
||
rgba(255, 255, 255, 0.4) 50%,
|
||
rgba(255, 255, 255, 0) 70%
|
||
);
|
||
transform: rotate(25deg);
|
||
animation: btnShine 4s infinite cubic-bezier(0.19, 1, 0.22, 1);
|
||
pointer-events: none;
|
||
}
|
||
}
|
||
|
||
&.secondary {
|
||
background: #1A1A1A !important;
|
||
color: $accent-gold !important;
|
||
box-shadow: 0 8rpx 20rpx rgba(0, 0, 0, 0.15);
|
||
}
|
||
|
||
&:active {
|
||
transform: scale(0.92);
|
||
}
|
||
}
|
||
|
||
@keyframes btnShine {
|
||
0% { left: -150%; }
|
||
100% { left: 150%; }
|
||
}
|
||
|
||
.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;
|
||
animation-delay: var(--delay, 0s);
|
||
}
|
||
|
||
/* ============= 全屏游戏样式 ============= */
|
||
.game-overlay {
|
||
position: fixed;
|
||
top: 0;
|
||
left: 0;
|
||
right: 0;
|
||
bottom: 0;
|
||
z-index: 9999;
|
||
}
|
||
|
||
.game-mask {
|
||
position: absolute;
|
||
top: 0;
|
||
left: 0;
|
||
right: 0;
|
||
bottom: 0;
|
||
background: rgba(0, 0, 0, 0.92);
|
||
backdrop-filter: blur(16rpx);
|
||
animation: fadeIn 0.3s ease-out;
|
||
}
|
||
|
||
.game-fullscreen {
|
||
position: relative;
|
||
z-index: 2;
|
||
height: 100%;
|
||
display: flex;
|
||
flex-direction: column;
|
||
animation: scaleIn 0.3s ease-out;
|
||
}
|
||
|
||
.game-topbar {
|
||
display: flex;
|
||
justify-content: center;
|
||
align-items: center;
|
||
padding: 40rpx 32rpx;
|
||
padding-top: calc(40rpx + env(safe-area-inset-top));
|
||
position: relative;
|
||
}
|
||
|
||
.game-topbar-title {
|
||
font-size: 36rpx;
|
||
font-weight: 800;
|
||
color: #fff;
|
||
letter-spacing: 2rpx;
|
||
}
|
||
|
||
.game-close-btn {
|
||
position: absolute;
|
||
right: 32rpx;
|
||
top: calc(40rpx + env(safe-area-inset-top));
|
||
width: 64rpx;
|
||
height: 64rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
background: rgba(255, 255, 255, 0.1);
|
||
border-radius: 50%;
|
||
|
||
text {
|
||
font-size: 48rpx;
|
||
color: rgba(255, 255, 255, 0.8);
|
||
line-height: 1;
|
||
}
|
||
|
||
&:active {
|
||
background: rgba(255, 255, 255, 0.2);
|
||
}
|
||
}
|
||
|
||
.game-stats {
|
||
display: flex;
|
||
justify-content: center;
|
||
gap: 48rpx;
|
||
padding: 24rpx 32rpx;
|
||
margin: 0 32rpx;
|
||
}
|
||
|
||
.stat-item {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
gap: 8rpx;
|
||
}
|
||
|
||
.stat-label {
|
||
font-size: 24rpx;
|
||
color: rgba(255, 255, 255, 0.6);
|
||
}
|
||
|
||
.stat-value {
|
||
font-size: 44rpx;
|
||
font-weight: 900;
|
||
color: $accent-gold;
|
||
font-family: 'DIN Alternate', sans-serif;
|
||
}
|
||
|
||
.game-content {
|
||
flex: 1;
|
||
display: flex;
|
||
flex-direction: column;
|
||
justify-content: center;
|
||
align-items: center;
|
||
padding: 32rpx;
|
||
}
|
||
|
||
.game-loading,
|
||
.game-error {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
gap: 20rpx;
|
||
}
|
||
|
||
.loading-icon,
|
||
.error-icon {
|
||
font-size: 80rpx;
|
||
}
|
||
|
||
.loading-text,
|
||
.error-text {
|
||
font-size: 28rpx;
|
||
color: rgba(255, 255, 255, 0.7);
|
||
}
|
||
|
||
.game-board {
|
||
width: 100%;
|
||
max-width: 700rpx;
|
||
padding: 32rpx;
|
||
}
|
||
|
||
.match-grid-fullscreen {
|
||
display: grid;
|
||
grid-template-columns: repeat(3, 1fr);
|
||
gap: 20rpx;
|
||
}
|
||
|
||
.match-cell-large {
|
||
position: relative;
|
||
aspect-ratio: 1 / 1;
|
||
border-radius: 20rpx;
|
||
overflow: hidden;
|
||
background: rgba(255, 255, 255, 0.1);
|
||
border: 2rpx solid rgba(255, 255, 255, 0.15);
|
||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||
|
||
&.empty {
|
||
background: rgba(255, 255, 255, 0.03);
|
||
border-style: dashed;
|
||
border-color: rgba(255, 255, 255, 0.1);
|
||
}
|
||
|
||
&.chosen {
|
||
border-color: $brand-primary;
|
||
box-shadow: 0 0 24rpx rgba($brand-primary, 0.5);
|
||
transform: scale(1.02);
|
||
}
|
||
|
||
&.picked {
|
||
border-color: $accent-gold;
|
||
box-shadow: 0 0 24rpx rgba($accent-gold, 0.5);
|
||
}
|
||
|
||
&:active {
|
||
transform: scale(0.95);
|
||
}
|
||
}
|
||
|
||
.match-cell-img-large {
|
||
width: 100%;
|
||
height: 100%;
|
||
background: rgba(255, 255, 255, 0.05);
|
||
}
|
||
|
||
.match-cell-type-large {
|
||
position: absolute;
|
||
left: 12rpx;
|
||
bottom: 12rpx;
|
||
max-width: 85%;
|
||
font-size: 24rpx;
|
||
font-weight: 700;
|
||
color: #fff;
|
||
background: rgba(0, 0, 0, 0.6);
|
||
padding: 6rpx 14rpx;
|
||
border-radius: 12rpx;
|
||
@include text-ellipsis(1);
|
||
}
|
||
|
||
.game-actions {
|
||
display: flex;
|
||
gap: 24rpx;
|
||
padding: 32rpx;
|
||
padding-bottom: calc(32rpx + env(safe-area-inset-bottom));
|
||
}
|
||
|
||
.game-btn {
|
||
flex: 1;
|
||
height: 96rpx;
|
||
border-radius: 48rpx;
|
||
font-size: 32rpx;
|
||
font-weight: 800;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
border: none;
|
||
transition: all 0.3s;
|
||
|
||
&:active {
|
||
transform: scale(0.95);
|
||
}
|
||
|
||
&[disabled] {
|
||
opacity: 0.4;
|
||
}
|
||
}
|
||
</style>
|