feat(新活动): 世界杯活动
This commit is contained in:
parent
5a7297d877
commit
f35e753383
@ -21,6 +21,7 @@ export const PUBLIC_PATHS = Object.freeze([
|
||||
'/daily-recharge',
|
||||
'/activities/gulben-festival',
|
||||
'/activities/bounty-football',
|
||||
'/activities/world-cup',
|
||||
'/activities/lucky-dollars-season5',
|
||||
'/activities/lucky-dollars-season4',
|
||||
'/activities/lesser-bairam',
|
||||
|
||||
@ -446,6 +446,12 @@ const router = createRouter({
|
||||
}, //2026年3月14日上午5点结束
|
||||
|
||||
//游戏活动
|
||||
{
|
||||
path: '/activities/world-cup',
|
||||
name: 'world-cup',
|
||||
component: () => import('../views/Activities/WorldCup/index.vue'),
|
||||
meta: { requiresAuth: false },
|
||||
}, // WorldCup
|
||||
{
|
||||
path: '/activities/bounty-football',
|
||||
name: 'bounty-football',
|
||||
|
||||
@ -65,6 +65,7 @@ const ACTIVITIES = [
|
||||
'/activities/lucky-dollars-season5', // 幸运美金活动
|
||||
'/activities/gulben-festival', // 古尔邦节活动
|
||||
'/activities/bounty-football', // BountyFootball
|
||||
'/activities/world-cup', // WorldCup
|
||||
'/activities/lucky-dollars-season4', // 幸运美金活动
|
||||
'/activities/lesser-bairam', // 开斋节打榜
|
||||
'/activities/lucky-dollars-season3', // 斋月打榜
|
||||
|
||||
182
src/views/Activities/WorldCup/components/topUser.vue
Normal file
182
src/views/Activities/WorldCup/components/topUser.vue
Normal file
@ -0,0 +1,182 @@
|
||||
<template>
|
||||
<!-- 前三名用户卡片:头像框、基础信息和等级行从上往下排列 -->
|
||||
<div
|
||||
class="top-user"
|
||||
:class="`top-user-${ranking}`"
|
||||
style="
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 1vw;
|
||||
color: #fff;
|
||||
text-align: center;
|
||||
"
|
||||
>
|
||||
<!-- 头像展示区:接口头像在下层,frameTop 图片作为头像框覆盖层 -->
|
||||
<div class="avatar-box" style="position: relative; width: 100%">
|
||||
<div class="avatar-inner">
|
||||
<img
|
||||
v-if="avatarUrl"
|
||||
:src="avatarUrl"
|
||||
alt=""
|
||||
style="display: block; width: 100%; height: 100%; object-fit: cover"
|
||||
@error="handleAvatarImageError"
|
||||
/>
|
||||
</div>
|
||||
<img
|
||||
v-smart-img.noFade
|
||||
:src="frameUrl"
|
||||
alt=""
|
||||
style="
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 1;
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: fill;
|
||||
pointer-events: none;
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 用户基础信息:名称和展示 id,空榜时保留占位高度 -->
|
||||
<div class="user-name">{{ name }}</div>
|
||||
<div class="user-id">{{ userId }}</div>
|
||||
|
||||
<!-- 等级展示行:VIP、魅力等级、财富等级规则同步 GulbenFestival -->
|
||||
<div
|
||||
style="
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.5vw;
|
||||
direction: ltr;
|
||||
"
|
||||
>
|
||||
<img v-if="vipUrl" v-smart-img.noFade :src="vipUrl" alt="" style="width: 24%; display: block" />
|
||||
<itemCenter
|
||||
:imgUrl="userLevelUrl"
|
||||
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>
|
||||
</itemCenter>
|
||||
<itemCenter
|
||||
:imgUrl="wealthLevelUrl"
|
||||
class="level-badge"
|
||||
style="width: 30%"
|
||||
contentStyle="padding: 0 0 0 36%; align-items: center; justify-content: center; box-sizing: border-box;"
|
||||
>
|
||||
<span>{{ wealthLevel || 0 }}</span>
|
||||
</itemCenter>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { handleAvatarImageError } from '@/utils/image/imageHandler.js'
|
||||
import itemCenter from '@/components/itemCenter.vue'
|
||||
|
||||
defineProps({
|
||||
ranking: {
|
||||
type: [String, Number],
|
||||
default: 1,
|
||||
},
|
||||
frameUrl: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
avatarUrl: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
name: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
userId: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
vipUrl: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
userLevelUrl: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
wealthLevelUrl: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
userLevel: {
|
||||
type: [String, Number],
|
||||
default: 0,
|
||||
},
|
||||
wealthLevel: {
|
||||
type: [String, Number],
|
||||
default: 0,
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.avatar-box {
|
||||
aspect-ratio: 237 / 273;
|
||||
}
|
||||
|
||||
.top-user-2 .avatar-box,
|
||||
.top-user-3 .avatar-box {
|
||||
aspect-ratio: 231 / 225;
|
||||
}
|
||||
|
||||
.avatar-inner {
|
||||
position: absolute;
|
||||
top: 25%;
|
||||
left: 50%;
|
||||
width: 64%;
|
||||
aspect-ratio: 1 / 1;
|
||||
overflow: hidden;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
.top-user-2 .avatar-inner,
|
||||
.top-user-3 .avatar-inner {
|
||||
top: 14%;
|
||||
width: 66%;
|
||||
}
|
||||
|
||||
.user-name,
|
||||
.user-id {
|
||||
width: 100%;
|
||||
min-height: 1.1em;
|
||||
overflow: hidden;
|
||||
color: #fff;
|
||||
line-height: 1.1;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.user-name {
|
||||
font-size: 1em;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.user-id {
|
||||
font-size: 0.75em;
|
||||
font-weight: 600;
|
||||
opacity: 0.88;
|
||||
}
|
||||
|
||||
.level-badge span {
|
||||
color: #fff;
|
||||
font-size: 0.7em;
|
||||
font-weight: 700;
|
||||
line-height: 1;
|
||||
}
|
||||
</style>
|
||||
573
src/views/Activities/WorldCup/index.vue
Normal file
573
src/views/Activities/WorldCup/index.vue
Normal file
@ -0,0 +1,573 @@
|
||||
<template>
|
||||
<!-- 页面根容器:承载加载态和活动正式内容 -->
|
||||
<div
|
||||
class="full-page"
|
||||
style="
|
||||
position: relative;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
background: #101e14;
|
||||
scrollbar-width: none;
|
||||
"
|
||||
>
|
||||
<!-- 首屏加载动画:关键图片和排行榜数据完成后再展示页面 -->
|
||||
<div
|
||||
v-if="isLoading"
|
||||
style="
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
"
|
||||
>
|
||||
<LoadingContent>{{ $t('loading') }}...</LoadingContent>
|
||||
</div>
|
||||
|
||||
<!-- 页面模块列表:当前按头图、排行榜从上往下排列 -->
|
||||
<main
|
||||
v-else
|
||||
style="
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2vw;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
"
|
||||
>
|
||||
<!-- 最底层背景图:bg2 拉伸铺满当前视口 -->
|
||||
<img
|
||||
v-smart-img.noFade
|
||||
:src="pngUrl('bg2')"
|
||||
alt=""
|
||||
style="
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 0;
|
||||
display: block;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
object-fit: fill;
|
||||
pointer-events: none;
|
||||
"
|
||||
/>
|
||||
|
||||
<!-- 头部模块:使用 bg 大图承载,内部只放贴右规则按钮 -->
|
||||
<itemCenter
|
||||
style="width: 100vw"
|
||||
:imgUrl="pngUrl('bg')"
|
||||
contentStyle="padding: 87vw 0 0; align-items: flex-start; justify-content: flex-start; box-sizing: border-box;"
|
||||
>
|
||||
<!-- 规则按钮行:后续接规则弹窗时在这里绑定点击事件 -->
|
||||
<div style="width: 100%; display: flex; justify-content: flex-end">
|
||||
<img
|
||||
v-smart-img.noFade
|
||||
:src="pngUrl('ruleBt')"
|
||||
alt=""
|
||||
style="width: 13.6vw; display: block"
|
||||
/>
|
||||
</div>
|
||||
</itemCenter>
|
||||
|
||||
<!-- 排行榜模块:主背景、前三名、列表项和底边框统一放在这里 -->
|
||||
<section style="position: relative; width: 100vw">
|
||||
<!-- 排行榜底色背景:配合 RankingMain、RankingItem 和底边框形成完整排行榜容器 -->
|
||||
<div
|
||||
style="position: absolute; inset: 15vw 2vw 4vw; z-index: 0; background-color: #053126"
|
||||
></div>
|
||||
|
||||
<!-- 排行榜主图区域:RankingMain 内展示前三名头像框 -->
|
||||
<itemCenter
|
||||
style="width: 100vw"
|
||||
:imgUrl="pngUrl('RankingMain')"
|
||||
contentStyle="direction: ltr; padding: 0; flex-direction: column; align-items: center; justify-content: space-between; box-sizing: border-box;"
|
||||
>
|
||||
<!-- 前三名模块:沿用原 RankingMain 内容区样式,包裹前三名领奖台 -->
|
||||
<div
|
||||
style="
|
||||
width: 100%;
|
||||
padding: 20vw 6vw 0;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: center;
|
||||
box-sizing: border-box;
|
||||
"
|
||||
>
|
||||
<!-- 前三名容器:同一行展示,通过 flex order 固定为第二名、第一名、第三名 -->
|
||||
<div
|
||||
style="
|
||||
width: 100%;
|
||||
height: 45vw;
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
justify-content: space-between;
|
||||
gap: 1vw;
|
||||
"
|
||||
>
|
||||
<TopUser
|
||||
style="width: 26vw; order: 2"
|
||||
ranking="1"
|
||||
:frameUrl="pngUrl('frameTop1')"
|
||||
:avatarUrl="topUsers[0].avatarUrl"
|
||||
:name="topUsers[0].name"
|
||||
:userId="topUsers[0].userId"
|
||||
:vipUrl="getVipLevelUrl(topUsers[0].vipLevelName)"
|
||||
:userLevelUrl="pngUrl(getLevelBgImageName('userLevel', topUsers[0].charmLevel))"
|
||||
:wealthLevelUrl="pngUrl(getLevelBgImageName('wealthLevel', topUsers[0].wealthLevel))"
|
||||
:userLevel="topUsers[0].charmLevel"
|
||||
:wealthLevel="topUsers[0].wealthLevel"
|
||||
@click="handleRankingClick(topUsers[0])"
|
||||
/>
|
||||
|
||||
<TopUser
|
||||
style="width: 24vw; order: 1"
|
||||
ranking="2"
|
||||
:frameUrl="pngUrl('frameTop2')"
|
||||
:avatarUrl="topUsers[1].avatarUrl"
|
||||
:name="topUsers[1].name"
|
||||
:userId="topUsers[1].userId"
|
||||
:vipUrl="getVipLevelUrl(topUsers[1].vipLevelName)"
|
||||
:userLevelUrl="pngUrl(getLevelBgImageName('userLevel', topUsers[1].charmLevel))"
|
||||
:wealthLevelUrl="pngUrl(getLevelBgImageName('wealthLevel', topUsers[1].wealthLevel))"
|
||||
:userLevel="topUsers[1].charmLevel"
|
||||
:wealthLevel="topUsers[1].wealthLevel"
|
||||
@click="handleRankingClick(topUsers[1])"
|
||||
/>
|
||||
|
||||
<TopUser
|
||||
style="width: 24vw; order: 3"
|
||||
ranking="3"
|
||||
:frameUrl="pngUrl('frameTop3')"
|
||||
:avatarUrl="topUsers[2].avatarUrl"
|
||||
:name="topUsers[2].name"
|
||||
:userId="topUsers[2].userId"
|
||||
:vipUrl="getVipLevelUrl(topUsers[2].vipLevelName)"
|
||||
:userLevelUrl="pngUrl(getLevelBgImageName('userLevel', topUsers[2].charmLevel))"
|
||||
:wealthLevelUrl="pngUrl(getLevelBgImageName('wealthLevel', topUsers[2].wealthLevel))"
|
||||
:userLevel="topUsers[2].charmLevel"
|
||||
:wealthLevel="topUsers[2].wealthLevel"
|
||||
@click="handleRankingClick(topUsers[2])"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 展示更多模块:点击后展开第四名以后排行榜列表 -->
|
||||
<div
|
||||
v-if="!showRankingList"
|
||||
style="
|
||||
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;
|
||||
"
|
||||
@click="handleShowRankingList"
|
||||
>
|
||||
<span>Show All Users</span>
|
||||
<img
|
||||
v-smart-img.noFade
|
||||
:src="pngUrl('showMoreBt')"
|
||||
alt=""
|
||||
style="width: 10vw; display: block"
|
||||
/>
|
||||
</div>
|
||||
</itemCenter>
|
||||
|
||||
<!-- 展开后的排行榜列表:点击 Show All Users 后才展示 RankingItem 模块 -->
|
||||
<template v-if="showRankingList">
|
||||
<!-- 第四名以后排行榜列表:使用 RankingItem 逐条承载接口数据 -->
|
||||
<itemCenter
|
||||
v-for="(rankingItem, index) in rankingListAfterTop3"
|
||||
:key="rankingItem.userId || index"
|
||||
style="width: 100vw; margin-top: -2vw"
|
||||
:imgUrl="pngUrl('RankingItem')"
|
||||
contentStyle="padding: 3vw 8vw 0; box-sizing: border-box; justify-content: space-between;"
|
||||
@click="handleRankingClick(rankingItem)"
|
||||
>
|
||||
<!-- 列表项用户信息区:排名、头像、昵称、id 和等级行 -->
|
||||
<div style="flex: 1; min-width: 0; display: flex; align-items: center; gap: 2vw">
|
||||
<div
|
||||
style="
|
||||
color: #fff;
|
||||
font-size: 1em;
|
||||
font-weight: 800;
|
||||
line-height: 1;
|
||||
white-space: nowrap;
|
||||
"
|
||||
>
|
||||
{{ rankingItem.rank }}
|
||||
</div>
|
||||
<img
|
||||
:src="rankingItem.avatarUrl || ''"
|
||||
alt=""
|
||||
style="
|
||||
width: 3.5em;
|
||||
aspect-ratio: 1 / 1;
|
||||
display: block;
|
||||
flex: 0 0 3.5em;
|
||||
border-radius: 50%;
|
||||
object-fit: cover;
|
||||
"
|
||||
@error="handleAvatarImageError"
|
||||
/>
|
||||
<!-- 名称信息区:昵称/id/等级分三行展示 -->
|
||||
<div
|
||||
class="ranking-name-box"
|
||||
style="flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 0.4vw"
|
||||
>
|
||||
<div class="ranking-name">{{ rankingItem.name }}</div>
|
||||
<div class="ranking-id">{{ $t('user_id_prefix') }} {{ rankingItem.userId }}</div>
|
||||
<!-- 等级行:VIP、魅力等级、财富等级和前三名同一套展示规则 -->
|
||||
<div style="display: flex; align-items: center; gap: 1vw; direction: ltr">
|
||||
<img
|
||||
v-if="getVipLevelUrl(rankingItem.vipLevelName)"
|
||||
v-smart-img.noFade
|
||||
:src="getVipLevelUrl(rankingItem.vipLevelName)"
|
||||
alt=""
|
||||
style="width: 12vw; display: block"
|
||||
/>
|
||||
<itemCenter
|
||||
:imgUrl="pngUrl(getLevelBgImageName('userLevel', rankingItem.charmLevel))"
|
||||
style="width: 12vw"
|
||||
contentStyle="padding: 0 0 0 42%; align-items: center; justify-content: center; box-sizing: border-box;"
|
||||
>
|
||||
<span style="color: #fff; font-size: 1em; font-weight: 700; line-height: 1">
|
||||
{{ rankingItem.charmLevel || 0 }}
|
||||
</span>
|
||||
</itemCenter>
|
||||
<itemCenter
|
||||
:imgUrl="pngUrl(getLevelBgImageName('wealthLevel', rankingItem.wealthLevel))"
|
||||
style="width: 12vw"
|
||||
contentStyle="padding: 0 0 0 36%; align-items: center; justify-content: center; box-sizing: border-box;"
|
||||
>
|
||||
<span style="color: #fff; font-size: 1em; font-weight: 700; line-height: 1">
|
||||
{{ rankingItem.wealthLevel || 0 }}
|
||||
</span>
|
||||
</itemCenter>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 列表项贡献值:接口 amount/quantity/total 归一化后的展示值 -->
|
||||
<div
|
||||
style="
|
||||
color: #fff;
|
||||
font-size: 1em;
|
||||
font-weight: 800;
|
||||
line-height: 1;
|
||||
white-space: nowrap;
|
||||
"
|
||||
>
|
||||
{{ rankingItem.amount }}
|
||||
</div>
|
||||
</itemCenter>
|
||||
|
||||
<!-- 收起列表模块:展示在最后一个 RankingItem 下方,点击后隐藏列表 -->
|
||||
<div
|
||||
style="
|
||||
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;
|
||||
"
|
||||
@click="handleCollapseRankingList"
|
||||
>
|
||||
<span>Collapse List</span>
|
||||
<img
|
||||
v-smart-img.noFade
|
||||
:src="pngUrl('showMoreBt')"
|
||||
alt=""
|
||||
style="width: 10vw; display: block; transform: scaleY(-1)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 排行榜底边框:用于收尾主背景和列表区域 -->
|
||||
<img
|
||||
v-smart-img.noFade
|
||||
:src="pngUrl('RankingBottomBorder')"
|
||||
alt=""
|
||||
style="
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: block;
|
||||
width: 100vw;
|
||||
margin-top: -9vw;
|
||||
pointer-events: none;
|
||||
"
|
||||
/>
|
||||
</section>
|
||||
</main>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { getPngUrl } from '@/config/imagePaths.js'
|
||||
import { preloadImages } from '@/utils/image/imagePreloader.js'
|
||||
import { getRankingListAndMyRanking } from '@/api/activity.js'
|
||||
import { viewUserInfo } from '@/utils/appBridge.js'
|
||||
import { handleAvatarImageError } from '@/utils/image/imageHandler.js'
|
||||
import itemCenter from '@/components/itemCenter.vue'
|
||||
import TopUser from './components/topUser.vue'
|
||||
import { pageConfig } from './page.config.js'
|
||||
|
||||
const assetPath = 'Activities/WorldCup/'
|
||||
const pngUrl = (filename) => getPngUrl(assetPath, filename)
|
||||
const isLoading = ref(true)
|
||||
const activityType = ref(pageConfig.activity.activityType || 19)
|
||||
const ranking = ref([])
|
||||
// 控制第四名以后排行榜是否展开,默认只展示前三名和 Show All Users。
|
||||
const showRankingList = ref(false)
|
||||
|
||||
const blockingImages = [
|
||||
'bg2',
|
||||
'bg',
|
||||
'ruleBt',
|
||||
'RankingMain',
|
||||
'RankingItem',
|
||||
'RankingBottomBorder',
|
||||
'showMoreBt',
|
||||
'frameTop1',
|
||||
'frameTop2',
|
||||
'frameTop3',
|
||||
'vip1',
|
||||
'userLevel1Bg',
|
||||
'wealthLevel1Bg',
|
||||
'vip2',
|
||||
'vip3',
|
||||
'vip4',
|
||||
'vip5',
|
||||
'vip6',
|
||||
'userLevel2Bg',
|
||||
'userLevel3Bg',
|
||||
'userLevel4Bg',
|
||||
'userLevel5Bg',
|
||||
'wealthLevel2Bg',
|
||||
'wealthLevel3Bg',
|
||||
'wealthLevel4Bg',
|
||||
'wealthLevel5Bg',
|
||||
]
|
||||
|
||||
const emptyTopUsers = [
|
||||
{
|
||||
avatarUrl: '',
|
||||
name: '',
|
||||
userId: '',
|
||||
amount: '',
|
||||
wealthLevel: 0,
|
||||
charmLevel: 0,
|
||||
vipLevelName: 'VISCOUNT',
|
||||
},
|
||||
{
|
||||
avatarUrl: '',
|
||||
name: '',
|
||||
userId: '',
|
||||
amount: '',
|
||||
wealthLevel: 0,
|
||||
charmLevel: 0,
|
||||
vipLevelName: 'VISCOUNT',
|
||||
},
|
||||
{
|
||||
avatarUrl: '',
|
||||
name: '',
|
||||
userId: '',
|
||||
amount: '',
|
||||
wealthLevel: 0,
|
||||
charmLevel: 0,
|
||||
vipLevelName: 'VISCOUNT',
|
||||
},
|
||||
]
|
||||
|
||||
const topUsers = computed(() => {
|
||||
const result = ranking.value.slice(0, 3)
|
||||
return [...result, ...emptyTopUsers].slice(0, 3)
|
||||
})
|
||||
|
||||
const rankingListAfterTop3 = computed(() => ranking.value.slice(3))
|
||||
|
||||
const vipLevelImageMap = {
|
||||
VISCOUNT: 'vip1',
|
||||
EARL: 'vip2',
|
||||
MARQUIS: 'vip3',
|
||||
DUKE: 'vip4',
|
||||
KING: 'vip5',
|
||||
EMPEROR: 'vip6',
|
||||
}
|
||||
|
||||
// VIP 等级图片映射:规则同步 GulbenFestival 的 Personal Rank / Ranking。
|
||||
const getVipLevelImageName = (vipLevelName = '') => {
|
||||
return vipLevelImageMap[String(vipLevelName || '').toUpperCase()] || ''
|
||||
}
|
||||
|
||||
// 只在接口返回有效 VIP 等级时展示对应图片。
|
||||
const getVipLevelUrl = (vipLevelName = '') => {
|
||||
const imageName = getVipLevelImageName(vipLevelName)
|
||||
return imageName ? pngUrl(imageName) : ''
|
||||
}
|
||||
|
||||
// 魅力等级和财富等级每 10 级一档,最高使用第 5 档背景。
|
||||
const getLevelBgImageName = (prefix, level) => {
|
||||
const levelValue = Number(level) || 0
|
||||
const imageIndex = Math.min(5, Math.max(1, Math.ceil(levelValue / 10) || 1))
|
||||
|
||||
return `${prefix}${imageIndex}Bg`
|
||||
}
|
||||
|
||||
// 排行榜字段归一化:接口和参数先复用 GulbenFestival Personal Rank / Ranking。
|
||||
const normalizeRankingItem = (item = {}, index = 0) => {
|
||||
const account = item.specialAccount || item.account || ''
|
||||
|
||||
return {
|
||||
...item,
|
||||
rank: item.rank || item.ranking || index + 1,
|
||||
userId: account,
|
||||
rawUserId: item.userId || item.id || '',
|
||||
avatarUrl: item.avatar || item.userAvatar || '',
|
||||
name: item.userName || item.userNickname || item.nickname || '',
|
||||
amount: item.amount ?? item.quantity ?? item.total ?? 0,
|
||||
account,
|
||||
specialAccount: item.specialAccount || '',
|
||||
wealthLevel: item.wealthLevel ?? item.userProfile?.wealthLevel ?? 0,
|
||||
charmLevel: item.charmLevel ?? item.userProfile?.charmLevel ?? 0,
|
||||
vipLevelName: item.vipLevelName || item.userProfile?.vipLevelName || '',
|
||||
}
|
||||
}
|
||||
|
||||
// 获取个人排行榜:接口使用 /ranking/list,参数暂沿用 GulbenFestival 的 activityType。
|
||||
const getPersonalRanking = async () => {
|
||||
try {
|
||||
const resRanking = await getRankingListAndMyRanking({
|
||||
activityType: activityType.value,
|
||||
})
|
||||
|
||||
if (resRanking.status && resRanking.body) {
|
||||
ranking.value = (resRanking.body.rankingList || []).map((item, index) =>
|
||||
normalizeRankingItem(item, index),
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
ranking.value = []
|
||||
} catch (error) {
|
||||
console.error('WorldCup 排行榜加载失败:', error)
|
||||
ranking.value = []
|
||||
}
|
||||
}
|
||||
|
||||
// 点击排行榜用户时打开 App 内用户资料页。
|
||||
const handleRankingClick = (rankingItem = {}) => {
|
||||
if (!rankingItem.rawUserId) return
|
||||
viewUserInfo(rankingItem.rawUserId)
|
||||
}
|
||||
|
||||
// 展开第四名以后排行榜列表,点击 Show All Users 时触发。
|
||||
const handleShowRankingList = () => {
|
||||
showRankingList.value = true
|
||||
}
|
||||
|
||||
// 收起第四名以后排行榜列表,点击 Collapse List 时触发。
|
||||
const handleCollapseRankingList = () => {
|
||||
showRankingList.value = false
|
||||
}
|
||||
|
||||
// 首屏图片和排行榜数据一起完成后再关闭 loading。
|
||||
const completePreloading = async () => {
|
||||
try {
|
||||
await Promise.all([
|
||||
getPersonalRanking(),
|
||||
preloadImages(blockingImages.map((imageName) => pngUrl(imageName))),
|
||||
])
|
||||
} catch (error) {
|
||||
console.error('WorldCup 预加载过程中发生错误:', error)
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
completePreloading()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.full-page::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* 页面所有模块隐藏可见滚动条,保留必要滚动能力但不露出滚动条样式。 */
|
||||
.full-page,
|
||||
.full-page * {
|
||||
scrollbar-width: none;
|
||||
-ms-overflow-style: none;
|
||||
}
|
||||
|
||||
.full-page *::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.ranking-name,
|
||||
.ranking-id {
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
color: #fff;
|
||||
font-size: 1em;
|
||||
line-height: 1.1;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.ranking-name {
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.ranking-id {
|
||||
font-weight: 600;
|
||||
opacity: 0.88;
|
||||
}
|
||||
|
||||
@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>
|
||||
47
src/views/Activities/WorldCup/page.config.js
Normal file
47
src/views/Activities/WorldCup/page.config.js
Normal file
@ -0,0 +1,47 @@
|
||||
export const pageConfig = {
|
||||
name: 'WorldCup',
|
||||
routePath: '/activities/world-cup',
|
||||
routeName: 'world-cup',
|
||||
assetPath: 'Activities/WorldCup/',
|
||||
|
||||
activity: {
|
||||
activityId: '',
|
||||
activityCode: '',
|
||||
activityType: 19,
|
||||
rankingActivityType: '',
|
||||
rewardActivityType: '',
|
||||
},
|
||||
|
||||
preload: {
|
||||
blocking: [],
|
||||
background: [],
|
||||
},
|
||||
|
||||
modules: {
|
||||
tabs: {
|
||||
enabled: false,
|
||||
defaultValue: '',
|
||||
items: [],
|
||||
},
|
||||
sections: [],
|
||||
popups: [],
|
||||
features: [
|
||||
{
|
||||
name: 'countdown',
|
||||
enabled: false,
|
||||
deadline: '',
|
||||
images: ['timeBg'],
|
||||
fields: {
|
||||
endTime: '',
|
||||
remainingText: '',
|
||||
},
|
||||
remark:
|
||||
'倒计时模块占位;本地图片清单尚未扫描到 timeBg,待补充素材和截止时间后再启用。',
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
images: {
|
||||
localized: {},
|
||||
},
|
||||
}
|
||||
279
src/views/Activities/WorldCup/页面创建信息.md
Normal file
279
src/views/Activities/WorldCup/页面创建信息.md
Normal file
@ -0,0 +1,279 @@
|
||||
# 页面创建信息
|
||||
|
||||
## 1. 基础信息
|
||||
|
||||
- 页面类型:Activities
|
||||
- 页面名称:WorldCup
|
||||
- 页面目录:src/views/Activities/WorldCup
|
||||
- 参考页面:
|
||||
- 备注:已生成 `index.vue` 可预览页面底座,目前完成头部和排行榜模块;排行榜接口和字段规则参考 `GulbenFestival` 的 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',
|
||||
name: 'world-cup',
|
||||
component: () => import('../views/Activities/WorldCup/index.vue'),
|
||||
meta: { requiresAuth: false },
|
||||
}
|
||||
```
|
||||
|
||||
### 2.2 安全白名单片段
|
||||
|
||||
```js
|
||||
'/activities/world-cup',
|
||||
```
|
||||
|
||||
### 2.3 权限页面片段
|
||||
|
||||
```js
|
||||
'/activities/world-cup', // WorldCup
|
||||
```
|
||||
|
||||
## 3. OSS 图片目录
|
||||
|
||||
- OSS 相对路径:Activities/WorldCup/
|
||||
- 本地开发目录:public/oss/h5/Azizi/Activities/WorldCup/
|
||||
- 是否需要 Codex 自动创建本地开发目录:是
|
||||
- 图片是否已放入本地目录:是
|
||||
|
||||
说明:本地开发目录只作为开发期 OSS 图片镜像使用,方便页面优先读取本地资源;发布时图片由你上传到真实 OSS,不随项目提交 Git。新建本地目录时不生成 `.gitkeep`,空目录不被 Git 跟踪是正常现象。
|
||||
|
||||
### 3.1 多语言图片清单
|
||||
|
||||
| 图片名 | 支持语言 | 备注 |
|
||||
| --- | --- | --- |
|
||||
| bg | 默认 | 头部主背景图 |
|
||||
| bg2 | 默认 | 页面最低层背景图,拉伸铺满 100vh 视口 |
|
||||
| RankingMain | 默认 | 排行榜主背景,内部上方展示前三名,下方展示 Show All Users |
|
||||
| RankingItem | 默认 | 第四名以后排行榜列表项背景 |
|
||||
| RankingBottomBorder | 默认 | 排行榜底边框 |
|
||||
| frameTop1 | 默认 | 第一名头像框 |
|
||||
| frameTop2 | 默认 | 第二名头像框 |
|
||||
| frameTop3 | 默认 | 第三名头像框 |
|
||||
| ruleBt | 默认 | 规则按钮 |
|
||||
| showMoreBt | 默认 | 展开/收起排行榜按钮图标 |
|
||||
| vip1-vip6 | 默认 | VIP 等级图片 |
|
||||
| userLevel1Bg-userLevel5Bg | 默认 | 魅力等级背景 |
|
||||
| wealthLevel1Bg-wealthLevel5Bg | 默认 | 财富等级背景 |
|
||||
|
||||
### 3.2 进入页面前同步预加载图片
|
||||
|
||||
| 图片名 | 是否包含多语言 | 备注 |
|
||||
| --- | --- | --- |
|
||||
| bg2 | 否 | 页面最低层背景 |
|
||||
| bg | 否 | 头部背景 |
|
||||
| ruleBt | 否 | 规则按钮 |
|
||||
| RankingMain | 否 | 排行榜主背景 |
|
||||
| RankingItem | 否 | 排行榜列表项背景 |
|
||||
| RankingBottomBorder | 否 | 排行榜底边框 |
|
||||
| showMoreBt | 否 | 展开/收起按钮图标 |
|
||||
| frameTop1 | 否 | 第一名头像框 |
|
||||
| frameTop2 | 否 | 第二名头像框 |
|
||||
| frameTop3 | 否 | 第三名头像框 |
|
||||
| vip1-vip6 | 否 | VIP 等级图片 |
|
||||
| userLevel1Bg-userLevel5Bg | 否 | 魅力等级背景 |
|
||||
| wealthLevel1Bg-wealthLevel5Bg | 否 | 财富等级背景 |
|
||||
|
||||
### 3.3 进入页面后后台预加载图片
|
||||
|
||||
| 图片名 | 是否包含多语言 | 备注 |
|
||||
| --- | --- | --- |
|
||||
| footballSign | 否 | 后续模块素材,当前未使用 |
|
||||
| giftBt | 否 | 后续模块素材,当前未使用 |
|
||||
| matchBg | 否 | 后续模块素材,当前未使用 |
|
||||
| myRankingBg | 否 | 后续我的排名素材,当前未使用 |
|
||||
| numPeople | 否 | 后续模块素材,当前未使用 |
|
||||
| rewardBg | 否 | 后续奖励模块素材,当前未使用 |
|
||||
| ruleBg | 否 | 后续规则弹窗素材,当前未接弹窗 |
|
||||
| statusEndedfail | 否 | 后续状态素材,当前未使用 |
|
||||
| statusEndedWin | 否 | 后续状态素材,当前未使用 |
|
||||
| statusInProgress | 否 | 后续状态素材,当前未使用 |
|
||||
| statusNotStarted | 否 | 后续状态素材,当前未使用 |
|
||||
|
||||
## 4. 模块配置
|
||||
|
||||
当前页面按用户逐步补充需求生成,只处理已明确指定模块。
|
||||
|
||||
### 4.1 标签页模块
|
||||
|
||||
```json
|
||||
{
|
||||
"enabled": false,
|
||||
"defaultValue": "",
|
||||
"items": []
|
||||
}
|
||||
```
|
||||
|
||||
### 4.2 页面区块模块
|
||||
|
||||
```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 内部从上到下分为前三名模块和展示更多模块;第四名以后列表默认隐藏。"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
倒计时模块占位:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "countdown",
|
||||
"enabled": false,
|
||||
"deadline": "",
|
||||
"images": ["timeBg"],
|
||||
"fields": {
|
||||
"endTime": "",
|
||||
"remainingText": ""
|
||||
},
|
||||
"remark": "倒计时模块占位;本地图片清单尚未扫描到 timeBg,待补充素材和截止时间后再启用。"
|
||||
}
|
||||
```
|
||||
|
||||
### 4.3 弹窗模块
|
||||
|
||||
```json
|
||||
[]
|
||||
```
|
||||
|
||||
## 5. 接口参数
|
||||
|
||||
```json
|
||||
{
|
||||
"activityId": "",
|
||||
"activityCode": "",
|
||||
"activityType": 19,
|
||||
"rankingActivityType": "",
|
||||
"rewardActivityType": "",
|
||||
"otherParams": {}
|
||||
}
|
||||
```
|
||||
|
||||
## 6. 组件需求
|
||||
|
||||
需要哪一个就填“是”,不需要或没填写的初版页面先不接。
|
||||
|
||||
```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": "抽奖格子"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
## 7. 建页后记录
|
||||
|
||||
### 7.1 已完成
|
||||
|
||||
- 创建页面目录:`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` 私有前三名组件
|
||||
- 完成统一加载动画、页面背景、头部模块、排行榜前三名、排行榜接口和展开/收起列表交互
|
||||
|
||||
### 7.2 未填写所以跳过
|
||||
|
||||
- 规则弹窗暂未接点击事件和弹窗内容。
|
||||
- 赛事、奖励、任务、我的排名等素材暂未生成对应业务模块。
|
||||
|
||||
### 7.3 待你微调
|
||||
|
||||
- 继续按预览效果微调排行榜位置、间距和展开/收起按钮位置。
|
||||
- 后续补规则弹窗、赛事、奖励、任务、我的排名等模块时继续先更新本 md,再同步 Vue。
|
||||
|
||||
### 7.4 验证结果
|
||||
|
||||
- `npm run build`:已运行并通过。
|
||||
- 备注:构建中存在项目原有 warning:`secondBg.png` 运行时解析提示,以及 `src/utils/http.js` 动静态混用 chunk 提示。
|
||||
Loading…
x
Reference in New Issue
Block a user