ufutx-pc-website/src/components/Marquee.vue
2025-07-11 14:51:12 +08:00

142 lines
3.8 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 ref="containerRef" :class="['marquee-container relative overflow-hidden', containerClass]">
<div
ref="contentRef"
class="marquee-content flex gap-[50px] whitespace-nowrap"
:style="{
'--marquee-gap': '50px',
'--initial-offset': initialOffset + 'px' // 传递初始偏移变量
}"
>
<div ref="originalRef" class="original-content flex gap-[50px]">
<slot />
</div>
<div class="clone-content flex gap-[50px]">
<slot />
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { ref, onMounted, watchEffect } from 'vue'
// 接收初始偏移量props
const props = withDefaults(
defineProps<{
duration: number
reverse?: boolean
repeatCount?: number | string
pauseOnHover?: boolean
containerClass?: string
initialOffset?: number // 新增初始偏移量px负数向左偏
}>(),
{
reverse: false,
repeatCount: 3,
pauseOnHover: true,
initialOffset: 0, // 默认无偏移
containerClass: '' // 添加默认值为空字符串
}
)
const containerRef = ref<HTMLDivElement | null>(null)
const contentRef = ref<HTMLDivElement | null>(null)
const originalRef = ref<HTMLDivElement | null>(null)
const originalWidth = ref(0)
// 计算单份内容宽度
const calculateWidth = () => {
if (originalRef.value) {
originalWidth.value = originalRef.value.offsetWidth
}
}
// 动态生成动画(包含初始偏移)
const updateAnimation = () => {
if (!contentRef.value || !originalRef.value) return
// 动画方向
const direction = props.reverse ? 'reverse' : ''
contentRef.value.style.animation = `marquee ${props.duration}s linear infinite ${direction}`
// 关键帧:从初始偏移位置开始滚动
const style = document.createElement('style')
style.id = 'marquee-keyframes'
style.textContent = `
@keyframes marquee {
0% { transform: translateX(var(--initial-offset)); }
/* 滚动距离 = 原始内容宽度,确保克隆内容衔接 */
100% { transform: translateX(calc(var(--initial-offset) - ${originalWidth.value}px)); }
}
`
// 替换旧关键帧
const existingStyle = document.head.querySelector('#marquee-keyframes')
if (existingStyle) document.head.removeChild(existingStyle)
document.head.appendChild(style)
}
// 图片加载完成后重新计算宽度
const watchImagesLoad = () => {
if (!originalRef.value) return
// 获取插槽中的所有图片
const images = originalRef.value.querySelectorAll('img')
if (images.length === 0) return
// 监听所有图片加载完成
let loadedCount = 0
images.forEach(img => {
// 图片已加载完成
if (img.complete) {
loadedCount++
} else {
// 图片加载完成后触发
img.addEventListener('load', () => {
loadedCount++
if (loadedCount === images.length) {
nextTick(calculateWidth) // 所有图片加载后重新计算宽度
}
})
}
})
}
// 初始化与监听
onMounted(() => {
nextTick(() => {
// 确保DOM渲染完成
calculateWidth()
watchImagesLoad() // 监听图片加载
updateAnimation()
})
})
watchEffect(() => {
calculateWidth()
updateAnimation()
})
// 悬停暂停逻辑
watchEffect(() => {
if (containerRef.value && props.pauseOnHover) {
containerRef.value.addEventListener('mouseenter', () => {
if (contentRef.value) contentRef.value.style.animationPlayState = 'paused'
})
containerRef.value.addEventListener('mouseleave', () => {
if (contentRef.value) contentRef.value.style.animationPlayState = 'running'
})
}
})
</script>
<style scoped lang="less">
.marquee-content {
display: flex;
flex-wrap: nowrap;
transition: transform 0.3s ease;
}
.original-content,
.clone-content {
display: flex;
flex-wrap: nowrap;
}
</style>