一个简单的Vue3 轮播图组件

<script setup>
import { onMounted, onUnmounted, ref, useSlots } from 'vue'

const slots = useSlots()
const slides = ref([])
const originalSlidesLength = ref(0)
const currentIndex = ref(0)
const isDragging = ref(false)
const startX = ref(0)
const currentTranslate = ref(0)
const prevTranslate = ref(0)
const props = defineProps({
  autoPlay: {
    type: Boolean,
    default: false
  }
})

onMounted(() => {
  const originalSlides = slots.default()[0].children.map((slide, index) => ({ content: slide, key: index }))
  originalSlidesLength.value = originalSlides.length
  slides.value = [...originalSlides, originalSlides[0]]
  startAutoPlay()
})

let autoPlayInterval
const startAutoPlay = () => {
  if (!props.autoPlay) return
  autoPlayInterval = setInterval(() => {
    goToNextSlide()
  }, 3000)
}

const goToNextSlide = () => {
  if (currentIndex.value < slides.value.length - 2) {
    currentIndex.value += 1
  } else {
    currentIndex.value = 0
    updateTranslate(true)
    return
  }
  updateTranslate()
}

const startDrag = (event) => {
  isDragging.value = true
  startX.value = event.clientX
  prevTranslate.value = currentTranslate.value
  clearInterval(autoPlayInterval)
}

const onDrag = (event) => {
  if (isDragging.value) {
    const currentX = event.clientX
    const diff = currentX - startX.value
    currentTranslate.value = prevTranslate.value + (diff / window.innerWidth) * 100
  }
}

const endDrag = () => {
  if (isDragging.value) {
    isDragging.value = false
    const movedBy = currentTranslate.value - prevTranslate.value

    if (movedBy < -10 && currentIndex.value < slides.value.length - 2) {
      currentIndex.value += 1
    } else if (movedBy > 10 && currentIndex.value > 0) {
      currentIndex.value -= 1
    } else if (movedBy > 10 && currentIndex.value === 0) {
      currentIndex.value = slides.value.length - 2
    } else if (movedBy < -10 && currentIndex.value === slides.value.length - 2) {
      currentIndex.value = 0
    }

    updateTranslate()
    startAutoPlay()
  }
}

const updateTranslate = (instant = false) => {
  prevTranslate.value = -currentIndex.value * 100
  currentTranslate.value = prevTranslate.value
  if (instant) {
    requestAnimationFrame(() => {
      currentTranslate.value = prevTranslate.value
    })
  }
}

const goToSlide = (index) => {
  currentIndex.value = index
  updateTranslate()
}

onUnmounted(() => {
  clearInterval(autoPlayInterval)
})
</script>

<template>
  <div class="carousel-container">
    <div
      class="carousel"
      @mousedown="startDrag"
      @mousemove="onDrag"
      @mouseup="endDrag"
      @mouseleave="endDrag"
      :style="{ transform: `translateX(${currentTranslate}%)`, transition: isDragging ? 'none' : 'transform 0.3s ease' }"
    >
      <div v-for="(slide, index) in slides" :key="slide.key" class="slide">
        <component :is="slide.content"></component>
      </div>
    </div>

    <div class="indicators">
      <span
        v-for="(indicator, index) in slides.slice(0, originalSlidesLength)"
        :key="index"
        class="indicator"
        :class="{ active: currentIndex === index || (currentIndex === slides.length - 1 && index === 0) }"
        @click="goToSlide(index)"
      ></span>
    </div>
  </div>
</template>

<style scoped>
.carousel-container {
  position: relative;
  width: 100%;
  height: 100%;
  margin: 0 auto;
  overflow: hidden;
  padding-bottom: 30px;
}

.carousel {
  height: 100%;
  display: flex;
}

.slide {
  min-width: 100%;
  height: 100%;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 2em;
  color: white;
  user-select: none;
}

.indicators {
  position: absolute;
  bottom: 10px;
  left: 50%;
  transform: translateX(-50%);
  display: flex;
  gap: 10px;
}

.indicator {
  width: 10px;
  height: 6px;
  background-color: rgba(255, 255, 255, 0.5);
  cursor: pointer;
  transition: background-color 0.3s ease;
}

.indicator.active {
  background-color: rgba(255, 255, 255, 1);
}
</style>