vue自定义Banner无限轮播图

创建 Banner.vue

<template>
  <div class="banner" :class="round ? 'round' : '' ">
    <div class="carousel">
      <div class="carousel-list" :style="listStyle">
        <!-- 添加最后一张图片到开头 -->
        <div class="carousel-item" :key="'last'">
          <img :src="items[items.length - 1].image" :alt="items[items.length - 1].alt">
          <div class="banner-overlay" v-if="overlay"></div>
        </div>
        <!-- 正常轮播项 -->
        <div class="carousel-item" v-for="(item, index) in items" :key="index">
          <img :src="item.image" :alt="item.alt">
          <div class="banner-overlay" v-if="overlay"></div>
        </div>
        <!-- 添加第一张图片到末尾 -->
        <div class="carousel-item" :key="'first'">
          <img :src="items[0].image" :alt="items[0].alt">
          <div class="banner-overlay" v-if="overlay"></div>
        </div>
      </div>

      <!-- 左箭头按钮 -->
      <template v-if="showArrows">
         <button class="carousel-arrow prev" @click="prev">
        <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="white" width="24px" height="24px">
          <path d="M15.41 16.59L10.83 12l4.58-4.59L14 6l-6 6 6 6 1.41-1.41z"/>
        </svg>
      </button>

      <!-- 右箭头按钮 -->
      <button class="carousel-arrow next" @click="next">
        <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="white" width="24px" height="24px">
          <path d="M8.59 16.59L13.17 12 8.59 7.41 10 6l6 6-6 6-1.41-1.41z"/>
        </svg>
      </button>
      </template>

      <!-- 指示器 -->
      <div class="indicators">
        <span v-for="(item, index) in items" :key="index" :class="{ active: currentRealIndex === index }"
              @click="goTo(index)"
        ></span>
      </div>
    </div>
  </div>
</template>

<script setup>
import {ref, computed, onMounted, onBeforeUnmount} from 'vue'

const props = defineProps({
  items: {
    type: Array,
    required: true,
    validator: value => value.every(item => 'image' in item && 'title' in item)
  },
  interval: {
    type: Number,
    default: 3000
  },
  autoplay: {
    type: Boolean,
    default: true
  },
  showArrows: {
    type: Boolean,
    default: true
  },
  round:{
    type: Boolean,
    default: true
  },
  overlay:{
    type: Boolean,
    default: false
  }
})

const currentIndex = ref(1) // 从1开始,因为0是额外添加的最后一张图片
const currentRealIndex = ref(0) // 用于指示器显示的实际索引
const timer = ref(null)
const isTransitioning = ref(false) // 是否正在过渡中

// 计算轮播图列表样式
const listStyle = computed(() => {
  return {
    transform: `translateX(-${currentIndex.value * 100}%)`,
    transition: isTransitioning.value ? `transform ${transitionDuration.value / 1000}s ease` : 'none'
  }
})

// 配置项
const transitionDuration = ref(500) // 过渡动画时间(ms)

// 切换到下一张
const next = () => {
  if (isTransitioning.value) return
  isTransitioning.value = true
  currentIndex.value++
  currentRealIndex.value = (currentRealIndex.value + 1) % props.items.length

  // 如果到达最后一张额外添加的图片(即第一张的副本)
  if (currentIndex.value === props.items.length + 1) {
    setTimeout(() => {
      isTransitioning.value = false
      currentIndex.value = 1 // 无缝跳转到真实的第一张
    }, transitionDuration.value)
  } else {
    setTimeout(() => {
      isTransitioning.value = false
    }, transitionDuration.value)
  }
  resetTimer()
}

// 切换到上一张
const prev = () => {
  if (isTransitioning.value) return
  isTransitioning.value = true
  currentIndex.value--
  currentRealIndex.value = (currentRealIndex.value - 1 + props.items.length) % props.items.length
  // 如果到达第一张额外添加的图片(即最后一张的副本)
  if (currentIndex.value === 0) {
    setTimeout(() => {
      isTransitioning.value = false
      currentIndex.value = props.items.length // 无缝跳转到真实的最后一张
    }, transitionDuration.value)
  } else {
    setTimeout(() => {
      isTransitioning.value = false
    }, transitionDuration.value)
  }
  resetTimer()
}

// 跳转到指定索引
const goTo = (index) => {
  if (isTransitioning.value) return
  isTransitioning.value = true
  currentIndex.value = index + 1
  currentRealIndex.value = index

  setTimeout(() => {
    isTransitioning.value = false
  }, transitionDuration.value)
  resetTimer()
}

// 重置定时器
const resetTimer = () => {
  if (timer.value) {
    clearInterval(timer.value)
  }
  if (props.autoplay) {
    timer.value = setInterval(next, props.interval)
  }
}

// 生命周期钩子
onMounted(() => {
  resetTimer()
})

onBeforeUnmount(() => {
  if (timer.value) {
    clearInterval(timer.value)
  }
})
</script>

<style scoped lang="scss">
.round{
  border-radius:10px;
};
.banner {
  position: relative;
  height: 100%;
  width: 100%;
  overflow: hidden;

  .carousel {
    position: relative;
    height: 100%;
    width: 100%;
    overflow: hidden;

    .carousel-list {
      display: flex;
      height: 100%;
      width: 100%;

      .carousel-item {
        flex: 0 0 100%;
        height: 100%;
        position: relative;

        img {
          width: 100%;
          height: 100%;
          object-fit: cover;
          position: relative;
          z-index: 1;
        }
      }
    }

    .carousel-arrow {
      position: absolute;
      top: 50%;
      transform: translateY(-50%);
      width: 40px;
      height: 40px;
      background-color: rgba(0, 0, 0, 0.2);
      border: none;
      border-radius: 50%;
      color: white;
      cursor: pointer;
      z-index: 10;
      display: flex;
      align-items: center;
      justify-content: center;
      transition: all 0.3s ease;

      &:hover {
        background-color: rgba(0, 0, 0, 0.6);
      }

      &.prev {
        left: 20px;
      }

      &.next {
        right: 20px;
      }

      svg {
        width: 24px;
        height: 24px;
      }
    }

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

      span {
        width: 12px;
        height: 12px;
        margin: 0 6px;
        border-radius: 50%;
        background-color: rgba(255, 255, 255, 0.5);
        cursor: pointer;
        transition: all 0.3s ease;

        &.active {
          background-color: #fff;
          transform: scale(1.2);
        }
      }
    }
  }

  .banner-overlay {
    position: absolute;
    bottom: 0;
    left: 0;
    width: 100%;
    height: 40%;
    background: linear-gradient(
            to top,
            rgba(14, 59, 130, 0.71) 0%,
            rgba(14, 59, 130, 0.3) 70%,
            transparent 100%
    );
    z-index: 2;
  }
}

/* 响应式设计 */
//@media (max-width: 1200px) {
//  .banner {
//    height: 600px;
//  }
//}

//@media (max-width: 992px) {
//  .banner {
//    height: 550px;
//  }
//}

@media (max-width: 768px) {
  .banner {
    //height: 500px;

    .carousel {
      .carousel-arrow {
        width: 36px;
        height: 36px;

        svg {
          width: 20px;
          height: 20px;
        }

        &.prev {
          left: 10px;
        }

        &.next {
          right: 10px;
        }
      }

      .indicators {
        bottom: 20px;

        span {
          width: 10px;
          height: 10px;
          margin: 0 4px;
        }
      }
    }
  }
}

@media (max-width: 576px) {
  .banner {
    //height: 450px;

    .carousel {
      .carousel-arrow {
        width: 32px;
        height: 32px;

        svg {
          width: 18px;
          height: 18px;
        }
      }

      .indicators {
        bottom: 15px;

        span {
          width: 8px;
          height: 8px;
        }
      }
    }
  }
}
</style>

使用

  <div style="height: 300px;margin: 0 auto;">
    <Banner
      :items="banners"
      :interval="2000"
      :showArrows="true"
      :round="false"
      :overlay="true"
  />
  </div>
</template>
<script setup>
import Banner from "@/components/banner/banner.vue";
const banners = [
  {
    image: 'https://自己选择.jpg',
    alt: '轮播图1',
  },
  {
    image: 'https://自己选择.jpg',
    alt: '轮播图2',
  },
  {
    image: 'https://自己选择.jpg',
    alt: '轮播图3',
  }
]
</script>
评论区
头像