feat(新活动): 世界杯活动

This commit is contained in:
hzj 2026-06-03 14:41:18 +08:00
parent 5a7297d877
commit f35e753383
7 changed files with 1089 additions and 0 deletions

View File

@ -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',

View File

@ -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',

View File

@ -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', // 斋月打榜

View 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>

View 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">
<!-- 排行榜底色背景配合 RankingMainRankingItem 和底边框形成完整排行榜容器 -->
<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>

View 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: {},
},
}

View File

@ -0,0 +1,279 @@
# 页面创建信息
## 1. 基础信息
- 页面类型Activities
- 页面名称WorldCup
- 页面目录src/views/Activities/WorldCup
- 参考页面:
- 备注:已生成 `index.vue` 可预览页面底座,目前完成头部和排行榜模块;排行榜接口和字段规则参考 `GulbenFestival` 的 personalRank / Ranking。
## 2. 路由和权限
- route path/activities/world-cup
- route nameworld-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 提示。