feat: 添加扫雷游戏功能并更新相关页面
- 新增扫雷游戏页面和组件 - 更新首页游戏入口为扫雷挑战 - 添加测试登录按钮用于开发环境 - 修改请求基础URL为本地开发环境 - 在订单详情页添加抽奖凭证展示
This commit is contained in:
parent
2571d4a698
commit
0e174f220b
248
components/MatchingGame.vue
Normal file
248
components/MatchingGame.vue
Normal file
@ -0,0 +1,248 @@
|
|||||||
|
<template>
|
||||||
|
<view class="matching-game-overlay" v-if="visible" @touchmove.stop.prevent>
|
||||||
|
<view class="game-mask"></view>
|
||||||
|
<view class="game-container">
|
||||||
|
<view class="game-header">
|
||||||
|
<text class="game-title">翻牌配对</text>
|
||||||
|
<view class="game-stats">
|
||||||
|
<text>已配对: {{ pairsFound }}</text>
|
||||||
|
<text>剩余: {{ cards.length / 2 - pairsFound }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="game-grid" :style="{ gridTemplateColumns: `repeat(${gridCols}, 1fr)` }">
|
||||||
|
<view
|
||||||
|
v-for="(card, index) in gameCards"
|
||||||
|
:key="index"
|
||||||
|
class="game-card"
|
||||||
|
:class="{ flipped: card.flipped || card.matched, matched: card.matched }"
|
||||||
|
@tap="onCardTap(index)"
|
||||||
|
>
|
||||||
|
<view class="card-inner">
|
||||||
|
<view class="card-front">
|
||||||
|
<view class="pattern"></view>
|
||||||
|
</view>
|
||||||
|
<view class="card-back">
|
||||||
|
<image :src="card.image" mode="aspectFit" class="card-img" />
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<button class="submit-btn" @tap="forceSubmit" v-if="gameOver">领 取 奖 励</button>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, computed, watch, onMounted } from 'vue'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
visible: { type: Boolean, default: false },
|
||||||
|
cards: { type: Array, default: () => [] }, // Array of { id, image, type... }
|
||||||
|
gameId: { type: String, default: '' }
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits(['finish', 'close'])
|
||||||
|
|
||||||
|
const gameCards = ref([])
|
||||||
|
const pairsFound = ref(0)
|
||||||
|
const selectedIndices = ref([])
|
||||||
|
const isProcessing = ref(false)
|
||||||
|
const gameOver = ref(false)
|
||||||
|
|
||||||
|
const gridCols = computed(() => {
|
||||||
|
const len = props.cards.length
|
||||||
|
if (len <= 9) return 3
|
||||||
|
if (len <= 16) return 4
|
||||||
|
return 4
|
||||||
|
})
|
||||||
|
|
||||||
|
watch(() => props.visible, (val) => {
|
||||||
|
if (val) initGame()
|
||||||
|
})
|
||||||
|
|
||||||
|
function initGame() {
|
||||||
|
// Initialize game cards with state
|
||||||
|
// Assuming props.cards is already the shuffled list of cards for the board
|
||||||
|
let list = JSON.parse(JSON.stringify(props.cards))
|
||||||
|
|
||||||
|
// If we only got types, we might need to duplicate them?
|
||||||
|
// User says "initializes the game session with shuffled cards".
|
||||||
|
// I assume the server sends the exact layout.
|
||||||
|
|
||||||
|
gameCards.value = list.map(c => ({
|
||||||
|
...c,
|
||||||
|
flipped: false,
|
||||||
|
matched: false
|
||||||
|
}))
|
||||||
|
|
||||||
|
pairsFound.value = 0
|
||||||
|
selectedIndices.value = []
|
||||||
|
isProcessing.value = false
|
||||||
|
gameOver.value = false
|
||||||
|
|
||||||
|
// Flash all cards briefly?
|
||||||
|
setTimeout(() => {
|
||||||
|
gameCards.value.forEach(c => c.flipped = true)
|
||||||
|
setTimeout(() => {
|
||||||
|
gameCards.value.forEach(c => c.flipped = false)
|
||||||
|
}, 2000)
|
||||||
|
}, 500)
|
||||||
|
}
|
||||||
|
|
||||||
|
function onCardTap(index) {
|
||||||
|
if (isProcessing.value) return
|
||||||
|
const card = gameCards.value[index]
|
||||||
|
if (card.flipped || card.matched) return
|
||||||
|
|
||||||
|
// Flip card
|
||||||
|
card.flipped = true
|
||||||
|
selectedIndices.value.push(index)
|
||||||
|
|
||||||
|
if (selectedIndices.value.length === 2) {
|
||||||
|
checkMatch()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkMatch() {
|
||||||
|
isProcessing.value = true
|
||||||
|
const [idx1, idx2] = selectedIndices.value
|
||||||
|
const card1 = gameCards.value[idx1]
|
||||||
|
const card2 = gameCards.value[idx2]
|
||||||
|
|
||||||
|
// Assuming 'title' or 'id' connects them.
|
||||||
|
// Better use an explicit 'type' or compare 'title/image'.
|
||||||
|
// Using image as the matcher for now if no type.
|
||||||
|
const isMatch = (card1.type && card1.type === card2.type) || (card1.image === card2.image)
|
||||||
|
|
||||||
|
if (isMatch) {
|
||||||
|
setTimeout(() => {
|
||||||
|
card1.matched = true
|
||||||
|
card2.matched = true
|
||||||
|
pairsFound.value++
|
||||||
|
selectedIndices.value = []
|
||||||
|
isProcessing.value = false
|
||||||
|
checkGameOver()
|
||||||
|
}, 500)
|
||||||
|
} else {
|
||||||
|
setTimeout(() => {
|
||||||
|
card1.flipped = false
|
||||||
|
card2.flipped = false
|
||||||
|
selectedIndices.value = []
|
||||||
|
isProcessing.value = false
|
||||||
|
}, 1000)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkGameOver() {
|
||||||
|
// Check if all pairs found
|
||||||
|
// Note: If odd number of cards (9), 1 will remain.
|
||||||
|
const totalPairsPossible = Math.floor(props.cards.length / 2)
|
||||||
|
if (pairsFound.value >= totalPairsPossible) {
|
||||||
|
gameOver.value = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function forceSubmit() {
|
||||||
|
emit('finish', {
|
||||||
|
gameId: props.gameId,
|
||||||
|
totalPairs: pairsFound.value
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.matching-game-overlay {
|
||||||
|
position: fixed; top: 0; left: 0; right: 0; bottom: 0;
|
||||||
|
z-index: 10000;
|
||||||
|
display: flex; align-items: center; justify-content: center;
|
||||||
|
}
|
||||||
|
.game-mask {
|
||||||
|
position: absolute; top: 0; left: 0; width: 100%; height: 100%;
|
||||||
|
background: rgba(0,0,0,0.85);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
}
|
||||||
|
.game-container {
|
||||||
|
position: relative; z-index: 10;
|
||||||
|
width: 680rpx;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 32rpx;
|
||||||
|
padding: 40rpx;
|
||||||
|
box-shadow: 0 20rpx 60rpx rgba(0,0,0,0.3);
|
||||||
|
animation: popIn 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275);
|
||||||
|
}
|
||||||
|
.game-header {
|
||||||
|
display: flex; justify-content: space-between; align-items: center;
|
||||||
|
margin-bottom: 40rpx;
|
||||||
|
}
|
||||||
|
.game-title {
|
||||||
|
font-size: 40rpx; font-weight: 800; color: #333;
|
||||||
|
}
|
||||||
|
.game-stats {
|
||||||
|
font-size: 28rpx; color: #666; font-weight: 600;
|
||||||
|
}
|
||||||
|
.game-grid {
|
||||||
|
display: grid; gap: 20rpx;
|
||||||
|
margin-bottom: 40rpx;
|
||||||
|
}
|
||||||
|
.game-card {
|
||||||
|
aspect-ratio: 1;
|
||||||
|
perspective: 1000rpx;
|
||||||
|
}
|
||||||
|
.card-inner {
|
||||||
|
position: relative; width: 100%; height: 100%;
|
||||||
|
transform-style: preserve-3d;
|
||||||
|
transition: transform 0.5s;
|
||||||
|
}
|
||||||
|
.game-card.flipped .card-inner {
|
||||||
|
transform: rotateY(180deg);
|
||||||
|
}
|
||||||
|
.game-card.matched .card-inner {
|
||||||
|
transform: rotateY(180deg);
|
||||||
|
}
|
||||||
|
.game-card.matched {
|
||||||
|
animation: pulse 1s infinite;
|
||||||
|
}
|
||||||
|
.card-front, .card-back {
|
||||||
|
position: absolute; top: 0; left: 0; width: 100%; height: 100%;
|
||||||
|
backface-visibility: hidden;
|
||||||
|
border-radius: 16rpx;
|
||||||
|
box-shadow: 0 4rpx 10rpx rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
.card-front {
|
||||||
|
background: linear-gradient(135deg, #ff9a9e 0%, #fecfef 99%, #fecfef 100%);
|
||||||
|
display: flex; align-items: center; justify-content: center;
|
||||||
|
}
|
||||||
|
.pattern {
|
||||||
|
width: 60%; height: 60%;
|
||||||
|
background: rgba(255,255,255,0.3);
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
.card-back {
|
||||||
|
background: #fff;
|
||||||
|
transform: rotateY(180deg);
|
||||||
|
display: flex; align-items: center; justify-content: center;
|
||||||
|
padding: 10rpx;
|
||||||
|
}
|
||||||
|
.card-img {
|
||||||
|
width: 80%; height: 80%;
|
||||||
|
}
|
||||||
|
.submit-btn {
|
||||||
|
background: linear-gradient(90deg, #ff758c 0%, #ff7eb3 100%);
|
||||||
|
color: #fff;
|
||||||
|
font-weight: 800;
|
||||||
|
border-radius: 50rpx;
|
||||||
|
margin-top: 20rpx;
|
||||||
|
box-shadow: 0 10rpx 20rpx rgba(255, 117, 140, 0.4);
|
||||||
|
}
|
||||||
|
@keyframes popIn {
|
||||||
|
from { transform: scale(0.8); opacity: 0; }
|
||||||
|
to { transform: scale(1); opacity: 1; }
|
||||||
|
}
|
||||||
|
@keyframes pulse {
|
||||||
|
0% { transform: scale(1); }
|
||||||
|
50% { transform: scale(1.05); }
|
||||||
|
100% { transform: scale(1); }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -114,6 +114,14 @@
|
|||||||
"navigationBarTitleText": "爬塔"
|
"navigationBarTitleText": "爬塔"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"path": "pages/game/webview",
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "游戏挑战",
|
||||||
|
"navigationBarBackgroundColor": "#000000",
|
||||||
|
"navigationBarTextStyle": "white"
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"path": "pages/register/register",
|
"path": "pages/register/register",
|
||||||
"style": {
|
"style": {
|
||||||
|
|||||||
@ -9,8 +9,8 @@
|
|||||||
<!-- 顶部信息 -->
|
<!-- 顶部信息 -->
|
||||||
<view class="header-section">
|
<view class="header-section">
|
||||||
<view class="title-box">
|
<view class="title-box">
|
||||||
<text class="main-title">{{ detail.name || detail.title || '爬塔挑战' }}</text>
|
<text class="main-title">扫雷挑战</text>
|
||||||
<text class="sub-title">层层突围 赢取大奖</text>
|
<text class="sub-title">福利放送 智勇通关</text>
|
||||||
</view>
|
</view>
|
||||||
<view class="rule-btn" @tap="showRules">规则</view>
|
<view class="rule-btn" @tap="showRules">规则</view>
|
||||||
</view>
|
</view>
|
||||||
@ -20,225 +20,101 @@
|
|||||||
<view class="tower-level current">
|
<view class="tower-level current">
|
||||||
<view class="level-info">
|
<view class="level-info">
|
||||||
<text class="level-num">当前挑战</text>
|
<text class="level-num">当前挑战</text>
|
||||||
<text class="level-name">{{ currentIssueTitle || '第1层' }}</text>
|
<text class="level-name">扫雷福利局</text>
|
||||||
</view>
|
</view>
|
||||||
<view class="level-status">进行中</view>
|
<view class="level-status">进行中</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<!-- 奖池预览 -->
|
<!-- 剩余次数展示 -->
|
||||||
<view class="rewards-preview" v-if="currentIssueRewards.length">
|
<view class="ticket-info">
|
||||||
<scroll-view scroll-x class="rewards-scroll">
|
<text class="ticket-label">剩余挑战次数</text>
|
||||||
<view class="reward-item" v-for="(r, idx) in currentIssueRewards" :key="idx">
|
<text class="ticket-count">{{ remainingTimes }}</text>
|
||||||
<image class="reward-img" :src="r.image" mode="aspectFill" />
|
|
||||||
<view class="reward-name">{{ r.title }}</view>
|
|
||||||
<view class="reward-prob" v-if="r.percent">概率 {{ r.percent }}%</view>
|
|
||||||
</view>
|
|
||||||
</scroll-view>
|
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<!-- 操作区 -->
|
<!-- 操作区 -->
|
||||||
<view class="action-area">
|
<view class="action-area">
|
||||||
<view class="price-display">
|
<button class="challenge-btn" :disabled="!canPlay" :class="{ disabled: !canPlay }" @tap="onStartChallenge">
|
||||||
<text class="currency">¥</text>
|
{{ canPlay ? '开始挑战' : '去获取资格' }}
|
||||||
<text class="amount">{{ (Number(detail.price_draw || 0) / 100).toFixed(2) }}</text>
|
|
||||||
<text class="unit">/次</text>
|
|
||||||
</view>
|
|
||||||
<button class="challenge-btn" :loading="drawLoading" @tap="onStartChallenge">
|
|
||||||
立即挑战
|
|
||||||
</button>
|
</button>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<!-- 结果弹窗 -->
|
|
||||||
<view v-if="showFlip" class="flip-overlay" @touchmove.stop.prevent>
|
|
||||||
<view class="flip-mask" @tap="closeFlip"></view>
|
|
||||||
<view class="flip-content">
|
|
||||||
<FlipGrid ref="flipRef" :rewards="winItems" :controls="false" />
|
|
||||||
<button class="close-btn" @tap="closeFlip">收下奖励</button>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
|
|
||||||
<PaymentPopup
|
|
||||||
v-model:visible="paymentVisible"
|
|
||||||
:amount="paymentAmount"
|
|
||||||
:coupons="coupons"
|
|
||||||
:propCards="propCards"
|
|
||||||
@confirm="onPaymentConfirm"
|
|
||||||
/>
|
|
||||||
</view>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, computed, onMounted } from 'vue'
|
import { ref, computed, onMounted } from 'vue'
|
||||||
import { onLoad } from '@dcloudio/uni-app'
|
import { onLoad, onShow } from '@dcloudio/uni-app'
|
||||||
import FlipGrid from '../../../components/FlipGrid.vue'
|
import { getActivityDetail } from '../../../api/appUser'
|
||||||
import PaymentPopup from '../../../components/PaymentPopup.vue'
|
|
||||||
import { getActivityDetail, getActivityIssues, getActivityIssueRewards, joinLottery, createWechatOrder, getLotteryResult, getItemCards, getUserCoupons } from '../../../api/appUser'
|
|
||||||
|
|
||||||
const activityId = ref('')
|
const activityId = ref('')
|
||||||
const detail = ref({})
|
const detail = ref({})
|
||||||
const issues = ref([])
|
const remainingTimes = ref(0) // 模拟剩余次数
|
||||||
const currentIssueId = ref('')
|
const ticketId = ref('') // 模拟入场券ID
|
||||||
const rewardsMap = ref({})
|
|
||||||
const drawLoading = ref(false)
|
|
||||||
const showFlip = ref(false)
|
|
||||||
const winItems = ref([])
|
|
||||||
const flipRef = ref(null)
|
|
||||||
|
|
||||||
// Payment
|
const canPlay = computed(() => remainingTimes.value > 0)
|
||||||
const paymentVisible = ref(false)
|
|
||||||
const paymentAmount = ref('0.00')
|
|
||||||
const coupons = ref([])
|
|
||||||
const propCards = ref([])
|
|
||||||
const selectedCoupon = ref(null)
|
|
||||||
const selectedCard = ref(null)
|
|
||||||
const pendingCount = ref(1)
|
|
||||||
|
|
||||||
const currentIssueTitle = computed(() => {
|
|
||||||
const i = issues.value.find(x => x.id === currentIssueId.value)
|
|
||||||
return i ? (i.title || `第${i.no}期`) : ''
|
|
||||||
})
|
|
||||||
|
|
||||||
const currentIssueRewards = computed(() => {
|
|
||||||
return (currentIssueId.value && rewardsMap.value[currentIssueId.value]) || []
|
|
||||||
})
|
|
||||||
|
|
||||||
const priceVal = computed(() => Number(detail.value.price_draw || 0) / 100)
|
|
||||||
|
|
||||||
async function loadData(id) {
|
async function loadData(id) {
|
||||||
try {
|
try {
|
||||||
const d = await getActivityDetail(id)
|
const d = await getActivityDetail(id)
|
||||||
detail.value = d || {}
|
detail.value = d || {}
|
||||||
|
|
||||||
const is = await getActivityIssues(id)
|
|
||||||
issues.value = normalizeIssues(is)
|
|
||||||
|
|
||||||
if (issues.value.length) {
|
|
||||||
const first = issues.value[0]
|
|
||||||
currentIssueId.value = first.id
|
|
||||||
loadRewards(id, first.id)
|
|
||||||
}
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e)
|
console.error(e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadRewards(aid, iid) {
|
// 模拟检查用户是否有资格
|
||||||
try {
|
async function checkEligibility() {
|
||||||
const res = await getActivityIssueRewards(aid, iid)
|
// TODO: Replace with actual API call to check bonus/ticket status
|
||||||
rewardsMap.value[iid] = normalizeRewards(res)
|
// e.g. const res = await getMinesweeperEligibility()
|
||||||
} catch (e) {}
|
|
||||||
|
// 模拟数据:假设用户有1次机会
|
||||||
|
setTimeout(() => {
|
||||||
|
remainingTimes.value = 1
|
||||||
|
ticketId.value = 'mock-ticket-123456'
|
||||||
|
}, 500)
|
||||||
}
|
}
|
||||||
|
|
||||||
function onStartChallenge() {
|
function onStartChallenge() {
|
||||||
|
if (!canPlay.value) {
|
||||||
|
uni.showToast({ title: '去玩其他游戏赢取资格吧!', icon: 'none' })
|
||||||
|
// TODO: Navigate to other games or shop
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const token = uni.getStorageSync('token')
|
const token = uni.getStorageSync('token')
|
||||||
const phoneBound = !!uni.getStorageSync('phone_bound')
|
if (!token) {
|
||||||
if (!token || !phoneBound) {
|
|
||||||
uni.showToast({ title: '请先登录', icon: 'none' })
|
uni.showToast({ title: '请先登录', icon: 'none' })
|
||||||
// In real app, redirect to login
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!currentIssueId.value) {
|
// Navigate to WebView Game
|
||||||
uni.showToast({ title: '暂无挑战场次', icon: 'none' })
|
// TODO: Replace with real game URL
|
||||||
return
|
const gameUrl = 'http://localhost:5174/'
|
||||||
}
|
|
||||||
|
|
||||||
paymentAmount.value = priceVal.value.toFixed(2)
|
uni.navigateTo({
|
||||||
pendingCount.value = 1
|
url: `/pages/game/webview?url=${encodeURIComponent(gameUrl)}&ticket=${ticketId.value}`
|
||||||
paymentVisible.value = true
|
|
||||||
// Fetch coupons/cards in background
|
|
||||||
fetchPropCards()
|
|
||||||
fetchCoupons()
|
|
||||||
}
|
|
||||||
|
|
||||||
async function onPaymentConfirm(data) {
|
|
||||||
selectedCoupon.value = data?.coupon || null
|
|
||||||
selectedCard.value = data?.card || null
|
|
||||||
paymentVisible.value = false
|
|
||||||
await doDraw()
|
|
||||||
}
|
|
||||||
|
|
||||||
async function doDraw() {
|
|
||||||
drawLoading.value = true
|
|
||||||
try {
|
|
||||||
const openid = uni.getStorageSync('openid')
|
|
||||||
const joinRes = await joinLottery({
|
|
||||||
activity_id: Number(activityId.value),
|
|
||||||
issue_id: Number(currentIssueId.value),
|
|
||||||
channel: 'miniapp',
|
|
||||||
count: 1,
|
|
||||||
coupon_id: selectedCoupon.value?.id ? Number(selectedCoupon.value.id) : 0
|
|
||||||
})
|
})
|
||||||
|
|
||||||
if (!joinRes) throw new Error('下单失败')
|
|
||||||
const orderNo = joinRes.order_no || joinRes.data?.order_no || joinRes.result?.order_no
|
|
||||||
|
|
||||||
// Simulate Wechat Pay flow (simplified)
|
|
||||||
const payRes = await createWechatOrder({ openid, order_no: orderNo })
|
|
||||||
await new Promise((resolve, reject) => {
|
|
||||||
uni.requestPayment({
|
|
||||||
provider: 'wxpay',
|
|
||||||
...payRes,
|
|
||||||
success: resolve,
|
|
||||||
fail: reject
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
// Get Result
|
|
||||||
const res = await getLotteryResult(orderNo)
|
|
||||||
const raw = res.list || res.items || res.data || res.result || (Array.isArray(res) ? res : [res])
|
|
||||||
winItems.value = raw.map(i => ({
|
|
||||||
title: i.title || i.name || '未知奖励',
|
|
||||||
image: i.image || i.img || ''
|
|
||||||
}))
|
|
||||||
|
|
||||||
showFlip.value = true
|
|
||||||
setTimeout(() => {
|
|
||||||
if(flipRef.value && flipRef.value.revealResults) flipRef.value.revealResults(winItems.value)
|
|
||||||
}, 100)
|
|
||||||
|
|
||||||
} catch (e) {
|
|
||||||
uni.showToast({ title: e.message || '挑战失败', icon: 'none' })
|
|
||||||
} finally {
|
|
||||||
drawLoading.value = false
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
function normalizeIssues(list) {
|
|
||||||
if (!Array.isArray(list)) return []
|
|
||||||
return list.map(i => ({
|
|
||||||
id: i.id,
|
|
||||||
title: i.title || i.name,
|
|
||||||
no: i.no,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
function normalizeRewards(list) {
|
|
||||||
if (!Array.isArray(list)) return []
|
|
||||||
return list.map(i => ({
|
|
||||||
title: i.name || i.title,
|
|
||||||
image: i.image || i.img || i.pic,
|
|
||||||
percent: i.percent || 0
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
async function fetchPropCards() { /* implementation same as other pages */ }
|
|
||||||
async function fetchCoupons() { /* implementation same as other pages */ }
|
|
||||||
|
|
||||||
function showRules() {
|
function showRules() {
|
||||||
uni.showModal({ title: '规则', content: detail.value.rules || '暂无规则', showCancel: false })
|
uni.showModal({
|
||||||
|
title: '规则',
|
||||||
|
content: '1. 参与平台其他游戏有机会获得扫雷挑战资格。\n2. 挑战成功可获得丰厚奖励。\n3. 扫雷过程中请保持网络通畅。',
|
||||||
|
showCancel: false
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function closeFlip() { showFlip.value = false }
|
|
||||||
|
|
||||||
onLoad((opts) => {
|
onLoad((opts) => {
|
||||||
if (opts.id) {
|
if (opts.id) {
|
||||||
activityId.value = opts.id
|
activityId.value = opts.id
|
||||||
loadData(opts.id)
|
loadData(opts.id)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
onShow(() => {
|
||||||
|
checkEligibility()
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
@ -429,50 +305,25 @@ $local-gold: #FFD700; // 特殊金色,比全局更亮
|
|||||||
z-index: 1;
|
z-index: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.rewards-preview {
|
.ticket-info {
|
||||||
width: 100%;
|
display: flex;
|
||||||
margin-top: 40rpx;
|
|
||||||
}
|
|
||||||
.rewards-scroll {
|
|
||||||
white-space: nowrap;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
.reward-item {
|
|
||||||
display: inline-flex;
|
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
width: 160rpx;
|
margin-top: 40rpx;
|
||||||
margin-right: 24rpx;
|
|
||||||
animation: fadeInUp 0.5s ease-out backwards;
|
animation: fadeInUp 0.5s ease-out backwards;
|
||||||
|
animation-delay: 0.2s;
|
||||||
@for $i from 1 through 5 {
|
|
||||||
&:nth-child(#{$i}) {
|
|
||||||
animation-delay: #{$i * 0.1}s;
|
|
||||||
}
|
}
|
||||||
}
|
.ticket-label {
|
||||||
}
|
font-size: 28rpx;
|
||||||
.reward-img {
|
|
||||||
width: 120rpx; height: 120rpx;
|
|
||||||
border-radius: 24rpx;
|
|
||||||
background: rgba(255,255,255,0.05);
|
|
||||||
margin-bottom: 16rpx;
|
|
||||||
border: 1px solid $border-dark;
|
|
||||||
box-shadow: 0 4rpx 12rpx rgba(0,0,0,0.2);
|
|
||||||
}
|
|
||||||
.reward-name {
|
|
||||||
font-size: 22rpx;
|
|
||||||
color: $text-dark-sub;
|
color: $text-dark-sub;
|
||||||
width: 100%;
|
margin-bottom: 10rpx;
|
||||||
text-align: center;
|
|
||||||
overflow: hidden;
|
|
||||||
white-space: nowrap;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
}
|
}
|
||||||
.reward-prob {
|
.ticket-count {
|
||||||
font-size: 20rpx;
|
font-size: 80rpx;
|
||||||
|
font-weight: 900;
|
||||||
color: $local-gold;
|
color: $local-gold;
|
||||||
font-weight: 600;
|
font-family: 'DIN Alternate', sans-serif;
|
||||||
margin-top: 4rpx;
|
text-shadow: 0 0 20rpx rgba($local-gold, 0.4);
|
||||||
}
|
}
|
||||||
|
|
||||||
.action-area {
|
.action-area {
|
||||||
@ -482,24 +333,13 @@ $local-gold: #FFD700; // 特殊金色,比全局更亮
|
|||||||
border-radius: 100rpx;
|
border-radius: 100rpx;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: center;
|
||||||
border: 1px solid $border-dark;
|
border: 1px solid $border-dark;
|
||||||
box-shadow: 0 20rpx 60rpx rgba(0,0,0,0.5);
|
box-shadow: 0 20rpx 60rpx rgba(0,0,0,0.5);
|
||||||
margin-bottom: calc(env(safe-area-inset-bottom) + 20rpx);
|
margin-bottom: calc(env(safe-area-inset-bottom) + 20rpx);
|
||||||
animation: slideUp 0.6s ease-out backwards;
|
animation: slideUp 0.6s ease-out backwards;
|
||||||
animation-delay: 0.3s;
|
animation-delay: 0.3s;
|
||||||
}
|
}
|
||||||
.price-display {
|
|
||||||
display: flex;
|
|
||||||
align-items: baseline;
|
|
||||||
color: $local-gold;
|
|
||||||
font-weight: 700;
|
|
||||||
margin-left: 20rpx;
|
|
||||||
text-shadow: 0 0 20rpx rgba(255, 215, 0, 0.2);
|
|
||||||
}
|
|
||||||
.currency { font-size: 28rpx; }
|
|
||||||
.amount { font-size: 48rpx; margin: 0 4rpx; font-family: 'DIN Alternate', sans-serif; }
|
|
||||||
.unit { font-size: 24rpx; opacity: 0.8; font-weight: normal; }
|
|
||||||
|
|
||||||
.challenge-btn {
|
.challenge-btn {
|
||||||
background: $gradient-brand;
|
background: $gradient-brand;
|
||||||
@ -515,6 +355,7 @@ $local-gold: #FFD700; // 特殊金色,比全局更亮
|
|||||||
position: relative;
|
position: relative;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
transition: all 0.2s;
|
transition: all 0.2s;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
&::after {
|
&::after {
|
||||||
content: '';
|
content: '';
|
||||||
@ -528,41 +369,12 @@ $local-gold: #FFD700; // 特殊金色,比全局更亮
|
|||||||
transform: scale(0.96);
|
transform: scale(0.96);
|
||||||
box-shadow: 0 4rpx 12rpx rgba(255, 107, 0, 0.2);
|
box-shadow: 0 4rpx 12rpx rgba(255, 107, 0, 0.2);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
.flip-overlay {
|
&.disabled {
|
||||||
position: fixed; top: 0; left: 0; right: 0; bottom: 0; z-index: 999;
|
background: #555;
|
||||||
}
|
color: #999;
|
||||||
.flip-mask {
|
box-shadow: none;
|
||||||
position: absolute; top: 0; bottom: 0; width: 100%; background: rgba(0,0,0,0.85);
|
&::after { display: none; }
|
||||||
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: zoomIn 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275);
|
|
||||||
}
|
|
||||||
.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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
58
pages/game/webview.vue
Normal file
58
pages/game/webview.vue
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
<template>
|
||||||
|
<view class="container">
|
||||||
|
<web-view :src="url" @message="onMessage"></web-view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import { onLoad } from '@dcloudio/uni-app'
|
||||||
|
|
||||||
|
const url = ref('')
|
||||||
|
|
||||||
|
onLoad((options) => {
|
||||||
|
if (options.url) {
|
||||||
|
let targetUrl = decodeURIComponent(options.url)
|
||||||
|
|
||||||
|
// Append auth info if not present
|
||||||
|
const token = uni.getStorageSync('token')
|
||||||
|
const uid = uni.getStorageSync('user_id')
|
||||||
|
|
||||||
|
const hasQuery = targetUrl.includes('?')
|
||||||
|
const separator = hasQuery ? '&' : '?'
|
||||||
|
|
||||||
|
// Append standard auth params for the game to consume
|
||||||
|
if (token) targetUrl += `${separator}token=${encodeURIComponent(token)}`
|
||||||
|
if (uid) targetUrl += `&uid=${encodeURIComponent(uid)}`
|
||||||
|
// Append ticket if provided
|
||||||
|
if (options.ticket) targetUrl += `&ticket=${encodeURIComponent(options.ticket)}`
|
||||||
|
|
||||||
|
console.log('Opening Game WebView:', targetUrl)
|
||||||
|
url.value = targetUrl
|
||||||
|
} else {
|
||||||
|
uni.showToast({ title: '游戏地址无效', icon: 'none' })
|
||||||
|
setTimeout(() => uni.navigateBack(), 1500)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
function onMessage(e) {
|
||||||
|
console.log('Message from Game:', e.detail)
|
||||||
|
const data = e.detail.data || []
|
||||||
|
|
||||||
|
// Handle specific messages
|
||||||
|
data.forEach(msg => {
|
||||||
|
if (msg.action === 'close') {
|
||||||
|
uni.navigateBack()
|
||||||
|
} else if (msg.action === 'game_over') {
|
||||||
|
// Optional: Refresh user balance or state
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.container {
|
||||||
|
width: 100%;
|
||||||
|
height: 100vh;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -77,10 +77,10 @@
|
|||||||
<image class="card-icon-small" src="https://via.placeholder.com/80/FFB6C1/000000?text=Match" mode="aspectFit" />
|
<image class="card-icon-small" src="https://via.placeholder.com/80/FFB6C1/000000?text=Match" mode="aspectFit" />
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<view class="game-card-small card-tower" @tap="navigateTo('/pages/activity/list/index?category=爬塔')">
|
<view class="game-card-small card-tower" @tap="navigateTo('/pages/activity/pata/index')">
|
||||||
<text class="card-title-small">爬塔</text>
|
<text class="card-title-small">扫雷</text>
|
||||||
<text class="card-subtitle-small">层层挑战</text>
|
<text class="card-subtitle-small">福利挑战</text>
|
||||||
<image class="card-icon-small" src="https://via.placeholder.com/80/9370DB/000000?text=Tower" mode="aspectFit" />
|
<image class="card-icon-small" src="https://via.placeholder.com/80/9370DB/000000?text=Mine" mode="aspectFit" />
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<view class="game-card-small card-more" @tap="navigateTo('#')">
|
<view class="game-card-small card-more" @tap="navigateTo('#')">
|
||||||
|
|||||||
@ -58,6 +58,11 @@
|
|||||||
<view class="btn-shine"></view>
|
<view class="btn-shine"></view>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
<!-- Test Login Button -->
|
||||||
|
<button class="btn test-login-btn" @click="handleTestLogin">
|
||||||
|
<text class="btn-text">测试账号登录 (Dev)</text>
|
||||||
|
</button>
|
||||||
|
|
||||||
<view class="register-link">
|
<view class="register-link">
|
||||||
<text class="register-text" @click="goToRegister">没有账号?<text class="highlight">立即注册</text></text>
|
<text class="register-text" @click="goToRegister">没有账号?<text class="highlight">立即注册</text></text>
|
||||||
</view>
|
</view>
|
||||||
@ -389,6 +394,14 @@ function onGetPhoneNumber(e) {
|
|||||||
margin-bottom: $spacing-xl;
|
margin-bottom: $spacing-xl;
|
||||||
border: none;
|
border: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.test-login-btn {
|
||||||
|
background: #555;
|
||||||
|
color: #fff;
|
||||||
|
margin-bottom: $spacing-xl;
|
||||||
|
border: none;
|
||||||
|
font-size: 28rpx;
|
||||||
|
}
|
||||||
.btn-shine {
|
.btn-shine {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0; left: -100%;
|
top: 0; left: -100%;
|
||||||
|
|||||||
@ -118,6 +118,54 @@
|
|||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
|
<!-- 抽奖凭证(有凭证数据时显示) -->
|
||||||
|
<view class="section-card proof-section" v-if="order.draw_receipts && order.draw_receipts.length > 0">
|
||||||
|
<view class="section-header">
|
||||||
|
<text class="section-title">抽奖凭证</text>
|
||||||
|
<text class="item-count help-btn" @tap="showProofHelp">?</text>
|
||||||
|
</view>
|
||||||
|
<view v-for="(receipt, idx) in order.draw_receipts" :key="idx" class="receipt-block">
|
||||||
|
<view class="info-row" v-if="receipt.algo_version">
|
||||||
|
<text class="label">算法版本</text>
|
||||||
|
<text class="value mono">{{ receipt.algo_version }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="info-row" v-if="receipt.server_seed_hash">
|
||||||
|
<text class="label">服务端种子哈希</text>
|
||||||
|
<view class="value-wrap">
|
||||||
|
<text class="value mono seed-text">{{ receipt.server_seed_hash }}</text>
|
||||||
|
<view class="copy-btn" @tap="copyText(receipt.server_seed_hash)">复制</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="info-row" v-if="receipt.server_sub_seed">
|
||||||
|
<text class="label">子种子</text>
|
||||||
|
<view class="value-wrap">
|
||||||
|
<text class="value mono seed-text">{{ receipt.server_sub_seed }}</text>
|
||||||
|
<view class="copy-btn" @tap="copyText(receipt.server_sub_seed)">复制</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="info-row" v-if="receipt.client_seed">
|
||||||
|
<text class="label">客户端种子</text>
|
||||||
|
<text class="value mono">{{ receipt.client_seed }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="info-row" v-if="receipt.draw_id">
|
||||||
|
<text class="label">抽奖ID</text>
|
||||||
|
<text class="value mono">{{ receipt.draw_id }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="info-row" v-if="receipt.timestamp">
|
||||||
|
<text class="label">时间戳</text>
|
||||||
|
<text class="value mono">{{ receipt.timestamp }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="info-row" v-if="receipt.round_id">
|
||||||
|
<text class="label">期次ID</text>
|
||||||
|
<text class="value">{{ receipt.round_id }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="proof-notice">
|
||||||
|
<text class="notice-icon">🔒</text>
|
||||||
|
<text class="notice-text">以上数据可用于验证抽奖结果的公正性</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<!-- 底部操作栏 -->
|
<!-- 底部操作栏 -->
|
||||||
@ -295,6 +343,14 @@ function getSourceTypeText(type) {
|
|||||||
if (type === 3) return '发奖记录'
|
if (type === 3) return '发奖记录'
|
||||||
return '其他'
|
return '其他'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function showProofHelp() {
|
||||||
|
uni.showModal({
|
||||||
|
title: '抽奖凭证说明',
|
||||||
|
content: '该凭证包含本次抽奖的随机种子和参数,可用于验证抽奖的公平性。您可以复制相关数据,自行进行核验。',
|
||||||
|
showCancel: false
|
||||||
|
})
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
@ -307,7 +363,7 @@ function getSourceTypeText(type) {
|
|||||||
|
|
||||||
/* 状态头部背景 */
|
/* 状态头部背景 */
|
||||||
.status-header-bg {
|
.status-header-bg {
|
||||||
height: 360rpx;
|
height: 150rpx;
|
||||||
position: relative;
|
position: relative;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
border-radius: 0 0 40rpx 40rpx;
|
border-radius: 0 0 40rpx 40rpx;
|
||||||
@ -328,7 +384,7 @@ function getSourceTypeText(type) {
|
|||||||
|
|
||||||
/* 状态卡片 */
|
/* 状态卡片 */
|
||||||
.status-card {
|
.status-card {
|
||||||
margin: -160rpx $spacing-lg 0;
|
margin: -60rpx $spacing-lg 0;
|
||||||
background: rgba(255, 255, 255, 0.95);
|
background: rgba(255, 255, 255, 0.95);
|
||||||
backdrop-filter: blur(10px);
|
backdrop-filter: blur(10px);
|
||||||
border-radius: $radius-xl;
|
border-radius: $radius-xl;
|
||||||
@ -675,4 +731,33 @@ function getSourceTypeText(type) {
|
|||||||
padding: 12rpx 48rpx;
|
padding: 12rpx 48rpx;
|
||||||
border-radius: 32rpx;
|
border-radius: 32rpx;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 抽奖凭证区 */
|
||||||
|
.proof-section {
|
||||||
|
.seed-text {
|
||||||
|
font-size: 22rpx;
|
||||||
|
word-break: break-all;
|
||||||
|
max-width: 360rpx;
|
||||||
|
@include text-ellipsis(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.proof-notice {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: $spacing-sm;
|
||||||
|
margin-top: $spacing-md;
|
||||||
|
padding: $spacing-sm $spacing-md;
|
||||||
|
background: rgba($brand-primary, 0.06);
|
||||||
|
border-radius: $radius-md;
|
||||||
|
|
||||||
|
.notice-icon {
|
||||||
|
font-size: 24rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notice-text {
|
||||||
|
font-size: 22rpx;
|
||||||
|
color: $text-sub;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
const BASE_URL = 'https://mini-chat.1024tool.vip'
|
const BASE_URL = 'http://localhost:9991'
|
||||||
|
|
||||||
let authModalShown = false
|
let authModalShown = false
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user