593 lines
15 KiB
Vue
593 lines
15 KiB
Vue
<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>
|