vue3从零实现你自己的走马灯组件

1,391 阅读2分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第13天,点击查看活动详情

效果图

动画scroll.gif

使用

<NoticeBar text="testtesttst" :scrollable="scrollable" @onClose="handleClose"/>

布局

采用 flex 布局,中间是走马灯内容部分,左边可以是标题,后退按钮或者自定义,右边关闭走马灯。

因为我们的项目是移动端项目,所以这个我就写成后退按钮。


<div class="notice-content">
   <div class="left-icon">&lt;</div>
   <div ref="wrapRef" class="wrap">
      <!-- scrollable为false 可给 notice-ellipsis 类名     -->
      <div ref="textRef" :class="['text ', animationClass]" :style="contentStyle" @animationend="onAnimationEnd">
         {{ text || 'no-content' }}
      </div>
   </div>
   <div class="right-icon" @click="handleCloseClick">×</div>
</div>
.notice-content {
    /*...*/
    white-space: nowrap;
    overflow: hidden;
}

.notice-content .wrap {
    flex: 1;
    width: 100%;
    overflow: hidden;
}

.notice-content .notice-ellipsis {
    width: 100%;
    overflow: hidden;
    white-space: nowrap;
    text-overflow: ellipsis;
}

.notice-content .text {
    width: fit-content;
}


/* 第一次滚动,只执行一次 */
.play {
    animation: nut-notice-bar-play linear both running;
}

.play-infinite {
    animation: nut-notice-bar-play-infinite linear infinite both running;
}

@keyframes nut-notice-bar-play {
    to {
        transform: translate3d(-100%, 0, 0);
    }
}

@keyframes nut-notice-bar-play-infinite {
    to {
        transform: translateX(-100%);
    }
}

notice-ellipsis 这个类可以当你不想滚动,但是内容宽度又比盒子大的时候,显示省略号。判断 scrollable

样式有个需要注意的地方


.notice-content .text {
    width: fit-content;
}

文字根据内容自动设置宽度,否则每次滚动的时候,都默认是中间盒子的高度。

  1. 如果文字内容长度大于等于盒子宽度的时候,这个时候没有问题。
  2. 如果文字内容长度小于盒子宽度的时候,就会导致等待走完整个盒子宽度才继续下一次滚动,这样体验就不太好。

属性

  1. text:展示的文字
  2. spped:滚动速度,默认 50
  3. scrollable: 只做展示的时候可以设置为 false, 则不会滚动
  4. delay: 第一次滚动延迟时间
  5. showxxx: 是否展示icon
const props = withDefaults(
    defineProps<{
        speed?: number
        delay?: number
        scrollable?: boolean
        text: string
        showLeftIcon?: boolean
        showRightIcon?: boolean
    }>(),
    {
        speed: 50,
        delay: 1000,
        scrollable: true,
        showLeftIcon: true,
        showRightIcon: true
    }
)

事件

  1. close:不传则默认关闭走马灯,传了触发对应操作后关闭。
  2. xxx: 点击左边 icon 的事件。
const emit = defineEmits<{
  (e: 'close', data: any): void
}>()

整体思路

初始化

获取中间内容宽度、文字宽度

判断 scrollable,1. true则执行 scroll() 逻辑;2. false则不执行,但是我们需要 watch(() => props.scrollable),可以控制是否开始,但不可暂停;

第2点的功能使用场景很少,一般都是进入页面就开始滚动或者直接展示。

onMounted(() => {
    widthParam.wrapWidth = wrapRef.value.getBoundingClientRect().width
    widthParam.textWidth = textRef.value.getBoundingClientRect().width
    console.log(widthParam)
    props.scrollable && scroll()
})

// 考虑是否需要
watch(() => props.scrollable, (val) => {
    if (widthParam.wrapWidth && widthParam.textWidth && val) scroll()
})

维护一个 firstLoop 变量,判断是否第一次滚动。

第一次滚动只滚动文字的宽度,第二次开始才滚动盒子的宽度。

scroll函数

const scroll = () => {
    animationClass.value = 'play'
    contentStyle.value = getContentStyle()
    firstLoop.value = false
}

// 动画结束,也就是判断第一次滚动结束之后重新计算滚动距离
const onAnimationEnd = () => {
    console.log('onAnimationEnd')
    animationClass.value = 'play-infinite'
    contentStyle.value = getContentStyle()
}

getContentStyle函数

const getContentStyle = (): string => {
    const distance = widthParam.textWidth + (firstLoop.value ? 0 : widthParam.wrapWidth)
    const duration = distance / props.speed
    const style = `animation-delay: ${firstLoop.value ? props.delay : 0}ms; animation-duration: ${duration}s; transform: translateX(${firstLoop.value ? 0 : widthParam.wrapWidth}px)`;
    return style
}

点击事件派发

const handleCloseClick = () => {
    /**
     * 你的逻辑....
     */
    
    // 派发事件
    emit('close', 123)
}