feat(新活动): 足球游戏活动

This commit is contained in:
hzj 2026-06-01 18:54:41 +08:00
parent 7d5d2fb62d
commit bf4e057107
12 changed files with 1801 additions and 5 deletions

View File

@ -20,6 +20,7 @@ export const PUBLIC_PATHS = Object.freeze([
'/activities/poker-ace',
'/daily-recharge',
'/activities/gulben-festival',
'/activities/bounty-football',
'/activities/lucky-dollars-season5',
'/activities/lucky-dollars-season4',
'/activities/lesser-bairam',

View File

@ -487,5 +487,12 @@
"gulben_room_rule_title": "قواعد النشاط:",
"gulben_room_rule_1": "1. سيتم احتساب إيرادات غرفتك خلال النشاط. بعد انتهاء النشاط، ستحصل أفضل 10 غرف في الترتيب على مكافآت سخية من العملات و VIP بناء على مستوى إيراداتها.",
"gulben_room_rule_2": "2. هذا الحدث غير مرتبط ب iPhone أو Google.",
"gulben_room_rule_3": "3. تحتفظ الجهة الرسمية بحق التفسير النهائي لهذا الحدث."
"gulben_room_rule_3": "3. تحتفظ الجهة الرسمية بحق التفسير النهائي لهذا الحدث.",
"bounty_football_help_rule_1": "1. كلما جمع المستخدم المزيد من العملات خلال الحدث، ارتفع ترتيبه.",
"bounty_football_help_rule_2": "2. سيستمر الحدث لمدة 15 يوما. بعد انتهاء الحدث، سيتم توزيع المكافآت على أفضل ثلاثة مستخدمين في لوحة الترتيب.",
"bounty_football_help_rule_3": "3. يجب على أفضل ثلاثة مستخدمين التواصل مع الموظفين الرسميين في الوقت المناسب لمناقشة كيفية الحصول على مكافأة الدولار الأمريكي.",
"bounty_football_help_rule_3_withdrawal": "(1) السحب: يرجى تقديم ID حساب استلام USDT أو رمز QR الخاص بالاستلام.",
"bounty_football_help_rule_3_no_withdrawal": "(2) عدم السحب: إذا تم تحويلها إلى عملات، فسنمنحك 5% عملات ذهبية إضافية فوق المبلغ الأصلي ($1:10500 coins).",
"bounty_football_help_rule_4": "4. قواعد ID: يمكن للمستخدمين التواصل مع الموظفين الرسميين من خلال تقديم لقطة شاشة لترتيبهم ورقم ID المحدد لاستخدامه (على سبيل المثال: عندما تكون المكافأة \"ID: ABBBBA\"، يمكن للمستخدم تقديم \"ID: 100001\" كمعرف خاص به، أو منح حق الاستخدام لشخص آخر).",
"bounty_football_help_note": "*يرجى ملاحظة أن ID يجب ألا يكون مستخدما حاليا من قبل أي شخص آخر."
}

View File

@ -487,5 +487,12 @@
"gulben_room_rule_title": "অ্যাক্টিভিটির নিয়ম:",
"gulben_room_rule_1": "1. অ্যাক্টিভিটি চলাকালীন আপনার রুমের আয় গণনা করা হবে। অ্যাক্টিভিটি শেষ হওয়ার পরে, শীর্ষ 10 র‍্যাঙ্ক করা রুম তাদের আয়ের স্তর অনুযায়ী উদার কয়েন এবং VIP পুরস্কার পাবে।",
"gulben_room_rule_2": "2. এই ইভেন্টটি iPhone বা Google-এর সাথে সম্পর্কিত নয়।",
"gulben_room_rule_3": "3. এই ইভেন্টের চূড়ান্ত ব্যাখ্যার অধিকার অফিসিয়াল পক্ষ সংরক্ষণ করে।"
"gulben_room_rule_3": "3. এই ইভেন্টের চূড়ান্ত ব্যাখ্যার অধিকার অফিসিয়াল পক্ষ সংরক্ষণ করে।",
"bounty_football_help_rule_1": "১. ইভেন্ট চলাকালীন একজন ব্যবহারকারী যত বেশি কয়েন জমা করবেন, তার র‍্যাঙ্কিং তত বেশি হবে।",
"bounty_football_help_rule_2": "২. ইভেন্টটি ১৫ দিন চলবে। ইভেন্ট শেষ হওয়ার পরে, লিডারবোর্ডের শীর্ষ তিন ব্যবহারকারীকে পুরস্কার বিতরণ করা হবে।",
"bounty_football_help_rule_3": "৩. শীর্ষ তিন ব্যবহারকারীকে USD পুরস্কার কীভাবে গ্রহণ করবেন তা আলোচনা করতে সময়মতো অফিসিয়াল স্টাফের সাথে যোগাযোগ করতে হবে।",
"bounty_football_help_rule_3_withdrawal": "(১) উত্তোলন: অনুগ্রহ করে আপনার USDT গ্রহণকারী অ্যাকাউন্ট ID বা গ্রহণকারী QR কোড প্রদান করুন।",
"bounty_football_help_rule_3_no_withdrawal": "(২) উত্তোলন নয়: কয়েনে রূপান্তর করলে, আমরা মূল পরিমাণের ওপর অতিরিক্ত ৫% গোল্ড কয়েন দেব ($1:10500 coins)।",
"bounty_football_help_rule_4": ".ID নিয়ম: ব্যবহারকারীরা নিজেদের র‍্যাঙ্কিংয়ের স্ক্রিনশট এবং নির্বাচিত ID নম্বর প্রদান করে অফিসিয়াল স্টাফের সাথে যোগাযোগ করে এটি ব্যবহার করতে পারেন (যেমন: পুরস্কার যদি \"ID: ABBBBA\" হয়, ব্যবহারকারী \"ID: 100001\" নিজের ID হিসেবে দিতে পারেন, অথবা অন্য কাউকে ব্যবহারের অধিকার দিতে পারেন)।",
"bounty_football_help_note": "*অনুগ্রহ করে মনে রাখবেন, ID বর্তমানে অন্য কেউ ব্যবহার করছে এমন হওয়া যাবে না।"
}

View File

@ -487,5 +487,12 @@
"gulben_room_rule_title": "Activity Rules:",
"gulben_room_rule_1": "1.Your room's revenue will be tallied during the activity. After the activity ends, the top 10 ranked rooms will receive generous coins and VIP rewards based on their revenue level.",
"gulben_room_rule_2": "2. This event is not related to iPhone or Google.",
"gulben_room_rule_3": "3. The official party reserves the final right of interpretation for this event."
"gulben_room_rule_3": "3. The official party reserves the final right of interpretation for this event.",
"bounty_football_help_rule_1": "1. The more coins a user accumulates during the event, the higher their ranking will be.",
"bounty_football_help_rule_2": "2. The event will last for 15 days. After the event ends, rewards will be distributed to the top three users on the leaderboard.",
"bounty_football_help_rule_3": "3. The top three users should contact the official staff in a timely manner to discuss how to obtain the USD reward.",
"bounty_football_help_rule_3_withdrawal": "(1) Withdrawal: Please provide your USDT receiving account ID or receiving QR code.",
"bounty_football_help_rule_3_no_withdrawal": "(2) No withdrawal: If converted to coins, we will give you an additional 5% gold coins on top of the original amount ($1:10500 coins).",
"bounty_football_help_rule_4": "4.ID Rules: Users can locate official staff by providing a screenshot of their ranking and the number of the selected ID to use it (for example: when the reward is \"ID: ABBBBA\", the user can provide \"ID: 100001\" as their own ID, or grant the usage rights to another person).",
"bounty_football_help_note": "*Please note that the ID must not currently be in use by anyone else."
}

View File

@ -487,5 +487,12 @@
"gulben_room_rule_title": "Etkinlik Kuralları:",
"gulben_room_rule_1": "1. Etkinlik süresince odanızın geliri hesaplanacaktır. Etkinlik sona erdikten sonra, ilk 10 sıradaki odalar gelir seviyelerine göre cömert coin ve VIP ödülleri alacaktır.",
"gulben_room_rule_2": "2. Bu etkinliğin iPhone veya Google ile ilgisi yoktur.",
"gulben_room_rule_3": "3. Bu etkinliğin nihai yorum hakkı resmi tarafa aittir."
"gulben_room_rule_3": "3. Bu etkinliğin nihai yorum hakkı resmi tarafa aittir.",
"bounty_football_help_rule_1": "1. Bir kullanıcı etkinlik boyunca ne kadar çok coin biriktirirse sıralaması o kadar yüksek olur.",
"bounty_football_help_rule_2": "2. Etkinlik 15 gün sürecektir. Etkinlik sona erdikten sonra ödüller liderlik tablosundaki ilk üç kullanıcıya dağıtılacaktır.",
"bounty_football_help_rule_3": "3. İlk üç kullanıcı, USD ödülünü nasıl alacaklarını görüşmek için resmi personelle zamanında iletişime geçmelidir.",
"bounty_football_help_rule_3_withdrawal": "(1) Para çekme: Lütfen USDT alıcı hesap ID'nizi veya alıcı QR kodunuzu sağlayın.",
"bounty_football_help_rule_3_no_withdrawal": "(2) Para çekmeme: Coin'e dönüştürülürse, orijinal tutara ek olarak %5 ekstra altın coin vereceğiz ($1:10500 coins).",
"bounty_football_help_rule_4": "4.ID Kuralları: Kullanıcılar, sıralamalarının ekran görüntüsünü ve seçilen ID numarasını sağlayarak resmi personelden bu ID'yi kullanmayı talep edebilir (örneğin: ödül \"ID: ABBBBA\" olduğunda, kullanıcı \"ID: 100001\" bilgisini kendi ID'si olarak sağlayabilir veya kullanım hakkını başka bir kişiye verebilir).",
"bounty_football_help_note": "*Lütfen bu ID'nin şu anda başka biri tarafından kullanılmıyor olması gerektiğini unutmayın."
}

View File

@ -487,5 +487,12 @@
"gulben_room_rule_title": "活动规则:",
"gulben_room_rule_1": "1.活动期间将统计你的房间流水。活动结束后,排名前 10 的房间将根据其流水等级获得丰厚金币和 VIP 奖励。",
"gulben_room_rule_2": "2. 本活动与 iPhone 或 Google 无关。",
"gulben_room_rule_3": "3. 本活动最终解释权归官方所有。"
"gulben_room_rule_3": "3. 本活动最终解释权归官方所有。",
"bounty_football_help_rule_1": "1. 活动期间用户累计的金币越多,排名越高。",
"bounty_football_help_rule_2": "2. 活动将持续15天。活动结束后奖励将发放给排行榜前三名用户。",
"bounty_football_help_rule_3": "3. 前三名用户应及时联系官方工作人员,沟通如何领取美元奖励。",
"bounty_football_help_rule_3_withdrawal": "(1) 提现:请提供您的 USDT 收款账户 ID 或收款二维码。",
"bounty_football_help_rule_3_no_withdrawal": "(2) 不提现:如果兑换为金币,我们将在原金额基础上额外赠送 5% 金币($1:10500 coins。",
"bounty_football_help_rule_4": "4.ID 规则:用户可以通过提供自己的排名截图和所选 ID 编号来联系官方工作人员使用该 ID例如奖励为 \"ID: ABBBBA\" 时,用户可以提供 \"ID: 100001\" 作为自己的 ID或将使用权授予他人。",
"bounty_football_help_note": "*请注意,该 ID 当前不得被其他任何人使用。"
}

View File

@ -414,6 +414,12 @@ const router = createRouter({
component: () => import('../views/Activities/GulbenFestival/index.vue'),
meta: { requiresAuth: true },
}, // 古尔邦节活动2026年6月7日上午5点结束
{
path: '/activities/bounty-football',
name: 'bounty-football',
component: () => import('../views/Activities/BountyFootball/index.vue'),
meta: { requiresAuth: false },
}, // BountyFootball
{
path: '/activities/lucky-dollars-season5',
name: 'lucky-dollars-season5',

View File

@ -64,6 +64,7 @@ const ACTIVITIES = [
'/daily-recharge', // 每日充值页面
'/activities/lucky-dollars-season5', // 幸运美金活动
'/activities/gulben-festival', // 古尔邦节活动
'/activities/bounty-football', // BountyFootball
'/activities/lucky-dollars-season4', // 幸运美金活动
'/activities/lesser-bairam', // 开斋节打榜
'/activities/lucky-dollars-season3', // 斋月打榜

View File

@ -0,0 +1,321 @@
<template>
<div style="width: 100%">
<div style="position: relative">
<img
v-smart-img.noFade
:src="BorderImgUrl"
alt=""
width="100%"
style="display: block; position: relative; z-index: 2"
/>
<!-- 头像 -->
<div
style="
position: absolute;
top: 0;
left: 0;
right: 0;
z-index: 1;
display: flex;
justify-content: center;
align-items: center;
"
:style="{ height: avatarContainerHeight }"
>
<div :style="{ width: avatarWidth }">
<img
:src="avatarUrl || ''"
alt=""
width="100%"
class="top-avatar"
:class="{ 'top-avatar-square': avatarSquare }"
style="display: block; aspect-ratio: 1/1; object-fit: cover"
@error="handleAvatarImageError"
/>
</div>
</div>
<!-- 用户名和贡献值 -->
<div
style="
position: absolute;
bottom: 0;
left: 0;
right: 0;
z-index: 3;
display: flex;
flex-direction: column;
align-items: center;
gap: 2vw;
"
:style="{ height: bottomSectionHeight }"
>
<div
style="display: flex; justify-content: center; align-items: center"
:style="{
width: nameValueWidth,
height: nameValueHeight,
}"
>
<div style="font-weight: 700" class="showText" :class="isTopOne ? 'top1Text' : 'text'">
{{ name }}
</div>
</div>
<div
class="distribution-value-pill"
:class="distributionValueClass"
style="padding: 1px 2px; display: flex; justify-content: center; align-items: center"
:style="{
width: distributionValueWidth,
}"
>
<img
v-smart-img.noFade
:src="resolvedValueIconUrl"
alt=""
style="display: block; aspect-ratio: 1/1; flex: 0 0 auto"
:style="{ width: coinWidth }"
/>
<div style="font-weight: 590" class="showText" :class="isTopOne ? 'top1Text' : 'text'">
{{ distributionValue }}
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { handleAvatarImageError } from '@/utils/image/imageHandler.js'
import { computed } from 'vue'
const props = defineProps({
ranking: {
type: String,
default: '',
},
avatarUrl: {
type: String,
default: '',
},
BorderImgUrl: {
type: String,
required: true,
},
name: {
type: String,
default: '',
},
distributionValue: {
type: [String, Number],
default: '',
},
valueIconUrl: {
type: String,
default: '',
},
avatarSquare: {
type: Boolean,
default: false,
},
})
//
const isTopOne = computed(() => {
return props.ranking === '1'
})
const resolvedValueIconUrl = computed(() => {
return props.valueIconUrl || new URL('/src/assets/icon/Azizi/coin.png', import.meta.url).href
})
const distributionValueClass = computed(() => {
switch (props.ranking) {
case '1':
return 'distribution-value-pill-top1'
case '2':
return 'distribution-value-pill-top2'
case '3':
return 'distribution-value-pill-top3'
default:
return ''
}
})
//
const avatarContainerHeight = computed(() => {
switch (props.ranking) {
case '1':
return '83%'
case '2':
return '83%'
case '3':
return '83%'
default:
return '83%'
}
})
//
const avatarWidth = computed(() => {
switch (props.ranking) {
case '1':
return '40%'
case '2':
return '40%'
case '3':
return '40%'
default:
return '40%'
}
})
//
const bottomSectionHeight = computed(() => {
switch (props.ranking) {
case '1':
return '40%'
case '2':
return '40%'
case '3':
return '40%'
default:
return '40%'
}
})
//
const nameValueWidth = computed(() => {
switch (props.ranking) {
case '1':
return '52%'
case '2':
return '52%'
case '3':
return '52%'
default:
return '52%'
}
})
//
const nameValueHeight = computed(() => {
switch (props.ranking) {
case '1':
return '35%'
case '2':
return '30%'
case '3':
return '30%'
default:
return '30%'
}
})
//
const distributionValueWidth = computed(() => {
switch (props.ranking) {
case '1':
return '55%'
case '2':
return '50%'
case '3':
return '50%'
default:
return '50%'
}
})
//
const coinWidth = computed(() => {
switch (props.ranking) {
case '1':
return '1em'
case '2':
return '0.9em'
case '3':
return '0.9em'
default:
return '0.9em'
}
})
</script>
<style lang="scss" scoped>
* {
color: black;
}
.showText {
color: #fff;
text-align: center;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.top1Text {
font-size: 0.75em;
}
.text {
font-size: 0.6em;
}
.top-avatar {
border-radius: 50%;
}
.top-avatar-square {
border-radius: 4px;
}
.distribution-value-pill {
border-radius: 40px;
border: 0.64px solid #ffdf36;
box-shadow: 0 0 2.558px 0 rgba(0, 0, 0, 0.4) inset;
}
.distribution-value-pill-top1 {
background: linear-gradient(270deg, #3a0606 0%, #d84949 47.45%, #3a0606 100%);
}
.distribution-value-pill-top2 {
background: linear-gradient(270deg, #223457 0%, #859de7 47.45%, #213655 100%);
}
.distribution-value-pill-top3 {
background: linear-gradient(270deg, #13301a 0%, #89db93 47.45%, #112a16 100%);
}
@media screen and (max-width: 360px) {
* {
font-size: 10px;
}
}
@media screen and (min-width: 360px) {
* {
font-size: 16px;
}
}
@media screen and (min-width: 768px) {
* {
font-size: 24px;
}
}
@media screen and (min-width: 1024px) {
* {
font-size: 32px;
}
}
</style>

View File

@ -0,0 +1,940 @@
<!-- src/views/Activities/Game/index.vue -->
<template>
<div class="fullPage">
<!-- 预加载状态 -->
<div v-if="isLoading" class="loading-container">
<LoadingContent>{{ $t('loading') }}...</LoadingContent>
</div>
<!-- 主要内容 -->
<div v-else>
<div ref="bgRef" class="bg" @scroll.passive="checkRankingLoadTrigger">
<img v-smart-img class="bg2-layer" :src="imageUrl('bg2')" alt="" />
<itemCenter
style="min-height: 150vw"
:imgUrl="imageUrl('bg')"
:contentStyle="`flex-direction: column;justify-content: flex-start;gap:10vw;`"
>
<!-- 状态栏占位区域仅在APP中显示 -->
<!-- <div v-if="isInAppEnvironment" style="height: 30px"></div> -->
<!-- 按钮 -->
<div
style="
padding: 90vw 0 0;
width: 100%;
display: flex;
justify-content: flex-end;
align-items: center;
"
>
<!-- 帮助按钮 -->
<img
v-smart-img
:src="imageUrl('helpBt')"
alt=""
style="display: block; position: relative; width: 10vw; margin-right: 4vw"
@click="maskLayerShow = true"
/>
</div>
</itemCenter>
<!-- 倒计时 -->
<itemCenter
style="min-height: 23vw; padding: 0 3vw"
:imgUrl="imageUrl('timeBg')"
:contentStyle="`direction:ltr;padding:0 3vw 5vw;gap:3.5vw`"
>
<div style="flex: 1; min-width: 0; display: flex; justify-content: center">
<div class="timeText">{{ Days }}</div>
</div>
<div style="flex: 1; min-width: 0; display: flex; justify-content: center">
<div class="timeText">{{ Hours }}</div>
</div>
<div style="flex: 1; min-width: 0; display: flex; justify-content: center">
<div class="timeText">{{ Minutes }}</div>
</div>
<div style="flex: 1; min-width: 0; display: flex; justify-content: center">
<div class="timeText">{{ Second }}</div>
</div>
</itemCenter>
<!-- 切换按钮 -->
<div>
<div style="display: flex; justify-content: space-around; margin-top: 2vw">
<!-- 排行榜按钮 -->
<div style="width: 40%; position: relative" @click="rankingShow = true">
<img
v-smart-img
:src="imageUrl(getImgName('rankingBt'))"
alt=""
:class="{ 'grayscale-container': !rankingShow }"
style="display: block; width: 100%"
/>
</div>
<!-- 奖励按钮 -->
<div style="width: 40%; position: relative" @click="rankingShow = false">
<img
v-smart-img
:src="imageUrl(getImgName('rewardBt'))"
alt=""
:class="{ 'grayscale-container': rankingShow }"
style="display: block; width: 100%"
/>
</div>
</div>
</div>
<!-- 展示内容 -->
<div>
<!-- Ranking -->
<div v-show="rankingShow" style="position: relative; width: 100%; margin: 10px 0">
<!-- 背景图 -->
<div class="ranking-bg" :style="`--ranking-bg-url: url(${imageUrl('rankBg')})`"></div>
<div
style="
position: relative;
z-index: 1;
display: flex;
flex-direction: column;
align-items: center;
"
>
<!-- 前三名 -->
<itemCenter
style="min-height: 75vw"
:imgUrl="imageUrl('RankingMain')"
:contentStyle="`padding: 21% 3% 0;flex-direction: column;`"
>
<div>
<!-- 第一 -->
<div style="display: flex; justify-content: center">
<TopUser
ranking="1"
:BorderImgUrl="imageUrl('top1')"
:avatarUrl="RankingHasTop3[0].avatar"
:name="RankingHasTop3[0].nickname"
:distributionValue="RankingHasTop3[0].quantity"
:valueIconUrl="coinIcon"
style="width: 42%"
@click="viewUserInfo(RankingHasTop3[0].userId)"
/>
</div>
<!-- 第二三 -->
<div
style="
position: relative;
z-index: 3;
display: flex;
justify-content: space-between;
margin-top: -40%;
pointer-events: none;
"
>
<div
style="pointer-events: auto; width: 32%"
@click.stop="viewUserInfo(RankingHasTop3[1].userId)"
>
<TopUser
ranking="2"
:BorderImgUrl="imageUrl('top2')"
:avatarUrl="RankingHasTop3[1].avatar"
:name="RankingHasTop3[1].nickname"
:distributionValue="RankingHasTop3[1].quantity"
:valueIconUrl="coinIcon"
style="width: 100%"
/>
</div>
<div
style="pointer-events: auto; width: 32%"
@click.stop="viewUserInfo(RankingHasTop3[2].userId)"
>
<TopUser
ranking="3"
:BorderImgUrl="imageUrl('top3')"
:avatarUrl="RankingHasTop3[2].avatar"
:name="RankingHasTop3[2].nickname"
:distributionValue="RankingHasTop3[2].quantity"
:valueIconUrl="coinIcon"
style="width: 100%"
/>
</div>
</div>
</div>
</itemCenter>
<!-- 第四名开始 -->
<div
v-if="showRanking.length > 0"
style="position: relative; z-index: 2; width: 100vw"
>
<itemCenter
v-for="(listItem, index) in showRanking"
:key="listItem.userId"
style="min-height: 22vw"
:imgUrl="imageUrl('RankingItem')"
:contentStyle="`padding: 3vw 8vw 0`"
:lazy="true"
:immediate="index < 2"
@click="viewUserInfo(listItem.userId)"
>
<!-- 基本信息 -->
<div
style="
flex: 1;
min-width: 0;
display: flex;
justify-content: space-around;
align-items: center;
gap: 4px;
"
>
<!-- 排名 -->
<div style="color: #ffe601; font-weight: 700">
{{ listItem?.rank }}
</div>
<!-- 头像 -->
<img
:src="listItem?.avatar || ''"
alt=""
style="
display: block;
width: 3.5em;
margin: 0 2%;
aspect-ratio: 1/1;
border-radius: 50%;
object-fit: cover;
"
@error="handleAvatarImageError"
/>
<!-- 名称id -->
<div style="flex: 1; min-width: 0; display: flex; flex-direction: column">
<div
style="
color: #fff;
font-weight: 860;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
"
class="UserNickname"
>
{{ listItem?.nickname }}
</div>
<div style="color: #fff; font-weight: 590" class="UserNickname">
{{ $t('user_id_prefix') }} {{ listItem?.account }}
</div>
</div>
</div>
<!-- 贡献值 -->
<div
style="
width: auto;
min-width: 0;
display: flex;
align-items: 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"
/>
<div style="color: #ffe601; font-weight: 700" class="UserNickname">
{{ listItem?.quantity }}
</div>
</div>
</itemCenter>
</div>
<div
v-show="rankingShow && hasMoreRanking"
ref="rankingLoadTrigger"
class="ranking-load-trigger"
></div>
<!-- 底框 -->
<img
v-smart-img
:src="imageUrl('RankingBottomBorder')"
alt=""
width="100%"
style="display: block; margin-top: -13vw; position: relative; z-index: 1"
/>
</div>
</div>
<!-- Rewards -->
<div
v-show="!rankingShow"
style="display: flex; flex-direction: column; align-items: center"
>
<div
v-for="(rewardsTop, top) in rewardsList"
:key="top"
style="position: relative; width: 100%; margin: 10px 0"
>
<itemCenter
style="min-height: 129vw"
:imgUrl="topImg[Number(rewardsTop.rankRange) - 1]"
:contentStyle="`display: grid;grid-template-columns: repeat(2, 1fr);gap: 3.5vw;padding: 24vw 8vw 5vw 8vw;`"
>
<div
v-for="(reward, rewardIndex) in rewardsTop.rewards"
:key="rewardIndex"
style="width: 100%"
>
<div
style="
width: 100%;
aspect-ratio: 1/1;
display: flex;
justify-content: center;
align-items: center;
"
>
<!-- 奖励图片 -->
<img
:src="reward.cover || ''"
alt=""
style="display: block; width: 70%; object-fit: cover"
@error="(e) => handleRewardImageError(e, reward.type)"
/>
</div>
<!-- 数值 -->
<div
style="
width: 100%;
display: flex;
justify-content: center;
align-items: center;
"
>
<div style="min-width: 0; font-size: 1.2em; font-weight: 590; color: #fff">
{{ showDetail(reward.type, reward) }}
</div>
</div>
</div>
</itemCenter>
</div>
</div>
</div>
<!-- 我的排名占位 -->
<div style="height: 25vw"></div>
<!-- 我的排名 -->
<itemCenter
style="min-height: 20vw; position: fixed; bottom: 0; z-index: 2"
:imgUrl="imageUrl('myRankingBg')"
:contentStyle="`padding: 0 8%`"
>
<!-- 基本信息 -->
<div
style="
flex: 1;
min-width: 0;
display: flex;
justify-content: space-around;
align-items: center;
gap: 8px;
"
>
<!-- 排名 -->
<div style="color: #ffe601; font-weight: 700">
{{ myRanking.rank || 999 }}
</div>
<!-- 头像 -->
<img
:src="myRanking.avatar || ''"
alt=""
style="
display: block;
width: 4em;
aspect-ratio: 1/1;
border-radius: 50%;
object-fit: cover;
"
@error="handleAvatarImageError"
/>
<!-- 名称 -->
<div style="flex: 1; min-width: 0; display: flex; flex-direction: column">
<div
style="
color: #fff;
font-weight: 860;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
"
class="UserNickname"
>
{{ myRanking.nickname }}
</div>
</div>
</div>
<!-- 贡献值 -->
<div
style="
width: auto;
min-width: 0;
display: flex;
align-items: 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"
/>
<div style="color: #fff; font-weight: 700">
{{ myRanking.quantity || 0 }}
</div>
</div>
</itemCenter>
<!-- 弹窗遮罩层 -->
<maskLayer :maskLayerShow="maskLayerShow" @click="maskLayerShow = false">
<!-- help弹窗 -->
<div style="margin: 20% 0" @click.stop>
<itemCenter
style="min-height: 152vw"
:imgUrl="imageUrl(getImgName('helpBg'))"
:contentStyle="`inset: 22vw 7vw 10vw; overflow-y: auto; display: block;`"
>
<div class="helpInfo">
<p>{{ $t('bounty_football_help_rule_1') }}</p>
<p>{{ $t('bounty_football_help_rule_2') }}</p>
<p>{{ $t('bounty_football_help_rule_3') }}</p>
<p>{{ $t('bounty_football_help_rule_3_withdrawal') }}</p>
<p>{{ $t('bounty_football_help_rule_3_no_withdrawal') }}</p>
<p>{{ $t('bounty_football_help_rule_4') }}</p>
<p>{{ $t('bounty_football_help_note') }}</p>
</div>
</itemCenter>
</div>
</maskLayer>
</div>
</div>
</div>
</template>
<script setup>
import { computed, nextTick, onMounted, onUnmounted, ref } from 'vue'
import { isInApp, viewUserInfo } from '@/utils/appBridge.js'
import { connectToApp } from '@/utils/appConnector.js'
import { useLangStore } from '@/stores/lang'
import { getPngUrl } from '@/config/imagePaths.js'
import { preloadImages } from '@/utils/image/imagePreloader.js'
import { handleAvatarImageError, handleRewardImageError } from '@/utils/image/imageHandler.js'
import { formatRewardDisplay } from '@/utils/rewardFormatter.js'
import { useDebounce } from '@/utils/useDebounce.js'
import { getThisWeekRewardsAndGifts, getRankingListAndMyRanking } from '@/api/activity.js'
import itemCenter from '@/components/itemCenter.vue'
import TopUser from './components/topUser.vue'
import maskLayer from '@/components/MaskLayer.vue'
const coinIcon = new URL('/src/assets/icon/Azizi/coin.png', import.meta.url).href
const isInAppEnvironment = ref(false) // APP
const maskLayerShow = ref(false) //
//
const isLoading = ref(true)
const activityType = ref(20)
const templateId = ref('2061370141245067265')
const cycleKey = ref('20260601')
const langStore = useLangStore()
//
const currentLangType = computed(() => {
return langStore.selectedLang?.type || 'en'
})
// OSSURL
const imageUrl = (filename) => getPngUrl('Activities/BountyFootball/', filename)
//
const getImgName = (filename) => {
return currentLangType.value === 'en' ? filename : `${filename}_${currentLangType.value}`
}
//
const Days = ref(0)
const Hours = ref(0)
const Minutes = ref(0)
const Second = ref(0)
let timer = null
//
const rankingShow = ref(true) //
const Ranking = ref([]) //
const rankingAllData = ref([])
const myRanking = ref({}) //
const rewardsList = ref([]) //
const topImg = ref([imageUrl('rewardBgTop1'), imageUrl('rewardBgTop2'), imageUrl('rewardBgTop3')]) // top
const bgRef = ref(null)
const rankingLoadTrigger = ref(null)
const rankingPageSize = 10
let rankingObserver = null
const normalizeRankingUser = (user = {}) => {
return {
...user,
userId: user.userId || user.uid || '',
avatar: user.avatar || user.userAvatar || '',
nickname: user.nickname || user.userName || '',
quantity: user.quantity ?? user.amount ?? 0,
rank: user.rank ?? user.ranking ?? '',
account: user.account || '',
specialAccount: user.specialAccount || '',
}
}
// 3
const RankingHasTop3 = computed(() => {
let RankingShow = [...Ranking.value]
if (Ranking.value.length < 3) {
let addNullUser = Array.from({ length: 3 - Ranking.value.length }, () => ({
avatar: '',
nickname: '',
quantity: '',
}))
RankingShow.push(...addNullUser)
}
return RankingShow
})
//
const showRanking = computed(() => {
return Ranking.value.filter((_, index) => index >= 3)
// return []
})
const hasMoreRanking = computed(() => {
return Ranking.value.length < rankingAllData.value.length
})
const isRankingTriggerVisible = () => {
if (!rankingLoadTrigger.value) return false
const triggerRect = rankingLoadTrigger.value.getBoundingClientRect()
const rootRect = bgRef.value?.getBoundingClientRect()
const viewportTop = rootRect?.top ?? 0
const viewportBottom = rootRect?.bottom ?? window.innerHeight
return triggerRect.top <= viewportBottom && triggerRect.bottom >= viewportTop
}
const appendRankingBatch = () => {
if (!hasMoreRanking.value) return
const start = Ranking.value.length
const nextRanking = rankingAllData.value.slice(start, start + rankingPageSize)
Ranking.value.push(...nextRanking)
}
const handleRankingLoadTrigger = () => {
if (!rankingShow.value || !hasMoreRanking.value || !isRankingTriggerVisible()) return
console.log('上拉加载')
appendRankingBatch()
nextTick(() => {
if (hasMoreRanking.value && isRankingTriggerVisible()) {
debouncedLoadMoreRanking()
}
})
}
const debouncedLoadMoreRanking = useDebounce(handleRankingLoadTrigger, 300)
const checkRankingLoadTrigger = () => {
if (!rankingShow.value || !hasMoreRanking.value) return
debouncedLoadMoreRanking()
}
const loadInitialRanking = () => {
appendRankingBatch()
nextTick(() => {
checkRankingLoadTrigger()
})
}
const destroyRankingObserver = () => {
if (rankingObserver) {
rankingObserver.disconnect()
rankingObserver = null
}
}
const initRankingObserver = async () => {
destroyRankingObserver()
await nextTick()
if (!rankingLoadTrigger.value || typeof IntersectionObserver === 'undefined') return
rankingObserver = new IntersectionObserver(
(entries) => {
if (entries.some((entry) => entry.isIntersecting)) {
debouncedLoadMoreRanking()
}
},
{
root: bgRef.value || null,
rootMargin: '0px 0px 150px 0px',
threshold: 0.1,
},
)
rankingObserver.observe(rankingLoadTrigger.value)
checkRankingLoadTrigger()
}
// 2026685
const getTargetTime = () => {
const targetDate = new Date(2026, 5, 8, 5, 0, 0)
return targetDate.getTime()
}
const targetTime = ref(getTargetTime())
//
const formatTime = (value) => {
return value < 10 ? `0${value}` : value
}
//
const getCountdown = () => {
const now = Date.now()
let diff = targetTime.value - now
//
if (diff <= 0) {
Days.value = 0
Hours.value = '00'
Minutes.value = '00'
Second.value = '00'
//
if (timer) {
clearInterval(timer)
timer = null
}
return
}
const totalSeconds = Math.floor(diff / 1000)
Days.value = Math.floor(totalSeconds / (24 * 3600))
Hours.value = formatTime(Math.floor((totalSeconds % (24 * 3600)) / 3600))
Minutes.value = formatTime(Math.floor((totalSeconds % 3600) / 60))
Second.value = formatTime(Math.floor(totalSeconds % 60))
}
//
const showDetail = (type, reward) => {
return formatRewardDisplay(type, reward)
}
//
const getListAndMy = async () => {
let data = {
activityType: activityType.value,
cycleKey: cycleKey.value,
}
const resMyWeekStarRanking = await getRankingListAndMyRanking(data)
console.log('resMyWeekStarRanking:', resMyWeekStarRanking)
if (resMyWeekStarRanking.status && resMyWeekStarRanking.body) {
myRanking.value = normalizeRankingUser(resMyWeekStarRanking.body.currentUserRank || {})
rankingAllData.value = (resMyWeekStarRanking.body.rankingList || []).map((item) =>
normalizeRankingUser(item),
)
Ranking.value = []
loadInitialRanking()
await initRankingObserver()
} else {
myRanking.value = {}
Ranking.value = []
rankingAllData.value = []
destroyRankingObserver()
}
}
//
const getRewardsAndGifts = async () => {
const resRewardsAndGifts = await getThisWeekRewardsAndGifts(templateId.value)
if (resRewardsAndGifts.status && resRewardsAndGifts.body) {
rewardsList.value = resRewardsAndGifts.body.butOneRewards
}
}
//
const initData = async () => {
await Promise.all([
getListAndMy(), //
getRewardsAndGifts(), //
])
}
//
const preloadCriticalImages = async () => {
const criticalImages = [
imageUrl('bg'),
imageUrl('bg2'),
imageUrl('helpBt'),
imageUrl('timeBg'),
imageUrl(getImgName('rankingBt')),
imageUrl(getImgName('rewardBt')),
imageUrl('rankBg'),
imageUrl('RankingItem'),
imageUrl('RankingMain'),
imageUrl('RankingBottomBorder'),
imageUrl('myRankingBg'),
imageUrl(getImgName('helpBg')),
imageUrl('rewardBgTop1'),
imageUrl('rewardBgTop2'),
imageUrl('rewardBgTop3'),
imageUrl('top1'),
imageUrl('top2'),
imageUrl('top3'),
]
await preloadImages(criticalImages)
}
//
const completePreloading = async () => {
try {
//
await Promise.all([initData(), preloadCriticalImages()])
} catch (error) {
console.error('预加载过程中发生错误:', error)
} finally {
//
isLoading.value = false
}
}
// 使APP
const connectToAppHandler = async () => {
await connectToApp(() => {
//
completePreloading() //
})
}
//
onMounted(() => {
connectToAppHandler()
isInAppEnvironment.value = isInApp()
getCountdown()
timer = setInterval(getCountdown, 1000) //
})
onUnmounted(() => {
clearInterval(timer) //
destroyRankingObserver()
debouncedLoadMoreRanking.cancel()
})
</script>
<style lang="scss" scoped>
* {
color: #fff;
}
.fullPage {
position: relative;
background: #053126;
}
.bg {
width: 100vw;
height: 100vh;
overflow-x: hidden;
overflow-y: auto;
position: relative;
z-index: 1;
scrollbar-width: none;
}
.bg::-webkit-scrollbar {
display: none;
}
.bg > :not(.bg2-layer):not(.mask-layer) {
position: relative;
z-index: 1;
}
.bg :deep(.mask-layer) {
position: fixed;
inset: 0;
z-index: 10000 !important;
}
.bg2-layer {
position: absolute;
top: 0;
left: 0;
z-index: 0;
display: block;
width: 100vw;
height: 100vh;
object-fit: cover;
pointer-events: none;
}
.grayscale-container {
filter: grayscale(100%) brightness(90%);
}
.background-overlay {
position: absolute;
inset: 120vw 0 0;
background-image: var(--bg2-url);
background-repeat: repeat-y;
background-position: top center;
background-size: 100% auto;
z-index: 0; /* 确保在.bg之下 */
}
.ranking-bg {
background-color: #053126;
background-image: var(--ranking-bg-url);
background-size: 100% auto;
background-repeat: no-repeat;
position: absolute;
inset: 15vw 2vw 4vw;
z-index: 1;
}
.ranking-load-trigger {
width: 100%;
height: 1px;
}
.timeText {
color: #fff;
font-weight: 590;
font-size: 2em;
}
.timeGap {
width: 6px;
align-self: stretch;
display: flex;
justify-content: center;
align-items: center;
}
.btTitle {
color: #fff;
font-weight: 700;
}
.topTitle {
font-size: 1.5em;
}
.history-name {
font-size: 0.8em;
}
.history-time {
font-size: 0.8em;
}
.UserNickname {
font-size: 0.8em;
}
.timeBox {
display: flex;
justify-content: center;
margin-top: -23vw;
}
/* 添加加载动画样式 */
.loading-container {
width: 100vw;
height: 100vh;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
color: #666;
}
@media screen and (max-width: 360px) {
* {
font-size: 10px;
}
}
@media screen and (min-width: 360px) {
* {
font-size: 14px;
}
}
@media screen and (min-width: 768px) {
* {
font-size: 24px;
}
}
@media screen and (min-width: 1024px) {
* {
font-size: 32px;
}
}
[dir='rtl'] .timeBox {
flex-direction: row-reverse;
}
.helpInfo {
display: flex;
flex-direction: column;
gap: 1vw;
}
.helpInfo > p {
color: #fff;
font-size: 1em;
font-weight: 400;
line-height: 1.4;
margin: 0;
}
</style>

View File

@ -0,0 +1,115 @@
export const pageConfig = {
name: 'BountyFootball',
routePath: '/activities/bounty-football',
routeName: 'bounty-football',
assetPath: 'Activities/BountyFootball/',
activity: {
templateId: '2061370141245067265',
activityType: 20,
cycleKey: '20260601',
},
preload: {
blocking: [
'bg',
'bg2',
'helpBt',
'timeBg',
'rankingBt',
'rewardBt',
'rankBg',
'RankingMain',
'RankingItem',
'RankingBottomBorder',
'myRankingBg',
'helpBg',
'top1',
'top2',
'top3',
'rewardBgTop1',
'rewardBgTop2',
'rewardBgTop3',
],
background: [],
},
modules: {
tabs: {
enabled: true,
defaultValue: 'ranking',
items: [
{
value: 'ranking',
label: 'Ranking',
buttonImage: 'rankingBt',
activeButtonImage: 'rankingBt',
remark: '排行榜按钮,未选中时页面使用灰度效果。',
},
{
value: 'reward',
label: 'Reward',
buttonImage: 'rewardBt',
activeButtonImage: 'rewardBt',
remark: '奖励按钮,未选中时页面使用灰度效果。',
},
],
},
sections: [
{
name: 'ranking',
label: 'Ranking',
enabled: true,
apis: ['getRankingListAndMyRanking'],
loadMode: 'client-incremental',
pageSize: 10,
images: ['rankBg', 'RankingMain', 'RankingItem', 'RankingBottomBorder', 'myRankingBg'],
previewImages: ['top1', 'top2', 'top3'],
remark: '默认展示排行榜;包含前三名、第四名以后列表和底部我的排名。',
children: {},
},
{
name: 'reward',
label: 'Reward',
enabled: true,
apis: ['getThisWeekRewardsAndGifts'],
images: ['rewardBgTop1', 'rewardBgTop2', 'rewardBgTop3'],
previewImages: [],
remark: '只展示奖励列表;已按需求取消 index 中的活动礼物模块。',
children: {},
},
],
popups: [
{
name: 'help',
triggerImage: 'helpBt',
backgroundImage: 'helpBg',
contentImage: '',
apis: [],
remark:
'规则说明弹窗helpBg 支持多语言图片。MaskLayer 必须保持 fixed 顶层,不要被 .bg 子层级规则覆盖;规则文案按编号拆段,白色字体,字号 1em。',
},
],
features: [
{
name: 'countdown',
enabled: true,
deadline: '2026-06-12 05:00:00',
images: ['timeBg'],
fields: {
endTime: '2026-06-12 05:00:00',
remainingText: '',
},
remark: '天/时/分/秒使用 timeBg 承载;当前 index.vue 目标时间为 2026-06-12 05:00:00。',
},
],
},
images: {
localized: {
ar: ['helpBg', 'rankingBt', 'rewardBt'],
bn: ['helpBg', 'rankingBt', 'rewardBt'],
tr: ['helpBg', 'rankingBt', 'rewardBt'],
},
},
}

View File

@ -0,0 +1,377 @@
# 页面创建信息
## 1. 基础信息
- 页面类型Activities
- 页面名称BountyFootball
- 页面目录src/views/Activities/BountyFootball
- 参考页面:已存在 `src/views/Activities/BountyFootball/index.vue`
- 备注:已按本地图片目录扫描结果同步图片清单、预加载和页面用图;按需求取消 index 中的活动礼物模块。
## 2. 路由和权限
- route path/activities/bounty-football
- route namebounty-football
- 是否需要登录:否
- 是否加入 `src/config/security.js`:是
- 是否加入 `src/utils/permissionManager.js`:是
### 2.1 路由代码片段
```js
{
path: '/activities/bounty-football',
name: 'bounty-football',
component: () => import('../views/Activities/BountyFootball/index.vue'),
meta: { requiresAuth: false },
}
```
### 2.2 安全白名单片段
```js
'/activities/bounty-football',
```
### 2.3 权限页面片段
```js
'/activities/bounty-football', // BountyFootball
```
## 3. OSS 图片目录
- OSS 相对路径Activities/BountyFootball/
- 本地开发目录public/oss/h5/Azizi/Activities/BountyFootball/
- 是否需要 Codex 自动创建本地开发目录:是
- 图片是否已放入本地目录:是
说明:本地开发目录只作为开发期 OSS 图片镜像使用,方便页面优先读取本地资源;发布时图片由你上传到真实 OSS不随项目提交 Git。新建本地目录时不生成 `.gitkeep`,空目录不被 Git 跟踪是正常现象。
### 3.1 多语言图片清单
| 图片名 | 支持语言 | 备注 |
| --- | --- | --- |
| bg | en | 页面主背景 |
| bg2 | en | `.bg` 容器第一层底图,作为内容最底层背景 |
| helpBg | en, ar, bn, tr | 规则说明弹窗背景 |
| helpBt | en | 规则/帮助按钮 |
| myRankingBg | en | 底部我的排名固定栏背景 |
| rankBg | en | 排行榜内容区背景 |
| RankingBottomBorder | en | 排行榜底部边框 |
| rankingBt | en, ar, bn, tr | 排行榜按钮 |
| RankingItem | en | 排行榜列表项背景 |
| RankingMain | en | 排行榜前三名展示区背景 |
| rewardBgTop1 | en | 第一名奖励区背景 |
| rewardBgTop2 | en | 第二名奖励区背景 |
| rewardBgTop3 | en | 第三名奖励区背景 |
| rewardBt | en, ar, bn, tr | 奖励按钮 |
| timeBg | en | 倒计时背景 |
| top1 | en | 第一名头像/排名装饰 |
| top2 | en | 第二名头像/排名装饰 |
| top3 | en | 第三名头像/排名装饰 |
### 3.2 进入页面前同步预加载图片
| 图片名 | 是否包含多语言 | 备注 |
| --- | --- | --- |
| bg | 否 | 页面主背景 |
| bg2 | 否 | `.bg` 容器第一层底图,作为内容最底层背景 |
| helpBt | 否 | 规则/帮助按钮 |
| timeBg | 否 | 倒计时背景 |
| rankingBt | 是 | 标签页排行榜按钮 |
| rewardBt | 是 | 标签页奖励按钮 |
| rankBg | 否 | 排行榜内容区背景 |
| RankingMain | 否 | 排行榜前三名展示区背景 |
| RankingItem | 否 | 排行榜列表项背景 |
| RankingBottomBorder | 否 | 排行榜底部边框 |
| myRankingBg | 否 | 底部我的排名固定栏背景 |
| helpBg | 是 | 规则说明弹窗背景 |
| top1 | 否 | 第一名头像/排名装饰 |
| top2 | 否 | 第二名头像/排名装饰 |
| top3 | 否 | 第三名头像/排名装饰 |
| rewardBgTop1 | 否 | 第一名奖励区背景 |
| rewardBgTop2 | 否 | 第二名奖励区背景 |
| rewardBgTop3 | 否 | 第三名奖励区背景 |
### 3.3 进入页面后后台预加载图片
| 图片名 | 是否包含多语言 | 备注 |
| --- | --- | --- |
| | | 当前 index.vue 会在进入页面前预加载全部已使用本地图片,暂无后台预加载项 |
## 4. 模块配置
没有填写的模块,初版页面先不处理。
### 4.1 标签页模块
```json
{
"enabled": true,
"defaultValue": "ranking",
"items": [
{
"value": "ranking",
"label": "Ranking",
"buttonImage": "rankingBt",
"activeButtonImage": "rankingBt",
"remark": "排行榜按钮,未选中时页面使用灰度效果"
},
{
"value": "reward",
"label": "Reward",
"buttonImage": "rewardBt",
"activeButtonImage": "rewardBt",
"remark": "奖励按钮,未选中时页面使用灰度效果"
}
]
}
```
### 4.2 页面区块模块
```json
[
{
"name": "ranking",
"label": "Ranking",
"enabled": true,
"apis": ["getRankingListAndMyRanking"],
"loadMode": "client-incremental",
"pageSize": 10,
"images": ["rankBg", "RankingMain", "RankingItem", "RankingBottomBorder", "myRankingBg"],
"previewImages": ["top1", "top2", "top3"],
"remark": "默认展示排行榜;包含前三名、第四名以后列表和底部我的排名",
"children": {}
},
{
"name": "reward",
"label": "Reward",
"enabled": true,
"apis": ["getThisWeekRewardsAndGifts"],
"images": ["rewardBgTop1", "rewardBgTop2", "rewardBgTop3"],
"previewImages": [],
"remark": "只展示奖励列表;已按需求取消 index 中的活动礼物模块",
"children": {}
}
]
```
倒计时模块固定保留:
```json
{
"name": "countdown",
"enabled": true,
"deadline": "2026-06-12 05:00:00",
"images": ["timeBg"],
"fields": {
"endTime": "2026-06-12 05:00:00",
"remainingText": ""
},
"remark": "天/时/分/秒使用 timeBg 承载;当前 index.vue 目标时间为 2026-06-12 05:00:00。"
}
```
### 4.3 弹窗模块
```json
[
{
"name": "help",
"triggerImage": "helpBt",
"backgroundImage": "helpBg",
"contentImage": "",
"apis": [],
"remark": "规则说明弹窗helpBg 支持多语言图片MaskLayer 必须保持 fixed 顶层,不要被 .bg 子层级规则覆盖;规则文案按编号拆段,白色字体,字号 1em。"
}
]
```
### 4.4 排行榜模块
```json
{
"enabled": "",
"api": "",
"loadMode": "",
"pageSize": "",
"topCount": "",
"myRankingPath": "",
"fieldMap": {
"avatar": "",
"name": "",
"value": "",
"userId": "",
"rank": ""
}
}
```
### 4.5 抽奖模块
```json
{
"enabled": "",
"drawApis": [],
"prizePoolApi": "",
"drawCountApi": "",
"extraApis": [],
"displayMode": "",
"slotCount": "",
"historyButtonPosition": "",
"images": {
"background": "",
"activeFrame": "",
"singleDrawButton": "",
"multiDrawButton": "",
"historyButton": "",
"resultBackground": ""
},
"remark": ""
}
```
## 5. 接口参数
```json
{
"templateId": "2061370141245067265",
"activityType": 20,
"cycleKey": "20260601"
}
```
## 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": "抽奖格子"
}
]
```
### 6.1 当前页面布局记录
本节按当前 `index.vue` 的实时结构记录,后续如从 md 同步回 Vue需要保留以下规则
```json
{
"scrollContainer": {
"selector": ".bg",
"style": {
"width": "100vw",
"height": "100vh",
"overflowX": "hidden",
"overflowY": "auto",
"scrollbar": "hidden",
"position": "relative",
"zIndex": 1
},
"remark": "页面上方内容在 .bg 内垂直滚动,隐藏滚动条。"
},
"backgroundLayer": {
"image": "bg2",
"placement": ".bg 的第一个子节点",
"layer": "内容最底层",
"style": {
"position": "absolute",
"top": 0,
"left": 0,
"zIndex": 0,
"width": "100vw",
"height": "100vh",
"objectFit": "cover",
"pointerEvents": "none"
}
},
"maskLayer": {
"component": "MaskLayer",
"position": "fixed",
"zIndex": 10000,
"remark": ".bg > :not(.bg2-layer) 这类子层级规则不能覆盖 MaskLayer 根节点;当前 Vue 使用 .bg > :not(.bg2-layer):not(.mask-layer) 并用 :deep(.mask-layer) 保持遮罩在视口最上层。"
},
"helpPopupText": {
"style": {
"color": "#fff",
"fontSize": "1em"
},
"paragraphs": [
"1. The more coins a user accumulates during the event, the higher their ranking will be.",
"2. The event will last for 15 days. After the event ends, rewards will be distributed to the top three users on the leaderboard.",
"3. The top three users should contact the official staff in a timely manner to discuss how to obtain the USD reward.",
"(1) Withdrawal: Please provide your USDT receiving account ID or receiving QR code.",
"(2) No withdrawal: If converted to coins, we will give you an additional 5% gold coins on top of the original amount ($1:10500 coins).",
"4.ID Rules: Users can locate official staff by providing a screenshot of their ranking and the number of the selected ID to use it (for example: when the reward is \"ID: ABBBBA\", the user can provide \"ID: 100001\" as their own ID, or grant the usage rights to another person).",
"*Please note that the ID must not currently be in use by anyone else."
]
}
}
```
## 7. 建页后记录
这部分由 Codex 建页后填写。
### 7.1 已完成
- 已创建页面目录src/views/Activities/BountyFootball
- 已创建专属页面信息文件src/views/Activities/BountyFootball/页面创建信息.md
- 已创建初始配置文件src/views/Activities/BountyFootball/page.config.js
- 已创建本地开发图片目录public/oss/h5/Azizi/Activities/BountyFootball/
- 已扫描本地图片目录并更新 3.1 图片清单、3.2/3.3 预加载清单
- 已同步更新 `page.config.js``preload``modules``popups``features``images.localized`
- 已将 `index.vue` 的资源路径切换为 `Activities/BountyFootball/` 下的 PNG 图片
- 已取消 `index.vue` 中的活动礼物模块,保留排行榜与奖励列表模块
- 已写入 `src/router/index.js``src/config/security.js``src/utils/permissionManager.js`
- 已将页面内不存在的 `Atu/coin.png` 修正为项目现有的 `Azizi/coin.png`
- 已补充 `bg2``.bg` 容器第一层底图,并将 `.bg` 改为 100vw/100vh 的隐藏滚动条纵向滚动容器
- 已更新 `templateId``2061370141245067265`,并移除 `page.config.js` 中当前未使用的空接口参数
- 已修复 MaskLayer 被 `.bg` 子层级样式覆盖导致遮罩出现在页面下方的问题,并补充规则弹窗文案与样式
- 已重新扫描图片并替换前三名头像框与前三名奖励背景的新命名:`top1/top2/top3``rewardBgTop1/rewardBgTop2/rewardBgTop3`
- 已将 help 弹窗硬编码文案切换为 `src/locales` 多语言 key`bounty_football_help_*`
### 7.2 未填写所以跳过
- 抽奖配置未补充,暂不处理
- 其余接口参数当前页面未使用,已从 `page.config.js` 移除
### 7.3 待你微调
- 确认规则文案是否需要后续接入多语言翻译 key
### 7.4 验证结果
- `npm run build`:通过
- 备注:构建通过;仍保留项目既有的 `secondBg.png` 运行时解析提示和 `http.js` 动态/静态导入提示。