864 lines
32 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<!-- 页面根容器赛事详情页使用 100vh 固定视口内部主内容滚动并隐藏滚动条 -->
<div
style="
position: relative;
width: 100vw;
height: 100vh;
overflow: hidden;
background: #101e14;
color: #fff;
"
>
<!-- 首屏加载动画详情接口和关键图片完成后再展示页面内容 -->
<div
v-if="isLoading"
style="
position: relative;
z-index: 2;
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
"
>
<LoadingContent>{{ $t('loading') }}...</LoadingContent>
</div>
<!-- 正式页面内容:加载完成后才渲染背景图、头部和业务模块 -->
<div
v-else
style="
position: relative;
z-index: 1;
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
overflow: hidden;
"
>
<!-- 最底层背景图detailPageBg 只按 100vw 宽度展示,高度按图片自身比例,不强制拉伸 -->
<img
v-smart-img.noFade
:src="pngUrl('detailPageBg')"
alt=""
style="position: absolute; inset: 0; z-index: 0; display: block; width: 100vw; height: auto"
/>
<!-- 顶部导航:使用公共 GeneralHeader标题为 World Cup非首页返回返回图标使用 arrowBack -->
<div style="position: relative; z-index: 1">
<GeneralHeader title="World Cup" :isHomePage="false" :backImg="arrowBackImg" />
</div>
<!-- 主内容滚动区:后续模块按 column 顺序补充,隐藏可见滚动条 -->
<main
class="world-cup-event-detail-scroll"
style="
width: 100%;
flex: 1;
display: flex;
flex-direction: column;
gap: 2vw;
overflow-x: hidden;
overflow-y: auto;
padding: 2vw 2vw 8vw;
box-sizing: border-box;
scrollbar-width: none;
-ms-overflow-style: none;
"
>
<!-- 赛事结果模块:按未开始、进行中、已结束中奖、已结束未中奖/未参与展示不同状态 -->
<itemCenter
style="position: relative; width: 100%; min-height: 53vw; max-height: 54vw"
:imgUrl="pngUrl(resultStatusConfig.image)"
contentStyle=" padding: 9vw 5vw 2vw 5vw; box-sizing: border-box; display: flex; flex-direction: column; align-items: stretch; justify-content: space-between; gap: 0vw;"
>
<!-- 状态角标:绝对定位贴左上角,颜色随赛事结果状态变化 -->
<div :style="resultStatusConfig.badgeStyle">
{{ resultStatusConfig.badgeText }}
</div>
<!-- 状态主文案2行只切换展示内容文本字体颜色、大小、字重保持同一套样式 -->
<div
style="
display: flex;
align-items: center;
justify-content: flex-start;
gap: 1vw;
color: #f8e89a;
font-size: 2em;
font-weight: 500;
"
>
<template v-if="resultStatusConfig.type === 'endedWin'">
<span style="color: #f8e89a; font-size: 1em; font-weight: 500"> + </span>
<img
v-smart-img
src="/src/assets/icon/Azizi/coin.png"
alt=""
style="display: block; width: 1.5em; aspect-ratio: 1/1; object-fit: cover"
/>
<span style="color: #f8e89a; font-size: 1em; font-weight: 500">
{{ winAmount }}
</span>
</template>
<span v-else style="color: #f8e89a; font-size: 1em; font-weight: 500">
{{ resultStatusConfig.mainText }}
</span>
</div>
<!-- 开赛时间说明:所有状态固定展示,时间内容统一根据 kickoffTime 字段格式化 -->
<div style="color: #f6efaa; font-size: 0.9em; font-weight: 500">
{{ kickoffText }}
</div>
<!-- 分割线:区分状态信息和奖池/人数信息 -->
<div style="width: 70%; height: 0.5px; background: rgba(246, 239, 170, 0.5)"></div>
<!-- 奖池和人数行:左侧总奖池,右侧参与人数 -->
<div
style="
width: 70%;
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 4vw;
"
>
<!-- 总奖池信息:上方标题,下方 coin 和 totalPool -->
<div style="display: flex; flex-direction: column; align-items: flex-start; gap: 1vw">
<div style="color: #f8e89a; font-size: 1em; font-weight: 500">total prize pool:</div>
<div style="display: flex; align-items: center; gap: 1vw">
<img
v-smart-img
src="/src/assets/icon/Azizi/coin.png"
alt=""
style="display: block; width: 2.5em; aspect-ratio: 1/1; object-fit: cover"
/>
<span style="color: #f8e89a; font-size: 1.5em; font-weight: 500">
{{ matchInfo.totalPool || 0 }}
</span>
</div>
</div>
<!-- 参与人数信息numPeopleGold 图片加 joinCount -->
<div
style="
display: flex;
align-items: center;
justify-content: flex-end;
gap: 1vw;
white-space: nowrap;
"
>
<img
v-smart-img.noFade
:src="pngUrl('numPeopleGold')"
alt=""
style="display: block; width: 2em; aspect-ratio: 1/1; object-fit: contain"
/>
<span style="color: #f8e89a; font-size: 1.5em; font-weight: 500">
{{ matchInfo.joinCount || 0 }}
</span>
</div>
</div>
</itemCenter>
<!-- 押注模块:未开始赛事可选择押注对象并点击金额下注,已开始/已结束赛事只展示已押注状态 -->
<section :style="betModuleStyle">
<!-- 押注对象模块:三列展示主胜、平局、客胜,结构对齐首页 Ended 列表项 -->
<div
style="
width: 100%;
display: flex;
align-items: stretch;
justify-content: center;
gap: 2vw;
box-sizing: border-box;
"
>
<!-- 单个押注对象:未开始时可点击选中,其他状态只展示用户已押注项 -->
<div
v-for="betOption in detailBetOptions"
:key="betOption.option"
:style="getDetailBetOptionStyle(betOption.option)"
@click="handleBetOptionClick(betOption.option)"
>
<!-- 押注对象赛事信息:旗帜、国家名和占比 -->
<div
style="
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: flex-start;
gap: 1vw;
"
>
<div
style="
width: 100%;
aspect-ratio: 1 / 1.1;
display: flex;
align-items: center;
justify-content: center;
position: relative;
"
>
<img
v-if="betOption.isOssImage"
v-smart-img.noFade
:src="betOption.flagUrl"
alt=""
style="display: block; width: 64%; aspect-ratio: 1 / 1; object-fit: contain"
/>
<img
v-else
:src="betOption.flagUrl"
alt=""
style="display: block; width: 68%; aspect-ratio: 1 / 1; object-fit: contain"
/>
<!-- 已结束胜方标识框:根据 result 字段叠加在主胜、平局或客胜对应图片上方 -->
<img
v-if="isDetailBetOptionWinner(betOption.option)"
v-smart-img.noFade
:src="pngUrl('frameWinner')"
alt=""
style="
position: absolute;
top: 0;
left: 0;
z-index: 3;
display: block;
width: 100%;
object-fit: contain;
pointer-events: none;
"
/>
</div>
<div
style="
max-width: 100%;
overflow: hidden;
color: #fff;
font-size: 1em;
font-weight: 500;
text-align: center;
text-overflow: ellipsis;
white-space: nowrap;
"
>
{{ betOption.name }}
</div>
<div :style="`color: ${betOption.percentColor}; font-size: 1em; font-weight: 500;`">
{{ formatMatchPercent(betOption.percent) }}
</div>
</div>
<!-- 押注对象金额信息:上方选项奖池,下方用户累计下注额 -->
<div
style="
border-radius: 4px;
border: 0.5px solid #e9d089;
background: linear-gradient(
180deg,
rgba(79, 77, 33, 0.5) 0%,
rgba(79, 77, 33, 0) 100%
);
backdrop-filter: blur(13.449894905090332px);
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 0vw;
margin-top: 1vw;
"
>
<!-- 选项奖池金额:使用详情接口格式化字段优先展示 -->
<div
style="display: flex; align-items: center; justify-content: center; padding: 4px"
>
<img
v-smart-img
src="/src/assets/icon/Azizi/coin.png"
alt=""
style="display: block; width: 1.5em; aspect-ratio: 1/1; object-fit: cover"
/>
<span style="color: #fff; font-size: 1em; font-weight: 500">
{{ betOption.poolAmount }}
</span>
</div>
<div style="width: 100%; height: 1px; background: #e9d089"></div>
<!-- 用户下注金额:未押注时展示 0 -->
<div
style="display: flex; align-items: center; justify-content: center; padding: 4px"
>
<img
v-smart-img
src="/src/assets/icon/Azizi/coin.png"
alt=""
style="display: block; width: 1.5em; aspect-ratio: 1/1; object-fit: cover"
/>
<span style="color: #fff; font-size: 1em; font-weight: 500">
{{ betOption.myBetAmount }}
</span>
</div>
</div>
</div>
</div>
<!-- 押注金额模块:仅未开始赛事展示,选中押注对象后点击金额才提交押注 -->
<div
v-if="canSubmitBet"
style="
width: 100%;
display: flex;
align-items: center;
justify-content: space-between;
gap: 2vw;
"
>
<!-- 单个押注金额按钮:点击后调用 apiPostWorldCupBet -->
<div
v-for="amount in betAmountOptions"
:key="amount"
:style="betAmountButtonStyle"
@click="handleBetAmountClick(amount)"
>
<img
v-smart-img
src="/src/assets/icon/Azizi/coin.png"
alt=""
style="display: block; width: 1.4em; aspect-ratio: 1/1; object-fit: cover"
/>
<span style="color: #fff; font-size: 1em; font-weight: 500">{{ amount }}</span>
</div>
</div>
</section>
<!-- 押注记录模块:展示当前赛事内用户押注对象、金额和押注时间 -->
<section :style="predictionRecordModuleStyle">
<!-- 固定标题模块说明下方列表为本场竞猜记录 -->
<div style="color: #ffe083; font-size: 1em; font-weight: 500">
My prediction for this game:
</div>
<!-- 竞猜记录空态没有本场记录时展示 footballSign 和空文案 -->
<div
v-if="!predictionRecords.length"
style="
width: 100%;
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 2vw;
"
>
<img
v-smart-img.noFade
:src="pngUrl('footballSign')"
alt=""
style="display: block; width: 30vw; aspect-ratio: 1/1; object-fit: contain"
/>
<div style="color: #ffe083; font-size: 1.5em; font-weight: 500">No records yet.</div>
</div>
<!-- 竞猜记录列表有记录时内部滚动并隐藏可见滚动条 -->
<div
v-else
class="world-cup-event-detail-record-scroll"
style="
width: 100%;
flex: 1;
display: flex;
flex-direction: column;
gap: 2vw;
overflow-x: hidden;
overflow-y: auto;
scrollbar-width: none;
-ms-overflow-style: none;
"
>
<!-- 单条竞猜记录左侧押注对象和时间右侧押注金额 -->
<div
v-for="recordItem in predictionRecords"
:key="recordItem.betId || `${recordItem.matchId}-${recordItem.betTime}`"
style="
width: 100%;
display: flex;
align-items: center;
justify-content: space-between;
gap: 3vw;
padding: 2vw 3vw;
box-sizing: border-box;
border-radius: 8px;
border: 0.5px solid #e9d089;
background: linear-gradient(
180deg,
rgba(79, 77, 33, 0.5) 0%,
rgba(79, 77, 33, 0) 100%
);
backdrop-filter: blur(13.449894905090332px);
"
>
<!-- 记录左侧押注选项名和押注时间 -->
<div
style="
flex: 1;
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 1vw;
overflow: hidden;
"
>
<div
style="
max-width: 100%;
overflow: hidden;
color: #ffe083;
font-size: 1em;
font-weight: 500;
text-overflow: ellipsis;
white-space: nowrap;
"
>
Bets placed: {{ recordItem.betOptionName || '' }}
</div>
<div style="color: #ffe083; font-size: 1em; font-weight: 500">
{{ formatPredictionTime(recordItem.betTime) }}
</div>
</div>
<!-- 记录右侧coin 图片和押注金额 -->
<div
style="
display: flex;
align-items: center;
justify-content: flex-end;
gap: 1vw;
flex: 0 0 auto;
white-space: nowrap;
"
>
<img
v-smart-img
src="/src/assets/icon/Azizi/coin.png"
alt=""
style="display: block; width: 1.5em; aspect-ratio: 1/1; object-fit: cover"
/>
<span style="color: #fff; font-size: 1em; font-weight: 500">
{{ recordItem.betAmount || 0 }}
</span>
</div>
</div>
</div>
</section>
</main>
</div>
</div>
</template>
<script setup>
import { computed, onBeforeUnmount, onMounted, ref } from 'vue'
import { useRoute } from 'vue-router'
import { getPngUrl } from '@/config/imagePaths.js'
import { preloadImages } from '@/utils/image/imagePreloader.js'
import {
apiGetWorldCupMatchDetail,
apiPostWorldCupBet,
apiPostWorldCupMyBetList,
} from '@/api/worldCup.js'
import { connectToApp } from '@/utils/appConnector.js'
import GeneralHeader from '@/components/GeneralHeader.vue'
import itemCenter from '@/components/itemCenter.vue'
import arrowBackImg from '@/assets/icon/Azizi/arrowBack.png'
import { showError, showSuccess, showWarning } from '@/utils/toast.js'
const route = useRoute()
const assetPath = 'Activities/WorldCup/'
const pngUrl = (filename) => getPngUrl(assetPath, filename)
const matchId = computed(() => route.query.matchId || '')
const isLoading = ref(true)
const matchInfo = ref({})
const myBets = ref([])
const nowTime = ref(Date.now())
const selectedBetOption = ref(null)
const isSubmittingBet = ref(false)
let countdownTimer = null
const blockingImages = [
'detailPageBg',
'statusNotStarted',
'statusInProgress',
'statusEndedWin',
'statusEndedfail',
'numPeopleGold',
'drawSign',
'frameWinner',
'footballSign',
]
const notStartedBadgeStyle =
'position: absolute; left: 0; top: 0; z-index: 2; padding: 1.5vw 3vw; border-radius: 7px 0; background: linear-gradient(152deg, #FB9631 7.01%, #B86F21 92.99%), linear-gradient(129deg, #823C1B 0%, #853D1D 14.44%, #AB6426 28.88%, #FF9A46 43.32%, #F28F28 57.76%, #AD6324 72.2%, #813D1C 96.26%); color: #FFF; font-size: 1em; font-weight: 500;'
const inProgressBadgeStyle =
'position: absolute; left: 0; top: 0; z-index: 2; padding: 1.5vw 3vw; border-radius: 7px 0; background: linear-gradient(152deg, #31FB5E 7.01%, #21B843 92.99%), linear-gradient(129deg, #1B8230 0%, #1D8532 14.44%, #26AB26 28.88%, #46FF5F 43.32%, #28F22B 57.76%, #2DAD24 72.2%, #1C8132 96.26%); color: #FFF; font-size: 1em; font-weight: 500;'
const endedBadgeStyle =
'position: absolute; left: 0; top: 0; z-index: 2; padding: 1.5vw 3vw; border-radius: 7px 0; background: linear-gradient(135deg, #484646 2.82%, #292424 99.15%), linear-gradient(129deg, #823C1B 0%, #853D1D 14.44%, #AB6426 28.88%, #FF9A46 43.32%, #F28F28 57.76%, #AD6324 72.2%, #813D1C 96.26%); color: #FFF; font-size: 1em; font-weight: 500;'
const betModuleStyle =
'width: 100%; display: flex; flex-direction: column; gap: 3vw; padding: 3vw; box-sizing: border-box; border-radius: 12px; border: 1px solid #5D6948; background: linear-gradient(180deg, #0B2415 0%, #051C12 100%); backdrop-filter: blur(14.252873420715332px);'
const betOptionDefaultStyle =
'flex: 1; min-width: 0; display: flex; flex-direction: column; justify-content: space-between; align-items: center; padding: 2vw 1.5vw; box-sizing: border-box; border-radius: 7.121px; border: 0.445px solid #E9D089; background: linear-gradient(180deg, rgba(79, 77, 33, 0.50) 0%, rgba(79, 77, 33, 0.00) 100%); backdrop-filter: blur(11.97230052947998px);'
const betOptionSelectedStyle =
'flex: 1; min-width: 0; display: flex; flex-direction: column; justify-content: space-between; align-items: center; padding: 2vw 1.5vw; box-sizing: border-box; border-radius: 7.121px; border: 1px solid #FFFA5D; background: linear-gradient(180deg, rgba(79, 77, 33, 0.50) 0%, rgba(79, 77, 33, 0.00) 100%); backdrop-filter: blur(11.97230052947998px); box-shadow: -0.223px -0.223px 0.89px 0 #D6C100, 0.223px 0.223px 0.89px 0 #D6C100, 0 0 2.67px 0 rgba(255, 250, 93, 0.60), 0 0 4.451px 0 rgba(255, 250, 93, 0.40), 0 0 3.561px 0 rgba(210, 204, 0, 0.30);'
const betAmountButtonStyle =
'flex: 1; min-width: 0; display: flex; align-items: center; justify-content: center; gap: 0.8vw; padding: 2vw 1vw; box-sizing: border-box; border-radius: 4px; border: 0.5px solid #E9D089; background: linear-gradient(180deg, rgba(79, 77, 33, 0.50) 0%, rgba(79, 77, 33, 0.00) 100%); backdrop-filter: blur(13.449894905090332px);'
const betAmountOptions = [100, 1000, 10000, 100000]
const predictionRecordModuleStyle =
'width: 100%; height: 72vw; display: flex; flex-direction: column; gap: 3vw; padding: 3vw; box-sizing: border-box; border-radius: 12px; border: 1px solid #5D6948; background: linear-gradient(180deg, #0B2415 0%, #051C12 100%); backdrop-filter: blur(14.252873420715332px);'
// 是否允许提交押注:只有赛事未开始时展示金额按钮并允许点击押注对象。
const canSubmitBet = computed(() => Number(matchInfo.value.status) === 0)
// 当前用户是否至少有一个中奖押注:详情接口 myBets 或我的竞猜列表中 betStatus=1 / payout>0 都算中奖。
const hasWinBet = computed(() => {
return myBets.value.some(
(betItem) => Number(betItem.betStatus) === 1 || Number(betItem.payout) > 0,
)
})
// 中奖金额汇总:结束且中奖状态展示所有中奖记录的 payout 合计。
const winAmount = computed(() => {
return myBets.value.reduce((total, betItem) => total + (Number(betItem.payout) || 0), 0)
})
// 详情页押注对象数据:三列分别为主胜、平局、客胜,上方为选项奖池,下方为我的下注额。
const detailBetOptions = computed(() => [
{
option: 1,
flagUrl: matchInfo.value.homeFlag,
isOssImage: false,
name: matchInfo.value.homeTeam || '',
percent: matchInfo.value.homePercent,
percentColor: '#71ce6e',
myBetAmount: getMyBetAmountText(1),
poolAmount: matchInfo.value.homePoolFormat || matchInfo.value.homePool || 0,
},
{
option: 2,
flagUrl: pngUrl('drawSign'),
isOssImage: true,
name: 'DRAW',
percent: matchInfo.value.drawPercent,
percentColor: '#a0a181',
myBetAmount: getMyBetAmountText(2),
poolAmount: matchInfo.value.drawPoolFormat || matchInfo.value.drawPool || 0,
},
{
option: 3,
flagUrl: matchInfo.value.awayFlag,
isOssImage: false,
name: matchInfo.value.awayTeam || '',
percent: matchInfo.value.awayPercent,
percentColor: '#71ce6e',
myBetAmount: getMyBetAmountText(3),
poolAmount: matchInfo.value.awayPoolFormat || matchInfo.value.awayPool || 0,
},
])
// 本场竞猜记录列表:使用我的竞猜记录接口返回的 records空数组时展示空态。
const predictionRecords = computed(() => myBets.value || [])
// 倒计时文案:未开始状态展示类似 2D 18 : 59 : 59。
const countdownText = computed(() => {
const remaining = Math.max(0, Number(matchInfo.value.kickoffTime || 0) - nowTime.value)
const totalSeconds = Math.floor(remaining / 1000)
const days = Math.floor(totalSeconds / 86400)
const hours = String(Math.floor((totalSeconds % 86400) / 3600)).padStart(2, '0')
const minutes = String(Math.floor((totalSeconds % 3600) / 60)).padStart(2, '0')
const seconds = String(totalSeconds % 60).padStart(2, '0')
return `${days}D ${hours} : ${minutes} : ${seconds}`
})
// 赛事结果状态配置:根据 status 和用户押注结果选择背景图、角标和主文案。
const resultStatusConfig = computed(() => {
const status = Number(matchInfo.value.status)
if (status === 0) {
return {
type: 'notStarted',
image: 'statusNotStarted',
badgeText: 'Not Started',
badgeStyle: notStartedBadgeStyle,
mainText: countdownText.value,
}
}
if (status === 1) {
return {
type: 'inProgress',
image: 'statusInProgress',
badgeText: 'In Progress',
badgeStyle: inProgressBadgeStyle,
mainText: 'The match is underway',
}
}
if (hasWinBet.value) {
return {
type: 'endedWin',
image: 'statusEndedWin',
badgeText: 'Ended',
badgeStyle: endedBadgeStyle,
mainText: '',
}
}
return {
type: 'endedFail',
image: 'statusEndedfail',
badgeText: 'Ended',
badgeStyle: endedBadgeStyle,
mainText: 'Better luck next time!',
}
})
// 开赛时间文案:所有赛事状态都固定展示,时间内容统一由 kickoffTime 字段格式化。
const kickoffText = computed(() => {
const kickoffTime = Number(matchInfo.value.kickoffTime || 0)
if (!kickoffTime) return ''
const kickoffDate = new Date(kickoffTime)
const timeText = `${String(kickoffDate.getHours()).padStart(2, '0')}:${String(
kickoffDate.getMinutes(),
).padStart(2, '0')}:${String(kickoffDate.getSeconds()).padStart(2, '0')}`
const monthText = kickoffDate.toLocaleString('en-US', { month: 'long' })
const dateText = `${kickoffDate.getDate()} ${monthText} ${kickoffDate.getFullYear()}`
return `Kick-off is at ${timeText} on ${dateText}.`
})
// 赛事占比展示:接口返回数字时补百分号,已带百分号的字符串直接展示。
const formatMatchPercent = (percent) => {
if (typeof percent === 'string' && percent.includes('%')) return percent
const percentValue = percent === '' || percent === null || percent === undefined ? 0 : percent
return `${percentValue}%`
}
// 格式化竞猜记录时间:展示为 MM-DD-YYYY HH:mm:ss。
const formatPredictionTime = (betTime) => {
const timestamp = Number(betTime)
if (!timestamp) return ''
const betDate = new Date(timestamp)
const month = String(betDate.getMonth() + 1).padStart(2, '0')
const day = String(betDate.getDate()).padStart(2, '0')
const year = betDate.getFullYear()
const hours = String(betDate.getHours()).padStart(2, '0')
const minutes = String(betDate.getMinutes()).padStart(2, '0')
const seconds = String(betDate.getSeconds()).padStart(2, '0')
return `${month}-${day}-${year} ${hours}:${minutes}:${seconds}`
}
// 按押注选项汇总我的竞猜金额:详情接口未返回格式化金额时用于兜底展示。
const getMyBetAmountSum = (option) => {
return myBets.value.reduce((total, betItem) => {
if (Number(betItem.betOption) !== Number(option)) return total
return total + (Number(betItem.betAmount) || 0)
}, 0)
}
// 获取我的押注金额展示:优先使用详情接口 my*BetFormat 字段,没有时按 myBets 兜底汇总。
const getMyBetAmountText = (option) => {
const formatMap = {
1: matchInfo.value.myHomeBetFormat,
2: matchInfo.value.myDrawBetFormat,
3: matchInfo.value.myAwayBetFormat,
}
const formatValue = formatMap[Number(option)]
if (formatValue !== null && formatValue !== undefined && formatValue !== '') return formatValue
const amountSum = getMyBetAmountSum(option)
return amountSum || 0
}
// 判断用户是否押注过当前选项:已开赛或已结束时用于展示已押注项选中态。
const hasUserBetOnOption = (option) => {
const formatMap = {
1: matchInfo.value.myHomeBetFormat,
2: matchInfo.value.myDrawBetFormat,
3: matchInfo.value.myAwayBetFormat,
}
const formatValue = formatMap[Number(option)]
if (formatValue !== null && formatValue !== undefined && formatValue !== '') return true
return getMyBetAmountSum(option) > 0
}
// 押注对象样式:未开始时只展示用户当前点击选中态,已开始后展示用户历史押注态。
const getDetailBetOptionStyle = (option) => {
if (canSubmitBet.value) {
return Number(selectedBetOption.value) === Number(option)
? betOptionSelectedStyle
: betOptionDefaultStyle
}
return hasUserBetOnOption(option) ? betOptionSelectedStyle : betOptionDefaultStyle
}
// 已结束胜方标识:只有已结束赛事根据 result 字段给对应押注项叠加 frameWinner。
const isDetailBetOptionWinner = (option) => {
return Number(matchInfo.value.status) === 2 && Number(matchInfo.value.result) === Number(option)
}
// 点击押注对象:仅未开始赛事允许选择押注对象,默认不选中任何项。
const handleBetOptionClick = (option) => {
if (!canSubmitBet.value) return
selectedBetOption.value = option
}
// 加载赛事详情:详情接口提供比赛基础信息、奖池明细和我的本场竞猜列表。
const loadMatchDetail = async () => {
if (!matchId.value) return
const detailRes = await apiGetWorldCupMatchDetail(matchId.value)
const detailBody = detailRes.body || detailRes
matchInfo.value = {
...(detailBody.match || {}),
homePool: detailBody.homePool ?? 0,
drawPool: detailBody.drawPool ?? 0,
awayPool: detailBody.awayPool ?? 0,
homePoolFormat: detailBody.match?.homePoolFormat ?? detailBody.homePoolFormat ?? '',
drawPoolFormat: detailBody.match?.drawPoolFormat ?? detailBody.drawPoolFormat ?? '',
awayPoolFormat: detailBody.match?.awayPoolFormat ?? detailBody.awayPoolFormat ?? '',
myHomeBetFormat: detailBody.match?.myHomeBetFormat ?? detailBody.myHomeBetFormat ?? null,
myDrawBetFormat: detailBody.match?.myDrawBetFormat ?? detailBody.myDrawBetFormat ?? null,
myAwayBetFormat: detailBody.match?.myAwayBetFormat ?? detailBody.myAwayBetFormat ?? null,
}
myBets.value = detailBody.myBets || []
}
// 加载我的竞猜记录:页面后续押注状态模块需要用到,当前作为详情接口 myBets 的补充。
const loadMyBetList = async () => {
if (!matchId.value) return
const betRes = await apiPostWorldCupMyBetList({
size: 100,
current: 1,
matchId: matchId.value,
})
const betBody = betRes.body || betRes
const records = betBody.records || []
myBets.value = records
}
// 页面初始化:遵循建页规则,预加载关键图片并拉取详情与我的竞猜记录后再关闭 loading。
const completePreloading = async () => {
try {
await Promise.all([
preloadImages(blockingImages.map((imageName) => pngUrl(imageName))),
loadMatchDetail(),
])
await loadMyBetList()
} catch (error) {
console.error('WorldCup 赛事详情加载失败:', error)
} finally {
isLoading.value = false
}
}
// 连接 App 并设置接口请求头:详情页所有接口请求必须在 connectToApp 成功后执行。
const connectToAppHandler = async () => {
await connectToApp(() => {
completePreloading()
})
}
// 押注接口占位:后续第三模块接入押注按钮时统一调用该方法。
const submitWorldCupBet = async (betOption, betAmount) => {
if (!matchId.value) return
return apiPostWorldCupBet({
matchId: String(matchId.value),
betOption,
betAmount,
})
}
// 点击押注金额:先校验是否已选押注对象,再提交押注并刷新详情数据。
const handleBetAmountClick = async (betAmount) => {
if (!canSubmitBet.value) return
if (!selectedBetOption.value) {
showWarning('Please select a prediction option first.')
return
}
if (isSubmittingBet.value) return
try {
isSubmittingBet.value = true
await submitWorldCupBet(selectedBetOption.value, betAmount)
showSuccess('Bet placed successfully.')
selectedBetOption.value = null
await loadMatchDetail()
await loadMyBetList()
} catch (error) {
showError(error.response?.errorMsg || error.errorMsg || error.message || 'Bet failed')
} finally {
isSubmittingBet.value = false
}
}
onMounted(() => {
countdownTimer = window.setInterval(() => {
nowTime.value = Date.now()
}, 1000)
connectToAppHandler()
})
onBeforeUnmount(() => {
if (countdownTimer) {
window.clearInterval(countdownTimer)
countdownTimer = null
}
})
</script>
<style lang="scss" scoped>
* {
color: #fff;
}
.world-cup-event-detail-scroll::-webkit-scrollbar {
display: none;
}
@media screen and (max-width: 360px) {
* {
font-size: 10px;
}
}
@media screen and (min-width: 360px) {
* {
font-size: 12px;
}
}
@media screen and (min-width: 768px) {
* {
font-size: 24px;
}
}
@media screen and (min-width: 1024px) {
* {
font-size: 32px;
}
}
</style>