【前端从0到1实战】第8篇:构建“轮播图/滑块” (Carousel)

【前端从0到1实战】第8篇:构建“轮播图/滑块” (Carousel)

欢迎来到本系列的第四篇。在上一篇中,我们通过“多步骤表单”掌握了状态管理。今天,我们将把这个概念应用到动画中,构建一个专业级的“轮播图”组件。

轮播图(也称“滑块”)是网页设计的基石之一。它允许我们在一个紧凑的空间内,以富有吸引力的方式循环展示多个内容块(如产品、文章、图片)。

本篇我们将手写一个功能完备的轮播图,它将包含:

“上一张/下一张”导航按钮。

底部“导航点” (Dots) 切换。

平滑的 transform 滑动动画。

第一部分:HTML 结构搭建 (骨架)

轮播图的 HTML 结构是一种精巧的“视觉欺骗”。它包含三个关键层级:

“视口” (.carousel-viewport):一个固定大小的“窗口”,它设置了 overflow: hidden,我们只能通过这个窗口看到内容。

“胶片” (.carousel-filmstrip):一个非常宽的容器,它包含了所有幻灯片,并排成一行 (display: flex)。它将在这个“窗口”后面左右移动。

“幻灯片” (.carousel-slide):每一个单独的内容项。

第二部分:CSS 样式 (皮肤与动画)

CSS 的核心是 overflow 和 transform。

.carousel-viewport 必须设置 overflow: hidden。

.carousel-filmstrip 必须设置 display: flex,并为其 transform 属性添加 transition。

.carousel-slide 必须设置 flex-basis: 100%,以确保每个幻灯片都占满“视口”的宽度。

/* --- 1. 基础容器样式 --- */
.carousel-container {
width: 800px;
max-width: 100%;
margin: 40px auto;
border-radius: 10px;
box-shadow: 0 5px 20px rgba(0,0,0,0.1);
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;

/* 核心:为导航按钮提供绝对定位的锚点 */
position: relative; 

}

/* --- 2. “视口” 样式 --- */
.carousel-viewport {
width: 100%;
border-radius: 10px;

/* 核心:隐藏所有超出“窗口”的内容 */
overflow: hidden; 

}

/* --- 3. “胶片” 样式 --- /
.carousel-filmstrip {
display: flex;
/
假设有 3 张幻灯片, 宽度就是 300% /
/
(在 JS 中动态设置会更健壮) */
width: 300%;

/* 核心:为“transform”属性添加动画 */
transition: transform 0.5s cubic-bezier(0.77, 0, 0.175, 1);

}

/* --- 4. “幻灯片” 样式 --- /
.carousel-slide {
/
核心:确保每张幻灯片占满“视口” */
width: 100%;
flex-basis: 100%;
flex-shrink: 0;

/* 仅为演示:设置高度和背景色 */
height: 400px;
display: flex;
justify-content: center;
align-items: center;
font-size: 1.5em;
color: #fff;

}
.carousel-slide .slide-content {
background-color: rgba(0,0,0,0.4);
padding: 20px;
border-radius: 8px;
text-align: center;
}
/* 演示背景色 */

slide-1

slide-2

slide-3

/* --- 5. 导航按钮样式 --- /
.carousel-btn {
/
核心:绝对定位于 .carousel-container 之上 */
position: absolute;
top: 50%;
transform: translateY(-50%);

background-color: rgba(0,0,0,0.5);
color: #fff;
border: none;
border-radius: 50%;
width: 45px;
height: 45px;
font-size: 1.5em;
font-weight: bold;
cursor: pointer;
transition: background-color 0.2s ease;
z-index: 10;

}
.carousel-btn:hover {
background-color: rgba(0,0,0,0.8);
}

carousel-btn-prev

carousel-btn-next

/* --- 6. 导航点样式 --- /
.carousel-dots {
/
定位在“视口”之外,但在主容器之内 /
text-align: center;
padding: 15px 0;
}
.carousel-dot {
display: inline-block;
width: 12px;
height: 12px;
background-color: #dfe4ea;
border-radius: 50%;
margin: 0 5px;
cursor: pointer;
transition: background-color 0.2s ease;
}
.carousel-dot:hover {
background-color: #57606f;
}
/
激活状态 */
.carousel-dot.is-active {
background-color: #007bff;
}

第三部分:JS 交互逻辑 (状态管理)

JS 是轮播图的“大脑”。它必须始终追踪一个核心状态:currentIndex (当前幻灯片的索引)。所有的移动和“导航点”的更新都依赖这个状态。

document.addEventListener('DOMContentLoaded', () => {

// --- 1. DOM 元素获取 ---
const filmstrip = document.querySelector('.carousel-filmstrip');
const allSlides = document.querySelectorAll('.carousel-slide');
const totalSlides = allSlides.length;

const btnPrev = document.getElementById('carousel-btn-prev');
const btnNext = document.getElementById('carousel-btn-next');

const allDots = document.querySelectorAll('.carousel-dot');

// --- 2. 状态管理 ---
let currentIndex = 0; // 当前幻灯片的索引 (从 0 开始)
let slideWidthPercent = 100 / totalSlides;

// (健壮性) 动态设置胶片宽度
filmstrip.style.width = `${100 * totalSlides}%`;

// --- 3. 核心功能函数:移动到指定索引的幻灯片 ---
const goToSlide = (slideIndex) => {
    // 边界检查 (确保索引在 0 和 totalSlides - 1 之间)
    if (slideIndex < 0) {
        slideIndex = totalSlides - 1; // 循环到最后一张
    } else if (slideIndex >= totalSlides) {
        slideIndex = 0; // 循环到第一张
    }
    
    // (A) 计算需要偏移的百分比
    const offsetPercent = -(slideIndex * slideWidthPercent);
    
    // (B) 应用 transform 来移动“胶片”
    filmstrip.style.transform = `translateX(${offsetPercent}%)`;

    // (C) 更新当前的索引状态
    currentIndex = slideIndex;

    // (D) 更新导航点的激活状态
    allDots.forEach((dot, index) => {
        if (index === currentIndex) {
            dot.classList.add('is-active');
        } else {
            dot.classList.remove('is-active');
        }
    });
};

// --- 4. 事件绑定 ---

// “下一步”按钮
btnNext.addEventListener('click', () => {
    goToSlide(currentIndex + 1);
});

// “上一步”按钮
btnPrev.addEventListener('click', () => {
    goToSlide(currentIndex - 1);
});

// “导航点”
allDots.forEach((dot) => {
    dot.addEventListener('click', (e) => {
        // 从 data-index 属性读取点击的是哪个点
        const dotIndex = parseInt(e.target.dataset.index);
        goToSlide(dotIndex);
    });
});

// --- 5. (可选) 自动播放 ---
// let autoPlayInterval = setInterval(() => {
//    goToSlide(currentIndex + 1);
// }, 3000);

// (可选) 鼠标悬停时停止自动播放
// const container = document.getElementById('hero-carousel');
// container.addEventListener('mouseenter', () => clearInterval(autoPlayInterval));
// container.addEventListener('mouseleave', () => {
//    autoPlayInterval = setInterval(() => {
//        goToSlide(currentIndex + 1);
//    }, 3000);
// });

// --- 6. 初始化 ---
// 确保页面加载时,UI 处于正确的第一张幻灯片状态
goToSlide(0);

});

总结

恭喜!我们完成了一个功能丰富、交互平滑的轮播图。

我们学到了:

HTML: 如何使用“视口 (viewport)” + “胶片 (filmstrip)”的嵌套结构来搭建舞台。

CSS:

如何使用 overflow: hidden 来“裁切”舞台。

如何使用 transform: translateX() 来移动“胶片”,并配合 transition 实现动画。

如何使用 flex-basis: 100% 来确保每个幻灯片占满“视口”。

JS (核心):

如何使用 currentIndex 变量来管理状态。

如何编写一个 goToSlide(index) 的中央函数来驱动所有 UI 变化(胶片移动、导航点更新)。

如何通过 data-* 属性将导航点与幻灯片索引关联起来。

在下一篇文章中,我们将迎来本系列第一个真正“动态”的挑战:异步数据加载。

posted @ 2025-11-17 15:51  GreenBoos2025  阅读(4)  评论(0)    收藏  举报