echarts滚动效果
This commit is contained in:
@ -1,62 +1,55 @@
|
||||
<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 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 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;
|
||||
<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 = `translateY(${Math.round(scrollY)}px)`;
|
||||
|
||||
content.value.style.transform = `translate3d(0, ${Math.round(scrollY)}px, 0)`;
|
||||
animationFrameId = requestAnimationFrame(step);
|
||||
}
|
||||
|
||||
@ -67,49 +60,79 @@ function startScroll() {
|
||||
}
|
||||
|
||||
function pauseScroll() {
|
||||
cancelAnimationFrame(animationFrameId);
|
||||
animationFrameId = null;
|
||||
if (animationFrameId) {
|
||||
cancelAnimationFrame(animationFrameId);
|
||||
animationFrameId = null;
|
||||
}
|
||||
}
|
||||
|
||||
// 鼠标滚轮事件:手动滚动并停止自动滚动
|
||||
function onWheel(e) {
|
||||
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();
|
||||
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();
|
||||
}
|
||||
content.value.style.transform = `translate3d(0, ${Math.round(scrollY)}px, 0)`;
|
||||
}
|
||||
|
||||
// 生命周期
|
||||
onMounted(() => {
|
||||
if (shouldScroll.value && props.autoScroll) {
|
||||
if (scrollEnabled.value && props.autoScroll) {
|
||||
startScroll();
|
||||
}
|
||||
container.value.addEventListener('wheel', onWheel);
|
||||
container.value?.addEventListener('wheel', onWheel, { passive: false });
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
pauseScroll();
|
||||
container.value.removeEventListener('wheel', onWheel);
|
||||
container.value?.removeEventListener('wheel', onWheel);
|
||||
});
|
||||
|
||||
// 响应 items 数量变化
|
||||
watch(shouldScroll, (newVal) => {
|
||||
// 监听数据是否滚动条件满足
|
||||
watch(scrollEnabled, (newVal) => {
|
||||
if (!newVal) {
|
||||
pauseScroll();
|
||||
} else if (props.autoScroll && !manualPaused) {
|
||||
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>
|
||||
@ -122,4 +145,7 @@ watch(shouldScroll, (newVal) => {
|
||||
flex-direction: column;
|
||||
will-change: transform;
|
||||
}
|
||||
.safety:nth-child(odd) {
|
||||
background-color: rgba(67, 226, 203, 0.2);
|
||||
}
|
||||
</style>
|
||||
|
Reference in New Issue
Block a user