323 lines
7.7 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="position: relative; width: 100%; height: 32vw; overflow: hidden; pointer-events: none"
>
<!-- 双行弹幕轨道 -->
<div
v-for="(track, index) in visibleTracks"
:key="index"
style="position: absolute; width: 100%; height: 50px"
:style="{ top: `calc(${index} * 16vw)` }"
>
<div
v-for="item in track"
:key="item.id"
class="barrage-item"
style="
position: absolute;
border-radius: 26px;
padding: 8px;
display: flex;
align-items: center;
gap: 2vw;
backdrop-filter: blur(1px);
"
:style="getBarrageStyle(item)"
@animationend="handleAnimationEnd(item.id)"
>
<!-- 情侣头像 -->
<div
style="
position: relative;
display: flex;
align-items: center;
gap: 2vw;
"
>
<!-- 爱心图形 -->
<div
style="
position: absolute;
inset: 0;
display: flex;
justify-content: center;
align-items: center;
"
>
<img
:src="imageUrl('coupleSign')"
alt=""
style="width: 5vw; display: block; object-fit: cover"
/>
</div>
<!-- 发送人 -->
<img
v-smart-img
:src="item.senderAvatar || ''"
alt=""
style="
width: 8vw;
border-radius: 50%;
display: block;
aspect-ratio: 1/1;
object-fit: cover;
"
@error="handleAvatarImageError"
/>
<!-- 接收人 -->
<img
v-smart-img
:src="item.receiverAvatar || ''"
alt=""
style="
width: 8vw;
border-radius: 50%;
display: block;
aspect-ratio: 1/1;
object-fit: cover;
"
@error="handleAvatarImageError"
/>
</div>
<div
style="
white-space: nowrap;
font-weight: 700;
background-clip: text;
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-image: linear-gradient(
180deg,
#fff9a9 26.92%,
#ed7d0a 52.08%,
#fffab5 73.08%
);
"
>
{{ item.content }}
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed, onMounted, onUnmounted, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import { getPngUrl } from '@/config/imagePaths.js'
import { handleAvatarImageError } from '@/utils/imageHandler.js'
const { locale, t } = useI18n()
// 获取OSS图片URL的函数
const imageUrl = (filename) => getPngUrl('Ranking/Couple/', filename)
const props = defineProps({
barrageList: {
type: Array,
required: true,
default: () => [],
},
maxItems: {
type: Number,
default: 4, // 每行最大弹幕数
},
loopInterval: {
type: Number,
default: 2000, // 弹幕生成间隔(ms)
},
})
const tracks = ref([[], []]) // 弹幕轨道数据
const idCounter = ref(0) // 弹幕ID计数器
const currentIndex = ref(0) // 用于取下一项弹幕的索引
// 计算实际显示的轨道(过滤空轨道)
const visibleTracks = computed(() => tracks.value.filter((track) => track.length > 0))
// 获取弹幕样式
const getBarrageStyle = (item) => {
const isRTL = locale.value === 'ar'
return {
animationDuration: `${item.duration}s`,
animationDelay: `${item.delay}s`,
'--start-pos': `${item.startPos}px`,
borderRadius: '200px',
background: 'linear-gradient(180deg, #FFACBB 0%, rgba(255, 172, 187, 0.00) 58.38%), #FF4768',
boxShadow:
'0 1px 4.5px 0 rgba(0, 0, 0, 0.10), -1px 0 5px 0 rgba(255, 255, 255, 0.70) inset, -3.5px 0 7.5px 0 rgba(255, 255, 255, 0.20) inset, 1px 0 5px 0 rgba(255, 255, 255, 0.70) inset, 3.5px 0 7.5px 0 rgba(255, 255, 255, 0.20) inset',
// RTL布局支持
...(isRTL
? {
right: '0',
left: 'auto',
transform: 'translateX(-100vw)',
}
: {
left: '0',
right: 'auto',
transform: 'translateX(100vw)',
}),
}
}
// 生成随机弹幕属性
const generateBarrageProps = (item) => {
const textLength = item.content.length
return {
duration: Math.min(5 + textLength * 0.1, 10), // 弹幕项持续时间每字符增加0.2秒持续时间不低于8秒
startPos: 0, // 弹幕项起始位置
}
}
// 添加弹幕到轨道
const addToTrack = (item) => {
// 寻找最短的轨道的下标值
const trackIndex = tracks.value.reduce(
(minIndex, track, index) => (track.length < tracks.value[minIndex].length ? index : minIndex),
0,
)
if (tracks.value[trackIndex].length < props.maxItems) {
tracks.value[trackIndex].push({
...item,
id: idCounter.value++, //寻找完成动画的弹幕项唯一标识
delay: 0, // 随机延迟0-4秒
...generateBarrageProps(item),
})
}
}
// 清理完成动画的弹幕(根据动画结束状态遍历)
const cleanCompleted = () => {
tracks.value.forEach((track) => {
for (let i = track.length - 1; i >= 0; i--) {
if (track[i].completed) {
track.splice(i, 1)
}
}
})
}
// 设置弹幕动画结束状态
const handleAnimationEnd = (id) => {
tracks.value.forEach((track) => {
const item = track.find((item) => item.id === id)
if (item) item.completed = true
})
}
// 获取下一项弹幕(循环逻辑)
const getNextBarrage = () => {
if (props.barrageList.length === 0) return null
const item = props.barrageList[currentIndex.value] // 取得下一项弹幕项
currentIndex.value = (currentIndex.value + 1) % props.barrageList.length //索引达到最大时则重置为0
return { ...item } // 返回副本避免污染原始数据
}
// 弹幕循环逻辑
let barrageInterval = null
// 根据设置的时间间隔自动获取下一项弹幕
const startBarrage = () => {
barrageInterval = setInterval(() => {
cleanCompleted() // 清理完成动画的弹幕
const nextBarrage = getNextBarrage() // 获取下一项弹幕
if (nextBarrage) {
addToTrack(nextBarrage) // 添加弹幕到轨道
}
}, props.loopInterval)
}
onMounted(() => {
startBarrage() // 开启弹幕循环
})
onUnmounted(() => {
clearInterval(barrageInterval) // 清空弹幕
})
// 监听数据源变化
watch(
() => props.barrageList,
(newList) => {
currentIndex.value = 0 // 重置索引,确保重新开始循环
clearInterval(barrageInterval) // 清空弹幕
// 如果列表为空,停止生成新弹幕
if (newList.length !== 0) {
startBarrage() // 重启弹幕循环
}
},
{ deep: true },
)
</script>
<style scoped>
/* 样式保持不变(同之前版本) */
.barrage-item {
will-change: transform;
animation: moveLeft linear forwards;
}
@keyframes moveLeft {
0% {
transform: translateX(100vw);
}
100% {
transform: translateX(-100%);
}
}
/* RTL支持 - 镜像弹幕动画 */
[dir='rtl'] .barrage-item {
animation: moveRight linear forwards;
}
@keyframes moveRight {
0% {
transform: translateX(-100vw);
}
100% {
transform: translateX(100%);
}
}
@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;
}
}
</style>