152 lines
3.7 KiB
Vue
152 lines
3.7 KiB
Vue
<template>
|
|
<div class="auto-scroll-container" @mouseenter="onMouseEnter" @mouseleave="onMouseLeave" ref="container">
|
|
<div class="auto-scroll-content" ref="content" :style="{ transform: scrollEnabled ? `translate3d(0, ${Math.round(scrollY)}px, 0)` : 'none' }">
|
|
<div class="auto-scroll-item" :class="safety ? 'safety' : ''" v-for="(item, index) in displayList" :key="index">
|
|
<slot :item="item" :index="index">{{ item }}</slot>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script lang="ts" setup>
|
|
interface Props {
|
|
items: any[];
|
|
speed?: number;
|
|
height?: number;
|
|
minItems?: number;
|
|
autoScroll?: boolean;
|
|
safety?: boolean;
|
|
}
|
|
|
|
const props = withDefaults(defineProps<Props>(), {
|
|
speed: 0.4,
|
|
minItems: 2,
|
|
autoScroll: true
|
|
});
|
|
|
|
const container = ref<HTMLElement | null>(null);
|
|
const content = ref<HTMLElement | null>(null);
|
|
|
|
let scrollY = 0;
|
|
let animationFrameId: number | null = null;
|
|
let manualPaused = false;
|
|
let manualControl = false;
|
|
|
|
// 是否满足滚动条件
|
|
const scrollEnabled = computed(() => props.items.length >= props.minItems);
|
|
|
|
// 展示数据列表(数据足够时复制一份)
|
|
const displayList = computed(() => (scrollEnabled.value ? [...props.items, ...props.items] : props.items));
|
|
|
|
// 修正 scrollY 范围
|
|
function normalizeScrollY(contentHeight: number) {
|
|
scrollY = (((scrollY % contentHeight) + contentHeight) % contentHeight) - contentHeight;
|
|
}
|
|
|
|
// 滚动逻辑
|
|
function step() {
|
|
if (!content.value) return;
|
|
const contentHeight = content.value.offsetHeight / 2;
|
|
scrollY -= props.speed;
|
|
normalizeScrollY(contentHeight);
|
|
content.value.style.transform = `translate3d(0, ${Math.round(scrollY)}px, 0)`;
|
|
animationFrameId = requestAnimationFrame(step);
|
|
}
|
|
|
|
function startScroll() {
|
|
if (!animationFrameId) {
|
|
animationFrameId = requestAnimationFrame(step);
|
|
}
|
|
}
|
|
|
|
function pauseScroll() {
|
|
if (animationFrameId) {
|
|
cancelAnimationFrame(animationFrameId);
|
|
animationFrameId = null;
|
|
}
|
|
}
|
|
|
|
function onMouseEnter() {
|
|
if (!manualControl) pauseScroll();
|
|
}
|
|
|
|
function onMouseLeave() {
|
|
if (!manualControl && props.autoScroll) startScroll();
|
|
}
|
|
|
|
function onWheel(e: WheelEvent) {
|
|
if (!content.value || !container.value) return;
|
|
|
|
const contentHeight = content.value.offsetHeight / (scrollEnabled.value ? 2 : 1);
|
|
const containerHeight = container.value.offsetHeight;
|
|
|
|
if (contentHeight <= containerHeight) {
|
|
e.preventDefault();
|
|
return;
|
|
}
|
|
|
|
manualPaused = true;
|
|
pauseScroll();
|
|
scrollY -= e.deltaY;
|
|
normalizeScrollY(contentHeight);
|
|
content.value.style.transform = `translate3d(0, ${Math.round(scrollY)}px, 0)`;
|
|
}
|
|
|
|
// 生命周期
|
|
onMounted(() => {
|
|
if (scrollEnabled.value && props.autoScroll) {
|
|
startScroll();
|
|
}
|
|
container.value?.addEventListener('wheel', onWheel, { passive: false });
|
|
});
|
|
|
|
onUnmounted(() => {
|
|
pauseScroll();
|
|
container.value?.removeEventListener('wheel', onWheel);
|
|
});
|
|
|
|
// 监听数据是否滚动条件满足
|
|
watch(scrollEnabled, (newVal) => {
|
|
if (!newVal) {
|
|
pauseScroll();
|
|
scrollY = 0;
|
|
if (content.value) content.value.style.transform = 'none';
|
|
} else if (props.autoScroll && !manualPaused && !manualControl) {
|
|
startScroll();
|
|
}
|
|
});
|
|
|
|
// 暴露控制方法
|
|
defineExpose({
|
|
pause: (): void => {
|
|
manualControl = true;
|
|
pauseScroll();
|
|
},
|
|
resume: (): void => {
|
|
manualControl = false;
|
|
if (scrollEnabled.value) startScroll();
|
|
},
|
|
reset: (): void => {
|
|
scrollY = 0;
|
|
if (content.value) {
|
|
content.value.style.transform = 'translate3d(0, 0, 0)';
|
|
}
|
|
}
|
|
});
|
|
</script>
|
|
|
|
<style scoped>
|
|
.auto-scroll-container {
|
|
overflow: hidden;
|
|
position: relative;
|
|
}
|
|
.auto-scroll-content {
|
|
display: flex;
|
|
flex-direction: column;
|
|
will-change: transform;
|
|
}
|
|
.safety:nth-child(odd) {
|
|
background-color: rgba(67, 226, 203, 0.2);
|
|
}
|
|
</style>
|