979 lines
29 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

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

<!-- src/views/Activities/Game/index.vue -->
<template>
<div class="fullPage">
<!-- 预加载状态 -->
<div v-if="isLoading" class="loading-container">
<div class="loading-spinner"></div>
<p>{{ $t('loading') }}...</p>
</div>
<!-- 主要内容 -->
<div v-else>
<!-- 新增的背景图层 -->
<img
v-smart-img
:src="imageUrl('bg2')"
alt=""
style="display: block; width: 100%; position: absolute; inset: 80vw 0 0; z-index: 0"
@click="maskLayerShow = true"
/>
<div class="bg">
<img
v-smart-img
:src="imageUrl('bg')"
alt=""
style="display: block; width: 100%; position: absolute; z-index: 0"
@click="maskLayerShow = true"
/>
<!-- 状态栏占位区域仅在APP中显示 -->
<div v-if="isInAppEnvironment" style="height: 30px"></div>
<!-- 第一名 -->
<div style="display: flex; justify-content: center; margin-top: 11vw">
<TopUser
ranking="1"
:BorderImgUrl="imageUrl('top1')"
:avatarUrl="RankingHasTop10[0].avatar"
:name="RankingHasTop10[0].nickname"
:distributionValue="RankingHasTop10[0].quantity || ''"
style="width: 55%"
@click="viewUserInfo(RankingHasTop10[0].userId)"
/>
</div>
<!-- 按钮 -->
<div style="margin-top: 6vw; display: flex; justify-content: flex-end; align-items: center">
<!-- 帮助按钮 -->
<img
v-smart-img
:src="imageUrl('helpBt')"
alt=""
style="display: block; position: relative; width: 8vw; margin-right: 4vw"
@click="maskLayerShow = true"
/>
</div>
<!-- 倒计时 -->
<div class="timeBox">
<div style="width: 20%">
<itemCenter :imgUrl="imageUrl('timeBg')">
<div class="timeText" style="">{{ Days }}D</div>
</itemCenter>
</div>
<div class="timeText timeGap"></div>
<div style="width: 20%">
<itemCenter :imgUrl="imageUrl('timeBg')">
<div class="timeText" style="">{{ Hours }}</div>
</itemCenter>
</div>
<div class="timeText timeGap">:</div>
<div style="width: 20%">
<itemCenter :imgUrl="imageUrl('timeBg')">
<div class="timeText" style="">{{ Minutes }}</div>
</itemCenter>
</div>
<div class="timeText timeGap">:</div>
<div style="width: 20%">
<itemCenter :imgUrl="imageUrl('timeBg')">
<div class="timeText" style="">{{ Second }}</div>
</itemCenter>
</div>
</div>
<!-- 切换按钮 -->
<div>
<div style="display: flex; justify-content: space-around; margin-top: 2vw">
<!-- 排行榜按钮 -->
<div style="width: 40%; position: relative" @click="rankingShow = true">
<img
v-show="!rankingShow"
v-smart-img
class="toggle-image"
:src="imageUrl(getImgName('rankingBt'))"
alt=""
style="display: block; width: 100%"
/>
<img
v-show="rankingShow"
v-smart-img
class="toggle-image"
:src="imageUrl(getImgName('rankingBtActive'))"
alt=""
style="display: block; width: 100%"
/>
</div>
<!-- 奖励按钮 -->
<div style="width: 40%; position: relative" @click="rankingShow = false">
<img
v-show="rankingShow"
v-smart-img
class="toggle-image"
:src="imageUrl(getImgName('rewardsBt'))"
alt=""
style="display: block; width: 100%"
/>
<img
v-show="!rankingShow"
v-smart-img
class="toggle-image"
:src="imageUrl(getImgName('rewardBtActive'))"
alt=""
style="display: block; width: 100%"
/>
</div>
</div>
</div>
<!-- 展示内容 -->
<div>
<!-- Ranking -->
<div v-show="rankingShow" style="position: relative; width: 100%; margin: 10px 0">
<div style="position: relative; z-index: 1">
<!-- 第2-4 -->
<itemCenter
:imgUrl="imageUrl(getImgName('RankingMain'))"
:contentStyle="`padding: 25% 4% 0;flex-direction: column;`"
>
<div>
<!-- 第2名 -->
<div style="display: flex; justify-content: center">
<TopUser
ranking="2"
:BorderImgUrl="imageUrl('top2')"
:avatarUrl="RankingHasTop10[1].avatar"
:name="RankingHasTop10[1].nickname"
:distributionValue="RankingHasTop10[1].quantity || ''"
style="width: 40%"
@click="viewUserInfo(RankingHasTop10[1].userId)"
/>
</div>
<!-- 第3-4 -->
<div style="display: flex; justify-content: space-between; margin-top: -5vw">
<TopUser
ranking="3"
:BorderImgUrl="imageUrl('top3')"
:avatarUrl="RankingHasTop10[2].avatar"
:name="RankingHasTop10[2].nickname"
:distributionValue="RankingHasTop10[2].quantity || ''"
style="width: 38%"
@click="viewUserInfo(RankingHasTop10[2].userId)"
/>
<TopUser
ranking="4"
:BorderImgUrl="imageUrl('top4')"
:avatarUrl="RankingHasTop10[3].avatar"
:name="RankingHasTop10[3].nickname"
:distributionValue="RankingHasTop10[3].quantity || ''"
style="width: 38%"
@click="viewUserInfo(RankingHasTop10[3].userId)"
/>
</div>
</div>
</itemCenter>
<!-- 第5-10 -->
<itemCenter
:imgUrl="imageUrl('RankingCenterBg')"
:contentStyle="`padding: 0 5%;flex-direction: column;justify-content: space-around;`"
>
<!-- 5-7 -->
<div style="width: 100%; display: flex; justify-content: space-between">
<TopUser
v-for="topUser in RankingHasTop10.slice(4, 7)"
:key="topUser.userId"
ranking="5-7"
:BorderImgUrl="imageUrl('top5_7')"
:avatarUrl="topUser.avatar"
:name="topUser.nickname"
:distributionValue="topUser.quantity || ''"
style="width: 30%"
@click="viewUserInfo(topUser.userId)"
/>
</div>
<!-- 8-10 -->
<div style="width: 100%; display: flex; justify-content: space-between">
<TopUser
v-for="topUser in RankingHasTop10.slice(7, 10)"
:key="topUser.userId"
ranking="8-10"
:BorderImgUrl="imageUrl('top8_10')"
:avatarUrl="topUser.avatar"
:name="topUser.nickname"
:distributionValue="topUser.quantity || ''"
style="width: 30%"
@click="viewUserInfo(topUser.userId)"
/>
</div>
</itemCenter>
<!-- 第11名开始 -->
<div v-if="showRanking.length > 0" style="position: relative; z-index: 2">
<itemCenter
v-for="(listItem, index) in showRanking"
:key="listItem.userId"
:imgUrl="imageUrl('RankingItem')"
:contentStyle="`padding: 0 10%`"
: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: 8px;
"
>
<!-- 排名 -->
<div style="color: #ffe601; font-weight: 700">
{{ listItem?.rank }}
</div>
<!-- 头像 -->
<img
v-smart-img
:src="listItem?.avatar || ''"
alt=""
style="
display: block;
width: 13vw;
aspect-ratio: 1/1;
border-radius: 50%;
object-fit: cover;
"
@error="handleAvatarImageError"
/>
<!-- 名称id -->
<div
style="
flex: 1;
min-width: 0;
align-self: stretch;
display: flex;
flex-direction: column;
justify-content: space-around;
"
>
<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/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>
<!-- 底框 -->
<img
v-smart-img
:src="imageUrl('RankingBottomBorder')"
alt=""
width="100%"
style="display: block; margin-top: -6vw; position: relative; z-index: 2"
/>
</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
v-if="top == 0"
:imgUrl="topImg[0]"
:contentStyle="`display: grid;grid-template-columns: repeat(2, 1fr);gap: 2vw;padding: 28vw 4vw 8vw 37vw;`"
>
<div
v-for="(reward, rewardIndex) in rewardsTop.rewards"
:key="rewardIndex"
style="
width: 100%;
min-width: 0;
align-self: stretch;
display: flex;
flex-direction: column;
justify-content: space-between;
align-items: center;
"
>
<div
style="
width: 100%;
aspect-ratio: 1/1;
display: flex;
justify-content: center;
align-items: center;
"
>
<!-- 奖励图片 -->
<img
v-smart-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;
color: #f9e447;
text-align: center;
font-size: 1.2em;
font-weight: 900;
font-family: Atma;
"
>
{{ showDetail(reward.type, reward) }}
</div>
</div>
</div>
<!-- 启动时间 -->
<div
style="
position: absolute;
left: 3.5vw;
bottom: 10vw;
width: 28vw;
display: flex;
justify-content: center;
align-items: center;
"
>
<div
style="color: #f9e447; text-align: center; font-size: 1.2em; font-weight: 900"
>
7Days
</div>
</div>
</itemCenter>
<!-- 第2-45-78-10名奖励 -->
<itemCenter
v-else
:imgUrl="topImg[Number(rewardsTop.rankRange) - 1]"
:contentStyle="`display: grid;grid-template-columns: repeat(3, 1fr);gap: 4vw;padding: 28vw 3vw 10vw;`"
>
<div
v-for="(reward, rewardIndex) in rewardsTop.rewards"
:key="rewardIndex"
style="width: 100%"
:style="rewardIndex == 3 ? `grid-row: 2;grid-column: 2;` : ''"
>
<div
style="
width: 100%;
aspect-ratio: 1/1;
display: flex;
justify-content: center;
align-items: center;
"
>
<!-- 奖励图片 -->
<img
v-smart-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;
color: #f9e447;
text-align: center;
font-size: 1.2em;
font-weight: 900;
font-family: Atma;
"
>
{{ showDetail(reward.type, reward) }}
</div>
</div>
</div>
</itemCenter>
</div>
</div>
</div>
<!-- 我的排名占位 -->
<div style="height: 25vw"></div>
<!-- 我的排名 -->
<itemCenter
style="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
v-smart-img
:src="myRanking.avatar || ''"
alt=""
style="
display: block;
width: 15vw;
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/coin.png"
alt=""
style="display: block; width: 1.5em; aspect-ratio: 1/1; object-fit: cover"
/>
<div style="color: #ffe601; font-weight: 700">
{{ myRanking.quantity || 0 }}
</div>
</div>
</itemCenter>
<!-- 弹窗遮罩层 -->
<maskLayer :maskLayerShow="maskLayerShow" @click="maskLayerShow = false">
<!-- help弹窗 -->
<div style="margin: 10vh 0 5vh" @click.stop>
<itemCenter
:imgUrl="imageUrl(getImgName('helpBg'))"
:contentStyle="`inset: 15% 8% 3%;overflow-y: auto;display:block;`"
>
<img
v-smart-img
:src="imageUrl(getImgName('helpInfo'))"
alt=""
style="display: block; width: 100%; object-fit: cover; margin-bottom: 6vw"
/>
</itemCenter>
</div>
</maskLayer>
</div>
</div>
</div>
</template>
<script setup>
import { computed, 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/imagePreloader.js'
import { handleAvatarImageError, handleRewardImageError } from '@/utils/imageHandler.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 isInAppEnvironment = ref(false) // 检测是否在APP环境中
const maskLayerShow = ref(false) //帮助模块
// 预加载状态
const isLoading = ref(true)
const langStore = useLangStore()
// 当前语言类型
const currentLangType = computed(() => {
return langStore.selectedLang?.type || 'en'
})
// 获取OSS图片URL的函数
const imageUrl = (filename) => getPngUrl('Ranking/GamesKing/', filename)
// 根据语言获取图片名
const getImgName = (filename) => {
return currentLangType.value === 'ar' ? `${filename}_ar` : filename
}
// 倒计时
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 myRanking = ref({}) // 我的排名
const rewardsList = ref([]) // 奖励列表
const topImg = ref([
imageUrl('top1RewardBg'),
imageUrl('top2_4RewardBg'),
imageUrl('top5_7RewardBg'),
imageUrl('top8_10RewardBg'),
]) // top奖励背景
// 处理排行榜小于10人时的情况
const RankingHasTop10 = computed(() => {
let RankingShow = [...Ranking.value]
if (Ranking.value.length < 10) {
let addNullUser = Array.from({ length: 10 - Ranking.value.length }, () => ({
avatar: '',
nickname: '',
quantity: '',
}))
RankingShow.push(...addNullUser)
}
return RankingShow
})
// 展示用的榜单
const showRanking = computed(() => {
return Ranking.value.filter((_, index) => index >= 10)
// return []
})
// 获取对应的时间戳
const getNextMondayInBeijing = () => {
const now = new Date()
const targetDate = new Date(now.getTime()) // 创建一个基于当前时间的新日期对象
// 转换为北京时间来判断星期几
const utcTime = now.getTime() + now.getTimezoneOffset() * 60000
const beijingTime = new Date(utcTime + 8 * 3600000) // UTC+8
const beijingDay = beijingTime.getDay() // 0=周日, 1=周一, ..., 6=周六
const beijingHour = beijingTime.getHours()
// 转换为 0=周一, 1=周二, ..., 6=周日
const normalizedDay = beijingDay === 0 ? 6 : beijingDay - 1
let daysToAdd
if (normalizedDay === 0) {
// 今天是周一
if (beijingHour >= 5) {
daysToAdd = 7 // 超过5点计算到下周一的所需时间
} else {
daysToAdd = 0 // 今天
}
} else {
daysToAdd = 7 - normalizedDay // 到下周一的天数
}
// 正确地添加天数
targetDate.setDate(targetDate.getDate() + daysToAdd) // 添加天数到下周一的当前时间
targetDate.setHours(5, 0, 0, 0) // 将时间重新设置为早上5点作为目标时间戳
return targetDate.getTime()
}
const targetTime = ref(getNextMondayInBeijing()) // 闭包保存目标时间 (核心修正)
// 格式化时间
const formatTime = (value) => {
return value < 10 ? `0${value}` : value
}
// 倒计时
const getCountdown = () => {
const now = Date.now()
let diff = targetTime.value - now
if (diff <= 0) {
// 初始化数据
initData()
targetTime.value += 7 * 86400000 // +7天 (更新闭包变量)
diff = targetTime.value - now
}
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) => {
let showamount = ['GOLD', 'DIAMOND', 'GIFT', 'FRAGMENTS']
if (showamount.includes(type)) {
return reward.content
} else if (type === 'DOLLARS') {
return `$${reward.content}`
} else {
return `${reward.quantity}Days`
}
}
// 获取排行榜和我的排名
const getListAndMy = async () => {
let data = {
activityType: 10,
}
const resMyWeekStarRanking = await getRankingListAndMyRanking(data)
console.log('resMyWeekStarRanking:', resMyWeekStarRanking)
if (resMyWeekStarRanking.status && resMyWeekStarRanking.body) {
myRanking.value = resMyWeekStarRanking.body.currentUserRank
Ranking.value = resMyWeekStarRanking.body.rankingList
}
}
// 获取每周奖励列表
const getRewardsAndGifts = async () => {
const resRewardsAndGifts = await getThisWeekRewardsAndGifts('2001207026499137537')
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('helpInfo'),
imageUrl('myRankingBg'),
imageUrl('RankingBottomBorder'),
imageUrl('rankingBt'),
imageUrl('rankingBtActive'),
imageUrl('RankingCenterBg'),
imageUrl('RankingItem'),
imageUrl('RankingMain'),
imageUrl('rewardBtActive'),
imageUrl('rewardsBt'),
imageUrl('timeBg'),
imageUrl('top1RewardBg'),
imageUrl('top2_4RewardBg'),
imageUrl('top5_7RewardBg'),
imageUrl('top8_10RewardBg'),
imageUrl('top1'),
imageUrl('top2'),
imageUrl('top3'),
imageUrl('top4'),
imageUrl('top5_7'),
imageUrl('top8_10'),
]
const criticalImages_ar = [
imageUrl('bg'),
imageUrl('bg2'),
imageUrl('helpBt'),
imageUrl('helpInfo_ar'),
imageUrl('myRankingBg'),
imageUrl('RankingBottomBorder'),
imageUrl('rankingBt_ar'),
imageUrl('rankingBtActive_ar'),
imageUrl('RankingCenterBg'),
imageUrl('RankingItem'),
imageUrl('RankingMain_ar'),
imageUrl('rewardBtActive_ar'),
imageUrl('rewardsBt_ar'),
imageUrl('timeBg'),
imageUrl('top1RewardBg'),
imageUrl('top2_4RewardBg'),
imageUrl('top5_7RewardBg'),
imageUrl('top8_10RewardBg'),
imageUrl('top1'),
imageUrl('top2'),
imageUrl('top3'),
imageUrl('top4'),
imageUrl('top5_7'),
imageUrl('top8_10'),
]
if (currentLangType.value == 'en') {
await preloadImages(criticalImages)
} else if (currentLangType.value == 'ar') {
await preloadImages(criticalImages_ar)
}
}
// 完成预加载
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) //摧毁计时器
})
</script>
<style lang="scss" scoped>
* {
color: #fff;
font-family: 'SF Pro';
}
.fullPage {
position: relative;
background: #5d0c00;
}
.bg {
width: 100vw;
min-height: 100vh;
position: relative;
z-index: 1;
}
.timeText {
color: #fff;
font-weight: 590;
font-size: 1.5em;
}
.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: 10vw;
}
/* 添加加载动画样式 */
.loading-container {
width: 100vw;
height: 100vh;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
color: #666;
}
.loading-spinner {
width: 32px;
height: 32px;
border: 3px solid #f3f3f3;
border-top: 3px solid #f59e0b;
border-radius: 50%;
animation: spin 1s linear infinite;
margin-bottom: 12px;
}
.toggle-image {
transition: opacity 0.3s ease-in-out;
}
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
@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;
}
</style>