Vue3轮播图组件,当前轮播区域有当前图和左右两边图,两边图各显示一半,支持点击跳转和手动滑动切换

功能:

  • 自动循环播放(到达末尾后回到第一张)、可设置切换间隔时间(interval属性)

  • 左右导航按钮(可自定义显示/隐藏)

  • 点击底部指示器跳转到指定幻灯片、且位置可调(轮播图内部/外部)

  • 鼠标拖拽滑动(PC端)

  • 触摸滑动(移动端适配)

  • 支持暂停/继续自动播放(autoPlay控制)

  • 用户交互(拖拽/点击按钮)时自动暂停轮播

  • 点击幻灯片可跳转页面(这里是跳转到详情)

封装好了,直接用就可以:components/Carousel.vue:

javascript 复制代码
<template>
  <div class="carousel-container">
    <!-- 左侧导航按钮 -->
    <button class="nav-button prev" @click="prevSlide" v-if="showNavButtons">‹</button>
    
    <div 
      class="carousel-viewport" 
      ref="viewport"
      @mousedown="startDrag"
      @mousemove="handleDrag"
      @mouseup="endDrag"
      @mouseleave="endDrag"
      @touchstart="startDrag"
      @touchmove="handleDrag"
      @touchend="endDrag"
    >
      <div class="carousel-track" :style="trackStyle">
        <div 
          class="slide" 
          v-for="(item, index) in items" 
          :key="item.id"
          @click="handleSlideClick(index)"
        >
          <img :src="item.cover" :alt="item.title">
          <div class="slide-title" :class="{ 'active': currentIndex === index }">
            {{ item.categoryTag }}
          </div>
        </div>
      </div>
    </div>
    
    <!-- 右侧导航按钮 -->
    <button class="nav-button next" @click="nextSlide" v-if="showNavButtons">›</button>
    
    <div class="indicators" :class="{ 'inside': indicatorsInside }">
      <span 
        v-for="(item, index) in items" 
        :key="item.id"
        :class="{ 'active': currentIndex === index }"
        @click="goToSlide(index)"
      ></span>
    </div>
    
    <!-- 标题和摘要显示区域 -->
    <div class="carousel-caption">
      <div class="carousel-title">{{ currentItem.title }}</div>
      <div class="carousel-summary">{{ currentItem.summary }}</div>
    </div>
  </div>
</template>

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

const props = defineProps({
  items: {  // 父组件传过来的数据,下面会有模拟数据
    type: Array,
    required: true,
    default: () => []
  },
  interval: { // 控制自动轮播的切换时间间隔
    type: Number,
    default: 3000
  },
  autoPlay: { // 是否自动切换
    type: Boolean,
    default: true
  },
  initialIndex: { // 指定轮播图初始显示的第几张幻灯片,默认显示第 1 张
    type: Number,
    default: 0
  },
  indicatorsInside: { // 控制指示器轮播区域内部/外部显示,true为在外部显示,false为在内部显示
    default: false
  },
  showNavButtons: {  // 控制左右按钮显示,这里默认隐藏
    type: Boolean,
    default: false
  }
});

const currentIndex = ref(props.initialIndex);
const viewport = ref(null);
let autoPlayTimer = null;

// 拖拽相关状态
const isDragging = ref(false);
const startPos = ref(0);
const currentTranslate = ref(0);
const prevTranslate = ref(0);
const animationId = ref(null);
const dragDistance = ref(0);

// 计算当前显示的item
const currentItem = computed(() => {
  return props.items[currentIndex.value] || {};
});

// 图片宽度配置
const slideConfig = {
  fullWidth: 680,
  visiblePart: 320,
  gap: 20
};

// 计算轨道偏移量
const trackStyle = computed(() => {
  if (isDragging.value) {
    return {
      transform: `translateX(${currentTranslate.value}px)`,
      transition: 'none'
    };
  }
  const offset = currentIndex.value * -(slideConfig.fullWidth + slideConfig.gap);
  return {
    transform: `translateX(${offset}px)`,
    transition: 'transform 0.5s ease'
  };
});

// 开始拖拽
const startDrag = (e) => {
  stopAutoPlay();
  isDragging.value = true;
  startPos.value = getPositionX(e);
  prevTranslate.value = currentIndex.value * -(slideConfig.fullWidth + slideConfig.gap);
  currentTranslate.value = prevTranslate.value;
  dragDistance.value = 0;
  cancelAnimationFrame(animationId.value);
};

// 处理拖拽
const handleDrag = (e) => {
  if (!isDragging.value) return;
  const currentPosition = getPositionX(e);
  const diff = currentPosition - startPos.value;
  currentTranslate.value = prevTranslate.value + diff;
  dragDistance.value = Math.abs(diff);
};

// 结束拖拽
const endDrag = () => {
  if (!isDragging.value) return;
  
  const movedBy = currentTranslate.value - prevTranslate.value;
  
  // 如果拖动距离超过100px,则切换幻灯片
  if (movedBy < -100 && currentIndex.value < props.items.length - 1) {
    currentIndex.value += 1;
  } else if (movedBy > 100 && currentIndex.value > 0) {
    currentIndex.value -= 1;
  }
  
  isDragging.value = false;
  resetAutoPlay();
};

// 获取当前位置
const getPositionX = (e) => {
  return e.type.includes('mouse') ? e.pageX : e.touches[0].clientX;
};

// 处理幻灯片点击
const handleSlideClick = (index) => {
  // 如果拖动距离大于10px,则不触发点击事件
  if (dragDistance.value > 10) return;
  goToDetail(props.items[index].id);
};

// 切换上一张
const prevSlide = () => {
  currentIndex.value = (currentIndex.value - 1 + props.items.length) % props.items.length;
  resetAutoPlay();
};

// 切换下一张
const nextSlide = () => {
  currentIndex.value = (currentIndex.value + 1) % props.items.length;
  resetAutoPlay();
};

// 跳转到指定图片
const goToSlide = (index) => {
  currentIndex.value = index;
  resetAutoPlay();
};

// 自动播放控制
const startAutoPlay = () => {
  if (!props.autoPlay) return;
  stopAutoPlay();
  autoPlayTimer = setInterval(() => {
    nextSlide();
  }, props.interval);
};

const stopAutoPlay = () => {
  if (autoPlayTimer) {
    clearInterval(autoPlayTimer);
    autoPlayTimer = null;
  }
};

const resetAutoPlay = () => {
  if (props.autoPlay) {
    stopAutoPlay();
    startAutoPlay();
  }
};

// 监听autoPlay变化
watch(() => props.autoPlay, (newVal) => {
  if (newVal) {
    startAutoPlay();
  } else {
    stopAutoPlay();
  }
});

// 监听initialIndex变化
watch(() => props.initialIndex, (newVal) => {
  if (newVal >= 0 && newVal < props.items.length) {
    currentIndex.value = newVal;
  }
});

const goToDetail = (id) => {
  navigateTo({ path: `/case/${id}.html` });
};

onMounted(() => {
  startAutoPlay();
});

onBeforeUnmount(() => {
  stopAutoPlay();
  cancelAnimationFrame(animationId.value);
});
</script>

<style scoped>
.carousel-container {
  position: relative;
  width: 100%;
  max-width: 1280px;
  margin: 0 auto;
  height: 400px;
  user-select: none;
}

.carousel-viewport {
  position: relative;
  width: 100%;
  height: 100%;
  overflow: hidden;
  cursor: grab;
}

.carousel-viewport:active {
  cursor: grabbing;
}

.carousel-track {
  display: flex;
  height: 100%;
  padding: 0 calc(50% - 340px);
  will-change: transform;
}

.slide {
  flex: 0 0 680px;
  height: 100%;
  margin-right: 20px;
  position: relative;
  cursor: pointer;
  touch-action: pan-y;
}

.slide img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  border-radius: 10px;
  pointer-events: none;
}

.slide-title {
  position: absolute;
  bottom: 0;
  right: 0;
  background: rgba(0, 0, 0, 0.5);
  color: white;
  text-align: center;
  padding: 8px 17px;
  border-radius: 10px 0px 10px 0px;
  font-size: 14px;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  transition: opacity 0.3s ease;
}

/* 指示点样式 - 默认在外部 */
.indicators {
  position: absolute;
  bottom: 20px;
  left: 0;
  right: 0;
  display: flex;
  justify-content: center;
  gap: 10px;
  z-index: 20;
}

/* 指示点在内部的样式 */
.indicators.inside {
  bottom: -30px;
}

.indicators span {
  width: 10px;
  height: 10px;
  border-radius: 50%;
  background-color: #E0E0E0;
  cursor: pointer;
  transition: all 0.3s ease;
}

.indicators span.active {
  background-color: #0A53B2;
  transform: scale(1.2);
}

/* 标题和摘要样式 */
.carousel-caption {
  margin-top: 70px;
  text-align: center;
  padding: 0 20px;
  text-align: center;
}

.carousel-title {
  font-size: 26px;
  font-weight: bold;
  margin-bottom: 16px;
  color: #333;
}

.carousel-summary {
  font-size: 16px;
  color: #808080;
  line-height: 1.5;
}

/* 导航按钮样式 */
.nav-button {
  position: absolute;
  top: 50%;
  transform: translateY(-50%);
  background: rgba(0, 0, 0, 0.5);
  color: white;
  border: none;
  width: 40px;
  height: 40px;
  border-radius: 50%;
  font-size: 20px;
  cursor: pointer;
  z-index: 20;
  display: flex;
  align-items: center;
  justify-content: center;
}

.nav-button:hover {
  background: rgba(0, 0, 0, 0.8);
}

.prev {
  left: 20px;
}

.next {
  right: 20px;
}

/* 响应式调整 */
@media (max-width: 768px) {
  .nav-button {
    width: 30px;
    height: 30px;
    font-size: 16px;
  }
  .prev {
    left: 10px;
  }
  .next {
    right: 10px;
  }
}
</style>

父组件使用:

javascript 复制代码
<Carousel :items="list" :interval="3000" :autoPlay="true" :initialIndex="1" :indicatorsInside="true"/>

list 模拟数据可以用这个:

javascript 复制代码
const list = ref([
    {
        "id": 303,
        "categoryTag": "政府办公",
        "title": "用智能化解决方案,助力政府完成办公基础设备搭建,系统管理全面升级",
        "cover": "http://szdxyp.com/images/c02f612c-dd69-47ee-aafd-86aaa73df208.png",
        "summary": "用智能化解决方案,简介xxxxxxxxxxxxxxxxxxxx",
    },
    {
        "id": 304,
        "categoryTag": "金融风控",
        "title": "财富基石:金融数据与资产积累",
        "cover": "http://szdxyp.com/images/1d128ffa-f4f0-485e-878c-86a708769a19.png",
        "summary": "用智能化金融方案,简介xxxxxxxxxxxxxxxxxxxx硬币象征财富,图表代表金融数据,整体传达了金融数据在财富积累中的关键作用。",
    },
    {
        "id": 305,
        "categoryTag": "政府办公",
        "title": "数字创想:代码世界的团队协作",
        "cover": "http://szdxyp.com/images/080f8bdc-9133-4aaa-a34a-d9b965c23c24.png",
        "summary": "用智能化金融方案,简介xxxxxxxxxxxxxxxxxxxx突出了编程协作在构建数字未来中的基础性作用,展示了团队合作的重要性。",
    },
    {
        "id": 306,
        "categoryTag": "智慧教育",
        "title": "《协同共生:城市脉动与绿色能源的双向赋能》",
        "cover": "http://szdxyp.com/images/3d670ed1-f68b-48d7-8bb6-d59d7d477846.png",
        "summary": "展现出城市化进程与清洁能源发展的共生关系。左侧图片聚焦现代化城市的楼群林立与繁华交通,象征经济与技术的高度聚合;右侧呈现风力发电装置与太阳能板的能量转换场景,凸显对可再生能源的实践追求。两者结合,揭示了在全球化与生态危机并存的当下,城市与自然的平衡共生成为可持续发展的核心命题",
    },
    {
        "id": 307,
        "categoryTag": "金融风控",
        "title": "清洁能源:生态可持续的技术突围",
        "cover": "http://szdxyp.com/images/f4226060-347a-45d5-a379-3716189bc344.png",
        "summary": "可再生能源实践:\n右侧风能、太阳能的规模化应用,体现了对化石能源的替代性突破,减少碳排放的同时,提高能源安全性与区域自主性。\n分布式能源趋势:\n结合城市楼宇光伏、社区微电网等场景,清洁能源可深度融入城市基础设施,形成"自给+共享"的能源网络。",
    }
])
相关推荐
微信公众号:AI创造财富30 分钟前
conda create -n modelscope python=3.8 conda: command not found
开发语言·python·conda
鱼会上树cy31 分钟前
空间解析几何10:三维圆弧拟合【附MATLAB代码】
开发语言·matlab
IT艺术家-rookie1 小时前
golang--channel的关键特性和行为
开发语言·后端·golang
yuanyxh1 小时前
《精通正则表达式》精华摘要
前端·javascript·正则表达式
小飞大王6662 小时前
简单实现HTML在线编辑器
前端·编辑器·html
Jimmy2 小时前
CSS 实现卡牌翻转
前端·css·html
百万蹄蹄向前冲2 小时前
大学期末考,AI定制个性化考试体验
前端·人工智能·面试
向明天乄3 小时前
在 Vue 3 项目中集成高德地图(附 Key 与安全密钥申请全流程)
前端·vue.js·安全
sunshine_程序媛3 小时前
vue3中的watch和watchEffect区别以及demo示例
前端·javascript·vue.js·vue3
Cyrus_柯3 小时前
C++(面向对象编程——关键字)
开发语言·c++·算法·面向对象