我改进了数据滚动方式!老板直接加薪

24,884 阅读7分钟

需求背景

前几天,甲方提了个需求,想让下面的数据循环展示,准备放在他们集团首页给他们领导演示用。

我们领导很重视这个事儿,拍了拍我,语重心长的说,小伙子,好好做。

我啪的一下就兴奋了,老板居然如此器重我,我必当鞠躬尽瘁,减少摸鱼,我要让老板拜倒在精湛的技术下!

于是,我搬出自己的库存代码,仅2min就实现了数据的滚动:

没错,我直接照搬了自己以前写过的文章:JS实现可滚动区域自动滚动展示 - 掘金

就在我准备告诉老板我做完了的时候,我突然想了想,这么快做完,老板一定觉得我没好好做,我以后还怎么升职加薪,赢取白富美?

于是,我连夜研究,终于改进了数据滚动方式,赢得了老板的大饼(以后涨500)。最终效果:

技术方案

技术选型

观察最终效果图,可以发现这其实就是一个数据循环滚动的效果,每条内容之间间隔1000ms,每条出现动的时间为500ms。用术语来说,这就是一个单步停顿滚动效果。

我百度了一下,社区还是有这个实现的现成方案的:vue-seamless-scroll,周下载也还行。

于是,我果断试了试,结果不知道什么原因,并不生效...

既然如此,直接手写一个吧!

实现思路

要实现上述效果其实很简单,如图

我们创造一个含有六个值的数组,每隔一段时间循环更改黄色区域的数据,当黄色区域数据变成最新的时候,红色区域整体向下移动,当有数值超出滚动区域后,在删除这个数据即可。

数据更新

如果不考虑动画,我们的代码应该这么写

<template>
    <div class="item-wrap" v-for="(item, index) in animationData">
          <!-- 模块内容 -->     
     </div>
</template>
<script setup lang="ts">
// #假设这是接口请求的10条最新数据
const allCarouseData = ref([1, 2, 3, 4, 5, 6, 7, 8, 9, 10])
// #需要轮播的数据
const animationData = ref<any>([])
// *定时器
const animationTimerMeta: any = {
    timer: null,
    // 这个函数负责设置轮播数据的更新逻辑。
    timeFuc() {
        let setTimeoutId: any = null
        if (this.timer) return
        this.timer = setInterval(() => {
            // 取轮播数据的第一条id
            let firstId = animationData.value[0].id
            // 为轮播数据添加最新的第一项数据 
            let index = allCarouseData.value.findIndex((res: any) => res.id === firstId)
            let addIndex = index - 1 < 0 ? allCarouseData.value.length - 1 : index - 1
            animationData.value.unshift(allCarouseData.value[addIndex])
            setTimeout(() => {
                // 删除数组的最后一项
                animationData.value.pop()
            }, 1000)
            
        }, 1500)
    }
}
animationData.value = allCarouseData.value.slice(-5)
animationTimerMeta.timeFuc()
</script>

上述代码的主要功能是:

  1. 从 allCarouseData 中取出最后5个元素作为初始的轮播数据。
  2. 每1.5秒更新一次轮播数据,具体逻辑是:移除当前 animationData 的第一个元素,并从 allCarouseData 中取出前一个元素(如果已经是第一个元素,则取最后一个)添加到 animationData 的开头。
  3. 每1秒从 animationData 的末尾移除一个元素。

上述代码没有实现动画,他的效果是这样的:

动画添加

<template>
    <div class="item-wrap" v-for="(item, index) in animationData" 
    :class="[{ moveToBottom: animationActive }, { show: animationActive && index === 0 }]"
    >
          <!-- 模块内容 -->     
     </div>
</template>
<script setup lang="ts">
// #假设这是接口请求的10条最新数据
const allCarouseData = ref([1, 2, 3, 4, 5, 6, 7, 8, 9, 10])
// #需要轮播的数据
const animationData = ref<any>([])
// #是否开启动画
const animationActive = ref(false)
// *定时器
const animationTimerMeta: any = {
    timer: null,
    // 这个函数负责设置轮播数据的更新逻辑。
    timeFuc() {
        let setTimeoutId: any = null
        if (this.timer) return
        this.timer = setInterval(() => {
            // 取轮播数据的第一条id
            let firstId = animationData.value[0].id
            // 为轮播数据添加最新的第一项数据 
            let index = allCarouseData.value.findIndex((res: any) => res.id === firstId)
            let addIndex = index - 1 < 0 ? allCarouseData.value.length - 1 : index - 1
            animationData.value.unshift(allCarouseData.value[addIndex])
            // #开启动画
            animationActive.value = true
            setTimeout(() => {
                animationActive.value = false
                // 删除数组的最后一项
                animationData.value.pop()
            }, 1000)
            
        }, 1500)
    }
}
animationData.value = allCarouseData.value.slice(-5)
animationTimerMeta.timeFuc()
</script>


@keyframes moveToBottom {
    0% {
        transform: translateY(-47px);
    }

    100% {
        transform: translateY(0);
    }
}

.moveToBottom {
    animation: moveToBottom 500ms ease-in-out forwards;
}

@keyframes fadeInFromTop {
    0% {
        opacity: 0;
        transform: translateY(-47px);
    }

    100% {
        opacity: 1;
        transform: translateY(0);
        color: #683BD6;
    }
}

.show {
    animation: fadeInFromTop 500ms ease-in-out forwards;
}

上述代码中,为了实现动画效果,采用了动态添加类名的技术方案。

animationData 数组中的元素会按照一定顺序进行显示和隐藏,同时伴随有动画效果。当第一个元素进入视图时,它会应用 fadeInFromTop 动画;其他元素会应用 moveToBottom 动画。通过定时器,元素会定期从 allCarouseData 中获取新的数据并更新 animationData。

代码释义:

  • moveToBottom: 当 animationActive 为真值时,此类名会被添加到 div 上。
  • show: 当 animationActive 为真值且当前元素是数组的第一个元素时,此类名会被添加到 div 上。

CSS 释义:

  • moveToBottom 动画:

定义一个名为 moveToBottom 的关键帧动画,使元素从上方移动到其原始位置。

moveToBottom 类将此动画应用到元素上。

  • fadeInFromTop 动画:

定义一个名为 fadeInFromTop 的关键帧动画,使元素从上方淡入并改变颜色。

show 类将此动画应用到元素上。

通过上述简单的实现方式,就能最终实现我们想要的效果

相比于普通滚动,这种方式看起来要好很多!

完整代码

注,上述代码做了简化,方便大家理解。下面提供完整的代码,大家可以直接复制运行


<template>
  <div class="content" @mouseenter="stop" @mouseleave="start" :style="{height: 5 * 47 + 'px'}">
    <div class="item-wrap" v-for="(item, index) in animationData" 
    :key="item.id"
    :class="[{ moveToBottom: animationActive }, { show: animationActive && index === 0 }]"
    >
          {{ item.name }}   
    </div>
  </div>

</template>
<script setup lang="ts">
import { ref ,onBeforeUnmount} from 'vue'
// #假设这是接口请求的10条最新数据
const allCarouseData = ref<any>([])
// #需要轮播的数据
const animationData = ref<any>([])
// #是否开启动画
const animationActive = ref(false)
// *定时器
const animationTimerMeta: any = {
    timer: null,
    timeFuc() {
        let setTimeoutId: any = null
        if (this.timer) return
        this.timer = setInterval(() => {
            if (setTimeoutId) {
                setTimeoutId = null
                clearTimeout(setTimeoutId);
            }
            // 取轮播数据的第一条id
            let firstId = animationData.value[0].id
            // 查询 
            let index = allCarouseData.value.findIndex((res: any) => res.id === firstId)
            let addIndex = index - 1 < 0 ? allCarouseData.value.length - 1 : index - 1
            animationData.value.unshift(allCarouseData.value[addIndex])
            // #开启动画
            animationActive.value = true
            setTimeoutId = setTimeout(() => {
                animationActive.value = false
                animationData.value.pop()
            }, 1000)
            // 删除数组的最后一项
        }, 1500)
    }
}

const mockData = () => {
  const axiosData = [
    {name:"文字1",id:1},
    {name:"文字2",id:2},
    {name:"文字3",id:3},
    {name:"文字3",id:4},
    {name:"文字4",id:5},
    {name:"文字5",id:6},
    {name:"文字6",id:7},
    {name:"文字7",id:8},
    {name:"文字8",id:9},
    {name:"文字9",id:10},

  ]
  allCarouseData.value = axiosData
  animationData.value = axiosData.slice(-5)
  animationTimerMeta.timeFuc()
}
mockData()


const stop = () => {
    clearInterval(animationTimerMeta.timer)
    animationTimerMeta.timer = null
}
const start = () => animationTimerMeta.timeFuc()

// !页面卸载时,关闭轮播
onBeforeUnmount(() => {
    clearInterval(animationTimerMeta.timer)
    animationTimerMeta.timer = null
})
</script>

<style>
.content{
  overflow: hidden;
  box-sizing: border-box;
}
.item-wrap{
  box-sizing: border-box;
  border: 1px solid #e4e4e4;
  margin-right: 13px;
  padding: 9px 12px;
  display: flex;
  height: 47px;
  justify-content: space-between;
  align-items: center;
}
@keyframes moveToBottom {
  0% {
      transform: translateY(-47px);
  }

  100% {
      transform: translateY(0);
  }
}

.moveToBottom {
  animation: moveToBottom 500ms ease-in-out forwards;
}

@keyframes fadeInFromTop {
  0% {
      opacity: 0;
      transform: translateY(-47px);
  }

  100% {
      opacity: 1;
      transform: translateY(0);
      color: #683BD6;
  }
}

.show {
  animation: fadeInFromTop 500ms ease-in-out forwards;
}
</style>

需要注意的是,我这里假设的是每一个item的高度是47px,所以父级高度设置为了5 * 47 + 'px'。大家可以根据自己的业务调整。

demo效果:

233.gif

向上滚动版本

<template>
  <div class="content" @mouseenter="stop" @mouseleave="start" :style="{height: 5 * 47 + 'px'}">
    <div class="item-wrap" v-for="(item, index) in animationData" 
    :key="item.id"
    :class="[{ moveToBottom: animationActive }, { show: animationActive && index === 0 }]"
    >
          {{ item.name }}   
    </div>
  </div>

</template>
<script setup lang="ts">
import { ref ,onBeforeUnmount} from 'vue'
// #假设这是接口请求的10条最新数据
const allCarouseData = ref<any>([])
// #需要轮播的数据
const animationData = ref<any>([])
// #是否开启动画
const animationActive = ref(false)
// *定时器
const animationTimerMeta: any = {
    timer: null,
    timeFuc() {
        let setTimeoutId: any = null
        if (this.timer) return
        this.timer = setInterval(() => {
            if (setTimeoutId) {
                setTimeoutId = null
                clearTimeout(setTimeoutId);
            }
            // 取轮播数据的第一条id
            let firstId = animationData.value[0].id
            // 查询 
            let index = allCarouseData.value.findIndex((res: any) => res.id === firstId)
            let addIndex = index - 1 < 0 ? allCarouseData.value.length - 1 : index - 1
            animationData.value.push(allCarouseData.value[addIndex])
            // #开启动画
            animationActive.value = true
            setTimeoutId = setTimeout(() => {
                animationActive.value = false
                // 删除数组的第一项
                animationData.value.shift()
            }, 1000)
            // 删除数组的最后一项
        }, 1500)
    }
}

const mockData = () => {
  const axiosData = [
    {name:"文字1",id:1},
    {name:"文字2",id:2},
    {name:"文字3",id:3},
    {name:"文字3",id:4},
    {name:"文字4",id:5},
    {name:"文字5",id:6},
    {name:"文字6",id:7},
    {name:"文字7",id:8},
    {name:"文字8",id:9},
    {name:"文字9",id:10},

  ]
  allCarouseData.value = axiosData
  animationData.value = axiosData.slice(-5)
  animationTimerMeta.timeFuc()
}
mockData()


const stop = () => {
    clearInterval(animationTimerMeta.timer)
    animationTimerMeta.timer = null
}
const start = () => animationTimerMeta.timeFuc()

// !页面卸载时,关闭轮播
onBeforeUnmount(() => {
    clearInterval(animationTimerMeta.timer)
    animationTimerMeta.timer = null
})
</script>

<style>
.content{
  overflow: hidden;
  box-sizing: border-box;
}
.item-wrap{
  box-sizing: border-box;
  border: 1px solid #e4e4e4;
  margin-right: 13px;
  padding: 9px 12px;
  display: flex;
  height: 47px;
  justify-content: space-between;
  align-items: center;
}
@keyframes moveToBottom {
  0% {
      transform: translateY(0);
  }

  100% {
      transform: translateY(-47px);
  }
}

.moveToBottom {
  animation: moveToBottom 500ms ease-in-out forwards;
}

@keyframes fadeInFromTop {
  0% {
      opacity: 0;
      transform: translateY(0);
  }

  100% {
      opacity: 1;
      transform: translateY(-47px);
      color: #683BD6;
  }
}

.show {
  animation: fadeInFromTop 500ms ease-in-out forwards;
}
</style>

结语

要想实现这种单步停帧的效果,其实有很多实现方式,这只是笔者实现的一种,核心逻辑就是动态改变数据、增添类名。如果大家还有更好的方式,也欢迎大家指点。