1051 lines
42 KiB
Vue
1051 lines
42 KiB
Vue
<template>
|
||
<view class="page-container">
|
||
<!-- Animated Branded Background -->
|
||
<view class="bg-layer">
|
||
<view class="game-glow glow-1"></view>
|
||
<view class="game-glow glow-2"></view>
|
||
<view class="game-glow glow-3"></view>
|
||
</view>
|
||
|
||
<!-- Top Navigation & Ticker -->
|
||
<view class="top-nav">
|
||
<view class="win-ticker" v-if="winRecords.length > 0">
|
||
<text class="ticker-icon">📣</text>
|
||
<swiper vertical autoplay circular interval="3000" class="ticker-swiper">
|
||
<swiper-item v-for="rec in winRecords" :key="rec.id">
|
||
<view class="ticker-item">
|
||
恭喜 <text class="ticker-user">玩家</text> 抽中 <text class="ticker-prize">{{ rec.title }}</text>
|
||
</view>
|
||
</swiper-item>
|
||
</swiper>
|
||
</view>
|
||
<view class="rules-btn" @click="showRules = true">
|
||
<text class="rules-icon">?</text>
|
||
玩法说明
|
||
</view>
|
||
</view>
|
||
|
||
<scroll-view class="page-content" scroll-y :enhanced="true" :show-scrollbar="false">
|
||
<!-- Loading State -->
|
||
<view class="loading-portal" v-if="loading && !issues.length">
|
||
<view class="loading-animation"></view>
|
||
<text class="loading-text">正在连接游戏大厅...</text>
|
||
</view>
|
||
|
||
<!-- Empty State -->
|
||
<view class="empty-portal animate-fade-in" v-else-if="!loading && issues.length === 0">
|
||
<view class="empty-art-wrapper">
|
||
<view class="empty-orb"></view>
|
||
<text class="empty-emoji">🎮</text>
|
||
</view>
|
||
<text class="empty-title">当前无活动上线</text>
|
||
<text class="empty-desc">别担心,去仓库看看其他好货吧</text>
|
||
<button class="return-home-btn" @click="goHome">返回大厅</button>
|
||
</view>
|
||
|
||
<block v-else>
|
||
<!-- Banner -->
|
||
<view class="banner-section animate-slide-up" v-if="detail.banner" @click="onPreviewBanner">
|
||
<image class="banner-image" :src="detail.banner" mode="widthFix" />
|
||
<view class="banner-overlay"></view>
|
||
</view>
|
||
|
||
<!-- Dynamic Game Arena -->
|
||
<block v-if="gameId">
|
||
<!-- Stats Header -->
|
||
<view class="arena-stats">
|
||
<view class="stat-gem animate-bounce-in">
|
||
<view class="gem-icon">💎</view>
|
||
<view class="gem-content">
|
||
<text class="gem-label">消除得分</text>
|
||
<text class="gem-value">{{ totalPairs }}</text>
|
||
</view>
|
||
</view>
|
||
<view class="stat-gem animate-bounce-in" style="animation-delay: 0.1s">
|
||
<view class="gem-icon">📦</view>
|
||
<view class="gem-content">
|
||
<text class="gem-label">牌堆容量</text>
|
||
<text class="gem-value">{{ deckCount }}</text>
|
||
</view>
|
||
</view>
|
||
<view class="stat-gem animate-bounce-in" style="animation-delay: 0.2s">
|
||
<view class="gem-icon">⏳</view>
|
||
<view class="gem-content">
|
||
<text class="gem-label">当前轮次</text>
|
||
<text class="gem-value">{{ currentRound }}</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- Apple Effect Overlay -->
|
||
<view class="special-overlay" v-if="showAppleEffect">
|
||
<view class="apple-card animate-zoom-in">
|
||
<view class="apple-glow"></view>
|
||
<text class="apple-emoji">🍎</text>
|
||
<text class="apple-title">苹果加成!</text>
|
||
<text class="apple-subtitle">检测到运气爆发,额外补牌中...</text>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- The Game Board -->
|
||
<view class="board-arena glass-panel animate-zoom-in">
|
||
<view class="board-grid">
|
||
<view v-for="(slot, idx) in board" :key="idx"
|
||
class="card-unit"
|
||
:class="{
|
||
'is-pairing': slot && pairingIds.includes(slot.id),
|
||
'is-disappearing': slot && disappearingIds.includes(slot.id),
|
||
'is-drawing': slot && newCardIds.includes(slot.id),
|
||
'is-rare': slot && slot.type === 'A'
|
||
}">
|
||
<view v-if="slot" class="card-face">
|
||
<view class="card-shine" v-if="slot.type === 'A'"></view>
|
||
<view class="match-badge" v-if="pairingIds.includes(slot.id)">MATCH!</view>
|
||
<image v-if="slot.image" class="card-thumb" :src="slot.image" mode="aspectFit" />
|
||
<text class="card-tag" v-else>{{ slot.name || slot.type }}</text>
|
||
</view>
|
||
<view v-else class="card-void">
|
||
<view class="void-circle"></view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</block>
|
||
|
||
<!-- Issue Selection (Hall State) -->
|
||
<view class="entry-hall glass-panel animate-slide-up" v-else>
|
||
<view class="arena-header">
|
||
<view class="hall-title">
|
||
<text class="title-main">选择竞技场</text>
|
||
<text class="title-sub">Select Your Arena</text>
|
||
</view>
|
||
<view class="issue-count">活跃中 {{ issues.length }}</view>
|
||
</view>
|
||
|
||
<!-- Horizontal Card Selector -->
|
||
<scroll-view scroll-x class="issue-scroll" :show-scrollbar="false">
|
||
<view class="issue-cards">
|
||
<view v-for="(it, idx) in issues" :key="it.id"
|
||
class="issue-card"
|
||
:class="{ active: selectedIssueIndex === idx }"
|
||
@click="onSelectIssue(idx)">
|
||
<view class="card-background-glow"></view>
|
||
<text class="issue-no">NO.{{ it.no || (idx + 1) }}</text>
|
||
<text class="issue-name">{{ it.title }}</text>
|
||
<view class="issue-tag" :class="'status-' + it.status">{{ it.status_text }}</view>
|
||
<view class="active-indicator" v-if="selectedIssueIndex === idx">
|
||
<view class="dot"></view>
|
||
SELECTED
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</scroll-view>
|
||
|
||
<!-- Info Tabs -->
|
||
<view class="hall-tabs">
|
||
<view class="hall-tab" :class="{ active: tabActive === 'pool' }" @click="tabActive = 'pool'">
|
||
奖励预览
|
||
<view class="tab-bar-glow" v-if="tabActive === 'pool'"></view>
|
||
</view>
|
||
<view class="hall-tab" :class="{ active: tabActive === 'records' }" @click="tabActive = 'records'">
|
||
中奖动态
|
||
<view class="tab-bar-glow" v-if="tabActive === 'records'"></view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- Content Lists -->
|
||
<view class="tab-container">
|
||
<!-- Pool Preview -->
|
||
<view v-if="tabActive === 'pool'" class="rewards-mosaic">
|
||
<view v-for="(rw, idx) in (rewardsMap[currentIssueId] || [])" :key="rw.id" class="mosaic-item animate-stagger" :style="{ '--delay': idx * 0.05 + 's' }">
|
||
<view class="mosaic-inner">
|
||
<view class="mosaic-img-box">
|
||
<image :src="rw.image" class="mosaic-img" mode="aspectFill" />
|
||
<view class="mosaic-boss" v-if="rw.boss">BOSS</view>
|
||
<view class="mosaic-prob">{{ rw.percent }}%</view>
|
||
</view>
|
||
<text class="mosaic-title">{{ rw.title }}</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- Win Records -->
|
||
<view v-if="tabActive === 'records'" class="dynamic-records">
|
||
<view v-for="(it, idx) in winRecords" :key="it.id" class="win-row animate-stagger" :style="{ '--delay': idx * 0.05 + 's' }">
|
||
<view class="win-avatar-box">
|
||
<image class="win-avatar" src="/static/images/common/default-avatar.png" mode="aspectFill" />
|
||
</view>
|
||
<view class="win-info">
|
||
<view class="win-top">
|
||
<text class="win-user">玩家</text>
|
||
<text class="win-time">刚刚</text>
|
||
</view>
|
||
<text class="win-item-name">获得 {{ it.title }} x{{ it.count }}</text>
|
||
</view>
|
||
<image class="win-thumb" :src="it.image" mode="aspectFill" />
|
||
</view>
|
||
<view class="empty-win" v-if="!winRecords.length">
|
||
<text class="empty-win-icon">🏜️</text>
|
||
<text class="empty-win-text">虚位以待,快来挑战</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</block>
|
||
|
||
<!-- Safe Bottom Padding -->
|
||
<view style="height: 140rpx"></view>
|
||
</scroll-view>
|
||
|
||
<!-- Floating Action Hub -->
|
||
<view class="action-hub" v-if="activityId && issues.length">
|
||
<view class="action-glow"></view>
|
||
|
||
<!-- Entry Mode -->
|
||
<button v-if="!gameId" class="hub-btn neon-btn pulsate" @click="handleStartGame" :loading="loading">
|
||
<text class="btn-text">立即进入竞技场</text>
|
||
<view class="btn-energy-line"></view>
|
||
</button>
|
||
|
||
<!-- Gaming Mode -->
|
||
<view v-else-if="!gameOver" class="gaming-controls">
|
||
<view class="auto-toggle-box" @click="isAutoMode = !isAutoMode">
|
||
<view class="toggle-track" :class="{ active: isAutoMode }">
|
||
<view class="toggle-thumb"></view>
|
||
</view>
|
||
<text class="toggle-label">{{ isAutoMode ? '自动模式 ON' : '手动模式 OFF' }}</text>
|
||
</view>
|
||
|
||
<button class="hub-btn primary-btn" @click="handlePlayRound" :loading="loading" :disabled="isAnimating">
|
||
<text v-if="isAnimating" class="matching-pulse">🧠 {{ isAutoMode ? '自动匹配中...' : '计算配对中...' }}</text>
|
||
<text v-else>执行下一轮消除</text>
|
||
</button>
|
||
</view>
|
||
|
||
<view class="game-over-hub" v-else>
|
||
<button class="hub-btn secondary-btn" @click="resetGame">退出游戏</button>
|
||
<button class="hub-btn gold-btn" @click="resetGame">再次发起挑战</button>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- Rule Modal -->
|
||
<view class="modal-layer" v-if="showRules" @tap="showRules = false">
|
||
<view class="premium-modal animate-bounce-in" @tap.stop>
|
||
<view class="modal-header">
|
||
<text class="modal-title">匹配挑战规则</text>
|
||
<view class="close-x" @tap="showRules = false">×</view>
|
||
</view>
|
||
<scroll-view class="modal-scroll" scroll-y>
|
||
<view class="rule-item">
|
||
<text class="rule-no">01</text>
|
||
<view class="rule-content">每局游戏开始时,将从牌堆中抽取卡牌填充至 3x3 棋盘。</view>
|
||
</view>
|
||
<view class="rule-item">
|
||
<text class="rule-no">02</text>
|
||
<view class="rule-content">当棋盘中出现 3 张相同卡牌时,将自动进行“对对碰”消除并获得奖励。</view>
|
||
</view>
|
||
<view class="rule-item">
|
||
<text class="rule-no">03</text>
|
||
<view class="rule-content">🍎 如果初始手牌中包含苹果卡,将触发暴击效果,额外抽取更多手牌!</view>
|
||
</view>
|
||
<view class="rule-item">
|
||
<text class="rule-no">04</text>
|
||
<view class="rule-content">消除后会自动补位,直到牌堆耗尽或无法继续消除,游戏结束。</view>
|
||
</view>
|
||
</scroll-view>
|
||
<button class="modal-confirm" @tap="showRules = false">我明白了</button>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- Result Modal -->
|
||
<view class="modal-layer" v-if="showResult" @tap="showResult = false">
|
||
<view class="settlement-card glass-panel animate-zoom-in" @tap.stop>
|
||
<view class="settlement-crown">👑</view>
|
||
<text class="settlement-title">挑战圆满结束</text>
|
||
<view class="settlement-stats">
|
||
<view class="set-stat">
|
||
<text class="set-val">{{ totalPairs }}</text>
|
||
<text class="set-lab">消除对数</text>
|
||
</view>
|
||
</view>
|
||
<text class="settlement-tip">恭喜!所有获得奖励已存入您的账户库</text>
|
||
<button class="set-btn" @click="resetGame">领取战利品</button>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { ref, computed, nextTick } from 'vue'
|
||
import { onLoad, onShow } from '@dcloudio/uni-app'
|
||
import {
|
||
getActivityDetail,
|
||
getActivityIssues,
|
||
getActivityIssueRewards,
|
||
getActivityWinRecords,
|
||
startMatchingGame,
|
||
playMatchingGame,
|
||
getMatchingGameState,
|
||
getMatchingCardTypes
|
||
} from '../../../api/appUser'
|
||
|
||
// Activity State
|
||
const activityId = ref('')
|
||
const detail = ref({})
|
||
const issues = ref([])
|
||
const rewardsMap = ref({})
|
||
const currentIssueId = ref('')
|
||
const selectedIssueIndex = ref(0)
|
||
const tabActive = ref('pool')
|
||
const winRecords = ref([])
|
||
const loading = ref(true) // Start as true to prevent empty-state flash
|
||
const hasInited = ref(false)
|
||
const cardMetaMap = ref({})
|
||
|
||
// Game State
|
||
const gameId = ref('')
|
||
const deckCount = ref(0)
|
||
const totalPairs = ref(0)
|
||
const currentRound = ref(0)
|
||
const gameOver = ref(false)
|
||
const isAnimating = ref(false)
|
||
const pairingIds = ref([])
|
||
const disappearingIds = ref([])
|
||
const newCardIds = ref([])
|
||
const isAutoMode = ref(false)
|
||
const showResult = ref(false)
|
||
const showAppleEffect = ref(false)
|
||
const showRules = ref(false)
|
||
const board = ref(Array(9).fill(null)) // Use a persistent array of 9 for the grid
|
||
|
||
// Activity Logic
|
||
async function fetchDetail(id) {
|
||
try {
|
||
const data = await getActivityDetail(id)
|
||
detail.value = data || {}
|
||
} catch (e) {}
|
||
}
|
||
|
||
async function fetchIssues(id) {
|
||
loading.value = true
|
||
try {
|
||
const data = await getActivityIssues(id)
|
||
issues.value = normalizeIssues(data)
|
||
const latestId = pickLatestIssueId(issues.value)
|
||
setSelectedById(latestId)
|
||
await fetchRewardsForIssues(id)
|
||
} catch (e) {
|
||
console.error('fetchIssues error:', e)
|
||
} finally {
|
||
loading.value = false
|
||
}
|
||
}
|
||
|
||
function unwrap(list) {
|
||
if (Array.isArray(list)) return list
|
||
const obj = list || {}
|
||
const data = obj.data || {}
|
||
// Deep search for array
|
||
const arr = obj.issues || obj.list || obj.items || data.issues || data.list || data.items || data
|
||
return Array.isArray(arr) ? arr : []
|
||
}
|
||
|
||
function normalizeIssues(list) {
|
||
console.log('[DEBUG] Raw Issues Data:', list)
|
||
const arr = unwrap(list)
|
||
console.log('[DEBUG] Unwrapped Issues:', arr)
|
||
return arr.map((i, idx) => ({
|
||
id: i.id ?? String(idx),
|
||
title: i.title ?? i.name ?? i.issue_no ?? `第${idx+1}期`,
|
||
no: i.no ?? i.index ?? i.issue_no ?? null,
|
||
status: i.status,
|
||
status_text: i.status === 1 ? '进行中' : i.status === 0 ? '未开始' : '已结束'
|
||
}))
|
||
}
|
||
|
||
async function fetchRewardsForIssues(aid) {
|
||
if (!currentIssueId.value) return
|
||
try {
|
||
const data = await getActivityIssueRewards(aid, currentIssueId.value)
|
||
const arr = Array.isArray(data) ? data : (data?.data || [])
|
||
rewardsMap.value[currentIssueId.value] = arr.map((i, idx) => ({
|
||
id: i.product_id ?? i.id ?? String(idx),
|
||
title: i.name ?? i.title ?? '',
|
||
image: i.product_image ?? i.image ?? '',
|
||
weight: Number(i.weight) || 0,
|
||
boss: !!(i.is_boss || i.boss),
|
||
percent: 0 // logic to calculate percentage if total_weight is known
|
||
}))
|
||
} catch (e) {}
|
||
}
|
||
|
||
async function fetchWinRecords(aid) {
|
||
try {
|
||
const data = await getActivityWinRecords(aid, 1, 20)
|
||
const arr = Array.isArray(data) ? data : (data?.data || [])
|
||
winRecords.value = arr.map((i, idx) => ({
|
||
id: i.id ?? String(idx),
|
||
title: i.title ?? i.name ?? '',
|
||
image: i.image ?? '',
|
||
count: i.count ?? 1,
|
||
percent: i.percent
|
||
}))
|
||
} catch (e) {}
|
||
}
|
||
|
||
function pickLatestIssueId(list) {
|
||
return list[list.length - 1]?.id || ''
|
||
}
|
||
|
||
function setSelectedById(id) {
|
||
const idx = issues.value.findIndex(x => x.id === id)
|
||
selectedIssueIndex.value = Math.max(0, idx)
|
||
currentIssueId.value = issues.value[selectedIssueIndex.value]?.id || ''
|
||
}
|
||
|
||
function onIssueChange(e) {
|
||
const idx = e.detail.value[0]
|
||
onSelectIssue(idx)
|
||
}
|
||
|
||
function onSelectIssue(idx) {
|
||
if (isAnimating.value) return
|
||
selectedIssueIndex.value = idx
|
||
currentIssueId.value = issues.value[idx]?.id || ''
|
||
fetchRewardsForIssues(activityId.value)
|
||
}
|
||
|
||
async function handleStartGame() {
|
||
if (!currentIssueId.value) return
|
||
loading.value = true
|
||
try {
|
||
const res = await startMatchingGame(issues.value[selectedIssueIndex.value].id)
|
||
gameId.value = res.game_id
|
||
deckCount.value = res.deck_count
|
||
totalPairs.value = 0
|
||
currentRound.value = 1
|
||
|
||
// Spec priority: res.board (9-slot) > res.hand (old list)
|
||
board.value = normalizeCards(res.board || res.hand)
|
||
|
||
// If we fell back to 'hand' (old API), we might need to pad it to 9 slots manually
|
||
// to match the new 9-slot grid expectation in the template
|
||
if (!res.board && board.value.length < 9) {
|
||
while(board.value.length < 9) board.value.push(null)
|
||
}
|
||
|
||
// Reset loop
|
||
isAutoMode.value = false
|
||
} catch (e) {
|
||
uni.showToast({ title: '初始化失败', icon: 'none' })
|
||
} finally {
|
||
loading.value = false
|
||
}
|
||
}
|
||
|
||
async function handlePlayRound() {
|
||
if (!gameId.value || isAnimating.value) return
|
||
loading.value = !isAutoMode.value // Don't show full-page loading in auto mode to avoid flicker
|
||
try {
|
||
const res = await playMatchingGame(gameId.value)
|
||
totalPairs.value = res.total_pairs
|
||
gameOver.value = res.game_over
|
||
deckCount.value = (res.round?.deck_count !== undefined) ? res.round.deck_count : (res.deck_count || deckCount.value)
|
||
|
||
await processRound(res.round)
|
||
|
||
if (res.game_over) {
|
||
uni.removeStorageSync('matching_game_id')
|
||
// Small pause after all animations to see the final board
|
||
setTimeout(() => { showResult.value = true }, 1500)
|
||
} else if (isAutoMode.value) {
|
||
// Auto-loop
|
||
setTimeout(() => {
|
||
handlePlayRound()
|
||
}, 500)
|
||
}
|
||
} catch (err) {
|
||
isAutoMode.value = false // Stop auto mode on error
|
||
uni.showToast({ title: '执行失败', icon: 'none' })
|
||
} finally {
|
||
loading.value = false
|
||
}
|
||
}
|
||
|
||
// Game Logic
|
||
async function initGame(aid) {
|
||
try {
|
||
loading.value = true
|
||
// Parallel fetch basic info + card metadata
|
||
const [detailRes, issuesRes, cardTypesRes] = await Promise.all([
|
||
getActivityDetail(aid),
|
||
getActivityIssues(aid),
|
||
getMatchingCardTypes()
|
||
])
|
||
|
||
detail.value = detailRes || {}
|
||
issues.value = normalizeIssues(issuesRes)
|
||
|
||
// Build card metadata map
|
||
const types = Array.isArray(cardTypesRes) ? cardTypesRes : (cardTypesRes?.list || cardTypesRes?.data || [])
|
||
const meta = {}
|
||
types.forEach(t => {
|
||
meta[t.code] = {
|
||
name: t.name,
|
||
image: t.image_url || t.image
|
||
}
|
||
})
|
||
cardMetaMap.value = meta
|
||
|
||
console.log('[DEBUG] Card Meta Map initialized:', Object.keys(meta).length)
|
||
|
||
if (issues.value.length > 0) {
|
||
const latestId = pickLatestIssueId(issues.value)
|
||
setSelectedById(latestId)
|
||
await fetchRewardsForIssues(aid)
|
||
|
||
// Auto-start or Restore
|
||
const restored = await checkGameStatus()
|
||
if (!restored && currentIssueId.value) {
|
||
await handleStartGame()
|
||
}
|
||
}
|
||
|
||
// Non-blocking fetch records
|
||
fetchWinRecords(aid)
|
||
} catch (err) {
|
||
console.error('[DEBUG] Init failed:', err)
|
||
} finally {
|
||
hasInited.value = true
|
||
loading.value = false
|
||
}
|
||
}
|
||
|
||
function normalizeCards(list) {
|
||
const arr = Array.isArray(list) ? list : []
|
||
return arr.map((item, idx) => {
|
||
// Pass through nulls (for empty slots in 9-grid)
|
||
if (item === null) return null
|
||
|
||
const code = typeof item === 'string' ? item : (item.type ?? item.card_type ?? item.cardType ?? item.code ?? item.name ?? item.title ?? '')
|
||
const meta = cardMetaMap.value[code] || {}
|
||
|
||
if (typeof item === 'string') {
|
||
return {
|
||
id: `card-${idx}-${Date.now()}`,
|
||
type: code,
|
||
image: meta.image || '',
|
||
name: meta.name || code
|
||
}
|
||
}
|
||
|
||
const normalized = {
|
||
id: item.id ?? item.card_id ?? `card-${idx}-${Date.now()}`,
|
||
type: code,
|
||
image: item.image ?? item.product_image ?? item.image_url ?? item.thumb ?? item.img ?? meta.image ?? '',
|
||
name: item.name ?? item.title ?? meta.name ?? code
|
||
}
|
||
return normalized
|
||
})
|
||
}
|
||
|
||
async function processRound(round, isFirstRound = false) {
|
||
if (!round) return
|
||
isAnimating.value = true
|
||
currentRound.value = round.round
|
||
|
||
// 1. Precise Elimination based on `card_ids` in pairs
|
||
// Spec: round.pairs contains { card_ids: ["c1", "c5"] }
|
||
if (round.pairs && round.pairs.length > 0) {
|
||
console.log('[DEBUG] Processing pairs:', round.pairs)
|
||
const toPairIds = []
|
||
|
||
round.pairs.forEach(p => {
|
||
if (p.card_ids && Array.isArray(p.card_ids)) {
|
||
toPairIds.push(...p.card_ids)
|
||
} else {
|
||
// Fallback for old API (count based logic)
|
||
const type = p.card_type ?? p.type
|
||
let count = p.count || 2
|
||
// Naive fallback: find first N cards of this type
|
||
// This is risky but keeps old logic working if partial update
|
||
board.value.forEach(c => {
|
||
if (c && c.type === type && count > 0 && !toPairIds.includes(c.id)) {
|
||
toPairIds.push(c.id)
|
||
count--
|
||
}
|
||
})
|
||
}
|
||
})
|
||
|
||
if (toPairIds.length > 0) {
|
||
pairingIds.value = toPairIds
|
||
await sleep(1200)
|
||
|
||
disappearingIds.value = [...toPairIds]
|
||
pairingIds.value = []
|
||
await sleep(600)
|
||
|
||
// Clear the slots for these exact IDs
|
||
const nextBoard = [...board.value]
|
||
toPairIds.forEach(id => {
|
||
const idx = nextBoard.findIndex(s => s && s.id === id)
|
||
if (idx !== -1) nextBoard[idx] = null
|
||
})
|
||
board.value = nextBoard
|
||
disappearingIds.value = []
|
||
}
|
||
}
|
||
|
||
// 2. Stable Filling: Sync with Final Board
|
||
// Spec: round.board is the final state.
|
||
// We compare final board with current board to find what changed (Filling empties).
|
||
if (round.board) {
|
||
const finalBoard = normalizeCards(round.board)
|
||
const currentBoard = [...board.value]
|
||
const addedIds = []
|
||
|
||
// Apply changes (fill nulls)
|
||
for (let i = 0; i < 9; i++) {
|
||
// If local is null but final is not null -> New Card filled here
|
||
if (currentBoard[i] === null && finalBoard[i] !== null) {
|
||
currentBoard[i] = finalBoard[i]
|
||
addedIds.push(finalBoard[i].id)
|
||
}
|
||
// Sync override (just in case)
|
||
else if (finalBoard[i] !== null && currentBoard[i]?.id !== finalBoard[i].id) {
|
||
currentBoard[i] = finalBoard[i]
|
||
// Maybe animate this too if it's a swap? Spec says stable, so mostly filling.
|
||
}
|
||
}
|
||
|
||
board.value = currentBoard
|
||
|
||
if (addedIds.length > 0) {
|
||
newCardIds.value = addedIds
|
||
await sleep(1000)
|
||
newCardIds.value = []
|
||
}
|
||
}
|
||
|
||
// Deck count update if available
|
||
// (Usually in root response, but if in round object...)
|
||
// if (round.deck_count !== undefined) deckCount.value = round.deck_count
|
||
|
||
isAnimating.value = false
|
||
}
|
||
|
||
|
||
// Reconnection logic
|
||
async function checkGameStatus() {
|
||
const savedGameId = uni.getStorageSync('matching_game_id')
|
||
if (!savedGameId) return false
|
||
|
||
loading.value = true
|
||
try {
|
||
const res = await getMatchingGameState(savedGameId)
|
||
gameId.value = savedGameId
|
||
|
||
// Spec: res.board is the fixed 9-slot array directly from storage including nulls
|
||
board.value = normalizeCards(res.board)
|
||
|
||
// Ensure it's 9 slots just in case
|
||
while (board.value.length < 9) {
|
||
board.value.push(null)
|
||
}
|
||
|
||
totalPairs.value = res.total_pairs
|
||
currentRound.value = res.round
|
||
deckCount.value = res.deck_count
|
||
|
||
uni.showToast({ title: '已恢复游戏进度', icon: 'none' })
|
||
return true
|
||
} catch (e) {
|
||
uni.removeStorageSync('matching_game_id')
|
||
return false
|
||
} finally {
|
||
loading.value = false
|
||
}
|
||
}
|
||
|
||
function resetGame() {
|
||
uni.removeStorageSync('matching_game_id')
|
||
gameId.value = ''
|
||
board.value = Array(9).fill(null)
|
||
totalPairs.value = 0
|
||
currentRound.value = 0
|
||
gameOver.value = false
|
||
showResult.value = false
|
||
}
|
||
|
||
function sleep(ms) {
|
||
return new Promise(resolve => setTimeout(resolve, ms))
|
||
}
|
||
|
||
function onPreviewBanner() {
|
||
const url = detail.value.banner
|
||
if (url) uni.previewImage({ urls: [url] })
|
||
}
|
||
|
||
onLoad((opts) => {
|
||
const id = opts.id || ''
|
||
if (id) {
|
||
activityId.value = id
|
||
initGame(id)
|
||
} else {
|
||
uni.showToast({ title: '参数错误', icon: 'none' })
|
||
loading.value = false
|
||
}
|
||
})
|
||
|
||
function goHome() {
|
||
uni.switchTab({ url: '/pages/shop/index' })
|
||
}
|
||
</script>
|
||
|
||
<style lang="scss" scoped>
|
||
.page-container {
|
||
min-height: 100vh;
|
||
background: #0f0c29;
|
||
position: relative;
|
||
overflow: hidden;
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
|
||
/* Branded Background */
|
||
.bg-layer {
|
||
position: absolute;
|
||
top: 0; left: 0; width: 100%; height: 100%;
|
||
z-index: 0;
|
||
}
|
||
|
||
.game-glow {
|
||
position: absolute;
|
||
border-radius: 50%;
|
||
filter: blur(120rpx);
|
||
opacity: 0.6;
|
||
}
|
||
|
||
.glow-1 { width: 600rpx; height: 600rpx; background: radial-gradient(circle, #ff6b35 0%, transparent 70%); top: -150rpx; left: -100rpx; }
|
||
.glow-2 { width: 700rpx; height: 700rpx; background: radial-gradient(circle, #e81cff 0%, transparent 70%); bottom: -200rpx; right: -100rpx; }
|
||
.glow-3 { width: 500rpx; height: 500rpx; background: radial-gradient(circle, #fcb045 0%, transparent 70%); top: 35%; right: -150rpx; }
|
||
|
||
/* Top Nav & Ticker */
|
||
.top-nav {
|
||
position: relative;
|
||
z-index: 10;
|
||
padding: $spacing-md $spacing-lg;
|
||
padding-top: calc($spacing-md + env(safe-area-inset-top));
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
}
|
||
|
||
.win-ticker {
|
||
flex: 1;
|
||
background: rgba(255, 255, 255, 0.1);
|
||
backdrop-filter: blur(10px);
|
||
height: 64rpx;
|
||
border-radius: 32rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
padding: 0 $spacing-md;
|
||
margin-right: $spacing-md;
|
||
border: 1rpx solid rgba(255, 255, 255, 0.15);
|
||
}
|
||
|
||
.ticker-icon { font-size: 24rpx; margin-right: 12rpx; }
|
||
.ticker-swiper { flex: 1; height: 100%; }
|
||
.ticker-item {
|
||
height: 100%;
|
||
display: flex;
|
||
align-items: center;
|
||
font-size: 22rpx;
|
||
color: rgba(255, 255, 255, 0.9);
|
||
}
|
||
.ticker-prize { color: #ffca28; font-weight: bold; margin-left: 8rpx; }
|
||
|
||
.rules-btn {
|
||
background: rgba(255, 255, 255, 0.1);
|
||
backdrop-filter: blur(10px);
|
||
padding: 12rpx $spacing-md;
|
||
border-radius: $radius-round;
|
||
color: #fff;
|
||
font-size: 22rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
border: 1rpx solid rgba(255, 255, 255, 0.2);
|
||
}
|
||
.rules-icon {
|
||
width: 28rpx; height: 28rpx; background: rgba(255,255,255,0.2); border-radius: 50%;
|
||
display: flex; align-items: center; justify-content: center; margin-right: 8rpx;
|
||
}
|
||
|
||
.page-content {
|
||
flex: 1;
|
||
position: relative;
|
||
z-index: 1;
|
||
}
|
||
|
||
/* Glass Panels */
|
||
.glass-panel {
|
||
background: rgba(255, 255, 255, 0.08);
|
||
backdrop-filter: blur(25px);
|
||
border: 1rpx solid rgba(255, 255, 255, 0.12);
|
||
border-radius: 40rpx;
|
||
box-shadow: 0 20rpx 50rpx rgba(0,0,0,0.3);
|
||
}
|
||
|
||
/* Loading Portal */
|
||
.loading-portal {
|
||
padding-top: 200rpx;
|
||
display: flex; flex-direction: column; align-items: center;
|
||
}
|
||
.loading-animation {
|
||
width: 120rpx; height: 120rpx;
|
||
border: 4rpx solid rgba(255,255,255,0.1);
|
||
border-top-color: #ff6b35;
|
||
border-radius: 50%;
|
||
animation: rotate 1s linear infinite;
|
||
margin-bottom: $spacing-xl;
|
||
}
|
||
.loading-text { color: rgba(255,255,255,0.6); font-size: $font-sm; letter-spacing: 2rpx; }
|
||
|
||
/* Empty States */
|
||
.empty-portal {
|
||
padding-top: 150rpx;
|
||
display: flex; flex-direction: column; align-items: center;
|
||
}
|
||
.empty-art-wrapper {
|
||
position: relative; width: 240rpx; height: 240rpx; margin-bottom: 60rpx;
|
||
}
|
||
.empty-orb {
|
||
position: absolute; top: 0; left: 0; width: 100%; height: 100%;
|
||
background: radial-gradient(circle, rgba(232, 28, 255, 0.2) 0%, transparent 70%);
|
||
animation: pulse 3s infinite;
|
||
}
|
||
.empty-emoji { font-size: 140rpx; position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); filter: drop-shadow(0 0 20rpx #e81cff); }
|
||
.empty-title { font-size: 40rpx; color: #fff; font-weight: 800; margin-bottom: 16rpx; }
|
||
.empty-desc { font-size: 26rpx; color: rgba(255,255,255,0.5); margin-bottom: 80rpx; }
|
||
.return-home-btn {
|
||
width: 320rpx; height: 90rpx; background: linear-gradient(90deg, #FF6B35, #ff9f43);
|
||
color: #fff; border-radius: 45rpx; font-weight: bold; border: none;
|
||
}
|
||
|
||
/* Banner */
|
||
.banner-section { margin: $spacing-md $spacing-lg $spacing-xl; border-radius: 32rpx; overflow: hidden; position: relative; }
|
||
.banner-image { width: 100%; display: block; }
|
||
.banner-overlay { position: absolute; bottom: 0; left: 0; width: 100%; height: 60%; background: linear-gradient(to top, rgba(15,12,41,0.8), transparent); }
|
||
|
||
/* Arena Stats */
|
||
.arena-stats {
|
||
display: grid; grid-template-columns: repeat(3, 1fr); gap: 20rpx; padding: 0 $spacing-lg; margin-bottom: 40rpx;
|
||
}
|
||
.stat-gem {
|
||
background: rgba(255, 255, 255, 0.05);
|
||
border: 1rpx solid rgba(255,255,255,0.1);
|
||
border-radius: 30rpx;
|
||
padding: 24rpx 16rpx;
|
||
display: flex; flex-direction: column; align-items: center;
|
||
backdrop-filter: blur(10px);
|
||
}
|
||
.gem-icon { font-size: 36rpx; margin-bottom: 8rpx; }
|
||
.gem-label { font-size: 18rpx; color: rgba(255,255,255,0.4); text-transform: uppercase; margin-bottom: 4rpx; }
|
||
.gem-value { font-size: 34rpx; font-weight: 900; color: #fff; font-family: 'DIN Alternate', sans-serif; text-shadow: 0 0 10rpx rgba(232, 28, 255, 0.5); }
|
||
|
||
/* Apple Effect */
|
||
.special-overlay { position: fixed; top: 0; left: 0; width: 100%; height: 100%; z-index: 1000; display: flex; align-items: center; justify-content: center; background: rgba(0,0,0,0.6); backdrop-filter: blur(5px); }
|
||
.apple-card { width: 500rpx; height: 600rpx; background: #fff; border-radius: 50rpx; display: flex; flex-direction: column; align-items: center; justify-content: center; position: relative; overflow: hidden; }
|
||
.apple-glow { position: absolute; width: 150%; height: 150%; background: radial-gradient(circle, rgba(255, 107, 53, 0.3) 0%, transparent 70%); animation: rotate 4s linear infinite; }
|
||
.apple-emoji { font-size: 160rpx; z-index: 1; margin-bottom: 40rpx; }
|
||
.apple-title { font-size: 44rpx; font-weight: 900; color: #f44336; z-index: 1; }
|
||
.apple-subtitle { font-size: 24rpx; color: #777; z-index: 1; margin-top: 10rpx; }
|
||
|
||
/* Board Arena */
|
||
.board-arena { margin: 0 $spacing-lg; padding: 30rpx; }
|
||
.board-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 20rpx; }
|
||
.card-unit {
|
||
aspect-ratio: 1; background: rgba(0, 0, 0, 0.2); border-radius: 24rpx; position: relative;
|
||
transition: all 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275);
|
||
|
||
&.is-pairing { transform: scale(1.1); box-shadow: 0 0 50rpx rgba(255, 107, 53, 0.8); z-index: 5; }
|
||
&.is-disappearing { transform: scale(0); opacity: 0; filter: blur(10px); }
|
||
&.is-drawing { animation: cardDraw 0.6s cubic-bezier(0.175, 0.885, 0.32, 1.275) backwards; }
|
||
&.is-rare { border: 2rpx solid #ffca28; box-shadow: inset 0 0 20rpx rgba(255, 202, 40, 0.2); }
|
||
}
|
||
|
||
.match-badge {
|
||
position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%) rotate(-15deg);
|
||
background: #ff6b35; color: #fff; font-size: 24rpx; font-weight: 900;
|
||
padding: 4rpx 12rpx; border-radius: 8rpx; z-index: 10;
|
||
animation: matchPop 0.5s cubic-bezier(0.175, 0.885, 0.32, 1.275) forwards;
|
||
}
|
||
@keyframes matchPop { from { transform: translate(-50%, -50%) scale(0) rotate(0deg); } to { transform: translate(-50%, -50%) scale(1) rotate(-15deg); } }
|
||
|
||
.card-face {
|
||
width: 100%; height: 100%; border-radius: 24rpx; background: #fff; padding: 12rpx; display: flex; flex-direction: column; align-items: center; justify-content: center; position: relative; overflow: hidden;
|
||
}
|
||
.card-shine { position: absolute; top: -100%; left: -100%; width: 200%; height: 200%; background: linear-gradient(135deg, transparent 45%, rgba(255,255,255,0.8) 50%, transparent 55%); animation: shine 3s infinite; }
|
||
.card-thumb { width: 80%; height: 80%; border-radius: 12rpx; }
|
||
.card-tag { font-size: 18rpx; color: #333; font-weight: 800; margin-top: 8rpx; @include text-ellipsis(1); }
|
||
|
||
.card-void { width: 100%; height: 100%; display: flex; align-items: center; justify-content: center; opacity: 0.3; }
|
||
.void-circle { width: 40rpx; height: 40rpx; border: 4rpx dashed rgba(255,255,255,0.2); border-radius: 50%; }
|
||
|
||
/* Entry Hall */
|
||
.entry-hall { margin: 0 $spacing-lg; padding: 40rpx 0; }
|
||
.arena-header { padding: 0 40rpx; display: flex; justify-content: space-between; align-items: flex-end; margin-bottom: 40rpx; }
|
||
.hall-title { display: flex; flex-direction: column; }
|
||
.title-main { font-size: 38rpx; font-weight: 900; color: #fff; letter-spacing: 2rpx; }
|
||
.title-sub { font-size: 18rpx; color: rgba(255,255,255,0.3); font-weight: bold; }
|
||
.issue-count { font-size: 20rpx; background: rgba(255,255,255,0.1); color: #ff6b35; padding: 8rpx 20rpx; border-radius: 20rpx; border: 1rpx solid rgba(255, 255, 255, 0.1); }
|
||
|
||
.issue-scroll { width: 100%; margin-bottom: 40rpx; }
|
||
.issue-cards { display: flex; padding: 20rpx 40rpx; gap: 30rpx; }
|
||
.issue-card {
|
||
flex-shrink: 0; width: 260rpx; height: 320rpx; border-radius: 36rpx; background: rgba(255,255,255,0.05); border: 1rpx solid rgba(255,255,255,0.1);
|
||
padding: 30rpx; display: flex; flex-direction: column; position: relative; overflow: hidden;
|
||
transition: all 0.3s;
|
||
|
||
&.active { background: rgba(255,255,255,0.12); border-color: #ff6b35; transform: translateY(-10rpx); box-shadow: 0 20rpx 40rpx rgba(255, 107, 53, 0.2); }
|
||
}
|
||
.card-background-glow { position: absolute; top: -50rpx; right: -50rpx; width: 150rpx; height: 150rpx; background: radial-gradient(circle, rgba(232, 28, 255, 0.1) 0%, transparent 70%); }
|
||
.issue-no { font-size: 20rpx; color: rgba(255,255,255,0.3); font-weight: 900; margin-bottom: 20rpx; }
|
||
.issue-name { font-size: 32rpx; color: #fff; font-weight: bold; line-height: 1.4; flex: 1; }
|
||
.issue-tag { font-size: 18rpx; padding: 6rpx 16rpx; border-radius: 10rpx; align-self: flex-start; background: rgba(255,255,255,0.1); color: rgba(255,255,255,0.6); }
|
||
.issue-tag.status-1 { background: rgba(76, 175, 80, 0.2); color: #81c784; }
|
||
.active-indicator { position: absolute; bottom: 24rpx; right: 24rpx; font-size: 16rpx; color: #ff6b35; font-weight: 900; display: flex; align-items: center; gap: 6rpx; }
|
||
.active-indicator .dot { width: 8rpx; height: 8rpx; background: #ff6b35; border-radius: 50%; animation: pulse 1s infinite; }
|
||
|
||
.hall-tabs { display: flex; padding: 0 40rpx; gap: 60rpx; margin-bottom: 30rpx; }
|
||
.hall-tab {
|
||
font-size: 28rpx; color: rgba(255,255,255,0.4); font-weight: bold; position: relative; padding-bottom: 15rpx;
|
||
&.active { color: #fff; }
|
||
}
|
||
.tab-bar-glow { position: absolute; bottom: 0; left: 0; width: 100%; height: 4rpx; background: #ff6b35; border-radius: 2rpx; box-shadow: 0 0 10rpx #ff6b35; }
|
||
|
||
/* Rewards Mosaic */
|
||
.rewards-mosaic { display: grid; grid-template-columns: repeat(2, 1fr); gap: 20rpx; padding: 0 30rpx; }
|
||
.mosaic-item { background: rgba(255, 255, 255, 0.03); border-radius: 24rpx; padding: 16rpx; }
|
||
.mosaic-inner { display: flex; flex-direction: column; gap: 12rpx; }
|
||
.mosaic-img-box { width: 100%; aspect-ratio: 1; background: rgba(0,0,0,0.2); border-radius: 16rpx; position: relative; overflow: hidden; }
|
||
.mosaic-img { width: 100%; height: 100%; }
|
||
.mosaic-boss { position: absolute; top: 12rpx; right: 12rpx; font-size: 16rpx; background: #ffca28; color: #000; font-weight: 900; padding: 4rpx 10rpx; border-radius: 6rpx; }
|
||
.mosaic-prob { position: absolute; bottom: 12rpx; left: 12rpx; font-size: 14rpx; background: rgba(0,0,0,0.6); color: #fff; padding: 4rpx 12rpx; border-radius: 6rpx; backdrop-filter: blur(4rpx); }
|
||
.mosaic-title { font-size: 22rpx; color: rgba(255,255,255,0.8); font-weight: bold; @include text-ellipsis(1); }
|
||
|
||
/* Dynamic Records */
|
||
.dynamic-records { display: flex; flex-direction: column; gap: 20rpx; padding: 0 30rpx; }
|
||
.win-row { display: flex; align-items: center; background: rgba(255,255,255,0.03); padding: 20rpx; border-radius: 24rpx; }
|
||
.win-avatar-box { width: 70rpx; height: 70rpx; border-radius: 50%; border: 2rpx solid rgba(255,255,255,0.1); overflow: hidden; margin-right: 20rpx; }
|
||
.win-avatar { width: 100%; height: 100%; }
|
||
.win-info { flex: 1; display: flex; flex-direction: column; gap: 4rpx; }
|
||
.win-top { display: flex; align-items: baseline; gap: 12rpx; }
|
||
.win-user { font-size: 22rpx; color: #fff; font-weight: bold; }
|
||
.win-time { font-size: 18rpx; color: rgba(255,255,255,0.3); }
|
||
.win-item-name { font-size: 20rpx; color: #ff6b35; font-weight: bold; }
|
||
.win-thumb { width: 80rpx; height: 80rpx; border-radius: 12rpx; }
|
||
|
||
/* Action Hub */
|
||
.action-hub {
|
||
position: fixed; bottom: 0; left: 0; width: 100%; z-index: 100;
|
||
padding: 40rpx; padding-bottom: calc(40rpx + env(safe-area-inset-bottom));
|
||
background: linear-gradient(to top, #0f0c29 70%, transparent);
|
||
}
|
||
.action-glow { position: absolute; bottom: 0; left: 10%; width: 80%; height: 100rpx; background: rgba(255, 107, 53, 0.3); filter: blur(40rpx); opacity: 0.6; }
|
||
|
||
.gaming-controls {
|
||
display: flex; flex-direction: column; gap: 30rpx; width: 100%;
|
||
}
|
||
.auto-toggle-box {
|
||
align-self: flex-center; display: flex; align-items: center; gap: 20rpx;
|
||
background: rgba(255, 255, 255, 0.05); padding: 12rpx 30rpx; border-radius: 40rpx;
|
||
border: 1rpx solid rgba(255,255,255,0.1);
|
||
}
|
||
.toggle-track {
|
||
width: 80rpx; height: 40rpx; background: rgba(255,255,255,0.1); border-radius: 20rpx;
|
||
position: relative; transition: all 0.3s;
|
||
&.active { background: #4caf50; .toggle-thumb { transform: translateX(40rpx); } }
|
||
}
|
||
.toggle-thumb {
|
||
position: absolute; top: 4rpx; left: 4rpx; width: 32rpx; height: 32rpx;
|
||
background: #fff; border-radius: 50%; transition: all 0.3s;
|
||
box-shadow: 0 0 10rpx rgba(0,0,0,0.3);
|
||
}
|
||
.toggle-label { font-size: 22rpx; color: #fff; font-weight: 800; }
|
||
|
||
/* Action Hub Standard Buttons */
|
||
.hub-btn {
|
||
width: 100%; height: 110rpx; border-radius: 55rpx; display: flex; align-items: center; justify-content: center;
|
||
font-weight: 900; font-size: 32rpx; letter-spacing: 2rpx; position: relative; overflow: hidden; transition: all 0.2s;
|
||
|
||
&:active { transform: scale(0.96); }
|
||
}
|
||
|
||
.neon-btn {
|
||
background: linear-gradient(90deg, #FF6B35, #E81CFF);
|
||
color: #fff; box-shadow: 0 15rpx 40rpx rgba(232, 28, 255, 0.4);
|
||
}
|
||
.btn-energy-line { position: absolute; bottom: 0; left: 0; width: 100%; height: 6rpx; background: rgba(255,255,255,0.3); animation: energyFlow 2s linear infinite; }
|
||
|
||
.primary-btn { background: #fff; color: #000; box-shadow: 0 10rpx 30rpx rgba(255,255,255,0.2); }
|
||
.secondary-btn { background: rgba(255,255,255,0.1); color: #fff; }
|
||
.gold-btn { background: $gradient-gold; color: #000; }
|
||
|
||
.game-over-hub { display: flex; gap: 20rpx; .hub-btn { flex: 1; } }
|
||
|
||
/* Modals */
|
||
.modal-layer { position: fixed; top: 0; left: 0; width: 100%; height: 100%; z-index: 1000; background: rgba(0,0,0,0.85); backdrop-filter: blur(10px); display: flex; align-items: center; justify-content: center; }
|
||
.premium-modal { width: 620rpx; max-height: 80vh; background: #1a1a2e; border: 1rpx solid rgba(255,255,255,0.1); border-radius: 50rpx; padding: 50rpx; display: flex; flex-direction: column; }
|
||
.modal-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 40rpx; }
|
||
.modal-title { font-size: 36rpx; color: #fff; font-weight: 900; }
|
||
.close-x { font-size: 40rpx; color: rgba(255,255,255,0.3); line-height: 1; }
|
||
|
||
.modal-scroll { flex: 1; overflow: hidden; }
|
||
.rule-item { display: flex; gap: 30rpx; margin-bottom: 40rpx; }
|
||
.rule-no { font-size: 32rpx; font-weight: 900; color: #ff6b35; font-family: 'DIN Alternate'; }
|
||
.rule-content { font-size: 26rpx; color: rgba(255,255,255,0.7); line-height: 1.6; }
|
||
.modal-confirm { width: 100%; height: 90rpx; border-radius: 45rpx; background: #fff; color: #000; font-weight: 900; margin-top: 40rpx; }
|
||
|
||
/* Settlement Card */
|
||
.settlement-card { width: 600rpx; padding: 80rpx 50rpx; display: flex; flex-direction: column; align-items: center; text-align: center; }
|
||
.settlement-crown { font-size: 120rpx; margin-bottom: 40rpx; filter: drop-shadow(0 0 20rpx #ffea00); }
|
||
.settlement-title { font-size: 44rpx; color: #fff; font-weight: 900; margin-bottom: 60rpx; }
|
||
.settlement-stats { margin-bottom: 60rpx; }
|
||
.set-stat { display: flex; flex-direction: column; align-items: center; }
|
||
.set-val { font-size: 100rpx; color: #ff6b35; font-weight: 900; font-family: 'DIN Alternate'; line-height: 1; }
|
||
.set-lab { font-size: 22rpx; color: rgba(255,255,255,0.4); margin-top: 10rpx; }
|
||
.settlement-tip { font-size: 24rpx; color: rgba(255,255,255,0.5); margin-bottom: 60rpx; }
|
||
.set-btn { width: 100%; height: 100rpx; border-radius: 50rpx; background: linear-gradient(90deg, #FF6B35, #E81CFF); color: #fff; font-weight: bold; }
|
||
|
||
/* Animations */
|
||
@keyframes cardDraw { from { transform: translateY(200rpx) scale(0.5); opacity: 0; } to { transform: translateY(0) scale(1.0); opacity: 1; } }
|
||
@keyframes energyFlow { from { transform: translateX(-100%); } to { transform: translateX(100%); } }
|
||
@keyframes shine { 0% { left: -100%; } 20% { left: 120%; } 100% { left: 120%; } }
|
||
@keyframes rotate { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }
|
||
@keyframes pulse { 0% { opacity: 0.6; transform: scale(1); } 50% { opacity: 1; transform: scale(1.05); } 100% { opacity: 0.6; transform: scale(1); } }
|
||
|
||
.animate-fade-in { animation: fadeIn 0.8s ease-out; }
|
||
@keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
|
||
|
||
.animate-slide-up { animation: slideUp 0.6s cubic-bezier(0.175, 0.885, 0.32, 1.275) backwards; }
|
||
@keyframes slideUp { from { transform: translateY(100rpx); opacity: 0; } to { transform: translateY(0); opacity: 1; } }
|
||
|
||
.animate-bounce-in { animation: bounceIn 0.8s cubic-bezier(0.175, 0.885, 0.32, 1.275) backwards; }
|
||
@keyframes bounceIn { 0% { transform: scale(0.3); opacity: 0; } 50% { transform: scale(1.05); opacity: 1; } 70% { transform: scale(0.9); } 100% { transform: scale(1); } }
|
||
|
||
.animate-zoom-in { animation: zoomIn 0.5s cubic-bezier(0.175, 0.885, 0.32, 1.275) backwards; }
|
||
@keyframes zoomIn { from { transform: scale(0.8); opacity: 0; } to { transform: scale(1); opacity: 1; } }
|
||
|
||
.matching-pulse { animation: mPulse 1.5s infinite; }
|
||
@keyframes mPulse { 0% { opacity: 1; } 50% { opacity: 0.6; } 100% { opacity: 1; } }
|
||
|
||
.pulsate { animation: pulsate 2s infinite ease-in-out; }
|
||
@keyframes pulsate {
|
||
0% { transform: scale(1); box-shadow: 0 15rpx 40rpx rgba(232, 28, 255, 0.4); }
|
||
50% { transform: scale(1.02); box-shadow: 0 25rpx 60rpx rgba(232, 28, 255, 0.6); }
|
||
100% { transform: scale(1); box-shadow: 0 15rpx 40rpx rgba(232, 28, 255, 0.4); }
|
||
}
|
||
</style>
|