AutoScroll 自动滚动
自动(无缝)滚动组件,支持水平和垂直方向。
组件源码
vue
<script lang="ts" setup>
import { computed, ref, watch } from 'vue';
import { useResizeObserver } from '@stao-ui/utils';
const $props = withDefaults(
defineProps<{
gap?: string;
speed?: number; // px/s
direction?: 'vertical' | 'horizontal';
timingFunction?: string;
animateDirection?: 'normal' | 'reverse' | 'alternate' | 'alternate-reverse';
}>(),
{
gap: '20px',
speed: 50,
direction: 'horizontal',
timingFunction: 'linear',
animateDirection: 'normal'
}
);
const translate3d = computed(() => {
if ($props.direction === 'vertical') {
return `translate3d(0, calc(-100% - ${$props.gap}), 0)`;
}
return `translate3d(calc(-100% - ${$props.gap}), 0, 0)`;
});
const isScroll = ref(false);
const { element } = useResizeObserver<HTMLDivElement>(() => {
isScroll.value = canScroll(element.value, $props.direction);
initDuration();
});
watch(
() => $props.speed,
() => {
initDuration();
}
);
const duration = ref('0s');
function initDuration() {
if (!isScroll.value) {
return;
}
const distance =
$props.direction === 'vertical'
? element.value.scrollHeight
: element.value.scrollWidth;
duration.value = (distance / $props.speed).toFixed(1) + 's';
}
function canScroll(element: HTMLElement, direction: 'vertical' | 'horizontal') {
const parent = element.parentElement!;
if (direction === 'vertical') {
return element.scrollHeight > parent.clientHeight;
}
return element.scrollWidth > parent.clientWidth;
}
</script>
<template>
<div
class="scroll-list"
:class="{ 'scroll-list--vertical': direction === 'vertical' }">
<div ref="element" class="scroll-list__item" :class="{ animate: isScroll }">
<slot />
</div>
<div v-if="isScroll" class="scroll-list__item animate">
<slot />
</div>
</div>
</template>
<style lang="scss" scoped>
.scroll-list {
overflow: hidden;
display: flex;
flex-direction: row;
gap: v-bind(gap);
&__item {
flex-shrink: 0;
display: flex;
flex-direction: row;
gap: v-bind(gap);
}
&:hover .animate {
animation-play-state: paused;
}
}
.scroll-list--vertical {
flex-direction: column;
height: 100%;
.scroll-list__item {
flex-direction: column;
}
}
.animate {
animation: marquee v-bind(duration) v-bind(timingFunction) infinite;
animation-direction: v-bind(animateDirection);
}
@keyframes marquee {
0% {
transform: translate3d(0, 0, 0);
}
100% {
transform: v-bind(translate3d);
}
}
</style>vue2.x 版本
vue
<template>
<div
class="scroll-list"
:style="style"
:class="{ 'scroll-list--vertical': direction === 'vertical' }"
>
<div ref="element" class="scroll-list__item" :class="{ animate: isScroll }">
<slot />
</div>
<div v-if="isScroll" class="scroll-list__item animate">
<slot />
</div>
</div>
</template>
<script>
export default {
props: {
direction: {
validator: function(value) {
return ['horizontal', 'vertical'].includes(value)
},
default: 'horizontal'
},
gap: {
type: String,
default: '20px'
},
speed: { // px/s
type: Number,
default: 40
}
},
data() {
return {
isScroll: false,
resizeObserver: null,
duration: '6s'
}
},
computed: {
style() {
let style = `--scroll-gap: ${this.gap};--scroll-duration: ${this.duration};`
if (this.direction === 'vertical') {
style += `--scroll-translate3d: translate3d(0, calc(-100% - ${this.gap}), 0);`
} else {
style += `--scroll-translate3d: translate3d(calc(-100% - ${this.gap}), 0, 0);`
}
return style
}
},
created() {
this.resizeObserver = new ResizeObserver(() => {
const element = this.$refs.element
const parent = element.parentElement
if (this.direction === 'vertical') {
this.isScroll = element.scrollHeight > parent.clientHeight
if (this.isScroll) {
this.duration = (element.scrollHeight / this.speed).toFixed(2) + 's'
}
return
}
this.isScroll = element.scrollWidth > parent.clientWidth
if (this.isScroll) {
this.duration = (element.scrollWidth / this.speed).toFixed(2) + 's'
}
})
},
mounted() {
this.init()
},
beforeDestroy() {
this.resizeObserver.disconnect()
this.resizeObserver = null
},
methods: {
init() {
const element = this.$refs.element
if (!element) {
return
}
this.resizeObserver.observe(element)
}
}
}
</script>
<style lang="scss" scoped>
.scroll-list {
overflow: hidden;
display: flex;
flex-direction: row;
gap: var(--scroll-gap, 20px);
&__item {
flex-shrink: 0;
display: flex;
flex-direction: row;
gap: var(--scroll-gap, 20px);
}
&:hover .animate {
animation-play-state: paused;
}
}
.scroll-list--vertical {
flex-direction: column;
height: 100%;
.scroll-list__item {
flex-direction: column;
}
}
.animate {
animation: marquee var(--scroll-duration) linear infinite;
}
@keyframes marquee {
0% {
transform: translate3d(0, 0, 0);
}
100% {
transform: var(--scroll-translate3d);
}
}
</style>基本用法
内容超出容器宽度/高度时,自动滚动。内容不足时,不滚动。内容大小变化时,自动调整滚动。
桔子🍊
香蕉🍌
苹果🍎
芒果🥭
菠萝🍍
桔子🍊
香蕉🍌
苹果🍎
芒果🥭
菠萝🍍
桔子🍊
香蕉🍌
苹果🍎
芒果🥭
菠萝🍍
桔子🍊
香蕉🍌
苹果🍎
芒果🥭
菠萝🍍
示例代码
vue
<template>
<div class="flex">
<div class="horizontal">
<auto-scroll>
<div v-for="item in texts" class="card" :key="item">{{ item }}</div>
</auto-scroll>
<auto-scroll animate-direction="reverse">
<div v-for="item in texts" class="card" :key="item">{{ item }}</div>
</auto-scroll>
<auto-scroll :speed="20">
<div v-for="item in texts" class="card" :key="item">{{ item }}</div>
</auto-scroll>
</div>
<div class="container">
<auto-scroll direction="vertical">
<div v-for="item in texts" class="card" :key="item">{{ item }}</div>
</auto-scroll>
</div>
</div>
</template>
<script lang="ts" setup>
import AutoScroll from '@/AutoScroll/AutoScroll.vue';
import { ref } from 'vue';
const texts = ref<string[]>(['桔子🍊', '香蕉🍌', '苹果🍎', '芒果🥭', '菠萝🍍']);
</script>
<style lang="scss" scoped>
.flex {
display: flex;
justify-content: space-between;
.card {
padding: 5px 10px;
border: 1px solid #666;
border-radius: 8px;
}
.horizontal {
width: 300px;
display: flex;
flex-direction: column;
justify-content: space-between;
}
}
.container {
flex-shrink: 0;
width: 300px;
height: 200px;
}
</style>API
Props
| 参数名 | 说明 | 类型 | 默认值 |
|---|---|---|---|
| direction | 滚动方向 | 'horizontal'|'vertical' | horizontal |
| speed | 滚动速度,单位:px/s | number | 50 |
| gap | 两项间隔(为了实现无缝滚动,会创建两个一样的子项) | string | 20px |
| timingFunction | 动画函数 | string | linear |
| animateDirection | 动画方向 | 'normal'|'reverse'|'alternate'|'alternate-reverse' | normal |
Slots
| 插槽名 | 说明 | 参数 |
|---|---|---|
| default | 需要滚动的内容 | - |