• 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
      在这里插入图片描述
  • 相关阅读:
    Git --- 基础介绍
    MySQL 基础知识(八)之用户权限管理
    leetcode 118-杨辉三角
    html+javascript 编写的自动根据当前时间问好 早上 中午下午 晚上 还有下班倒计时等等
    Spring AOP 分享
    接口隔离原则~
    Android14 WMS启动流程
    Java:软件开发中最流行的Java框架是什么?
    Mycat【Java提高】
    飞凌嵌入式RK3399平台的V10系统适配
  • 原文地址:https://blog.csdn.net/glorydx/article/details/134251405