当前位置:   article > 正文

vue3 组件篇 Carousel_vue3-carousel

vue3-carousel


在这里插入图片描述

组件介绍

Carousel(走马灯)是一种常见的前端组件,通常用于展示多个项目(通常是图片或内容块)的轮播效果。它是网页和应用中的常见UI元素之一,通常用于滚动广告、产品展示、图片轮播、新闻滚动等场景。

主要特点和功能:

  1. 图片/内容轮播:Carousel能够以水平或垂直的方式,循环地显示多个项目,使用户能够逐个或自动浏览这些项目。
  2. 自动播放:通常,Carousel支持自动播放功能,允许项目在不需要用户干预的情况下自动切换。
  3. 导航控件:通常,Carousel提供导航控件,如箭头或小圆点,用户可以点击它们来切换到不同的项目。
  4. 响应式设计:现代Carousel组件通常支持响应式设计,可以根据屏幕大小和设备类型进行适应,以确保在不同的屏幕上有良好的显示效果。
  5. 自定义样式:开发人员可以根据项目需求自定义Carousel的外观和样式,包括项目尺寸、过渡效果等。

使用场景:
Carousel组件适用于各种情境,包括但不限于:

  1. 广告轮播:在网站或应用中展示不同的广告内容。
  2. 产品展示:在电子商务网站上展示产品图片和详细信息。
  3. 新闻滚动:用于滚动新闻标题或摘要。
  4. 图片画廊:创建图片画廊或幻灯片展示。
  5. 特色内容展示:用于突出特色文章、功能或信息。

开发思路

直接整无缝轮播,来计算每一次移动的距离,什么是无缝轮播?

<Carousel :carouselItemHeight='300'>
  <CarouselItem> <div class='carousel-item-content'>4</div> </CarouselItem>
  <CarouselItem> <div class='carousel-item-content'>1</div> </CarouselItem>
  <CarouselItem> <div class='carousel-item-content'>2</div> </CarouselItem>
  <CarouselItem> <div class='carousel-item-content'>3</div> </CarouselItem>
  <CarouselItem> <div class='carousel-item-content'>4</div> </CarouselItem>
  <CarouselItem> <div class='carousel-item-content'>1</div> </CarouselItem>
</Carousel>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

如上所示,实际展示的轮播内容只有1,2,3,4。但在最前面重复了最后一个,在最后面重复了第一个。
当轮播图移动到最后面1的时候,默默跳转到第二个的1,这样就可以一直往一个方向移动。反向同理,当移动到最前面4的时候,默默移动到倒数第二个4。

在切换位置的时候,用户是没有感知的,这样就可以实现无缝轮播。

在移动的时候最好采用requestAnimationFrame,这样可以让动画更稳定,并且移动变量,通过css3 transform属性来实现,减少动画重排带来的性能问题。

组件安装与使用

需要先安装vue3-dxui

yarn add vue3-dxui
  • 1

或者

npm install vue3-dxui
  • 1

全局main.ts中引入css

import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'
import 'vue3-dxui/dxui/dxui.css'

createApp(App).use(store).use(router).mount('#app')
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

按需引入,Carousel组件的使用需要配合CarouselItem组件一起。

<script>
import { Carousel, CarouselItem } from 'vue3-dxui'
 
export default {
  components: {
  	Carousel,
  	CarouselItem 
  }
}
</script>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

组件代码

Carousel组件代码

<template>
  <div class="carousel-warpper" ref="warpper" @mouseenter="stopAutoPaly" @mouseleave="autoPlay">
    <div
      class="carousel-all-warpper"
      :style="{ width: allWarpperWidth, transform: `translateX(${leftVal}px)` }"
      ref="allWarpper"
    >
      <slot />
    </div>

    <!-- 左箭头 -->
    <button class="carousel-arrow carousel-left-arrow" @click="clickPreItem">
      <Icon class="carousel-icon" iconName="chevron-left" />
    </button>

    <!-- 右箭头 -->
    <button class="carousel-arrow carousel-right-arrow" @click="clickNextItem">
      <Icon class="carousel-icon" iconName="chevron-right" />
    </button>

    <!-- 原点 -->
    <div class="carousel-point" v-if="openDot">
      <span
        class="carousel-point-item"
        :class="index === currentIndex ? 'active' : ''"
        v-for="(item, index) in realCountArray"
        :key="index"
        @click="clickDot(index)"
      ></span>
    </div>
  </div>
</template>

<script lang="ts">
import { ref, SetupContext, computed, onMounted, onBeforeUnmount, provide } from 'vue'
import Icon from '@/components/icon/Icon.vue'

export default {
  name: 'Carousel',
  components: {
    Icon
  },
  props: {
    // 同时展示在视野上carouselItem的数量
    visionCount: {
      type: Number,
      required: false,
      default: 1,
      validator: (value: number) => {
        return value >= 1 && value <= 10
      }
    },
    // 自动播放时,当动画播放后,距离下一次移动的间隔时间,单位ms
    intervalTime: {
      type: Number,
      required: false,
      default: 4000,
      validator: (value: number) => {
        return typeof value === 'number'
      }
    },
    // carousel 播放滚动时的移动速度,数字越大,移动速度越快
    transitionSpeed: {
      type: Number,
      required: false,
      default: 6,
      validator: (value: number) => {
        return typeof value === 'number'
      }
    },
    // 轮播里面内容的间隔距离,单位px
    gap: {
      type: Number,
      required: false,
      default: 16,
      validator: (value: number) => {
        return typeof value === 'number'
      }
    },
    // 轮播的内容数量
    count: {
      type: Number,
      required: false,
      default: 0,
      validator: (value: number) => {
        return typeof value === 'number'
      }
    },
    // 定义carouselItem的高度
    carouselItemHeight: {
      type: Number,
      required: false,
      default: 150,
      validator: (value: number) => {
        return typeof value === 'number'
      }
    },
    // 是否开启自动播放
    openAutoPlay: {
      type: Boolean,
      required: false,
      default: true,
      validator: (value: boolean) => {
        return typeof value === 'boolean'
      }
    },
    // 是否开启自动播放
    openDot: {
      type: Boolean,
      required: false,
      default: true,
      validator: (value: boolean) => {
        return typeof value === 'boolean'
      }
    }
  },
  setup(props: any, context: SetupContext) {
    // 整个轮播盒子的宽度
    const allWarpperWidth = ref<string>('')
    // 向左移动的距离
    const leftVal = ref<number>(0)
    // 初始左移距离
    const initLeftVal = ref<number>(0)
    // 已经移动过几次
    const showIndex = ref<number>(0)
    // 监测自动播放是否开启的关键,自动播放的定时器
    const autoPlayInterval = ref<number | null>(null)
    // 除去无缝轮播后,轮播的真实数量
    const realCount = ref<number>(0)
    // 真实的carouselItem的数量,包含无缝轮播新增的内容
    const itemLength = ref<number>(0)
    // 单个item计算后的宽度
    const itemWidth = ref<number>(0)
    // 动画是否正在进行中,其它控制会失效
    const transitionIng = ref<boolean>(false)

    const allWarpper: any = ref(null)
    const warpper: any = ref(null)

    const realCountArray = computed(() => new Array(realCount.value))

    // 当前激活的圆点是第几个
    const currentIndex = computed(() => {
      const result = showIndex.value - props.visionCount
      if (result >= 0 && result <= realCount.value - 1) {
        return result
      } else if (result < 0) {
        return realCount.value + result
      } else {
        return result - realCount.value
      }
    })

    const getItemLength = () => {
      const children = allWarpper.value.children
      realCount.value = children?.length - 2 * props.visionCount
      itemLength.value = props.count || children?.length
      return props.count || children?.length
    }

    const getWarpperWidth = () => {
      return warpper?.value?.clientWidth
    }

    // 计算单个carouselItem的宽度
    const getItemWidth = () => {
      // 减掉间距后除以展示个数
      const carouselItemWidth: number =
        (getWarpperWidth() - props.gap * (props.visionCount - 1)) / props.visionCount
      itemWidth.value = carouselItemWidth
      return carouselItemWidth
    }

    // 计算整个carousel的宽度
    const getAllCarouselWidth = () => {
      return itemWidth.value * itemLength.value + props.gap * (itemLength.value - 1)
    }

    // 动画移动函数
    const RequestAnimationFrameFun = () => {
      if (leftVal.value >= -(itemWidth.value + props.gap) * (showIndex.value + 1)) {
        leftVal.value -= props.transitionSpeed
        requestAnimationFrame(RequestAnimationFrameFun)
      } else {
        showIndex.value += 1
        leftVal.value = -(itemWidth.value + props.gap) * showIndex.value
        // 动画结束
        transitionIng.value = false
      }
    }

    // 下一个item
    const nextCarouselItem = () => {
      // 如果已经移动到最后,返回表面上的第一个
      if (
        leftVal.value <=
        -(itemWidth.value + props.gap) * (itemLength.value - props.visionCount)
      ) {
        leftVal.value = initLeftVal.value
        showIndex.value = props.visionCount
      }

      requestAnimationFrame(RequestAnimationFrameFun)
      // 动画开始
      transitionIng.value = true
    }

    // 自动开始走轮播 初次加载,移出当前dom,返回页面需要重启自动轮播
    const autoPlay = () => {
      if (props.openAutoPlay) {
        // 动画过程需要的时间,动画最长时间一般是60帧/秒,每帧大约16.6ms
        const transitionTime = ((itemWidth.value + props.gap) / props.transitionSpeed) * 16.6

        if (!autoPlayInterval.value) {
          autoPlayInterval.value = window.setInterval(() => {
            nextCarouselItem()
          }, transitionTime + props.intervalTime)
        }
      }
    }

    // 清除自动轮播(点击左右按钮,移入目标dom,离开当前页面,都需要清除dom)
    const stopAutoPaly = () => {
      if (autoPlayInterval.value) {
        clearInterval(autoPlayInterval.value)
      }
      autoPlayInterval.value = null
    }

    // 点击下一个轮播item
    const clickNextItem = () => {
      const handleClickNextItem = () => {
        // 暂停自动轮播
        stopAutoPaly()
        nextCarouselItem()
      }
      // 某种意义上的节流
      if (!transitionIng.value) {
        handleClickNextItem()
      }
    }

    // 点击返回前一个轮播需要执行的动画
    const RequestAnimationFrameFunPre = () => {
      if (leftVal.value <= -(itemWidth.value + props.gap) * (showIndex.value - 1)) {
        leftVal.value += props.transitionSpeed
        requestAnimationFrame(RequestAnimationFrameFunPre)
      } else {
        showIndex.value -= 1
        leftVal.value = -(itemWidth.value + props.gap) * showIndex.value
        // 动画结束
        transitionIng.value = false
      }
    }

    // 点击返回前一个轮播
    const clickPreItem = () => {
      const handleClickPreItem = () => {
        // 暂停自动轮播
        stopAutoPaly()

        // 如果已经移动到最前边,返回表面上的最后一个
        if (leftVal.value >= 0) {
          leftVal.value =
            -(itemWidth.value + props.gap) * (itemLength.value - 2 * props.visionCount)
          showIndex.value = itemLength.value - 2 * props.visionCount
        }

        requestAnimationFrame(RequestAnimationFrameFunPre)
        // 动画开始
        transitionIng.value = true
      }

      // 某种意义上的节流
      if (!transitionIng.value) {
        handleClickPreItem()
      }
    }

    const visibilitychangeHidden = () => {
      if (document.hidden) {
        stopAutoPaly()
      } else {
        autoPlay()
      }
    }

    // 点击跳转圆点移动到对应的距离
    const clickDot = (dotIndex: number) => {
      showIndex.value = dotIndex + props.visionCount
      leftVal.value = -(itemWidth.value + props.gap) * showIndex.value
    }

    onMounted(() => {
      // 初始偏移item的数量
      showIndex.value = props.visionCount

      getItemWidth()
      getItemLength()

      setTimeout(() => {
        allWarpperWidth.value = getAllCarouselWidth() + 'px'
        // 计算初始位置
        initLeftVal.value = leftVal.value = -(itemWidth.value + props.gap) * props.visionCount
      }, 20)

      autoPlay()

      // 如果离开页面停止自动播放页面
      document.addEventListener('visibilitychange', visibilitychangeHidden)
    })

    onBeforeUnmount(() => {
      document.removeEventListener('visibilitychange', visibilitychangeHidden)
    })

    // 提供父组件指定的宽度,避免CarouselItem重新计算
    provide('getItemWidth', getItemWidth)
    provide('gap', props.gap)
    provide('carouselItemHeight', props.carouselItemHeight)

    return {
      autoPlayInterval,
      autoPlay,
      stopAutoPaly,
      showIndex,
      initLeftVal,
      leftVal,
      allWarpperWidth,
      realCountArray,
      allWarpper,
      warpper,
      currentIndex,
      clickNextItem,
      clickPreItem,
      clickDot
    }
  }
}
</script>

<style lang="scss">
@import '@/scss/layout.scss';
.carousel-warpper {
  width: 100%;
  height: 100%;
  min-height: 100px;
  overflow: hidden;
  position: relative;

  .carousel-all-warpper {
    height: 100%;
    white-space: nowrap;
    padding-bottom: 24px;
  }

  .carousel-arrow {
    border-radius: 18px;
    width: 36px;
    height: 36px;
    position: absolute;
    background: $border-color;
    line-height: 36px;
    font-size: 18px;
    color: $black-color;
    padding: 0px;
    border: none;
    cursor: pointer;

    .carousel-icon {
      font-size: 24px;
      line-height: 36px;
      color: $white-color;
    }
  }

  .carousel-left-arrow {
    position: absolute;
    top: 50%;
    transform: translateY(-100%);
    left: 24px;
  }

  .carousel-right-arrow {
    position: absolute;
    top: 50%;
    transform: translateY(-100%);
    right: 24px;
  }

  .carousel-point {
    position: absolute;
    bottom: 0;
    left: 50%;
    transform: translateX(-50%);

    .carousel-point-item {
      width: 8px;
      height: 8px;
      border-radius: 4px;
      border: $black-border;
      display: inline-block;
      margin-right: 8px;
      cursor: pointer;
    }
    .carousel-point-item:last-child {
      margin-right: 0;
    }

    .carousel-point-item.active {
      background: $black-color;
    }
  }
}
</style>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124
  • 125
  • 126
  • 127
  • 128
  • 129
  • 130
  • 131
  • 132
  • 133
  • 134
  • 135
  • 136
  • 137
  • 138
  • 139
  • 140
  • 141
  • 142
  • 143
  • 144
  • 145
  • 146
  • 147
  • 148
  • 149
  • 150
  • 151
  • 152
  • 153
  • 154
  • 155
  • 156
  • 157
  • 158
  • 159
  • 160
  • 161
  • 162
  • 163
  • 164
  • 165
  • 166
  • 167
  • 168
  • 169
  • 170
  • 171
  • 172
  • 173
  • 174
  • 175
  • 176
  • 177
  • 178
  • 179
  • 180
  • 181
  • 182
  • 183
  • 184
  • 185
  • 186
  • 187
  • 188
  • 189
  • 190
  • 191
  • 192
  • 193
  • 194
  • 195
  • 196
  • 197
  • 198
  • 199
  • 200
  • 201
  • 202
  • 203
  • 204
  • 205
  • 206
  • 207
  • 208
  • 209
  • 210
  • 211
  • 212
  • 213
  • 214
  • 215
  • 216
  • 217
  • 218
  • 219
  • 220
  • 221
  • 222
  • 223
  • 224
  • 225
  • 226
  • 227
  • 228
  • 229
  • 230
  • 231
  • 232
  • 233
  • 234
  • 235
  • 236
  • 237
  • 238
  • 239
  • 240
  • 241
  • 242
  • 243
  • 244
  • 245
  • 246
  • 247
  • 248
  • 249
  • 250
  • 251
  • 252
  • 253
  • 254
  • 255
  • 256
  • 257
  • 258
  • 259
  • 260
  • 261
  • 262
  • 263
  • 264
  • 265
  • 266
  • 267
  • 268
  • 269
  • 270
  • 271
  • 272
  • 273
  • 274
  • 275
  • 276
  • 277
  • 278
  • 279
  • 280
  • 281
  • 282
  • 283
  • 284
  • 285
  • 286
  • 287
  • 288
  • 289
  • 290
  • 291
  • 292
  • 293
  • 294
  • 295
  • 296
  • 297
  • 298
  • 299
  • 300
  • 301
  • 302
  • 303
  • 304
  • 305
  • 306
  • 307
  • 308
  • 309
  • 310
  • 311
  • 312
  • 313
  • 314
  • 315
  • 316
  • 317
  • 318
  • 319
  • 320
  • 321
  • 322
  • 323
  • 324
  • 325
  • 326
  • 327
  • 328
  • 329
  • 330
  • 331
  • 332
  • 333
  • 334
  • 335
  • 336
  • 337
  • 338
  • 339
  • 340
  • 341
  • 342
  • 343
  • 344
  • 345
  • 346
  • 347
  • 348
  • 349
  • 350
  • 351
  • 352
  • 353
  • 354
  • 355
  • 356
  • 357
  • 358
  • 359
  • 360
  • 361
  • 362
  • 363
  • 364
  • 365
  • 366
  • 367
  • 368
  • 369
  • 370
  • 371
  • 372
  • 373
  • 374
  • 375
  • 376
  • 377
  • 378
  • 379
  • 380
  • 381
  • 382
  • 383
  • 384
  • 385
  • 386
  • 387
  • 388
  • 389
  • 390
  • 391
  • 392
  • 393
  • 394
  • 395
  • 396
  • 397
  • 398
  • 399
  • 400
  • 401
  • 402
  • 403
  • 404
  • 405
  • 406
  • 407
  • 408
  • 409
  • 410
  • 411
  • 412
  • 413
  • 414
  • 415

carouselItem组件代码

<template>
  <div class="carousel-item-warpper" :style="{ width: itemWidth, marginRight: gap, height: height }">
    <slot />
  </div>
</template>

<script lang="ts">
import {
  ref,
  onMounted,
  inject
} from 'vue'

export default {
  name: 'CarouselItem',
  setup(props: any) {
    // item的 宽度
    const itemWidth = ref<string>('')

    // item 的间距
    const gap = ref<string>('')

    // item的高度
    const height = ref<string>('')

    onMounted(() => {
      const getItemWidth = inject('getItemWidth') as any
      itemWidth.value = getItemWidth() + 'px'
      gap.value = inject('gap') + 'px'
      height.value = inject('carouselItemHeight') + 'px'
    })
    return {
      gap,
      itemWidth,
      height
    }
  }
}
</script>

<style lang="scss">
@import '@/scss/layout.scss';
.carousel-item-warpper {
  height: 100%;
  display: inline-block;
  white-space: initial;
  color: $black-color;
}

.carousel-item-warpper:last-child {
  margin-right: 0px !important;
}
</style>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53

参数说明

名称说明
visionCount同时展示的item数量
openAutoPlay是否需要自动播放
openDot是否需要dot
intervalTime轮播间隔时间
transitionSpeed轮播动画的速度
carouselItemHeight轮播高度
gap轮播内容的间隙距离

关于dxui组件库

dxui组件库是我个人搭建的vue3 前端交互组件库,倾向于pc网站的交互模式。

  1. 如果你有任何问题,请在博客下方评论留言,我尽可能24小时内回复。
  2. dxui新上线的官网域名变更 http://dxui.cn
  3. npm 官方链接 https://www.npmjs.com/package/vue3-dxui
  4. 如果你想看完整源码 https://github.com/757363985/dxui
    在这里插入图片描述
声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/盐析白兔/article/detail/250903
推荐阅读
相关标签
  

闽ICP备14008679号