894 lines
27 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/RevolutionUnityDay/index.vue -->
<template>
<div class="fullPage">
<div class="bg">
<!-- 状态栏占位区域仅在APP中显示 -->
<div v-if="isInAppEnvironment" style="height: 30px"></div>
<!-- 按钮 -->
<div
style="
margin-top: 100vw;
display: flex;
justify-content: space-between;
align-items: center;
"
>
<!-- 历史榜首按钮 -->
<img
:src="images.history"
alt=""
style="display: block; width: 20%"
@click="historyShow = true"
/>
<!-- 帮助按钮 -->
<img
:src="images.help"
alt=""
style="display: block; width: 10%; margin-right: 8px"
@click="helpInfoShow = true"
/>
</div>
<!-- 倒计时 -->
<div style="display: flex; justify-content: center; margin-top: 1vw">
<div style="width: 15%">
<itemCenter :imgUrl="images.timeDayBg">
<div class="timeText" style="margin-bottom: 20%">{{ Days }}D</div>
</itemCenter>
</div>
<div class="timeText timeGap" style="width: 6px"></div>
<div style="width: 15%">
<itemCenter :imgUrl="images.timeHourBg">
<div class="timeText" style="margin-bottom: 20%">{{ Hours }}</div>
</itemCenter>
</div>
<div class="timeText timeGap">:</div>
<div style="width: 15%">
<itemCenter :imgUrl="images.timeMinBg">
<div class="timeText" style="margin-bottom: 20%">{{ Minutes }}</div>
</itemCenter>
</div>
<div class="timeText timeGap">:</div>
<div style="width: 15%">
<itemCenter :imgUrl="images.timeSecBg">
<div class="timeText" style="margin-bottom: 20%">{{ Second }}</div>
</itemCenter>
</div>
</div>
<!-- 每周礼物 -->
<div style="display: flex; justify-content: center; margin-top: 10px">
<div style="width: 100%; position: relative">
<img :src="images.giftsBg" alt="" style="width: 100%" />
<div
style="
position: absolute;
left: 0;
right: 0;
top: 0;
bottom: 0;
display: flex;
justify-content: center;
align-items: center;
"
>
<div style="width: 80%; display: flex; justify-content: space-around; margin-top: 10%">
<div
v-for="(gift, index) in gifts"
:key="index"
style="width: 30%; display: flex; flex-direction: column"
>
<div style="position: relative">
<img style="" :src="images.giftItemBg" alt="" width="100%" />
<div
style="
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
display: flex;
justify-content: center;
align-items: center;
"
>
<img :src="gift.giftPhoto" alt="" width="50%" />
</div>
</div>
<div style="display: flex; justify-content: center; align-items: center; gap: 5px">
<img
src="/src/assets/icon/coin.png"
alt=""
style="width: 18%; aspect-ratio: 1/1"
/>
<div style="color: rgba(255, 255, 255, 1); font-weight: 590">
{{ gift.giftCandy }}
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 切换按钮 -->
<div style="display: flex; justify-content: space-around">
<img
:src="rankingShow ? images.rankingBtActive : images.rankingBt"
alt=""
style="display: block; width: 40%"
@click="rankingShow = true"
/>
<img
:src="!rankingShow ? images.rewardBtActive : images.rewardsBt"
alt=""
style="display: block; width: 40%"
@click="rankingShow = false"
/>
</div>
<!-- 展示内容 -->
<div>
<!-- Ranking -->
<div
v-show="rankingShow"
style="position: relative; width: 90%; left: 50%; transform: translateX(-50%)"
>
<div
style="
position: absolute;
inset: 2;
background: radial-gradient(50% 50% at 50% 50%, #10614b 0%, #053126 100%);
z-index: 1;
"
></div>
<div style="position: relative; z-index: 1">
<!-- 前三名 -->
<div style="position: relative">
<!-- 头像框 -->
<img :src="images.RankingMain" alt="" width="100%" style="display: block" />
<!-- 用户信息 -->
<div style="position: absolute; inset: 0; padding: 18% 5% 2%">
<div style="margin-top: 3%">
<!-- 第一 -->
<div style="display: flex; justify-content: center">
<TopUser
:BorderImgUrl="images.topOne"
:avatarUrl="RankingHasTop3[0].userAvatar"
:name="RankingHasTop3[0].userNickname"
:distributionValue="RankingHasTop3[0].quantity"
style="width: 43%"
@click="viewUserInfo(RankingHasTop3[0].userId)"
/>
</div>
<!-- 第二三 -->
<div style="display: flex; justify-content: space-between; margin-top: -20%">
<TopUser
:isTopOne="false"
:BorderImgUrl="images.topTwo"
:avatarUrl="RankingHasTop3[1].userAvatar"
:name="RankingHasTop3[1].userNickname"
:distributionValue="RankingHasTop3[1].quantity"
style="width: 30%"
@click="viewUserInfo(RankingHasTop3[1].userId)"
/>
<TopUser
:isTopOne="false"
:BorderImgUrl="images.topThree"
:avatarUrl="RankingHasTop3[2].userAvatar"
:name="RankingHasTop3[2].userNickname"
:distributionValue="RankingHasTop3[2].quantity"
style="width: 30%"
@click="viewUserInfo(RankingHasTop3[2].userId)"
/>
</div>
</div>
</div>
</div>
<!-- 第四名开始 -->
<div
v-for="listItem in showRanking"
style="position: relative"
@click="viewUserInfo(listItem.userId)"
>
<img :src="images.RankingItem" alt="" width="100%" style="display: block" />
<div
style="
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
display: flex;
align-items: center;
padding: 0 8%;
"
>
<div style="width: 100%; display: flex; align-items: center">
<div style="color: #edb247; font-weight: 700; width: 10%" class="UserNickname">
{{ listItem.rank }}
</div>
<img
:src="listItem.userAvatar || ''"
alt=""
style="
width: 20%;
margin: 0 2%;
aspect-ratio: 1/1;
border-radius: 50%;
object-fit: cover;
"
@error="defaultAvatarUrl"
/>
<div
style="
width: calc(100% - 10% - 28% - 16px);
display: flex;
flex-direction: column;
"
>
<div
style="
color: #fff;
font-weight: 860;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
width: 100%;
"
class="UserNickname"
>
{{ listItem.userNickname }}
</div>
<div style="color: #fff; font-weight: 590" class="UserNickname">
ID:{{ listItem.account }}
</div>
</div>
</div>
<div style="display: flex; width: 30%; display: flex; align-items: center">
<img src="/src/assets/icon/coin.png" alt="" style="width: 30%" />
<div style="color: #edb247; font-weight: 700" class="UserNickname">
{{ listItem.quantity }}
</div>
</div>
</div>
</div>
<!-- 触发加载功能 -->
<div ref="RankingLoadmore" v-show="showRankingLoading && rankingShow"></div>
<!-- 底框 -->
<img
:src="images.RankingBottomBorder"
alt=""
width="100%"
style="display: block; margin-top: -20%"
/>
</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: 90%; margin-bottom: 20px"
>
<div style="position: relative">
<img :src="topImg[Number(top)]" alt="" style="width: 100%; margin: 24px 0" />
<div
style="
position: absolute;
inset: 0;
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 8px;
padding: 25vw 5vw 15vw;
"
>
<div
v-for="(reward, rewardIndex) in rewardsTop.propsGroup.activityRewardProps"
:key="rewardIndex"
style="width: 100%"
>
<div style="position: relative">
<img :src="images.giftItemBg" style="" alt="" width="100%" />
<div
style="
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
display: flex;
justify-content: center;
align-items: center;
"
>
<img
:src="reward.cover || ''"
alt=""
width="50%"
style="aspect-ratio: 1/1"
@error="(e) => handleImageError(e, reward.type)"
/>
</div>
</div>
<div style="color: rgba(255, 255, 255, 1); font-weight: 590; text-align: center">
{{ showDetail(reward.type, reward.quantity) }}
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 我的排名 -->
<div
style="
position: sticky;
bottom: 0;
left: 0;
width: 100vw;
background-color: #053126;
display: flex;
justify-content: center;
margin-top: 10px;
z-index: 999;
"
>
<img :src="images.myRankingBg" alt="" style="width: calc(100% - 1px); display: block" />
<div
style="
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
display: flex;
align-items: center;
justify-content: center;
padding: 2% 5% 0;
"
>
<div
style="width: 70%; display: flex; justify-content: space-around; align-items: center"
>
<!-- 排名 -->
<div style="color: #edb247; font-weight: 700">{{ myRanking.rank || 999 }}</div>
<!-- 头像 -->
<img
:src="myRanking.userAvatar || ''"
alt=""
style="
display: block;
width: 25%;
aspect-ratio: 1/1;
border-radius: 50%;
object-fit: cover;
margin: 0 12px;
"
@error="defaultAvatarUrl"
/>
<!-- 名称 -->
<div style="display: flex; flex-direction: column; width: calc(75% - 24px - 20%)">
<div
style="
color: #fff;
font-weight: 860;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
"
class="UserNickname"
>
{{ myRanking.userNickname }}
</div>
</div>
</div>
<!-- 贡献值 -->
<div style="width: 30%; display: flex; justify-content: center; align-items: center">
<img src="/src/assets/icon/coin.png" alt="" style="width: 20%; height: 20%" />
<div style="color: #edb247; font-weight: 700">
{{ myRanking.quantity || 0 }}
</div>
</div>
</div>
</div>
<!-- 弹窗遮罩层 -->
<maskLayer :maskLayerShow="maskLayerShow" @click="closedPopup">
<!-- help弹窗 -->
<div v-if="helpInfoShow" style="margin: 20% 0" @click.stop>
<img :src="images.helpInfo" alt="" style="width: 100%" />
</div>
<!-- history弹窗 -->
<div v-if="historyShow" style="position: relative; margin: 20% 0" @click.stop>
<!-- 背景图 -->
<img :src="images.historyBg" alt="" style="width: 100%; display: block" />
<!-- 内容 -->
<div
style="
position: absolute;
inset: 0;
padding: 17% 5% 10%;
display: flex;
flex-direction: column;
justify-content: space-around;
align-items: center;
"
>
<div style="position: relative; width: 75%">
<!-- 头像框 -->
<img
:src="images.topOneHistory"
alt=""
style="width: 100%; display: block; position: relative; z-index: 1"
/>
<div style="position: absolute; inset: 0">
<div
style="height: 100%; display: flex; justify-content: center; align-items: center"
>
<img
:src="showTopOne.userAvatar || ''"
alt=""
style="
width: 50%;
display: block;
border-radius: 50%;
aspect-ratio: 1/1;
object-fit: cover;
"
@error="defaultAvatarUrl"
/>
</div>
<!-- 名字 -->
<div
style="
width: 100%;
position: absolute;
left: 0;
right: 0;
bottom: 10vw;
z-index: 2;
display: flex;
justify-content: center;
"
>
<div
style="
width: 25%;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
text-align: center;
"
class="history-name"
>
{{ showTopOne.userNickname }}
</div>
</div>
</div>
</div>
<!-- 历史时间 -->
<div
style="width: 50%; display: flex; justify-content: space-between; align-items: center"
>
<img :src="images.backGift" alt="" width="13%" @click="changeHistoryIndex(1)" />
<div
style="
flex: 1;
text-align: center;
font-weight: 590;
background: linear-gradient(180deg, #00f331 0%, #c6ffc6 100%);
background-clip: text;
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
"
class="history-time"
>
{{ showTopOne.group }}
</div>
<img :src="images.nextGift" alt="" width="13%" @click="changeHistoryIndex(-1)" />
</div>
</div>
</div>
</maskLayer>
</div>
</div>
</template>
<script setup>
import { isInApp, viewUserInfo } from '@/utils/appBridge.js'
import { computed, onMounted, onUnmounted, ref } from 'vue'
import itemCenter from '@/components/WeeklyStar/itemCenter.vue'
import TopUser from '../RevolutionUnityDay/topUser.vue'
import maskLayer from '@/components/MaskLayer.vue'
import { useDebounce, useThrottle } from '@/utils/useDebounce'
import { getMemberProfile } from '@/api/wallet'
import {
getWeekStarRanking,
getWeekRewards,
getWeekGifts,
getMyWeekStarRanking,
getHistoryTopOne,
} from '@/api/weeklyStar.js'
import { connectToApp } from '@/utils/appConnector.js'
// vite动态批量导入图片
const imageModules = import.meta.glob('@/assets/images/Activities/revolution/*.{png,jpg,svg}', {
eager: true,
})
// 生成文件名映射对象(自动移除路径和扩展名)
const images = Object.fromEntries(
Object.entries(imageModules).map(([path, module]) => {
const fileName = path.split('/').pop().split('.')[0]
return [fileName, module.default]
})
)
const userInfo = ref({})
const isInAppEnvironment = ref(false) // 检测是否在APP环境中
const helpInfoShow = ref(false) //帮助模块
const historyShow = ref(false) //历史模块
// 展示遮罩层
const maskLayerShow = computed(() => {
return helpInfoShow.value || historyShow.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 current = ref(1) //排行榜第几页
const RankingLoadmore = ref(null) //触底模块
const showRankingLoading = ref(true) //触底加载功能
const Ranking = ref([]) // 排行榜
const RankingLoadParams = computed(() => {
return {
type: 'THIS_WEEK',
current: current.value,
size: 10,
}
})
const myRanking = ref([]) // 我的排名
const rewardsList = ref([]) // 奖励列表
const gifts = ref([]) // 本周礼物
const historyRankingTop1 = ref([]) // 历史榜首
const historyIndex = ref(0)
const showTopOne = computed(() => {
return historyRankingTop1.value[historyIndex.value] || {}
})
const topImg = ref([images.top1RewardBg, images.top2RewardBg, images.top3RewardBg]) // top图片
const changeHistoryIndex = (num) => {
let targetIndex = historyIndex.value + num
let length = historyRankingTop1.value.length
if (targetIndex >= 0 && targetIndex < length) {
historyIndex.value = targetIndex
}
}
// 处理排行榜小于3人时的情况
const RankingHasTop3 = computed(() => {
let RankingShow = [...Ranking.value]
if (Ranking.value.length < 3) {
let addNullUser = Array.from({ length: 3 - Ranking.value.length }, () => ({
userAvatar: '',
userNickname: '',
quantity: '',
}))
RankingShow.push(...addNullUser)
}
return RankingShow
})
// 展示用的榜单
const showRanking = computed(() => {
return Ranking.value.filter((_, index) => index >= 3)
})
// 奖励图片资源出错处理
const handleImageError = (e, type) => {
console.log('图片资源出错')
e.target.onerror = null //防止循环
if ((type = 'GOLD')) {
e.target.src = new URL('/src/assets/icon/coin.png', import.meta.url).href
}
}
// 头像资源出错处理
const defaultAvatarUrl = (e) => {
console.log('头像资源出错')
e.target.onerror = null //防止循环
e.target.src = new URL('/src/assets/images/WeeklyStar/defaultAvatar.png', import.meta.url).href
}
// 关闭弹窗
const closedPopup = () => {
helpInfoShow.value = false
historyShow.value = false
}
// 获取沙特下周一早上六点的时间戳
const getNextMondayInSaudi = () => {
const now = new Date()
const utcDay = now.getUTCDay() //获取UTC星期几0(周日)-6(周六)
// 计算到下周一的所需天数
const daysToAdd = utcDay === 1 ? 7 : (1 + 7 - utcDay) % 7
const nextMondayUTC = new Date(now) //以当前UTC时间为基准而不是重新获取
nextMondayUTC.setUTCDate(now.getUTCDate() + daysToAdd) //增加到下周一那天
nextMondayUTC.setUTCHours(3, 0, 0, 0) // 设置为沙特下周一当天早上6点北京时间3点
return nextMondayUTC.getTime() //得到下周一00:00:00.000的时间戳
}
const targetTime = ref(getNextMondayInSaudi()) // 闭包保存目标时间 (核心修正)
// 格式化时间
const formatTime = (value) => {
return value < 10 ? `0${value}` : value
}
// 倒计时
const getCountdown = () => {
const now = Date.now()
let diff = targetTime.value - now
if (diff <= 0) {
// 初始化数据
getUserInfo()
resetPage()
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 resetPage = () => {
current.value = 1 //排行榜第几页
showRankingLoading.value = true //触底加载功能
Ranking.value = [] // 排行榜
historyIndex.value = 0 //历史榜首重置展示当前
initData() // 刷新页面数据
}
// 格式化奖励的展示信息
const showDetail = (type, quantity) => {
let showamount = ['GOLD', 'DIAMOND', 'GIFT', 'FRAGMENTS']
if (showamount.includes(type)) {
return quantity
} else {
return `${quantity}Days`
}
}
//获取用户信息
const getUserInfo = async () => {
if (Object.keys(userInfo.value).length === 0) {
const resUserInfo = await getMemberProfile()
if (resUserInfo.status && resUserInfo.body) {
userInfo.value = resUserInfo.body
}
}
getMyThisWeekRanking() // 获取本周我的排名
}
// 获取本周排名
const getThisWeekRanking = async () => {
const resWeekStarRanking = await getWeekStarRanking(RankingLoadParams.value)
console.log('resWeekStarRanking:', resWeekStarRanking)
if (
resWeekStarRanking.status &&
resWeekStarRanking.body &&
resWeekStarRanking.body.rankUsers?.records
) {
let rankUsers = resWeekStarRanking.body.rankUsers
Ranking.value.push(...rankUsers.records)
if (current.value * rankUsers.size >= rankUsers.total) {
showRankingLoading.value = false
}
}
}
// 获取历史榜首
const getHistoryRankingTop1 = async () => {
const resHistoryTopOne = await getHistoryTopOne()
console.log('resHistoryTopOne:', resHistoryTopOne)
if (resHistoryTopOne.status && resHistoryTopOne.body) {
historyRankingTop1.value = resHistoryTopOne.body
}
}
// 获取本周我的排名
const getMyThisWeekRanking = async () => {
let data = {
userId: userInfo.value.id,
type: 'THIS_WEEK',
}
const resMyWeekStarRanking = await getMyWeekStarRanking(data)
console.log('resMyWeekStarRanking:', resMyWeekStarRanking)
if (resMyWeekStarRanking.status && resMyWeekStarRanking.body) {
myRanking.value = resMyWeekStarRanking.body
}
}
// 获取本周礼物
const getThisWeekGifts = async () => {
const resThisWeekGifts = await getWeekGifts('THIS_WEEK')
console.log('resThisWeekGifts:', resThisWeekGifts)
if (resThisWeekGifts.status && resThisWeekGifts.body) {
gifts.value = resThisWeekGifts.body
}
}
// 获取本周奖励
const getThisWeekRewards = async () => {
const resThisWeekRewards = await getWeekRewards()
console.log('resThisWeekRewards:', resThisWeekRewards)
if (resThisWeekRewards.status && resThisWeekRewards.body) {
rewardsList.value = resThisWeekRewards.body
}
}
// 刷新页面数据
const initData = () => {
getUserInfo()
getThisWeekRanking() // 获取本周排名
getHistoryRankingTop1() // 获取历史top1
getThisWeekRewards() // 获取本周奖励
getThisWeekGifts() //获取礼物
}
const debouceGetThisWeekRanking = useThrottle(() => {
getThisWeekRanking()
}, 1000)
// IntersectionObserver配置
const observer = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
if (entry.intersectionRatio > 0 && Ranking.value.length != 0) {
console.log('触发RankingLoadmore加载')
current.value++
debouceGetThisWeekRanking()
}
})
})
// 使用工具函数连接APP
const connectToAppHandler = async () => {
await connectToApp(() => {
// 连接成功回调
initData() //初始化数据
})
}
// 组件挂载时检测环境
onMounted(() => {
connectToAppHandler()
isInAppEnvironment.value = isInApp()
getCountdown()
timer = setInterval(getCountdown, 1000) //每秒更新
// 监听加载模块
if (RankingLoadmore.value) {
observer.observe(RankingLoadmore.value)
}
})
onUnmounted(() => {
clearInterval(timer) //摧毁计时器
})
</script>
<style lang="scss" scoped>
* {
color: #fff;
font-family: 'SF Pro';
}
.fullPage {
background: linear-gradient(0deg, #053126 0%, #053126 100%);
position: relative;
}
.bg {
width: 100vw;
min-height: 100vh;
background-image: url(../../../assets/images/Activities/revolution/bg.png);
background-size: 100% auto;
background-repeat: no-repeat;
}
.timeText {
color: #fff;
font-weight: 860;
}
.timeGap {
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;
}
@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>