

功能:
-
自动循环播放(到达末尾后回到第一张)、可设置切换间隔时间(
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结合城市楼宇光伏、社区微电网等场景,清洁能源可深度融入城市基础设施,形成"自给+共享"的能源网络。",
}
])