feat(世界杯活动): 对接接口,并完成两个页面的基本内容
This commit is contained in:
parent
4b73849827
commit
91fc37a0ee
65
src/api/worldCup.js
Normal file
65
src/api/worldCup.js
Normal file
@ -0,0 +1,65 @@
|
||||
import { get, post } from '../utils/http.js'
|
||||
|
||||
const WORLD_CUP_API_PREFIX = '/activity/worldcup'
|
||||
|
||||
const logWorldCupApiError = (message, error) => {
|
||||
console.error(message, error)
|
||||
console.error('error:' + (error.response?.errorMsg || error.errorMsg || error.message || ''))
|
||||
}
|
||||
|
||||
// 获取世界杯比赛列表:tab 为 0 时返回进行中/可投注,tab 为 1 时返回已结束。
|
||||
export const apiPostWorldCupMatchList = async (data) => {
|
||||
try {
|
||||
const response = await post(`${WORLD_CUP_API_PREFIX}/match/list`, data)
|
||||
return response
|
||||
} catch (error) {
|
||||
logWorldCupApiError('Failed to get world cup match list:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// 提交世界杯比赛押注:betOption 为 1 主胜、2 平局、3 客胜。
|
||||
export const apiPostWorldCupBet = async (data) => {
|
||||
try {
|
||||
const response = await post(`${WORLD_CUP_API_PREFIX}/bet`, data)
|
||||
return response
|
||||
} catch (error) {
|
||||
logWorldCupApiError('Failed to submit world cup bet:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// 获取我的世界杯竞猜记录:matchId 为空时查询全部比赛记录。
|
||||
export const apiPostWorldCupMyBetList = async (data) => {
|
||||
try {
|
||||
const response = await post(`${WORLD_CUP_API_PREFIX}/my/bet/list`, data)
|
||||
return response
|
||||
} catch (error) {
|
||||
logWorldCupApiError('Failed to get world cup my bet list:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// 获取世界杯排行榜:接口没有业务参数,POST 请求仍传空对象。
|
||||
export const apiPostWorldCupRankList = async () => {
|
||||
try {
|
||||
const response = await post(`${WORLD_CUP_API_PREFIX}/rank/list`, {})
|
||||
return response
|
||||
} catch (error) {
|
||||
logWorldCupApiError('Failed to get world cup rank list:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// 获取世界杯比赛详情,同时返回当前用户本场竞猜记录。
|
||||
export const apiGetWorldCupMatchDetail = async (matchId) => {
|
||||
try {
|
||||
const response = await get(
|
||||
`${WORLD_CUP_API_PREFIX}/match/detail?matchId=${encodeURIComponent(matchId)}`,
|
||||
)
|
||||
return response
|
||||
} catch (error) {
|
||||
logWorldCupApiError('Failed to get world cup match detail:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
@ -23,6 +23,7 @@ export const PUBLIC_PATHS = Object.freeze([
|
||||
'/activities/gulben-festival',
|
||||
'/activities/bounty-football',
|
||||
'/activities/world-cup',
|
||||
'/activities/world-cup/event-detail',
|
||||
'/activities/lucky-dollars-season5',
|
||||
'/activities/lucky-dollars-season4',
|
||||
'/activities/lesser-bairam',
|
||||
|
||||
@ -452,6 +452,12 @@ const router = createRouter({
|
||||
component: () => import('../views/Activities/WorldCup/index.vue'),
|
||||
meta: { requiresAuth: false },
|
||||
}, // WorldCup
|
||||
{
|
||||
path: '/activities/world-cup/event-detail',
|
||||
name: 'world-cup-event-detail',
|
||||
component: () => import('../views/Activities/WorldCup/EventDetail/index.vue'),
|
||||
meta: { requiresAuth: false },
|
||||
}, // WorldCup EventDetail
|
||||
{
|
||||
path: '/activities/games/the-greedy-king',
|
||||
name: 'games-the-greedy-king',
|
||||
|
||||
@ -69,6 +69,7 @@ const ACTIVITIES = [
|
||||
'/activities/spring-festival', // 春节打榜
|
||||
|
||||
'/activities/world-cup', // WorldCup
|
||||
'/activities/world-cup/event-detail', // WorldCup EventDetail
|
||||
'/activities/games/the-greedy-king', //贪金王游戏排行榜
|
||||
'/activities/bounty-football', // BountyFootball
|
||||
'/activities/poker-ace', // 扑克王牌排行榜
|
||||
|
||||
827
src/views/Activities/WorldCup/EventDetail/index.vue
Normal file
827
src/views/Activities/WorldCup/EventDetail/index.vue
Normal file
@ -0,0 +1,827 @@
|
||||
<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%"
|
||||
: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;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
"
|
||||
>
|
||||
<img
|
||||
v-if="betOption.isOssImage"
|
||||
v-smart-img.noFade
|
||||
:src="betOption.flagUrl"
|
||||
alt=""
|
||||
style="display: block; width: 78%; aspect-ratio: 1 / 1; object-fit: contain"
|
||||
/>
|
||||
<img
|
||||
v-else
|
||||
:src="betOption.flagUrl"
|
||||
alt=""
|
||||
style="display: block; width: 78%; aspect-ratio: 1 / 1; object-fit: contain"
|
||||
/>
|
||||
</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: 1.2vw;
|
||||
margin-top: 2vw;
|
||||
"
|
||||
>
|
||||
<!-- 用户下注金额:未押注时展示 0 -->
|
||||
<div style="display: flex; align-items: center; justify-content: center">
|
||||
<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 style="width: 100%; height: 1px; background: #e9d089"></div>
|
||||
<!-- 选项奖池金额:使用详情接口格式化字段优先展示 -->
|
||||
<div style="display: flex; align-items: center; justify-content: center">
|
||||
<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>
|
||||
</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 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',
|
||||
'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
|
||||
}
|
||||
|
||||
// 点击押注对象:仅未开始赛事允许选择押注对象,默认不选中任何项。
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
// 押注接口占位:后续第三模块接入押注按钮时统一调用该方法。
|
||||
const submitWorldCupBet = async (betOption, betAmount) => {
|
||||
if (!matchId.value) return
|
||||
return apiPostWorldCupBet({
|
||||
matchId: Number(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)
|
||||
completePreloading()
|
||||
})
|
||||
|
||||
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>
|
||||
97
src/views/Activities/WorldCup/EventDetail/页面创建信息.md
Normal file
97
src/views/Activities/WorldCup/EventDetail/页面创建信息.md
Normal file
@ -0,0 +1,97 @@
|
||||
# 页面创建信息
|
||||
|
||||
## 0. 建页规则关联
|
||||
|
||||
- 页面改动、模块补充、接口对接、样式同步和文档同步,都需要先联系 `docs/活动页/建页/` 下的建页规则执行。
|
||||
- 本页面为 `WorldCup` 的赛事详情页,后续生成或更新时需要同步回 `src/views/Activities/WorldCup/页面创建信息.md` 的入口说明。
|
||||
- 页面模块、div 结构、方法、接口处理都需要补充中文注释。
|
||||
- 页面根容器使用 `100vh` 方案,内部滚动并隐藏滚动条,不展示可见滚动条。
|
||||
- 能使用内联样式处理的页面样式,优先使用内联样式。
|
||||
- 新建或更新 `index.vue` 时必须使用统一加载动画:`isLoading` 阶段只渲染 `LoadingContent`,关键图片和初始化接口完成后再渲染正式背景图与业务节点。
|
||||
- 普通业务模块不提前设置 `min-height` 和 `line-height`;`min-height` 只在基础页面容器按需使用,`itemCenter` 的 `min-height` 只在页面开发完成后确实需要稳定图片容器高度时再补。
|
||||
|
||||
## 1. 基础信息
|
||||
|
||||
- 页面类型:Activities
|
||||
- 页面名称:WorldCup EventDetail
|
||||
- 页面目录:`src/views/Activities/WorldCup/EventDetail`
|
||||
- 页面入口:`src/views/Activities/WorldCup/EventDetail/index.vue`
|
||||
- 路由 path:`/activities/world-cup/event-detail`
|
||||
- 路由 name:`world-cup-event-detail`
|
||||
- 进入来源:WorldCup 首页赛事列表项下方的 `TOTAL PRIZE POOL` 模块。
|
||||
- Query 参数:`matchId`,来自比赛列表接口返回的 `matchId`。
|
||||
|
||||
## 2. 业务规则
|
||||
|
||||
- 本页面用于展示世界杯赛事详情,并给用户提供押注服务。
|
||||
- 押注只允许在赛事未开始时进行。
|
||||
- 只要赛事开始,用户就不能再押注,只展示赛事状态和用户押注状态。
|
||||
- 页面详情接口使用 `apiGetWorldCupMatchDetail(matchId)`。
|
||||
- 押注接口使用 `apiPostWorldCupBet(data)`。
|
||||
- 我的本场竞猜记录优先使用详情接口返回的 `myBets`。
|
||||
- 页面进入参数为 `matchId`,由首页赛事列表总奖池入口跳转时通过 query 传入。
|
||||
|
||||
## 3. 页面结构
|
||||
|
||||
- 根容器固定 `100vw * 100vh`,背景色为 `#101E14`。
|
||||
- 最底层背景图使用 `detailPageBg`,宽度设置为 `100vw`,高度按图片自身比例展示,不强制拉伸到页面高度。
|
||||
- 顶部使用公共组件 `src/components/GeneralHeader.vue`,标题为 `World Cup`,非首页,使用组件默认返回键。
|
||||
- `GeneralHeader` 返回键图片通过 `backImg` 指定为 `src/assets/icon/Azizi/arrowBack.png`,只影响当前详情页。
|
||||
- 主内容区为内部滚动区域,使用 column 布局,`gap: 2vw`,隐藏可见滚动条。
|
||||
- 第一模块为赛事结果模块,使用 `itemCenter` 组件承载不同状态背景图。
|
||||
- 第二模块为押注模块,负责展示押注对象、押注状态和未开赛时的押注金额入口。
|
||||
- 第三模块为押注记录模块,负责展示当前赛事内用户押注对象、金额和押注时间。
|
||||
|
||||
## 4. 赛事结果模块
|
||||
|
||||
- 未开始状态使用 `statusNotStarted`,角标文案为 `Not Started`,主文案根据 `kickoffTime` 展示倒计时,例如 `2D 18 : 59 : 59`。
|
||||
- 正在进行状态使用 `statusInProgress`,角标文案为 `In Progress`,主文案展示 `The match is underway`。
|
||||
- 已结束且至少有一个中奖押注时使用 `statusEndedWin`,角标文案为 `Ended`,主文案展示 `+ coin + payout合计`。
|
||||
- 已结束且未中奖或未参与时使用 `statusEndedfail`,角标文案为 `Ended`,主文案展示 `Better luck next time!`。
|
||||
- 第(2)行主文案只按状态切换展示内容,字体样式固定一致:`color: #F8E89A`、`font-size: 2em`、`font-weight: 500`。
|
||||
- 开赛时间文案在所有状态下固定展示,文案根据 `kickoffTime` 格式化,例如 `Kick-off is at 18:00:00 on 12 June 2026.`。
|
||||
- 分割线和底部信息行在所有状态下固定展示;底部信息行左侧展示 `total prize pool:`、coin 图片和 `totalPool`,右侧展示 `numPeopleGold` 图片和 `joinCount`。
|
||||
- 中奖判断来自 `myBets`,`betStatus === 1` 或 `payout > 0` 都视为中奖。
|
||||
- 当前页面会预加载 `detailPageBg`、`statusNotStarted`、`statusInProgress`、`statusEndedWin`、`statusEndedfail`、`numPeopleGold`。
|
||||
|
||||
## 5. 押注模块
|
||||
|
||||
- 押注模块使用普通 `section`,整体样式为绿色渐变背景、`12px` 圆角、`#5D6948` 边框和 `backdrop-filter`。
|
||||
- 模块内部从上往下分为押注对象和押注金额两个小模块。
|
||||
- 押注对象结构对齐首页 `Ended` 列表项中的三列押注选项:主胜、平局、客胜。
|
||||
- 押注对象上方金额展示用户在当前选项的累计下注额:`myHomeBetFormat` / `myDrawBetFormat` / `myAwayBetFormat`,详情接口缺失时按 `myBets` 中 `betOption` 汇总兜底。
|
||||
- 押注对象下方金额展示当前选项奖池:`homePoolFormat` / `drawPoolFormat` / `awayPoolFormat`,没有格式化字段时回退 `homePool` / `drawPool` / `awayPool`。
|
||||
- 未开始赛事中,押注对象允许点击选择,默认不选中任何项;选中的押注项使用首页 `Ended` 选中态样式。
|
||||
- 已开始或已结束赛事中,押注对象不可点击,选中态用于展示用户已押注过的选项。
|
||||
- 押注金额模块只在未开始赛事展示,金额选项为 `100`、`1000`、`10000`、`100000`,每个金额左侧展示 coin 图片。
|
||||
- 押注流程为:先选择押注对象,再点击押注金额;点击金额时调用 `apiPostWorldCupBet({ matchId, betOption, betAmount })`。
|
||||
- 押注成功后清空当前选中押注对象,并重新拉取 `apiGetWorldCupMatchDetail` 和 `apiPostWorldCupMyBetList` 刷新页面状态。
|
||||
|
||||
## 6. 押注记录模块
|
||||
|
||||
- 押注记录模块使用普通 `section`,整体样式为绿色渐变背景、`12px` 圆角、`#5D6948` 边框和 `backdrop-filter`。
|
||||
- 数据接口使用 `apiPostWorldCupMyBetList({ size: 100, current: 1, matchId })`,只获取一次,不做上拉加载。
|
||||
- 模块内部从上往下分为固定标题和竞猜记录两个小模块。
|
||||
- 固定标题为 `My prediction for this game:`,样式为 `color: #FFE083`、`font-weight: 500`。
|
||||
- 竞猜记录为空时居中展示两行:第一行为 `footballSign` 图片,宽度暂定 `30vw`;第二行为 `No records yet.`,样式为 `color: #FFE083`、`font-size: 1.5em`、`font-weight: 500`。
|
||||
- 竞猜记录不为空时,记录区域内部滚动并隐藏可见滚动条。
|
||||
- 单条记录样式为金色描边渐变背景,内部左右 `space-between` 布局。
|
||||
- 记录左侧展示两行:`Bets placed:` + `betOptionName`,以及格式化后的 `betTime`,时间格式为 `MM-DD-YYYY HH:mm:ss`。
|
||||
- 记录右侧展示 coin 图片和 `betAmount`,金额文字为白色、`font-weight: 500`。
|
||||
|
||||
## 7. 路由代码片段
|
||||
|
||||
```js
|
||||
{
|
||||
path: '/activities/world-cup/event-detail',
|
||||
name: 'world-cup-event-detail',
|
||||
component: () => import('../views/Activities/WorldCup/EventDetail/index.vue'),
|
||||
meta: { requiresAuth: false },
|
||||
}
|
||||
```
|
||||
|
||||
## 8. 验证记录
|
||||
|
||||
- `npm run build`:已通过。
|
||||
- 当前已完成赛事结果模块、押注模块和押注记录模块。
|
||||
- 本次已按 `docs/活动页/建页/` 规则调整详情页底座:loading/v-else 分离、中文注释、内部滚动隐藏滚动条、`<style lang="scss" scoped>`。
|
||||
@ -43,7 +43,7 @@
|
||||
|
||||
<!-- 用户基础信息:名称和展示 id,空榜时保留占位高度 -->
|
||||
<div class="user-name">{{ name }}</div>
|
||||
<div class="user-id">{{ userId }}</div>
|
||||
<div class="user-id">{{ $t('user_id_prefix') }} {{ userId }}</div>
|
||||
|
||||
<!-- 等级展示行:VIP、魅力等级、财富等级规则同步 GulbenFestival -->
|
||||
<div
|
||||
@ -56,14 +56,20 @@
|
||||
direction: ltr;
|
||||
"
|
||||
>
|
||||
<img v-if="vipUrl" v-smart-img.noFade :src="vipUrl" alt="" style="width: 24%; display: block" />
|
||||
<img
|
||||
v-if="vipUrl"
|
||||
v-smart-img.noFade
|
||||
:src="vipUrl"
|
||||
alt=""
|
||||
style="width: 24%; display: block"
|
||||
/>
|
||||
<itemCenter
|
||||
:imgUrl="userLevelUrl"
|
||||
:imgUrl="charmLevelUrl"
|
||||
class="level-badge"
|
||||
style="width: 30%"
|
||||
contentStyle="padding: 0 0 0 42%; align-items: center; justify-content: center; box-sizing: border-box;"
|
||||
>
|
||||
<span>{{ userLevel || 0 }}</span>
|
||||
<span>{{ charmLevel || 0 }}</span>
|
||||
</itemCenter>
|
||||
<itemCenter
|
||||
:imgUrl="wealthLevelUrl"
|
||||
@ -106,7 +112,7 @@ defineProps({
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
userLevelUrl: {
|
||||
charmLevelUrl: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
@ -114,7 +120,7 @@ defineProps({
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
userLevel: {
|
||||
charmLevel: {
|
||||
type: [String, Number],
|
||||
default: 0,
|
||||
},
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -13,8 +13,50 @@ export const pageConfig = {
|
||||
},
|
||||
|
||||
preload: {
|
||||
blocking: [],
|
||||
background: [],
|
||||
blocking: [
|
||||
'bg2',
|
||||
'bg',
|
||||
'ruleBt',
|
||||
'ruleBg',
|
||||
'EventlistBg',
|
||||
'matchBg',
|
||||
'drawSign',
|
||||
'numPeople',
|
||||
'rightBt',
|
||||
'RankingMain',
|
||||
'RankingItem',
|
||||
'RankingBottomBorder',
|
||||
'myRankingBg',
|
||||
'giftBt',
|
||||
'rewardBg',
|
||||
'showMoreBt',
|
||||
'frameTop1',
|
||||
'frameTop2',
|
||||
'frameTop3',
|
||||
'vip1',
|
||||
'vip2',
|
||||
'vip3',
|
||||
'vip4',
|
||||
'vip5',
|
||||
'vip6',
|
||||
'userLevel1Bg',
|
||||
'userLevel2Bg',
|
||||
'userLevel3Bg',
|
||||
'userLevel4Bg',
|
||||
'userLevel5Bg',
|
||||
'wealthLevel1Bg',
|
||||
'wealthLevel2Bg',
|
||||
'wealthLevel3Bg',
|
||||
'wealthLevel4Bg',
|
||||
'wealthLevel5Bg',
|
||||
],
|
||||
background: [
|
||||
'footballSign',
|
||||
'statusEndedfail',
|
||||
'statusEndedWin',
|
||||
'statusInProgress',
|
||||
'statusNotStarted',
|
||||
],
|
||||
},
|
||||
|
||||
modules: {
|
||||
@ -24,7 +66,24 @@ export const pageConfig = {
|
||||
items: [],
|
||||
},
|
||||
sections: [],
|
||||
popups: [],
|
||||
popups: [
|
||||
{
|
||||
name: 'rule',
|
||||
enabled: true,
|
||||
triggerImage: 'ruleBt',
|
||||
backgroundImage: 'ruleBg',
|
||||
component: 'MaskLayer',
|
||||
remark: '头部规则按钮弹窗,ruleBg 宽度 100vw,占满弹窗宽度。',
|
||||
},
|
||||
{
|
||||
name: 'reward',
|
||||
enabled: true,
|
||||
triggerImage: 'giftBt',
|
||||
backgroundImage: 'rewardBg',
|
||||
component: 'MaskLayer',
|
||||
remark: '排行榜右上角礼物按钮弹窗,当前只完成视觉容器。',
|
||||
},
|
||||
],
|
||||
features: [
|
||||
{
|
||||
name: 'countdown',
|
||||
@ -35,8 +94,7 @@ export const pageConfig = {
|
||||
endTime: '',
|
||||
remainingText: '',
|
||||
},
|
||||
remark:
|
||||
'倒计时模块占位;本地图片清单尚未扫描到 timeBg,待补充素材和截止时间后再启用。',
|
||||
remark: '倒计时模块占位;本地图片清单尚未扫描到 timeBg,待补充素材和截止时间后再启用。',
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
358
src/views/Activities/WorldCup/世界杯接口说明.md
Normal file
358
src/views/Activities/WorldCup/世界杯接口说明.md
Normal file
@ -0,0 +1,358 @@
|
||||
# 世界杯接口说明
|
||||
|
||||
本文件记录 `src/api/worldCup.js` 中世界杯活动专用接口的参数、返回字段和页面对接注意事项。页面后续补比赛、竞猜、我的记录、排行榜模块时,先对照本文件和 `页面创建信息.md`。
|
||||
|
||||
## 1. 接口文件
|
||||
|
||||
- 文件:`src/api/worldCup.js`
|
||||
- 公共前缀:`/activity/worldcup`
|
||||
- 请求封装:`src/utils/http.js` 中的 `get`、`post`
|
||||
|
||||
## 2. POST 接口
|
||||
|
||||
### 2.1 比赛列表
|
||||
|
||||
- 方法名:`apiPostWorldCupMatchList(data)`
|
||||
- 请求方式:`POST`
|
||||
- 路径:`/activity/worldcup/match/list`
|
||||
- 用途:分页获取比赛列表。
|
||||
|
||||
Body 参数:
|
||||
|
||||
```json
|
||||
{
|
||||
"size": 0,
|
||||
"current": 0,
|
||||
"tab": 0
|
||||
}
|
||||
```
|
||||
|
||||
页面当前调用固定为:
|
||||
|
||||
```json
|
||||
{
|
||||
"size": 10,
|
||||
"current": 1,
|
||||
"tab": 0
|
||||
}
|
||||
```
|
||||
|
||||
切换到 `Ended` 时仅把 `tab` 改为 `1`,暂不做上拉加载。
|
||||
|
||||
参数说明:
|
||||
|
||||
| 字段 | 必填 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| size | 是 | 每页数量 |
|
||||
| current | 是 | 当前页 |
|
||||
| tab | 是 | `0` 进行中,可投注或比赛中;`1` 已结束 |
|
||||
|
||||
返回示例:
|
||||
|
||||
```json
|
||||
{
|
||||
"records": [
|
||||
{
|
||||
"matchId": 0,
|
||||
"homeTeam": "string",
|
||||
"awayTeam": "string",
|
||||
"homeFlag": "string",
|
||||
"awayFlag": "string",
|
||||
"kickoffTime": 0,
|
||||
"status": 0,
|
||||
"result": 0,
|
||||
"totalPool": 0,
|
||||
"totalPoolFormat": "string",
|
||||
"homePoolFormat": "string",
|
||||
"drawPoolFormat": "string",
|
||||
"awayPoolFormat": "string",
|
||||
"homePercent": 0,
|
||||
"drawPercent": 0,
|
||||
"awayPercent": 0,
|
||||
"myHomeBetFormat": "string",
|
||||
"myDrawBetFormat": "string",
|
||||
"myAwayBetFormat": "string",
|
||||
"joinCount": 0,
|
||||
"joined": true,
|
||||
"myPayout": 0
|
||||
}
|
||||
],
|
||||
"total": 0,
|
||||
"size": 0,
|
||||
"current": 0
|
||||
}
|
||||
```
|
||||
|
||||
字段说明:
|
||||
|
||||
| 字段 | 说明 |
|
||||
| --- | --- |
|
||||
| matchId | 比赛 ID |
|
||||
| homeTeam / awayTeam | 主队 / 客队国家名称 |
|
||||
| homeFlag / awayFlag | 主队 / 客队旗帜图片链接 |
|
||||
| kickoffTime | 开赛时间戳,毫秒 |
|
||||
| status | `0` 未开赛,`1` 比赛中,`2` 已结束 |
|
||||
| result | `1` 主胜,`2` 平局,`3` 客胜,未结算为 `null` |
|
||||
| totalPool | 总奖池 |
|
||||
| totalPoolFormat | 总奖池格式化显示,例如 `123.12M` |
|
||||
| homePoolFormat / drawPoolFormat / awayPoolFormat | 主胜 / 平局 / 客胜各选项奖池格式化显示,例如 `123.12M` |
|
||||
| homePercent / drawPercent / awayPercent | 主胜 / 平局 / 客胜占比,百分比,保留 1 位 |
|
||||
| myHomeBetFormat / myDrawBetFormat / myAwayBetFormat | 当前用户在主胜 / 平局 / 客胜各选项的累计下注额格式化显示,未下注为 `null` |
|
||||
| joinCount | 参与人次 |
|
||||
| joined | 当前用户是否已在本场投注 |
|
||||
| myPayout | 当前用户本场赢得金额(中奖且已结算时有值,其余为null). |
|
||||
|
||||
页面对接注意:
|
||||
|
||||
- `Predict Now` 和 `Ended` 底部总奖池入口优先展示 `totalPoolFormat`,没有格式化字段时回退 `totalPool`。
|
||||
- `Ended` 列表三列押注项的上方金额展示 `myHomeBetFormat` / `myDrawBetFormat` / `myAwayBetFormat`;字段不为 `null` 的选项视为当前用户已押注并展示选中态。
|
||||
- `Ended` 列表三列押注项的下方金额展示 `homePoolFormat` / `drawPoolFormat` / `awayPoolFormat`。
|
||||
|
||||
### 2.2 押注
|
||||
|
||||
- 方法名:`apiPostWorldCupBet(data)`
|
||||
- 请求方式:`POST`
|
||||
- 路径:`/activity/worldcup/bet`
|
||||
- 用途:对指定比赛提交竞猜押注。
|
||||
|
||||
Body 参数:
|
||||
|
||||
```json
|
||||
{
|
||||
"matchId": 0,
|
||||
"betOption": 0,
|
||||
"betAmount": 0
|
||||
}
|
||||
```
|
||||
|
||||
参数说明:
|
||||
|
||||
| 字段 | 必填 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| matchId | 是 | 比赛 ID |
|
||||
| betOption | 是 | 押注选项:`1` 主胜,`2` 平局,`3` 客胜 |
|
||||
| betAmount | 是 | 押注金额:`100` / `1000` / `10000` / `100000` |
|
||||
|
||||
### 2.3 我的竞猜记录
|
||||
|
||||
- 方法名:`apiPostWorldCupMyBetList(data)`
|
||||
- 请求方式:`POST`
|
||||
- 路径:`/activity/worldcup/my/bet/list`
|
||||
- 用途:分页获取我的竞猜记录。
|
||||
|
||||
Body 参数:
|
||||
|
||||
```json
|
||||
{
|
||||
"size": 0,
|
||||
"current": 0,
|
||||
"matchId": 0
|
||||
}
|
||||
```
|
||||
|
||||
参数说明:
|
||||
|
||||
| 字段 | 必填 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| size | 是 | 每页数量 |
|
||||
| current | 是 | 当前页 |
|
||||
| matchId | 否 | 比赛 ID,为空查全部 |
|
||||
|
||||
返回示例:
|
||||
|
||||
```json
|
||||
{
|
||||
"records": [
|
||||
{
|
||||
"betId": 0,
|
||||
"matchId": 0,
|
||||
"betOption": 0,
|
||||
"betOptionName": "string",
|
||||
"betAmount": 0,
|
||||
"betStatus": 0,
|
||||
"payout": 0,
|
||||
"betTime": 0
|
||||
}
|
||||
],
|
||||
"total": 0,
|
||||
"size": 0,
|
||||
"current": 0
|
||||
}
|
||||
```
|
||||
|
||||
字段说明:
|
||||
|
||||
| 字段 | 说明 |
|
||||
| --- | --- |
|
||||
| betId | 竞猜记录 ID |
|
||||
| matchId | 比赛 ID |
|
||||
| betOption | 押注选项:`1` 主胜,`2` 平局,`3` 客胜 |
|
||||
| betOptionName | 押注选项展示文案,队名或平局 |
|
||||
| betAmount | 押注金额 |
|
||||
| betStatus | `0` 待结算,`1` 中奖,`2` 未中奖 |
|
||||
| payout | 派彩金额,中奖时展示 |
|
||||
| betTime | 下注时间戳,毫秒 |
|
||||
|
||||
### 2.4 排行榜
|
||||
|
||||
- 方法名:`apiPostWorldCupRankList()`
|
||||
- 请求方式:`POST`
|
||||
- 路径:`/activity/worldcup/rank/list`
|
||||
- Body 参数:无业务参数,POST 请求仍传空对象 `{}`
|
||||
- 用途:获取世界杯活动排行榜和当前用户排名。
|
||||
- 页面字段映射:
|
||||
- `rankingList` 对应页面排行榜列表,第四名以后才进入展开列表。
|
||||
- `currentUserRank` 对应贴底“我的排名”模块。
|
||||
- `quantity` 对应页面贡献值 `amount`。
|
||||
- 带 `user_id_prefix` 的展示 ID 取 `account`;打开用户资料使用 `userId`。
|
||||
- 昵称取 `nickname`,头像取 `avatar`,魅力等级取 `charmLevel`,财富等级取 `wealthLevel`,VIP 取 `vipLevelName`;`userLevel` 当前页面暂不使用。
|
||||
|
||||
返回示例:
|
||||
|
||||
```json
|
||||
{
|
||||
"activityType": 0,
|
||||
"activityTypeName": "string",
|
||||
"cycleType": 0,
|
||||
"cycleTypeName": "string",
|
||||
"cycleKey": "string",
|
||||
"cycleDisplay": "string",
|
||||
"dimension": 0,
|
||||
"dimensionName": "string",
|
||||
"dimensionUnit": "string",
|
||||
"rankingList": [
|
||||
{
|
||||
"userId": 0,
|
||||
"encryptedId": "string",
|
||||
"account": "string",
|
||||
"specialAccount": "string",
|
||||
"nickname": "string",
|
||||
"avatar": "string",
|
||||
"userSex": 0,
|
||||
"userLevel": 0,
|
||||
"rank": 0,
|
||||
"quantity": "string",
|
||||
"quantityNumber": 0,
|
||||
"activityType": 0,
|
||||
"activityTypeName": "string",
|
||||
"cycleType": 0,
|
||||
"cycleTypeName": "string",
|
||||
"cycleKey": "string",
|
||||
"dimension": 0,
|
||||
"dimensionName": "string",
|
||||
"dimensionUnit": "string",
|
||||
"extData": "string",
|
||||
"age": 0,
|
||||
"wealthLevel": 0,
|
||||
"vipLevelName": "string",
|
||||
"isCurrentUser": true
|
||||
}
|
||||
],
|
||||
"currentUserRank": {
|
||||
"userId": 0,
|
||||
"account": "string",
|
||||
"userSex": 0,
|
||||
"nickname": "string",
|
||||
"avatar": "string",
|
||||
"rank": 0,
|
||||
"quantity": "string",
|
||||
"quantityNumber": 0,
|
||||
"isRanked": true,
|
||||
"quantityToRank": 0,
|
||||
"rankChange": 0,
|
||||
"lastRank": 0,
|
||||
"age": 0,
|
||||
"wealthLevel": 0,
|
||||
"userLevel": 0,
|
||||
"vipLevelName": "string"
|
||||
},
|
||||
"totalParticipants": 0,
|
||||
"updateTime": "string"
|
||||
}
|
||||
```
|
||||
|
||||
字段说明:
|
||||
|
||||
| 字段 | 说明 |
|
||||
| --- | --- |
|
||||
| cycleDisplay | 周期显示文本,例如日榜 `2025-01-01`、周榜 `2025年第1周`、月榜 `2025年1月` |
|
||||
| dimension / dimensionName / dimensionUnit | 排行统计维度编码、名称、单位 |
|
||||
| rankingList | 排行榜 TOP N 列表 |
|
||||
| currentUserRank | 当前用户排名,`rank=0` 表示未上榜 |
|
||||
| quantity / quantityNumber | 统计数值展示文本 / 数值 |
|
||||
| quantityToRank | 距离上榜还需要的数值,仅未上榜时有值 |
|
||||
| rankChange | 排名变化,正数上升,负数下降,0 不变 |
|
||||
| totalParticipants | 参与总人数 |
|
||||
| updateTime | 榜单更新时间 |
|
||||
|
||||
页面对接注意:
|
||||
|
||||
- 排行榜用户头像、昵称、VIP、魅力等级、财富等级按 `apiPostWorldCupRankList` 当前返回字段映射。
|
||||
- 页面贡献值别名 `amount` 使用 `quantity`,`quantityNumber` 原样保留备用。
|
||||
- `encryptedId` 可用于隐身用户和 `currentUserRank` 匹配。
|
||||
|
||||
## 3. GET 接口
|
||||
|
||||
### 3.1 比赛详情
|
||||
|
||||
- 方法名:`apiGetWorldCupMatchDetail(matchId)`
|
||||
- 请求方式:`GET`
|
||||
- 路径:`/activity/worldcup/match/detail`
|
||||
- Query 参数:`matchId` 必填
|
||||
- 用途:获取比赛详情和当前用户本场竞猜记录。
|
||||
|
||||
返回示例:
|
||||
|
||||
```json
|
||||
{
|
||||
"match": {
|
||||
"matchId": 0,
|
||||
"homeTeam": "string",
|
||||
"awayTeam": "string",
|
||||
"homeFlag": "string",
|
||||
"awayFlag": "string",
|
||||
"kickoffTime": 0,
|
||||
"status": 0,
|
||||
"result": 0,
|
||||
"totalPool": 0,
|
||||
"totalPoolFormat": "string",
|
||||
"homePoolFormat": "string",
|
||||
"drawPoolFormat": "string",
|
||||
"awayPoolFormat": "string",
|
||||
"homePercent": 0,
|
||||
"drawPercent": 0,
|
||||
"awayPercent": 0,
|
||||
"myHomeBetFormat": "string",
|
||||
"myDrawBetFormat": "string",
|
||||
"myAwayBetFormat": "string",
|
||||
"joinCount": 0,
|
||||
"joined": true
|
||||
},
|
||||
"homePool": 0,
|
||||
"drawPool": 0,
|
||||
"awayPool": 0,
|
||||
"myBets": [
|
||||
{
|
||||
"betId": 0,
|
||||
"matchId": 0,
|
||||
"betOption": 0,
|
||||
"betOptionName": "string",
|
||||
"betAmount": 0,
|
||||
"betStatus": 0,
|
||||
"payout": 0,
|
||||
"betTime": 0
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
字段说明:
|
||||
|
||||
| 字段 | 说明 |
|
||||
| --- | --- |
|
||||
| match | 比赛基础信息,字段同比赛列表 |
|
||||
| match.totalPoolFormat | 总奖池格式化显示,例如 `123.12M` |
|
||||
| match.homePoolFormat / match.drawPoolFormat / match.awayPoolFormat | 主胜 / 平局 / 客胜各选项奖池格式化显示 |
|
||||
| match.myHomeBetFormat / match.myDrawBetFormat / match.myAwayBetFormat | 当前用户在主胜 / 平局 / 客胜各选项的累计下注额格式化显示,未下注为 `null` |
|
||||
| homePool / drawPool / awayPool | 主胜 / 平局 / 客胜奖池明细 |
|
||||
| myBets | 我的本场竞猜列表,字段同我的竞猜记录 |
|
||||
@ -1,23 +1,28 @@
|
||||
# 页面创建信息
|
||||
|
||||
## 0. 建页规则关联
|
||||
|
||||
- 页面改动、模块补充、接口对接、样式同步和文档同步,都需要先联系 `docs/活动页/建页/` 下的建页规则执行。
|
||||
- 如果建页规则变化,先更新 `docs/活动页/建页/` 下的建页 md,再同步当前页面的 `页面创建信息.md` 和 Vue 代码。
|
||||
- 新建或更新页面时,页面模块、div 结构、方法、接口处理都需要补充中文注释。
|
||||
- 能使用内联样式处理的页面样式,优先使用内联样式。
|
||||
- 页面根容器使用 `height: 100vh`,内部主内容垂直滚动并隐藏滚动条,页面任何模块都不展示可见滚动条。
|
||||
- 接口返回的远程图片链接不要使用 `v-smart-img`,直接使用原生 `<img :src="接口字段">` 展示,不走本地图片缓存。
|
||||
|
||||
## 1. 基础信息
|
||||
|
||||
- 页面类型:Activities
|
||||
- 页面名称:WorldCup
|
||||
- 页面目录:src/views/Activities/WorldCup
|
||||
- 参考页面:
|
||||
- 备注:已生成 `index.vue` 可预览页面底座,目前完成头部和排行榜模块;排行榜接口和字段规则参考 `GulbenFestival` 的 personalRank / Ranking。
|
||||
- 页面目录:`src/views/Activities/WorldCup`
|
||||
- 路由 path:`/activities/world-cup`
|
||||
- 页面入口:`src/views/Activities/WorldCup/index.vue`
|
||||
- 页面配置:`src/views/Activities/WorldCup/page.config.js`
|
||||
- 专用接口:`src/api/worldCup.js`
|
||||
- 接口说明:`src/views/Activities/WorldCup/世界杯接口说明.md`
|
||||
- 参考页面:排行榜用户信息和等级展示规则参考 `src/views/Activities/GulbenFestival/index.vue` 的 `personalRank` / `Ranking` 模块。
|
||||
|
||||
## 2. 路由和权限
|
||||
|
||||
- route path:/activities/world-cup
|
||||
- route name:world-cup
|
||||
- 是否需要登录:否
|
||||
- 是否加入 `src/config/security.js`:是
|
||||
- 是否加入 `src/utils/permissionManager.js`:是
|
||||
|
||||
### 2.1 路由代码片段
|
||||
|
||||
```js
|
||||
{
|
||||
path: '/activities/world-cup',
|
||||
@ -27,253 +32,203 @@
|
||||
}
|
||||
```
|
||||
|
||||
### 2.2 安全白名单片段
|
||||
|
||||
```js
|
||||
'/activities/world-cup',
|
||||
```
|
||||
|
||||
### 2.3 权限页面片段
|
||||
|
||||
```js
|
||||
'/activities/world-cup', // WorldCup
|
||||
```
|
||||
- `src/config/security.js` 已加入 `/activities/world-cup`。
|
||||
- `src/utils/permissionManager.js` 已加入 `/activities/world-cup`。
|
||||
|
||||
## 3. OSS 图片目录
|
||||
|
||||
- OSS 相对路径:Activities/WorldCup/
|
||||
- 本地开发目录:public/oss/h5/Azizi/Activities/WorldCup/
|
||||
- 是否需要 Codex 自动创建本地开发目录:是
|
||||
- 图片是否已放入本地目录:是
|
||||
- OSS 相对路径:`Activities/WorldCup/`
|
||||
- 本地开发目录:`public/oss/h5/Azizi/Activities/WorldCup/`
|
||||
- 本地目录只作为开发期 OSS 图片镜像使用,发布时图片由用户上传到真实 OSS。
|
||||
|
||||
说明:本地开发目录只作为开发期 OSS 图片镜像使用,方便页面优先读取本地资源;发布时图片由你上传到真实 OSS,不随项目提交 Git。新建本地目录时不生成 `.gitkeep`,空目录不被 Git 跟踪是正常现象。
|
||||
### 3.1 当前图片清单
|
||||
|
||||
### 3.1 多语言图片清单
|
||||
| 图片名 | 备注 |
|
||||
| --- | --- |
|
||||
| bg2 | 页面最低层背景图,宽高和页面一致,直接拉伸填满 |
|
||||
| bg | 头部背景图 |
|
||||
| ruleBt | 头部右侧规则按钮 |
|
||||
| ruleBg | 规则弹窗背景 |
|
||||
| RankingMain | 排行榜主背景 |
|
||||
| RankingBottomBorder | 排行榜底部边框 |
|
||||
| RankingItem | 第四名以后的排行榜列表项背景 |
|
||||
| frameTop1 | 第一名头像框 |
|
||||
| frameTop2 | 第二名头像框 |
|
||||
| frameTop3 | 第三名头像框 |
|
||||
| giftBt | RankingMain 右上角奖励按钮 |
|
||||
| rewardBg | 奖励弹窗背景 |
|
||||
| showMoreBt | 展开 / 收起排行榜按钮图标 |
|
||||
| myRankingBg | 我的排名贴底背景 |
|
||||
| EventlistBg | 赛事列表模块背景 |
|
||||
| matchBg | Predict Now 赛事列表项背景 |
|
||||
| drawSign | Ended 平局押注项图片 |
|
||||
| numPeople | 赛事列表右上角参与人数图标 |
|
||||
| rightBt | 总奖池入口右侧箭头 |
|
||||
| vip1-vip6 | VIP 等级图片 |
|
||||
| userLevel1Bg-userLevel5Bg | 魅力等级背景图,图片名沿用旧素材命名 |
|
||||
| wealthLevel1Bg-wealthLevel5Bg | 财富等级背景图 |
|
||||
|
||||
| 图片名 | 支持语言 | 备注 |
|
||||
| --- | --- | --- |
|
||||
| bg | 默认 | 头部主背景图 |
|
||||
| bg2 | 默认 | 页面最低层背景图,拉伸铺满 100vh 视口 |
|
||||
| RankingMain | 默认 | 排行榜主背景,内部上方展示前三名,下方展示 Show All Users |
|
||||
| RankingItem | 默认 | 第四名以后排行榜列表项背景 |
|
||||
| RankingBottomBorder | 默认 | 排行榜底边框 |
|
||||
| frameTop1 | 默认 | 第一名头像框 |
|
||||
| frameTop2 | 默认 | 第二名头像框 |
|
||||
| frameTop3 | 默认 | 第三名头像框 |
|
||||
| ruleBt | 默认 | 规则按钮 |
|
||||
| showMoreBt | 默认 | 展开/收起排行榜按钮图标 |
|
||||
| vip1-vip6 | 默认 | VIP 等级图片 |
|
||||
| userLevel1Bg-userLevel5Bg | 默认 | 魅力等级背景 |
|
||||
| wealthLevel1Bg-wealthLevel5Bg | 默认 | 财富等级背景 |
|
||||
### 3.2 阻塞预加载图片
|
||||
|
||||
### 3.2 进入页面前同步预加载图片
|
||||
`bg2`、`bg`、`ruleBt`、`ruleBg`、`EventlistBg`、`matchBg`、`drawSign`、`numPeople`、`rightBt`、`RankingMain`、`RankingItem`、`RankingBottomBorder`、`myRankingBg`、`giftBt`、`rewardBg`、`showMoreBt`、`frameTop1`、`frameTop2`、`frameTop3`、`vip1-vip6`、`userLevel1Bg-userLevel5Bg`、`wealthLevel1Bg-wealthLevel5Bg`。
|
||||
|
||||
| 图片名 | 是否包含多语言 | 备注 |
|
||||
| --- | --- | --- |
|
||||
| bg2 | 否 | 页面最低层背景 |
|
||||
| bg | 否 | 头部背景 |
|
||||
| ruleBt | 否 | 规则按钮 |
|
||||
| RankingMain | 否 | 排行榜主背景 |
|
||||
| RankingItem | 否 | 排行榜列表项背景 |
|
||||
| RankingBottomBorder | 否 | 排行榜底边框 |
|
||||
| showMoreBt | 否 | 展开/收起按钮图标 |
|
||||
| frameTop1 | 否 | 第一名头像框 |
|
||||
| frameTop2 | 否 | 第二名头像框 |
|
||||
| frameTop3 | 否 | 第三名头像框 |
|
||||
| vip1-vip6 | 否 | VIP 等级图片 |
|
||||
| userLevel1Bg-userLevel5Bg | 否 | 魅力等级背景 |
|
||||
| wealthLevel1Bg-wealthLevel5Bg | 否 | 财富等级背景 |
|
||||
页面进入时必须展示统一加载动画,阻塞预加载完成后再展示主内容。
|
||||
|
||||
### 3.3 进入页面后后台预加载图片
|
||||
## 4. 页面结构
|
||||
|
||||
| 图片名 | 是否包含多语言 | 备注 |
|
||||
| --- | --- | --- |
|
||||
| footballSign | 否 | 后续模块素材,当前未使用 |
|
||||
| giftBt | 否 | 后续模块素材,当前未使用 |
|
||||
| matchBg | 否 | 后续模块素材,当前未使用 |
|
||||
| myRankingBg | 否 | 后续我的排名素材,当前未使用 |
|
||||
| numPeople | 否 | 后续模块素材,当前未使用 |
|
||||
| rewardBg | 否 | 后续奖励模块素材,当前未使用 |
|
||||
| ruleBg | 否 | 后续规则弹窗素材,当前未接弹窗 |
|
||||
| statusEndedfail | 否 | 后续状态素材,当前未使用 |
|
||||
| statusEndedWin | 否 | 后续状态素材,当前未使用 |
|
||||
| statusInProgress | 否 | 后续状态素材,当前未使用 |
|
||||
| statusNotStarted | 否 | 后续状态素材,当前未使用 |
|
||||
### 4.1 根容器
|
||||
|
||||
## 4. 模块配置
|
||||
- 根容器背景色:`#101E14`。
|
||||
- 最底层背景图:`bg2`,使用绝对定位铺满 `100vw * 100vh`。
|
||||
- 主内容:`flex-direction: column`,`gap: 2vw`,内部滚动并隐藏滚动条。
|
||||
- 底部固定展示我的排名模块,主内容底部保留 `20vw` 占位,防止遮挡。
|
||||
|
||||
当前页面按用户逐步补充需求生成,只处理已明确指定模块。
|
||||
### 4.2 头部模块
|
||||
|
||||
### 4.1 标签页模块
|
||||
- 使用 `itemCenter` 承载 `bg`。
|
||||
- 通过 `contentStyle` 设置 padding。
|
||||
- 内部右侧放置 `ruleBt` 图片按钮。
|
||||
- 点击 `ruleBt` 打开规则弹窗。
|
||||
|
||||
```json
|
||||
{
|
||||
"enabled": false,
|
||||
"defaultValue": "",
|
||||
"items": []
|
||||
}
|
||||
```
|
||||
### 4.3 排行榜模块
|
||||
|
||||
### 4.2 页面区块模块
|
||||
- 使用 `RankingMain` 的 `itemCenter` 承载排行榜主内容。
|
||||
- `RankingMain` 的 `contentStyle` 为 column,`justify-content: space-between`。
|
||||
- `RankingMain` 内部右上角绝对定位 `giftBt`,宽度暂定 `10vw`,点击打开奖励弹窗。
|
||||
- `RankingMain` 内部上方为前三名模块,下方为 `Show All Users` 展示更多模块。
|
||||
- 前三名为同一行,使用 `direction: ltr`,排序为左二、中一、右三,并设置 `align-items: flex-end`。
|
||||
- 前三名私有组件为 `components/topUser.vue`,头像框分别使用 `frameTop1`、`frameTop2`、`frameTop3`。
|
||||
- `topUser` 内部从上到下展示头像、名称、ID、等级行,整体使用 column 并水平居中。
|
||||
- 等级行展示 VIP、魅力等级、财富等级,展示规则参考 `GulbenFestival` 的 `personalRank` / `Ranking`。
|
||||
- 魅力等级字段使用接口返回的 `charmLevel`,当前页面暂不使用 `userLevel` 字段。
|
||||
- 带 `{{ $t('user_id_prefix') }}` 的 ID 展示后面使用接口字段 `account`。
|
||||
- 第四名以后列表使用 `RankingItem`,列表项右侧贡献值前加 `/src/assets/icon/Azizi/coin.png`,金币和贡献值使用 flex 居中。
|
||||
- `Show All Users` 只有在第四名以后存在数据时展示。
|
||||
- 点击 `Show All Users` 后展示第四名以后列表和 `RankingBottomBorder`。
|
||||
- `Collapse List` 放在 `RankingBottomBorder` 下方,点击后隐藏第四名以后列表。
|
||||
- `Collapse List` 下方保留 `20vw` 底部占位。
|
||||
- 我的排名贴底模块使用 `myRankingBg` 的 `itemCenter`,固定在页面底部,不需要标签页判断。
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"name": "header",
|
||||
"enabled": true,
|
||||
"images": ["bg", "ruleBt"],
|
||||
"layout": {
|
||||
"container": "itemCenter",
|
||||
"containerStyle": "width: 100vw",
|
||||
"contentStyle": "padding: 87vw 0 0; align-items: flex-start; justify-content: flex-start; box-sizing: border-box;",
|
||||
"ruleRowStyle": "width: 100%; display: flex; justify-content: flex-end",
|
||||
"ruleButtonStyle": "width: 13.6vw; display: block"
|
||||
},
|
||||
"remark": "头部背景使用 bg,内部只放贴右 ruleBt。"
|
||||
},
|
||||
{
|
||||
"name": "ranking",
|
||||
"enabled": true,
|
||||
"images": [
|
||||
"RankingMain",
|
||||
"RankingItem",
|
||||
"RankingBottomBorder",
|
||||
"frameTop1",
|
||||
"frameTop2",
|
||||
"frameTop3",
|
||||
"showMoreBt",
|
||||
"vip1-vip6",
|
||||
"userLevel1Bg-userLevel5Bg",
|
||||
"wealthLevel1Bg-wealthLevel5Bg"
|
||||
],
|
||||
"layout": {
|
||||
"sectionStyle": "position: relative; width: 100vw",
|
||||
"backgroundStyle": "position: absolute; inset: 15vw 2vw 4vw; z-index: 0; background-color: #053126",
|
||||
"rankingMainStyle": "width: 100vw",
|
||||
"rankingMainContentStyle": "direction: ltr; padding: 0; flex-direction: column; align-items: center; justify-content: space-between; box-sizing: border-box;",
|
||||
"topModuleStyle": "width: 100%; padding: 20vw 6vw 0; display: flex; align-items: flex-start; justify-content: center; box-sizing: border-box;",
|
||||
"topThreeStyle": "width: 100%; height: 45vw; display: flex; align-items: flex-end; justify-content: space-between; gap: 1vw;",
|
||||
"topUserOrder": "左二、中一、右三;第二名 width:24vw order:1,第一名 width:26vw order:2,第三名 width:24vw order:3",
|
||||
"showMoreStyle": "width: 100%; padding: 0 0 4vw; position: relative; z-index: 2; display: flex; align-items: center; justify-content: center; gap: 2vw; color: #fff; font-size: 1.2em; font-weight: 800; line-height: 1;",
|
||||
"showMoreImageStyle": "width: 10vw; display: block",
|
||||
"rankingItemStyle": "width: 100vw; margin-top: -2vw",
|
||||
"rankingItemContentStyle": "padding: 3vw 8vw 0; box-sizing: border-box; justify-content: space-between;",
|
||||
"collapseStyle": "position: relative; z-index: 2; width: 100vw; padding: 2vw 0 4vw; display: flex; align-items: center; justify-content: center; gap: 2vw; color: #fff; font-size: 1.2em; font-weight: 800; line-height: 1;",
|
||||
"collapseImageStyle": "width: 10vw; display: block; transform: scaleY(-1)",
|
||||
"bottomBorderStyle": "position: relative; z-index: 1; display: block; width: 100vw; margin-top: -9vw; pointer-events: none"
|
||||
},
|
||||
"interaction": {
|
||||
"defaultExpanded": false,
|
||||
"showText": "Show All Users",
|
||||
"collapseText": "Collapse List",
|
||||
"showAction": "点击 Show All Users 后 showRankingList=true,开始展示 RankingItem 列表。",
|
||||
"collapseAction": "点击 Collapse List 后 showRankingList=false,隐藏 RankingItem 列表。"
|
||||
},
|
||||
"remark": "RankingMain 内部从上到下分为前三名模块和展示更多模块;第四名以后列表默认隐藏。"
|
||||
}
|
||||
]
|
||||
```
|
||||
### 4.4 赛事列表模块
|
||||
|
||||
倒计时模块占位:
|
||||
- 赛事列表放在排行榜模块下方,使用 `EventlistBg` 的 `itemCenter`。
|
||||
- `EventlistBg` 内部样式为 column,`justify-content: flex-start`,`align-items: center`,`gap: 2vw`,`padding: 0`。
|
||||
- 第一个小模块为文字切换按钮,左右各占 50%。
|
||||
- 切换按钮文案为 `Predict Now!` 和 `Ended`。
|
||||
- 选中按钮使用金色边框和渐变背景,文本颜色 `#FFE083`。
|
||||
- 未选中按钮文本颜色 `#E9CB78`。
|
||||
- 第二个小模块为赛事列表区域,`flex: 1`,可垂直滚动并隐藏滚动条。
|
||||
- 接口使用 `apiPostWorldCupMatchList({ size: 10, current: 1, tab })`。
|
||||
- `Predict Now!` 对应 `tab: 0`,`Ended` 对应 `tab: 1`。
|
||||
- 每次切换按钮都重新发送接口,暂不做上拉加载。
|
||||
- 两个 tab 的数据必须分开承载:`predictMatchList` 保存 Predict Now 数据,`endedMatchList` 保存 Ended 数据,避免接口慢返回时两个列表数据混用。
|
||||
- 请求保护使用每个 tab 独立的 request token:只忽略同 tab 内更旧的响应,不影响另一个 tab 的缓存。
|
||||
- Predict Now 使用 `matchBg` 的 `itemCenter` 渲染列表项。
|
||||
- Ended 不使用 `matchBg`,直接使用普通 `div` 卡片样式渲染列表项。
|
||||
- Predict Now 和 Ended 的单条列表都使用新的 column wrapper:上方是原赛事卡片,下方是总奖池入口,用 `gap` 控制上下间距。
|
||||
- 总奖池入口样式为金色描边渐变背景,内部一行展示 `TOTAL PRIZE POOL`、coin 图片、总奖池金额和右侧 `rightBt`;金额优先展示 `totalPoolFormat`,没有格式化字段时回退 `totalPool`。
|
||||
- 点击总奖池入口进入 `/activities/world-cup/event-detail?matchId=...`,详情页目录为 `src/views/Activities/WorldCup/EventDetail`。
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "countdown",
|
||||
"enabled": false,
|
||||
"deadline": "",
|
||||
"images": ["timeBg"],
|
||||
"fields": {
|
||||
"endTime": "",
|
||||
"remainingText": ""
|
||||
},
|
||||
"remark": "倒计时模块占位;本地图片清单尚未扫描到 timeBg,待补充素材和截止时间后再启用。"
|
||||
}
|
||||
```
|
||||
Predict Now 列表项规则:
|
||||
|
||||
### 4.3 弹窗模块
|
||||
- 每条记录使用 `matchBg` 的 `itemCenter`。
|
||||
- `matchBg` 内部从上往下分为状态行和赛事简要信息行。
|
||||
- 状态行左侧按 `status` 和 `joined` 展示三种状态:
|
||||
- `status === 1 && joined === true`:绿色小圆点加文本 `join`,绿色渐变背景。
|
||||
- `status === 1 && joined === false`:文本 `Not Joined`,灰色渐变背景。
|
||||
- 其他状态:文本 `Kick-off: HH:mm(UTC+8)`,时间来自 `kickoffTime`。
|
||||
- 状态行右侧展示 `numPeople` 图片和 `joinCount`。
|
||||
- 赛事简要信息行为三列:主国、平局、客国。
|
||||
- 主国展示 `homeFlag`、`homeTeam`、`homePercent`。
|
||||
- 平局展示 `DRAW`、`drawPercent`,不放旗帜。
|
||||
- 客国展示 `awayFlag`、`awayTeam`、`awayPercent`。
|
||||
- 主国和客国占比颜色为 `#71CE6E`,平局占比颜色为 `#A0A181`。
|
||||
|
||||
```json
|
||||
[]
|
||||
```
|
||||
Ended 列表项规则:
|
||||
|
||||
## 5. 接口参数
|
||||
- 每条记录使用普通 `div`,样式为 `border-radius: 10.682px; border: 0.89px solid #5D6948; background: linear-gradient(180deg, #0B2415 0%, #051C12 100%); backdrop-filter: blur(12.687064170837402px);`。
|
||||
- 内部从上往下分为状态行和押注选项行。
|
||||
- 状态行左侧按参与结果展示三种状态:参与且中奖、参与未中奖、未参与。
|
||||
- 参与且中奖展示 `+`、coin 图片和中奖金额;中奖金额优先使用 `myPayout`,并兼容 `payout` / `winAmount` 字段。
|
||||
- 参与未中奖展示 `Better luck next time`。
|
||||
- 未参与展示 `Not Joined`。
|
||||
- 状态行右侧展示 `numPeople` 图片和 `joinCount`,和 Predict Now 保持一致。
|
||||
- 押注选项行分为主方、平局、客方三列,仅做展示,不绑定押注点击。
|
||||
- 未选中押注项使用默认金色边框渐变样式;选中押注项使用黄色描边和发光阴影样式。
|
||||
- 选中态根据当前用户在各选项的累计下注额判断:`myHomeBetFormat`、`myDrawBetFormat`、`myAwayBetFormat` 不为空的押注项展示选中态,支持多个押注项同时选中。
|
||||
- 主方展示 `homeFlag`、`homeTeam`、`homePercent`。
|
||||
- 平局展示 `drawSign`、`DRAW`、`drawPercent`。
|
||||
- 客方展示 `awayFlag`、`awayTeam`、`awayPercent`。
|
||||
- Ended 押注项旗帜图片来源不同:主客方 `homeFlag` / `awayFlag` 是接口返回链接,不使用 `v-smart-img`;平局 `drawSign` 是 OSS 素材,单独使用 `v-smart-img`。
|
||||
- 押注项金额信息分为上下两行,中间使用 `background: #E9D089` 分割线;上方展示用户累计下注额 `myHomeBetFormat` / `myDrawBetFormat` / `myAwayBetFormat`,下方展示选项奖池 `homePoolFormat` / `drawPoolFormat` / `awayPoolFormat`,空值展示 `0`。
|
||||
|
||||
```json
|
||||
{
|
||||
"activityId": "",
|
||||
"activityCode": "",
|
||||
"activityType": 19,
|
||||
"rankingActivityType": "",
|
||||
"rewardActivityType": "",
|
||||
"otherParams": {}
|
||||
}
|
||||
```
|
||||
### 4.5 弹窗模块
|
||||
|
||||
## 6. 组件需求
|
||||
- 规则弹窗和奖励弹窗都使用公共组件 `src/components/MaskLayer.vue`。
|
||||
- 规则弹窗使用 `ruleBg` 的 `itemCenter`,弹窗宽度暂定 `100vw`。
|
||||
- 奖励弹窗使用 `rewardBg` 的 `itemCenter`,弹窗宽度暂定 `100vw`。
|
||||
- `ruleBg` 和 `rewardBg` 图片高度偏长,弹窗内容不做垂直居中,外层保留顶部和底部空白区域,点击空白区域退出弹窗。
|
||||
|
||||
需要哪一个就填“是”,不需要或没填写的初版页面先不接。
|
||||
## 5. 接口说明
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"name": "BackgroundLayer",
|
||||
"enabled": "是",
|
||||
"remark": "长背景分段"
|
||||
},
|
||||
{
|
||||
"name": "itemCenter",
|
||||
"enabled": "是",
|
||||
"remark": "图片容器"
|
||||
},
|
||||
{
|
||||
"name": "topUser / TopUser",
|
||||
"enabled": "是",
|
||||
"remark": "前三名用户展示"
|
||||
},
|
||||
{
|
||||
"name": "Barrage",
|
||||
"enabled": "",
|
||||
"remark": "弹幕"
|
||||
},
|
||||
{
|
||||
"name": "ActivityTabs",
|
||||
"enabled": "",
|
||||
"remark": "标签页"
|
||||
},
|
||||
{
|
||||
"name": "ActivityLotteryGrid",
|
||||
"enabled": "",
|
||||
"remark": "抽奖格子"
|
||||
}
|
||||
]
|
||||
```
|
||||
- `apiPostWorldCupMatchList(data)`:比赛列表,`POST /activity/worldcup/match/list`。
|
||||
- `apiPostWorldCupBet(data)`:押注,`POST /activity/worldcup/bet`。
|
||||
- `apiPostWorldCupMyBetList(data)`:我的竞猜记录,`POST /activity/worldcup/my/bet/list`。
|
||||
- `apiPostWorldCupRankList()`:排行榜,`POST /activity/worldcup/rank/list`,接口无业务参数但 POST 仍传 `{}`。
|
||||
- `apiGetWorldCupMatchDetail(matchId)`:比赛详情,`GET /activity/worldcup/match/detail`。
|
||||
- 详细字段和返回示例见 `src/views/Activities/WorldCup/世界杯接口说明.md`。
|
||||
|
||||
## 7. 建页后记录
|
||||
### 5.1 排行榜字段映射
|
||||
|
||||
### 7.1 已完成
|
||||
- `rankingList` 映射为页面排行榜列表。
|
||||
- `currentUserRank` 映射为我的排名贴底模块。
|
||||
- `rank` 使用接口字段 `rank`。
|
||||
- 用户资料点击使用接口字段 `userId`,接口没有 `rawUserId`。
|
||||
- 头像使用 `avatar`。
|
||||
- 昵称使用 `nickname`。
|
||||
- ID 展示使用 `account`。
|
||||
- 页面贡献值 `amount` 使用 `quantity`。
|
||||
- `quantityNumber` 原样保留。
|
||||
- 魅力等级使用 `charmLevel`。
|
||||
- 财富等级使用 `wealthLevel`。
|
||||
- VIP 使用 `vipLevelName`。
|
||||
|
||||
- 创建页面目录:`src/views/Activities/WorldCup/`
|
||||
- 创建专属页面信息文件:`src/views/Activities/WorldCup/页面创建信息.md`
|
||||
- 创建初始配置文件:`src/views/Activities/WorldCup/page.config.js`
|
||||
- 创建本地开发图片目录:`public/oss/h5/Azizi/Activities/WorldCup/`
|
||||
- 写入路由:`/activities/world-cup`
|
||||
- 写入 `src/config/security.js` 白名单
|
||||
- 写入 `src/utils/permissionManager.js` 活动页权限列表
|
||||
- 创建并持续更新 `index.vue` 可预览页面
|
||||
- 创建 `components/topUser.vue` 私有前三名组件
|
||||
- 完成统一加载动画、页面背景、头部模块、排行榜前三名、排行榜接口和展开/收起列表交互
|
||||
### 5.2 赛事列表字段映射
|
||||
|
||||
### 7.2 未填写所以跳过
|
||||
- `matchId`:比赛 ID。
|
||||
- `homeTeam` / `awayTeam`:主国 / 客国名称。
|
||||
- `homeFlag` / `awayFlag`:主国 / 客国旗帜,属于接口远程图片,直接用原生 `<img>` 展示,不加 `v-smart-img`。
|
||||
- `kickoffTime`:开赛时间戳,页面格式化为 `HH:mm(UTC+8)`。
|
||||
- `status`:比赛状态,`0` 未开赛,`1` 比赛中,`2` 已结束。
|
||||
- `joined`:当前用户是否已参与本场竞猜。
|
||||
- `joinCount`:参与人次。
|
||||
- `homePercent` / `drawPercent` / `awayPercent`:主胜 / 平局 / 客胜占比。
|
||||
|
||||
- 规则弹窗暂未接点击事件和弹窗内容。
|
||||
- 赛事、奖励、任务、我的排名等素材暂未生成对应业务模块。
|
||||
## 6. 建页后记录
|
||||
|
||||
### 7.3 待你微调
|
||||
### 6.1 已完成
|
||||
|
||||
- 继续按预览效果微调排行榜位置、间距和展开/收起按钮位置。
|
||||
- 后续补规则弹窗、赛事、奖励、任务、我的排名等模块时继续先更新本 md,再同步 Vue。
|
||||
- 创建页面目录 `src/views/Activities/WorldCup/`。
|
||||
- 创建页面信息文档 `src/views/Activities/WorldCup/页面创建信息.md`。
|
||||
- 创建页面配置 `src/views/Activities/WorldCup/page.config.js`。
|
||||
- 创建世界杯接口文件 `src/api/worldCup.js`。
|
||||
- 创建接口说明文档 `src/views/Activities/WorldCup/世界杯接口说明.md`。
|
||||
- 创建本地开发图片目录 `public/oss/h5/Azizi/Activities/WorldCup/`。
|
||||
- 写入路由 `/activities/world-cup`。
|
||||
- 写入安全白名单和活动页权限列表。
|
||||
- 完成统一加载动画、页面背景、头部模块、排行榜前三名、排行榜接口、展开 / 收起列表交互。
|
||||
- 完成我的排名贴底模块、奖励弹窗入口、规则弹窗。
|
||||
- 完成赛事列表初版、Predict Now 列表项、赛事列表 tab 数据分开承载。
|
||||
- 完成 Predict Now / Ended 列表项下方总奖池入口,并接入赛事详情页路由。
|
||||
- 创建赛事详情页占位目录 `src/views/Activities/WorldCup/EventDetail/` 和页面创建信息 md。
|
||||
|
||||
### 7.4 验证结果
|
||||
### 6.2 后续补充
|
||||
|
||||
- Ended 列表已完成当前接口字段下的展示结构,后续如果接口补充用户押注选项、派彩金额、单项奖池和押注金额,只需要更新字段映射。
|
||||
- 比赛详情、下注弹窗、奖励弹窗内容后续补充。
|
||||
- 规则弹窗内容后续补充。
|
||||
|
||||
### 6.3 验证记录
|
||||
|
||||
- `npm run build`:已运行并通过。
|
||||
- 备注:构建中存在项目原有 warning:`secondBg.png` 运行时解析提示,以及 `src/utils/http.js` 动静态混用 chunk 提示。
|
||||
- 构建中存在项目原有 warning:`secondBg.png` 运行时解析提示,以及 `src/utils/http.js` 动静态混用 chunk 提示。
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user