126 lines
2.9 KiB
Vue
126 lines
2.9 KiB
Vue
|
<template>
|
||
|
<div class="auto-scroll-container" @mouseenter="pauseScroll" @mouseleave="resumeScroll" ref="container">
|
||
|
<div class="auto-scroll-content" ref="content">
|
||
|
<div class="auto-scroll-item" v-for="(item, index) in duplicatedList" :key="index">
|
||
|
<slot :item="item">{{ item }}</slot>
|
||
|
</div>
|
||
|
</div>
|
||
|
</div>
|
||
|
</template>
|
||
|
|
||
|
<script setup>
|
||
|
import { ref, computed, onMounted, onUnmounted, watch } from 'vue';
|
||
|
|
||
|
// Props
|
||
|
const props = defineProps({
|
||
|
items: {
|
||
|
type: Array,
|
||
|
required: true
|
||
|
},
|
||
|
speed: {
|
||
|
type: Number,
|
||
|
default: 0.5 // px/frame
|
||
|
},
|
||
|
height: {
|
||
|
type: Number,
|
||
|
default: 100 // px
|
||
|
},
|
||
|
minItems: {
|
||
|
type: Number,
|
||
|
default: 2 // 小于这个数量不滚动
|
||
|
},
|
||
|
autoScroll: {
|
||
|
type: Boolean,
|
||
|
default: true // 控制是否自动滚动
|
||
|
}
|
||
|
});
|
||
|
|
||
|
// Refs and Computed
|
||
|
const container = ref(null);
|
||
|
const content = ref(null);
|
||
|
const duplicatedList = computed(() => [...props.items, ...props.items]);
|
||
|
const shouldScroll = computed(() => props.items.length >= props.minItems);
|
||
|
|
||
|
let scrollY = 0;
|
||
|
let animationFrameId = null;
|
||
|
let manualPaused = false; // 记录是否因为滚轮手动停止
|
||
|
|
||
|
// 滚动核心逻辑
|
||
|
function normalizeScrollY(contentHeight) {
|
||
|
if (scrollY <= -contentHeight) scrollY += contentHeight;
|
||
|
if (scrollY >= 0) scrollY -= contentHeight;
|
||
|
}
|
||
|
|
||
|
function step() {
|
||
|
const contentHeight = content.value.offsetHeight / 2;
|
||
|
scrollY -= props.speed;
|
||
|
normalizeScrollY(contentHeight);
|
||
|
content.value.style.transform = `translateY(${Math.round(scrollY)}px)`;
|
||
|
|
||
|
animationFrameId = requestAnimationFrame(step);
|
||
|
}
|
||
|
|
||
|
function startScroll() {
|
||
|
if (!animationFrameId) {
|
||
|
animationFrameId = requestAnimationFrame(step);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
function pauseScroll() {
|
||
|
cancelAnimationFrame(animationFrameId);
|
||
|
animationFrameId = null;
|
||
|
}
|
||
|
|
||
|
// 鼠标滚轮事件:手动滚动并停止自动滚动
|
||
|
function onWheel(e) {
|
||
|
pauseScroll();
|
||
|
manualPaused = true; // 标记为手动停止
|
||
|
const contentHeight = content.value.offsetHeight / 2;
|
||
|
scrollY -= e.deltaY;
|
||
|
normalizeScrollY(contentHeight);
|
||
|
content.value.style.transform = `translateY(${Math.round(scrollY)}px)`;
|
||
|
}
|
||
|
|
||
|
// 鼠标移出时恢复自动滚动(如果不是手动暂停)
|
||
|
function resumeScroll() {
|
||
|
if (props.autoScroll && shouldScroll.value) {
|
||
|
manualPaused = false; // 重置手动暂停标志
|
||
|
startScroll();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// 生命周期
|
||
|
onMounted(() => {
|
||
|
if (shouldScroll.value && props.autoScroll) {
|
||
|
startScroll();
|
||
|
}
|
||
|
container.value.addEventListener('wheel', onWheel);
|
||
|
});
|
||
|
|
||
|
onUnmounted(() => {
|
||
|
pauseScroll();
|
||
|
container.value.removeEventListener('wheel', onWheel);
|
||
|
});
|
||
|
|
||
|
// 响应 items 数量变化
|
||
|
watch(shouldScroll, (newVal) => {
|
||
|
if (!newVal) {
|
||
|
pauseScroll();
|
||
|
} else if (props.autoScroll && !manualPaused) {
|
||
|
startScroll();
|
||
|
}
|
||
|
});
|
||
|
</script>
|
||
|
|
||
|
<style scoped>
|
||
|
.auto-scroll-container {
|
||
|
overflow: hidden;
|
||
|
position: relative;
|
||
|
}
|
||
|
.auto-scroll-content {
|
||
|
display: flex;
|
||
|
flex-direction: column;
|
||
|
will-change: transform;
|
||
|
}
|
||
|
</style>
|