593 lines
15 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.

<template>
<div style="width: 100%">
<itemCenter
:style="{ minHeight: ranking == '1' ? '58.51vw' : '39.07vw' }"
:imgUrl="BorderImgUrl"
:flip="true"
:contentStyle="`display:block;`"
>
<!-- 第一名 -->
<div v-if="isTopOne" style="width: 98vw; height: 80%; position: relative">
<!-- 查看个人信息按钮 -->
<div style="position: absolute; z-index: 2; inset: 0; display: flex">
<!-- 情侣1 -->
<div
style="flex: 1; min-width: 0; align-self: stretch"
@click="viewUserInfo(rankingItem.userId)"
></div>
<!-- 情侣1 -->
<div
style="flex: 1; min-width: 0; align-self: stretch"
@click="viewUserInfo(rankingItem.cpUserId)"
></div>
</div>
<!-- 头像 -->
<div
style="
position: absolute;
z-index: -1;
top: 12vw;
left: 0;
width: 100%;
height: 25vw;
display: flex;
justify-content: center;
align-items: center;
gap: 14vw;
"
>
<!-- 情侣1头像 -->
<img
:src="rankingItem.userAvatar"
alt=""
style="
display: block;
width: 25vw;
object-fit: cover;
aspect-ratio: 1/1;
border-radius: 50%;
"
@error="handleAvatarImageError"
@click="viewUserInfo(rankingItem.userId)"
/>
<!-- 情侣2头像 -->
<img
:src="rankingItem.cpUserAvatar"
alt=""
style="
display: block;
width: 25vw;
object-fit: cover;
aspect-ratio: 1/1;
border-radius: 50%;
"
@error="handleAvatarImageError"
@click="viewUserInfo(rankingItem.cpUserId)"
/>
</div>
<!-- 顶部距离 -->
<div style="height: 22vw"></div>
<!-- 贡献值 -->
<div style="height: 17vw; display: flex; justify-content: center; align-items: center">
<div style="color: #fff; font-weight: 700">
{{ rankingItem.total }}
</div>
</div>
<!-- 名称 -->
<div
style="
margin-top: -1vw;
height: 5vw;
display: flex;
justify-content: center;
align-items: center;
"
>
<div style="width: 76vw; display: flex; align-items: center; gap: 2vw">
<!-- 情侣1名称 -->
<div
style="
flex: 1;
min-width: 0;
display: flex;
justify-content: center;
align-items: center;
"
>
<div
style="
padding: 0 20%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-size: 0.9em;
font-weight: 600;
"
>
{{ rankingItem.userNickname || '' }}
</div>
</div>
<!-- 情侣2名称 -->
<div
style="
flex: 1;
min-width: 0;
display: flex;
justify-content: center;
align-items: center;
"
>
<div
style="
padding: 0 20%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-size: 0.9em;
font-weight: 600;
"
>
{{ rankingItem.cpUserNickname || '' }}
</div>
</div>
</div>
</div>
</div>
<!-- 第二三名 -->
<div v-else style="height: 65%; padding: 0 7%; display: flex; align-items: center">
<!-- 排名 -->
<itemCenter
style="width: 9vw"
class="rankingBg"
:imgUrl="imageUrl(`top${ranking}RankingBg`)"
:flip="true"
:contentStyle="``"
>
<img
v-smart-img
:src="imageUrl(`num${ranking}`)"
alt=""
style="width: 3vw; display: block"
/>
</itemCenter>
<!-- 头像 -->
<div style="width: 36vw; position: relative">
<!-- 查看个人信息按钮 -->
<div style="position: absolute; z-index: 2; inset: 0; display: flex">
<!-- 情侣1 -->
<div
style="flex: 1; min-width: 0; align-self: stretch"
@click="viewUserInfo(rankingItem.userId)"
></div>
<!-- 情侣1 -->
<div
style="flex: 1; min-width: 0; align-self: stretch"
@click="viewUserInfo(rankingItem.cpUserId)"
></div>
</div>
<!-- 内容 -->
<itemCenter
style="width: 100%; z-index: 1"
:imgUrl="imageUrl(`top${ranking}Frame`)"
:contentStyle="`z-index: -1;`"
>
<div
style="
width: 100%;
padding: 3vw 5vw;
height: 80%;
display: flex;
justify-content: space-between;
align-items: center;
"
>
<!-- 情侣1头像 -->
<img
:src="rankingItem.userAvatar"
alt=""
style="
display: block;
min-width: 0;
width: 10.5vw;
object-fit: cover;
aspect-ratio: 1/1;
border-radius: 50%;
"
@error="handleAvatarImageError"
/>
<!-- 情侣2头像 -->
<img
:src="rankingItem.cpUserAvatar"
alt=""
style="
display: block;
min-width: 0;
width: 10.5vw;
object-fit: cover;
aspect-ratio: 1/1;
border-radius: 50%;
"
@error="handleAvatarImageError"
/>
</div>
</itemCenter>
</div>
<!-- 名称 -->
<div
style="
flex: 1;
min-width: 0;
height: 100%;
display: flex;
flex-direction: column;
justify-content: center;
gap: 1vw;
"
>
<div style="width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap">
{{ rankingItem.userNickname || '' }}
</div>
<div style="width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap">
{{ rankingItem.cpUserNickname || '' }}
</div>
</div>
<!-- 贡献 -->
<div style="width: auto; min-width: 0; display: flex; align-items: center">
<img
v-smart-img
:src="imageUrl('distribution')"
alt=""
style="width: 10vw; display: block; object-fit: cover"
/>
<div style="font-weight: 700">{{ rankingItem.total || '0' }}</div>
</div>
</div>
<!-- 奖励 -->
<div class="reward-wrapper">
<div ref="rewardViewportRef" class="reward-scroll-viewport">
<!-- 滚动容器 -->
<div v-if="rewardList.length" class="scroll-wrapper">
<!-- 3套内容减少 iOS WebView 动画循环边界闪烁 -->
<div class="scroll-content" v-for="value in 3" :key="value">
<div
v-for="(item, index) in rewardList"
:key="index"
class="reward-scroll-item"
@click="clickReward(item)"
>
<!-- 预览图 -->
<img
v-if="item.cover != ''"
:src="item.cover"
alt=""
class="reward-scroll-cover"
@error="handleRewardImageError($event, item.type)"
/>
<div class="reward-scroll-text">
{{ formatRewardDisplay(item.type, item) }}
</div>
</div>
</div>
</div>
</div>
</div>
</itemCenter>
</div>
</template>
<script setup>
import { computed, nextTick, onUnmounted, ref, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { useLangStore } from '@/stores/lang'
import { useI18n } from 'vue-i18n'
import { getPngUrl } from '@/config/imagePaths.js'
import { setUserInfo } from '@/utils/userStore.js'
import { viewUserInfo } from '@/utils/appBridge.js'
import { connectToApp } from '@/utils/appConnector.js'
import { preloadImages } from '@/utils/image/imagePreloader.js'
import { formatUTCCustom } from '@/utils/utcFormat.js'
import { showError, showSuccess } from '@/utils/toast.js'
import { handleAvatarImageError, handleRewardImageError } from '@/utils/image/imageHandler.js'
import { useThrottle } from '@/utils/useDebounce.js'
import { useCoupleStore } from '@/stores/couple'
import { storeToRefs } from 'pinia'
import { formatRewardDisplay } from '@/utils/rewardFormatter.js'
import itemCenter from '@/components/itemCenter.vue'
const { t } = useI18n()
const router = useRouter()
const route = useRoute()
const coupleStore = useCoupleStore()
const langStore = useLangStore()
// 当前语言类型
const currentLangType = computed(() => {
return langStore.selectedLang?.type || 'en'
})
// 获取OSS图片URL的函数
const imageUrl = (filename) => getPngUrl('Ranking/Couple/', filename)
// 根据语言获取图片名
const getImgName = (filename) => {
return currentLangType.value === 'ar' ? `${filename}_ar` : filename
}
const {
topRewardShow, // 前三名奖励展示
topReward, // 前三名奖励项
} = storeToRefs(coupleStore)
const props = defineProps({
ranking: {
type: String,
default: '0',
},
rankingItem: {
type: Object,
default: () => ({}),
},
rewards: {
type: Object,
default: () => ({}),
},
})
const rewardViewportRef = ref(null)
const rewardList = computed(() => {
return Array.isArray(props.rewards?.activityRewardProps) ? props.rewards.activityRewardProps : []
})
const BorderImgUrl = computed(() => {
switch (props.ranking) {
case '1':
return imageUrl('top1')
case '2':
return imageUrl('top2-3')
case '3':
return imageUrl('top2-3')
default:
return imageUrl('top2-3')
}
})
// 是否为第一名
const isTopOne = computed(() => {
return props.ranking === '1'
})
// 点击奖品
const clickReward = (item) => {
topReward.value = item // 展示奖品赋值
topRewardShow.value = true // 显示奖品
}
let rewardRestartTimer = null
let rewardAnimationFrame = null
let rewardLastFrameTime = 0
let rewardLoopDistance = 0
let rewardScrollPosition = 0
const waitForLayoutFrame = () => {
return new Promise((resolve) => {
const requestFrame =
typeof window !== 'undefined' && window.requestAnimationFrame
? window.requestAnimationFrame
: (callback) => setTimeout(callback, 16)
requestFrame(() => {
requestFrame(resolve)
})
})
}
const restartRewardScroll = async () => {
if (rewardRestartTimer) {
clearTimeout(rewardRestartTimer)
}
if (rewardAnimationFrame) {
cancelAnimationFrame(rewardAnimationFrame)
rewardAnimationFrame = null
}
await nextTick()
rewardRestartTimer = setTimeout(async () => {
await waitForLayoutFrame()
const viewport = rewardViewportRef.value
if (!viewport || !rewardList.value.length) {
rewardRestartTimer = null
return
}
const scrollCopies = viewport.querySelectorAll('.scroll-content')
if (scrollCopies.length < 2) {
rewardRestartTimer = null
return
}
rewardLoopDistance =
Math.abs(scrollCopies[1].offsetLeft - scrollCopies[0].offsetLeft) ||
scrollCopies[0].scrollWidth
if (!rewardLoopDistance) {
rewardRestartTimer = null
return
}
rewardScrollPosition = 0
viewport.scrollLeft = 0
rewardLastFrameTime = performance.now()
const step = (now) => {
const elapsed = now - rewardLastFrameTime
rewardLastFrameTime = now
const distancePerMs = rewardLoopDistance / 8000
rewardScrollPosition += distancePerMs * elapsed
if (rewardScrollPosition >= rewardLoopDistance) {
rewardScrollPosition -= rewardLoopDistance
}
viewport.scrollLeft = rewardScrollPosition
rewardAnimationFrame = requestAnimationFrame(step)
}
rewardAnimationFrame = requestAnimationFrame(step)
rewardRestartTimer = null
}, 80)
}
watch(
() =>
rewardList.value
.map((item) => `${item.type || ''}-${item.cover || ''}-${item.content || ''}-${item.quantity || ''}`)
.join('|'),
() => {
restartRewardScroll()
},
{ flush: 'post', immediate: true },
)
onUnmounted(() => {
if (rewardRestartTimer) {
clearTimeout(rewardRestartTimer)
rewardRestartTimer = null
}
if (rewardAnimationFrame) {
cancelAnimationFrame(rewardAnimationFrame)
rewardAnimationFrame = null
}
})
</script>
<style lang="scss" scoped>
* {
color: #fff;
}
.showText {
color: #fff;
text-align: center;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.top1Text {
font-size: 0.75em;
}
.text {
font-size: 0.6em;
}
.reward-wrapper {
position: absolute;
bottom: 0;
left: 15vw;
right: 4vw;
height: 13.5vw;
}
[dir='rtl'] .reward-wrapper {
left: 4vw;
right: 15vw;
}
.reward-scroll-viewport {
height: 100%;
display: flex;
align-items: center;
overflow: hidden;
direction: ltr;
scroll-behavior: auto;
overflow-anchor: none;
}
.scroll-wrapper {
display: flex;
width: fit-content;
}
.scroll-content {
flex: 0 0 auto;
display: flex;
align-items: center;
}
.reward-scroll-item {
flex: 0 0 auto;
align-self: stretch;
display: flex;
align-items: center;
padding: 0 1vw;
}
.reward-scroll-cover {
flex: 0 0 auto;
display: block;
width: 10vw;
object-fit: cover;
}
.reward-scroll-text {
flex: 0 0 auto;
color: #fff;
white-space: nowrap;
}
[dir='ltr'] .scroll-content {
margin-left: 4vw;
}
[dir='rtl'] .scroll-content {
direction: rtl;
margin-right: 4vw;
}
@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>