Files
td_official/src/views/gisHome/component/autoScroller.vue
2025-05-12 18:31:23 +08:00

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>